fix bug of measurement recommend api

This commit is contained in:
douxu 2025-10-20 15:06:23 +08:00
parent 86199269f8
commit 54128bedac
8 changed files with 347 additions and 139 deletions

View File

@ -9,12 +9,54 @@ const docTemplate = `{
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"contact": {},
"contact": {
"name": "douxu",
"url": "http://www.swagger.io/support",
"email": "douxu@clea.com.cn"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/api/v1/recommend/measurement": {
"post": {
"description": "根据用户输入的字符串,从 Redis 中查询可能的测量点或结构路径,并提供推荐列表。",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Recommend"
],
"summary": "测量点推荐(搜索框自动补全)",
"parameters": [
{
"description": "查询输入参数,例如 'trans' 或 'grid_key.'",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/network.MeasurementRecommendRequest"
}
}
],
"responses": {
"200": {
"description": "请求或查询失败。**注意**HTTP 状态码始终为 200但内部 Code 为 400 (参数错误) 或 500 (内部错误)。",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
}
}
}
},
"/model/diagram_load/{page_id}": {
"get": {
"description": "load circuit diagram info by page id",
@ -55,35 +97,16 @@ const docTemplate = `{
}
},
"definitions": {
"network.FailResponseHeader": {
"type": "object",
"properties": {
"err_msg": {
"type": "string"
},
"status": {
"type": "integer",
"example": 400
}
}
},
"network.FailureResponse": {
"type": "object",
"properties": {
"header": {
"$ref": "#/definitions/network.FailResponseHeader"
"code": {
"type": "integer",
"example": 500
},
"payload": {
"type": "object",
"additionalProperties": true
}
}
},
"network.SuccessResponse": {
"type": "object",
"properties": {
"header": {
"$ref": "#/definitions/network.SuccessResponseHeader"
"msg": {
"type": "string",
"example": "failed to get recommend data from redis"
},
"payload": {
"type": "object",
@ -96,15 +119,57 @@ const docTemplate = `{
}
}
},
"network.SuccessResponseHeader": {
"network.MeasurementRecommendPayload": {
"type": "object",
"properties": {
"err_msg": {
"type": "string"
"input": {
"type": "string",
"example": "transformfeeder1_220."
},
"status": {
"offset": {
"type": "integer",
"example": 20
},
"recommended_list": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"[\"I_A_rms\"",
" \"I_B_rms\"]"
]
}
}
},
"network.MeasurementRecommendRequest": {
"type": "object",
"properties": {
"input": {
"type": "string",
"example": "trans"
}
}
},
"network.SuccessResponse": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"example": 200
},
"msg": {
"type": "string",
"example": "success"
},
"payload": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"example": {
"key": "value"
}
}
}
}
@ -113,12 +178,12 @@ const docTemplate = `{
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "",
Host: "",
BasePath: "",
Version: "1.0",
Host: "localhost:8080",
BasePath: "/api/v1",
Schemes: []string{},
Title: "",
Description: "",
Title: "ModelRT 实时模型服务 API 文档",
Description: "实时数据计算和模型运行服务的 API 服务",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",

View File

@ -1,9 +1,56 @@
{
"swagger": "2.0",
"info": {
"contact": {}
"description": "实时数据计算和模型运行服务的 API 服务",
"title": "ModelRT 实时模型服务 API 文档",
"contact": {
"name": "douxu",
"url": "http://www.swagger.io/support",
"email": "douxu@clea.com.cn"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "1.0"
},
"host": "localhost:8080",
"basePath": "/api/v1",
"paths": {
"/api/v1/recommend/measurement": {
"post": {
"description": "根据用户输入的字符串,从 Redis 中查询可能的测量点或结构路径,并提供推荐列表。",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Recommend"
],
"summary": "测量点推荐(搜索框自动补全)",
"parameters": [
{
"description": "查询输入参数,例如 'trans' 或 'grid_key.'",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/network.MeasurementRecommendRequest"
}
}
],
"responses": {
"200": {
"description": "请求或查询失败。**注意**HTTP 状态码始终为 200但内部 Code 为 400 (参数错误) 或 500 (内部错误)。",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
}
}
}
},
"/model/diagram_load/{page_id}": {
"get": {
"description": "load circuit diagram info by page id",
@ -44,35 +91,16 @@
}
},
"definitions": {
"network.FailResponseHeader": {
"type": "object",
"properties": {
"err_msg": {
"type": "string"
},
"status": {
"type": "integer",
"example": 400
}
}
},
"network.FailureResponse": {
"type": "object",
"properties": {
"header": {
"$ref": "#/definitions/network.FailResponseHeader"
"code": {
"type": "integer",
"example": 500
},
"payload": {
"type": "object",
"additionalProperties": true
}
}
},
"network.SuccessResponse": {
"type": "object",
"properties": {
"header": {
"$ref": "#/definitions/network.SuccessResponseHeader"
"msg": {
"type": "string",
"example": "failed to get recommend data from redis"
},
"payload": {
"type": "object",
@ -85,15 +113,57 @@
}
}
},
"network.SuccessResponseHeader": {
"network.MeasurementRecommendPayload": {
"type": "object",
"properties": {
"err_msg": {
"type": "string"
"input": {
"type": "string",
"example": "transformfeeder1_220."
},
"status": {
"offset": {
"type": "integer",
"example": 20
},
"recommended_list": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"[\"I_A_rms\"",
" \"I_B_rms\"]"
]
}
}
},
"network.MeasurementRecommendRequest": {
"type": "object",
"properties": {
"input": {
"type": "string",
"example": "trans"
}
}
},
"network.SuccessResponse": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"example": 200
},
"msg": {
"type": "string",
"example": "success"
},
"payload": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"example": {
"key": "value"
}
}
}
}

View File

@ -1,24 +1,13 @@
basePath: /api/v1
definitions:
network.FailResponseHeader:
properties:
err_msg:
type: string
status:
example: 400
type: integer
type: object
network.FailureResponse:
properties:
header:
$ref: '#/definitions/network.FailResponseHeader'
payload:
additionalProperties: true
type: object
type: object
network.SuccessResponse:
properties:
header:
$ref: '#/definitions/network.SuccessResponseHeader'
code:
example: 500
type: integer
msg:
example: failed to get recommend data from redis
type: string
payload:
additionalProperties:
type: string
@ -26,17 +15,79 @@ definitions:
key: value
type: object
type: object
network.SuccessResponseHeader:
network.MeasurementRecommendPayload:
properties:
err_msg:
input:
example: transformfeeder1_220.
type: string
status:
offset:
example: 20
type: integer
recommended_list:
example:
- '["I_A_rms"'
- ' "I_B_rms"]'
items:
type: string
type: array
type: object
network.MeasurementRecommendRequest:
properties:
input:
example: trans
type: string
type: object
network.SuccessResponse:
properties:
code:
example: 200
type: integer
msg:
example: success
type: string
payload:
additionalProperties:
type: string
example:
key: value
type: object
type: object
host: localhost:8080
info:
contact: {}
contact:
email: douxu@clea.com.cn
name: douxu
url: http://www.swagger.io/support
description: 实时数据计算和模型运行服务的 API 服务
license:
name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
title: ModelRT 实时模型服务 API 文档
version: "1.0"
paths:
/api/v1/recommend/measurement:
post:
consumes:
- application/json
description: 根据用户输入的字符串,从 Redis 中查询可能的测量点或结构路径,并提供推荐列表。
parameters:
- description: 查询输入参数,例如 'trans' 或 'grid_key.'
in: body
name: request
required: true
schema:
$ref: '#/definitions/network.MeasurementRecommendRequest'
produces:
- application/json
responses:
"200":
description: 请求或查询失败。**注意**HTTP 状态码始终为 200但内部 Code 为 400 (参数错误) 或 500
(内部错误)。
schema:
$ref: '#/definitions/network.FailureResponse'
summary: 测量点推荐(搜索框自动补全)
tags:
- Recommend
/model/diagram_load/{page_id}:
get:
consumes:

View File

@ -2,7 +2,6 @@
package handler
import (
"fmt"
"net/http"
"modelRT/logger"
@ -13,6 +12,16 @@ import (
)
// MeasurementRecommendHandler define measurement recommend API
// MeasurementRecommendHandler define measurement recommend API
// @Summary 测量点推荐(搜索框自动补全)
// @Description 根据用户输入的字符串,从 Redis 中查询可能的测量点或结构路径,并提供推荐列表。
// @Tags Recommend
// @Accept json
// @Produce json
// @Param request body network.MeasurementRecommendRequest true "查询输入参数,例如 'trans' 或 'grid_key.'"
// @Success 200 {object} network.SuccessResponse{payload=network.MeasurementRecommendPayload} "成功返回推荐列表"
// @Failure 200 {object} network.FailureResponse "请求或查询失败。**注意**HTTP 状态码始终为 200但内部 Code 为 400 (参数错误) 或 500 (内部错误)。"
// @Router /api/v1/recommend/measurement [post]
func MeasurementRecommendHandler(c *gin.Context) {
var request network.MeasurementRecommendRequest
@ -26,8 +35,6 @@ func MeasurementRecommendHandler(c *gin.Context) {
}
recommends, isFuzzy, err := model.RedisSearchRecommend(c, request.Input)
// TODO delete debug info
fmt.Printf("recommends in handler:%+v\n", recommends)
if err != nil {
logger.Error(c, "failed to get recommend data from redis", "input", request.Input, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
@ -40,8 +47,6 @@ func MeasurementRecommendHandler(c *gin.Context) {
return
}
fmt.Printf("isFuzzy:%v\n", isFuzzy)
var finalOffset int
if isFuzzy {
var maxOffset int
@ -63,15 +68,11 @@ func MeasurementRecommendHandler(c *gin.Context) {
finalOffset = minOffset
}
fmt.Printf("finalOffset:%v\n", finalOffset)
resultRecommends := make([]string, 0, len(recommends))
seen := make(map[string]struct{})
for _, recommend := range recommends {
recommendTerm := recommend[finalOffset:]
fmt.Printf("resultRecommend:%s\n", recommendTerm)
fmt.Printf("len of resultRecommend:%d\n", len(recommendTerm))
if len(recommendTerm) != 0 {
if _, exists := seen[recommendTerm]; !exists {
seen[recommendTerm] = struct{}{}

16
main.go
View File

@ -53,6 +53,22 @@ var (
)
// TODO 使用 wire 依赖注入管理 DVIE 面板注册的 panel
// @title ModelRT 实时模型服务 API 文档
// @version 1.0
// @description 实时数据计算和模型运行服务的 API 服务
// TODO termsOfService服务条款待后续优化
// // @termsOfService http://swagger.io/terms/
//
// @contact.name douxu
// TODO 修改支持的文档地址
// @contact.url http://www.swagger.io/support
// @contact.email douxu@clea.com.cn
//
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
//
// @host localhost:8080
// @BasePath /api/v1
func main() {
flag.Parse()
ctx := context.TODO()

View File

@ -31,10 +31,11 @@ func RedisSearchRecommend(ctx context.Context, input string) ([]string, bool, er
return getAllGridKeys(ctx, constants.RedisAllGridSetKey)
}
inputs := strings.Split(input, ".")
inputLen := len(inputs)
fmt.Printf("inputLen:%d\n", inputLen)
switch inputLen {
inputSlice := strings.Split(input, ".")
inputSliceLen := len(inputSlice)
originInputLen := len(inputSlice)
switch inputSliceLen {
case 1:
// TODO 优化成NewSet的形式
gridExist, err := rdb.SIsMember(ctx, constants.RedisAllGridSetKey, input).Result()
@ -43,11 +44,8 @@ func RedisSearchRecommend(ctx context.Context, input string) ([]string, bool, er
return []string{}, false, err
}
// TODO delete debug info
fmt.Println("gridExist", gridExist)
searchInput := input
inputLen := len(searchInput)
inputLen := inputSliceLen
for inputLen != 0 && !gridExist {
results, err := ac.SuggestOpts(searchInput, redisearch.SuggestOptions{
Num: math.MaxInt16,
@ -55,9 +53,6 @@ func RedisSearchRecommend(ctx context.Context, input string) ([]string, bool, er
WithScores: false,
WithPayloads: false,
})
// TODO delete debug info
fmt.Printf("results:%+v\n", results)
fmt.Println("err", err)
if err != nil {
logger.Error(ctx, "query info by fuzzy failed", "query_key", input, "error", err)
return []string{}, false, err
@ -65,50 +60,48 @@ func RedisSearchRecommend(ctx context.Context, input string) ([]string, bool, er
if len(results) == 0 {
// TODO 构建 for 循环返回所有可能的补全
searchInput = input[:inputLen-1]
searchInput = searchInput[:len(searchInput)-1]
inputLen = len(searchInput)
fmt.Printf("next search input:%s\n", searchInput)
fmt.Printf("next search input len:%d\n", inputLen)
continue
}
var grids []string
var recommends []string
for _, result := range results {
grids = append(grids, result.Term)
termSlice := strings.Split(result.Term, ".")
if len(termSlice) <= originInputLen {
recommends = append(recommends, result.Term)
}
}
// 返回模糊查询结果
return grids, true, nil
return recommends, true, nil
}
// 处理 input 不为空、不含有.并且 input 是一个完整的 grid key 的情况
if strings.HasSuffix(input, ".") == false {
return []string{"."}, false, nil
}
// 处理 input 不为空并且以.结尾的情况
if strings.HasSuffix(input, ".") == true {
setKey := fmt.Sprintf(constants.RedisSpecGridZoneSetKey, input)
return getSpecificZoneKeys(ctx, setKey)
recommend := input + "."
return []string{recommend}, false, nil
}
default:
lastInput := inputs[inputLen-1]
lastInput := inputSlice[inputSliceLen-1]
// 判断 queryKey 是否是空值空值则返回上一级别下的所有key
if lastInput == "" {
fmt.Println("last token is empty")
// TODO 根据所在层级拼接相关setKey
setKey := getCombinedConstantsKeyByLength(inputs[inputLen-2], inputLen)
fmt.Printf("default case set key:%s\n", setKey)
setKey := getCombinedConstantsKeyByLength(inputSlice[inputSliceLen-2], inputSliceLen)
targetSet := diagram.NewRedisSet(ctx, setKey, 10, true)
results, err := targetSet.SMembers(setKey)
keys, err := targetSet.SMembers(setKey)
if err != nil {
logger.Error(ctx, "get all recommend key by setKey failed", "set_key", setKey, "error", err)
return []string{}, false, fmt.Errorf("get all recommend key by setKey failed,%w", err)
}
var results []string
for _, key := range keys {
result := input + key
results = append(results, result)
}
return results, false, nil
}
setKey := getCombinedConstantsKeyByLength(inputs[inputLen-2], inputLen)
fmt.Printf("default case set key:%s\n", setKey)
setKey := getCombinedConstantsKeyByLength(inputSlice[inputSliceLen-2], inputSliceLen)
targetSet := diagram.NewRedisSet(ctx, setKey, 10, true)
exist, err := targetSet.SIsMember(setKey, lastInput)
if err != nil {
@ -134,12 +127,9 @@ func RedisSearchRecommend(ctx context.Context, input string) ([]string, bool, er
// TODO 构建 for 循环返回所有可能的补全
searchInput = input[:inputLen-1]
inputLen = len(searchInput)
fmt.Printf("next search input:%s\n", searchInput)
fmt.Printf("next search input len:%d\n", inputLen)
continue
}
fmt.Printf("default fuzzy search results:%v\n", results)
var terms []string
for _, result := range results {
terms = append(terms, result.Term)
@ -162,14 +152,20 @@ func getAllGridKeys(ctx context.Context, setKey string) ([]string, bool, error)
return keys, false, nil
}
func getSpecificZoneKeys(ctx context.Context, setKey string) ([]string, bool, error) {
func getSpecificZoneKeys(ctx context.Context, input string) ([]string, bool, error) {
setKey := fmt.Sprintf(constants.RedisSpecGridZoneSetKey, input)
// TODO 从redis set 中获取指定 grid 下的 zone key
zoneSets := diagram.NewRedisSet(ctx, setKey, 10, true)
keys, err := zoneSets.SMembers(setKey)
if err != nil {
return []string{}, false, fmt.Errorf("get all root keys failed, error: %v", err)
}
return keys, false, nil
var results []string
for _, key := range keys {
result := input + "." + key
results = append(results, result)
}
return results, false, nil
}
func getConstantsKeyByLength(inputLen int) string {

View File

@ -3,11 +3,11 @@ package network
// MeasurementGetRequest defines the request payload for getting an measurement
type MeasurementGetRequest struct {
MeasurementID int64 `json:"measurement_id"`
MeasurementToken string `json:"token"`
MeasurementID int64 `json:"measurement_id" example:"1001"`
MeasurementToken string `json:"token" example:"some-token"`
}
// MeasurementRecommendRequest defines the request payload for an measurement recommend
type MeasurementRecommendRequest struct {
Input string `json:"input"`
Input string `json:"input" example:"trans"`
}

View File

@ -1,14 +1,23 @@
// Package network define struct of network operation
package network
// FailureResponse define struct of standard failure API response format
type FailureResponse struct {
Code int `json:"code" example:"200"`
Msg string `json:"msg"`
Code int `json:"code" example:"500"`
Msg string `json:"msg" example:"failed to get recommend data from redis"`
PayLoad map[string]interface{} `json:"payload" swaggertype:"object,string" example:"key:value"`
}
// SuccessResponse define struct of standard successful API response format
type SuccessResponse struct {
Code int `json:"code" example:"200"`
Msg string `json:"msg"`
Msg string `json:"msg" example:"success"`
PayLoad map[string]interface{} `json:"payload" swaggertype:"object,string" example:"key:value"`
}
// MeasurementRecommendPayload define struct of represents the data payload for the successful recommendation response.
type MeasurementRecommendPayload struct {
Input string `json:"input" example:"transformfeeder1_220."`
Offset int `json:"offset" example:"20"`
RecommendedList []string `json:"recommended_list" example:"[\"I_A_rms\", \"I_B_rms\"]"`
}