Refine handling of POST /api/v1/datasets/search in GO (#15583)

### What problem does this PR solve?

Refine handling of POST /api/v1/datasets/search in GO

### Type of change

- [x] Refactoring
This commit is contained in:
qinling0210
2026-06-08 11:49:37 +08:00
committed by GitHub
parent 074c331cdf
commit c960dc2a4c
70 changed files with 8580 additions and 1915 deletions

View File

@@ -6,6 +6,13 @@ import (
"strings"
)
func tokenTypeDescription(t int, tok Token) string {
if tok.Type == t && tok.Value != "" {
return fmt.Sprintf("%s %q", tokenTypeToString(t), tok.Value)
}
return tokenTypeToString(t)
}
// Command parsers
func (p *Parser) parseLogout() (*Command, error) {
cmd := NewCommand("logout")
@@ -138,8 +145,6 @@ func (p *Parser) parseListCommand() (*Command, error) {
return p.parseListDatasets()
case TokenDocuments:
return p.parseListDatasetDocuments()
case TokenMetadata:
return p.parseListMetadata()
case TokenAgents:
return p.parseListAgents()
case TokenTokens:
@@ -210,7 +215,7 @@ func (p *Parser) parseListDatasetDocuments() (*Command, error) {
return cmd, nil
}
func (p *Parser) parseListMetadata() (*Command, error) {
func (p *Parser) parseGetMetadata() (*Command, error) {
p.nextToken() // consume METADATA
if p.curToken.Type != TokenOf {
@@ -233,6 +238,10 @@ func (p *Parser) parseListMetadata() (*Command, error) {
datasetNames = append(datasetNames, name)
p.nextToken()
if p.curToken.Type == TokenComma {
return nil, fmt.Errorf("syntax error: dataset names must be space-separated, not comma-separated (got %q after %q)", "'", name)
}
// Stop at semicolon or non-quoted (dataset name must be quoted)
if p.curToken.Type == TokenSemicolon {
break
@@ -243,7 +252,7 @@ func (p *Parser) parseListMetadata() (*Command, error) {
}
}
cmd := NewCommand("list_metadata")
cmd := NewCommand("get_metadata")
cmd.Params["dataset_names"] = datasetNames
// Semicolon is optional
@@ -2379,6 +2388,27 @@ func (p *Parser) parseInsertMetadataFromFile() (*Command, error) {
func (p *Parser) parseSearchCommand() (*Command, error) {
p.nextToken() // consume SEARCH
// Handle help flag: -h / --help. The lexer tokenizes each leading
// `-` as a separate `TokenDash` and then the rest of the flag name
// (e.g. "help") as a `TokenIdentifier`. We collect any leading
// dashes before checking the identifier value. Short-circuit with
// a dedicated command type so the dispatcher can print the search
// usage instead of erroring out on the missing question.
if p.curToken.Type == TokenDash {
dashCount := 0
for p.curToken.Type == TokenDash {
dashCount++
p.nextToken()
}
if dashCount > 0 && p.curToken.Type == TokenIdentifier {
switch strings.ToLower(p.curToken.Value) {
case "h", "help":
return NewCommand("search_help"), nil
}
}
return nil, fmt.Errorf("expected quoted string or identifier")
}
var err error
var question string
if p.curToken.Type == TokenQuotedString {
@@ -2415,7 +2445,119 @@ func (p *Parser) parseSearchCommand() (*Command, error) {
cmd.Params["datasets"] = datasets
p.nextToken()
// Semicolon is optional for UNSET TOKEN
// Parse optional WITH clause for additional parameters
if p.curToken.Type == TokenWith || (p.curToken.Type == TokenIdentifier && strings.ToLower(p.curToken.Value) == "with") {
if p.curToken.Type == TokenWith {
p.nextToken()
} else {
p.nextToken() // skip "with" identifier
}
for p.curToken.Type != TokenEOF && p.curToken.Type != TokenSemicolon {
if p.curToken.Type == TokenComma {
return nil, fmt.Errorf("syntax error: WITH options must be space-separated, not comma-separated")
}
// Parse parameter name
if p.curToken.Type != TokenIdentifier {
break
}
paramName := strings.ToLower(p.curToken.Value)
p.nextToken()
// Parse parameter value
var paramValue interface{}
valueToken := p.curToken.Type
var valueErr error
switch p.curToken.Type {
case TokenInteger:
paramValue, valueErr = p.parseNumber()
if valueErr != nil {
return nil, valueErr
}
p.nextToken() // step past the integer
case TokenFloat:
paramValue, valueErr = p.parseFloat()
if valueErr != nil {
return nil, valueErr
}
p.nextToken() // step past the float
case TokenQuotedString:
paramValue, valueErr = p.parseQuotedString()
if valueErr != nil {
return nil, valueErr
}
p.nextToken() // step past the closing quote
case TokenIdentifier:
// Bare identifiers are only meaningful for the
// boolean keys (keyword / use_kg = true|false);
// everything else rejects them.
paramValue = p.curToken.Value
p.nextToken()
case TokenLBracket:
// List value: parsed inside the switch below by
// cross_languages / doc_ids. No value is captured here.
paramValue = nil
default:
// EOF, ';', or any other non-value token: the option
// is missing a value, which is a hard error rather
// than a silent drop.
return nil, fmt.Errorf("WITH option %q is missing a value", paramName)
}
switch paramName {
case "top_k", "page_size", "page":
if valueToken != TokenInteger {
return nil, fmt.Errorf("WITH option %q must be an integer, got %s", paramName, tokenTypeDescription(valueToken, p.curToken))
}
cmd.Params[paramName] = paramValue
case "similarity_threshold", "vector_similarity_weight":
switch n := paramValue.(type) {
case int:
cmd.Params[paramName] = float64(n)
case float64:
cmd.Params[paramName] = n
default:
return nil, fmt.Errorf("WITH option %q must be a number, got %s", paramName, tokenTypeDescription(valueToken, p.curToken))
}
case "keyword", "use_kg":
s, ok := paramValue.(string)
if !ok {
return nil, fmt.Errorf("WITH option %q must be true or false, got %s", paramName, tokenTypeDescription(valueToken, p.curToken))
}
switch strings.ToLower(s) {
case "true":
cmd.Params[paramName] = true
case "false":
cmd.Params[paramName] = false
default:
return nil, fmt.Errorf("WITH option %q must be true or false, got %q", paramName, s)
}
case "rerank_id", "tenant_rerank_id", "search_id", "meta_data_filter":
if valueToken != TokenQuotedString {
return nil, fmt.Errorf("WITH option %q must be a quoted string, got %s", paramName, tokenTypeDescription(valueToken, p.curToken))
}
// meta_data_filter JSON string is decoded into a map in
// the SearchOnDatasets handler; parser stores the raw
// string so the handler can surface a clean error on
// invalid JSON.
cmd.Params[paramName] = paramValue
case "cross_languages", "doc_ids":
if p.curToken.Type != TokenLBracket {
return nil, fmt.Errorf("WITH option %q must be a list, e.g. %q ['a', 'b']", paramName, paramName)
}
list, err := p.parseQuotedStringList()
if err != nil {
return nil, err
}
cmd.Params[paramName] = list
default:
return nil, fmt.Errorf("unknown WITH option %q", paramName)
}
}
}
// Semicolon is optional
if p.curToken.Type == TokenSemicolon {
p.nextToken()
}
@@ -3599,13 +3741,16 @@ func (p *Parser) parseUnsetCommand() (*Command, error) {
return NewCommand("unset_token"), nil
}
// parseGetCommand parses: GET CHUNK 'chunk_id'
// parseGetCommand parses: GET CHUNK or GET METADATA
func (p *Parser) parseGetCommand() (*Command, error) {
p.nextToken() // consume GET
if p.curToken.Type == TokenChunk {
return p.parseGetChunk()
}
if p.curToken.Type == TokenMetadata {
return p.parseGetMetadata()
}
return nil, fmt.Errorf("unknown GET target: %s", p.curToken.Value)
}