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": { "info": {
"description": "{{escape .Description}}", "description": "{{escape .Description}}",
"title": "{{.Title}}", "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}}" "version": "{{.Version}}"
}, },
"host": "{{.Host}}", "host": "{{.Host}}",
"basePath": "{{.BasePath}}", "basePath": "{{.BasePath}}",
"paths": { "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}": { "/model/diagram_load/{page_id}": {
"get": { "get": {
"description": "load circuit diagram info by page id", "description": "load circuit diagram info by page id",
@ -55,35 +97,16 @@ const docTemplate = `{
} }
}, },
"definitions": { "definitions": {
"network.FailResponseHeader": {
"type": "object",
"properties": {
"err_msg": {
"type": "string"
},
"status": {
"type": "integer",
"example": 400
}
}
},
"network.FailureResponse": { "network.FailureResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"header": { "code": {
"$ref": "#/definitions/network.FailResponseHeader" "type": "integer",
"example": 500
}, },
"payload": { "msg": {
"type": "object", "type": "string",
"additionalProperties": true "example": "failed to get recommend data from redis"
}
}
},
"network.SuccessResponse": {
"type": "object",
"properties": {
"header": {
"$ref": "#/definitions/network.SuccessResponseHeader"
}, },
"payload": { "payload": {
"type": "object", "type": "object",
@ -96,15 +119,57 @@ const docTemplate = `{
} }
} }
}, },
"network.SuccessResponseHeader": { "network.MeasurementRecommendPayload": {
"type": "object", "type": "object",
"properties": { "properties": {
"err_msg": { "input": {
"type": "string",
"example": "transformfeeder1_220."
},
"offset": {
"type": "integer",
"example": 20
},
"recommended_list": {
"type": "array",
"items": {
"type": "string" "type": "string"
}, },
"status": { "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", "type": "integer",
"example": 200 "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 // SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{ var SwaggerInfo = &swag.Spec{
Version: "", Version: "1.0",
Host: "", Host: "localhost:8080",
BasePath: "", BasePath: "/api/v1",
Schemes: []string{}, Schemes: []string{},
Title: "", Title: "ModelRT 实时模型服务 API 文档",
Description: "", Description: "实时数据计算和模型运行服务的 API 服务",
InfoInstanceName: "swagger", InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate, SwaggerTemplate: docTemplate,
LeftDelim: "{{", LeftDelim: "{{",

View File

@ -1,9 +1,56 @@
{ {
"swagger": "2.0", "swagger": "2.0",
"info": { "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": { "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}": { "/model/diagram_load/{page_id}": {
"get": { "get": {
"description": "load circuit diagram info by page id", "description": "load circuit diagram info by page id",
@ -44,35 +91,16 @@
} }
}, },
"definitions": { "definitions": {
"network.FailResponseHeader": {
"type": "object",
"properties": {
"err_msg": {
"type": "string"
},
"status": {
"type": "integer",
"example": 400
}
}
},
"network.FailureResponse": { "network.FailureResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"header": { "code": {
"$ref": "#/definitions/network.FailResponseHeader" "type": "integer",
"example": 500
}, },
"payload": { "msg": {
"type": "object", "type": "string",
"additionalProperties": true "example": "failed to get recommend data from redis"
}
}
},
"network.SuccessResponse": {
"type": "object",
"properties": {
"header": {
"$ref": "#/definitions/network.SuccessResponseHeader"
}, },
"payload": { "payload": {
"type": "object", "type": "object",
@ -85,15 +113,57 @@
} }
} }
}, },
"network.SuccessResponseHeader": { "network.MeasurementRecommendPayload": {
"type": "object", "type": "object",
"properties": { "properties": {
"err_msg": { "input": {
"type": "string",
"example": "transformfeeder1_220."
},
"offset": {
"type": "integer",
"example": 20
},
"recommended_list": {
"type": "array",
"items": {
"type": "string" "type": "string"
}, },
"status": { "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", "type": "integer",
"example": 200 "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: definitions:
network.FailResponseHeader:
properties:
err_msg:
type: string
status:
example: 400
type: integer
type: object
network.FailureResponse: network.FailureResponse:
properties: properties:
header: code:
$ref: '#/definitions/network.FailResponseHeader' example: 500
payload: type: integer
additionalProperties: true msg:
type: object example: failed to get recommend data from redis
type: object type: string
network.SuccessResponse:
properties:
header:
$ref: '#/definitions/network.SuccessResponseHeader'
payload: payload:
additionalProperties: additionalProperties:
type: string type: string
@ -26,17 +15,79 @@ definitions:
key: value key: value
type: object type: object
type: object type: object
network.SuccessResponseHeader: network.MeasurementRecommendPayload:
properties: properties:
err_msg: input:
example: transformfeeder1_220.
type: string 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 example: 200
type: integer type: integer
msg:
example: success
type: string
payload:
additionalProperties:
type: string
example:
key: value
type: object type: object
type: object
host: localhost:8080
info: 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: 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}: /model/diagram_load/{page_id}:
get: get:
consumes: consumes:

View File

@ -2,7 +2,6 @@
package handler package handler
import ( import (
"fmt"
"net/http" "net/http"
"modelRT/logger" "modelRT/logger"
@ -13,6 +12,16 @@ import (
) )
// MeasurementRecommendHandler define measurement recommend API // 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) { func MeasurementRecommendHandler(c *gin.Context) {
var request network.MeasurementRecommendRequest var request network.MeasurementRecommendRequest
@ -26,8 +35,6 @@ func MeasurementRecommendHandler(c *gin.Context) {
} }
recommends, isFuzzy, err := model.RedisSearchRecommend(c, request.Input) recommends, isFuzzy, err := model.RedisSearchRecommend(c, request.Input)
// TODO delete debug info
fmt.Printf("recommends in handler:%+v\n", recommends)
if err != nil { if err != nil {
logger.Error(c, "failed to get recommend data from redis", "input", request.Input, "error", err) logger.Error(c, "failed to get recommend data from redis", "input", request.Input, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{ c.JSON(http.StatusOK, network.FailureResponse{
@ -40,8 +47,6 @@ func MeasurementRecommendHandler(c *gin.Context) {
return return
} }
fmt.Printf("isFuzzy:%v\n", isFuzzy)
var finalOffset int var finalOffset int
if isFuzzy { if isFuzzy {
var maxOffset int var maxOffset int
@ -63,15 +68,11 @@ func MeasurementRecommendHandler(c *gin.Context) {
finalOffset = minOffset finalOffset = minOffset
} }
fmt.Printf("finalOffset:%v\n", finalOffset)
resultRecommends := make([]string, 0, len(recommends)) resultRecommends := make([]string, 0, len(recommends))
seen := make(map[string]struct{}) seen := make(map[string]struct{})
for _, recommend := range recommends { for _, recommend := range recommends {
recommendTerm := recommend[finalOffset:] recommendTerm := recommend[finalOffset:]
fmt.Printf("resultRecommend:%s\n", recommendTerm)
fmt.Printf("len of resultRecommend:%d\n", len(recommendTerm))
if len(recommendTerm) != 0 { if len(recommendTerm) != 0 {
if _, exists := seen[recommendTerm]; !exists { if _, exists := seen[recommendTerm]; !exists {
seen[recommendTerm] = struct{}{} seen[recommendTerm] = struct{}{}

16
main.go
View File

@ -53,6 +53,22 @@ var (
) )
// TODO 使用 wire 依赖注入管理 DVIE 面板注册的 panel // 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() { func main() {
flag.Parse() flag.Parse()
ctx := context.TODO() ctx := context.TODO()

View File

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

View File

@ -3,11 +3,11 @@ package network
// MeasurementGetRequest defines the request payload for getting an measurement // MeasurementGetRequest defines the request payload for getting an measurement
type MeasurementGetRequest struct { type MeasurementGetRequest struct {
MeasurementID int64 `json:"measurement_id"` MeasurementID int64 `json:"measurement_id" example:"1001"`
MeasurementToken string `json:"token"` MeasurementToken string `json:"token" example:"some-token"`
} }
// MeasurementRecommendRequest defines the request payload for an measurement recommend // MeasurementRecommendRequest defines the request payload for an measurement recommend
type MeasurementRecommendRequest struct { 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 define struct of network operation
package network package network
// FailureResponse define struct of standard failure API response format
type FailureResponse struct { type FailureResponse struct {
Code int `json:"code" example:"200"` Code int `json:"code" example:"500"`
Msg string `json:"msg"` Msg string `json:"msg" example:"failed to get recommend data from redis"`
PayLoad map[string]interface{} `json:"payload" swaggertype:"object,string" example:"key:value"` PayLoad map[string]interface{} `json:"payload" swaggertype:"object,string" example:"key:value"`
} }
// SuccessResponse define struct of standard successful API response format
type SuccessResponse struct { type SuccessResponse struct {
Code int `json:"code" example:"200"` 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"` 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\"]"`
}