Feat: add skills space to context engine (#13908)

### What problem does this PR solve?

issue #13714

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
Yingfeng
2026-04-30 12:36:03 +08:00
committed by GitHub
parent bb3b99f0a5
commit 4ee0702aed
101 changed files with 19161 additions and 633 deletions

View File

@@ -129,10 +129,16 @@ type infinityEngine struct {
// NewEngine creates an Infinity engine
func NewEngine(cfg interface{}) (*infinityEngine, error) {
if cfg == nil {
return nil, fmt.Errorf("infinity config is nil, please check your configuration file for 'doc_engine.infinity' settings")
}
infConfig, ok := cfg.(*server.InfinityConfig)
if !ok {
return nil, fmt.Errorf("invalid infinity config type, expected *config.InfinityConfig")
}
if infConfig == nil {
return nil, fmt.Errorf("infinity config is nil, please check your configuration file for 'doc_engine.infinity' settings")
}
client, err := NewInfinityClient(infConfig)
if err != nil {
@@ -168,8 +174,8 @@ func NewEngine(cfg interface{}) (*infinityEngine, error) {
return engine, nil
}
// Type returns the engine type
func (e *infinityEngine) Type() string {
// GetType returns the engine type
func (e *infinityEngine) GetType() string {
return "infinity"
}

View File

@@ -312,3 +312,27 @@ func buildFilterFromCondition(condition map[string]interface{}, tableColumns map
}
return strings.Join(conditions, " AND ")
}
// columnExists checks if a column exists in the table
func (e *infinityEngine) columnExists(table *infinity.Table, columnName string) (bool, error) {
colsResp, err := table.ShowColumns()
if err != nil {
return false, err
}
result, ok := colsResp.(*infinity.QueryResult)
if !ok {
return false, fmt.Errorf("unexpected response type: %T", colsResp)
}
// ShowColumns returns a result set where Data contains arrays of column values
if nameArr, ok := result.Data["name"]; ok {
for i := 0; i < len(nameArr); i++ {
colName, _ := nameArr[i].(string)
if colName == columnName {
return true, nil
}
}
}
return false, nil
}

View File

@@ -36,14 +36,28 @@ import (
// CreateDataset creates a table in Infinity
// indexName is the table name prefix (e.g., "ragflow_<tenant_id>")
// The full table name is built as "{indexName}_{datasetID}"
// For skill index (datasetID="skill"), tableName is just indexName and uses skill_infinity_mapping.json
func (e *infinityEngine) CreateDataset(ctx context.Context, indexName, datasetID string, vectorSize int, parserID string) error {
vecSize := vectorSize
// Build full table name: {indexName}_{datasetID}
tableName := fmt.Sprintf("%s_%s", indexName, datasetID)
// Determine table name and mapping file based on index type
var tableName string
var mappingFile string
if datasetID == "skill" {
// Skill index: table name is just indexName (e.g., "skill_abc123_def456")
tableName = indexName
mappingFile = "skill_infinity_mapping.json"
logger.Info("Creating skill index table", zap.String("tableName", tableName), zap.String("mappingFile", mappingFile))
} else {
// Regular document index: table name is {indexName}_{datasetID}
tableName = fmt.Sprintf("%s_%s", indexName, datasetID)
mappingFile = e.mappingFileName
logger.Info("Creating regular index table", zap.String("tableName", tableName), zap.String("mappingFile", mappingFile))
}
// Use configured schema
fpMapping := filepath.Join(utility.GetProjectRoot(), "conf", e.mappingFileName)
fpMapping := filepath.Join(utility.GetProjectRoot(), "conf", mappingFile)
schemaData, err := os.ReadFile(fpMapping)
if err != nil {
@@ -61,54 +75,90 @@ func (e *infinityEngine) CreateDataset(ctx context.Context, indexName, datasetID
return fmt.Errorf("Failed to get database: %w", err)
}
// Determine vector column name
vectorColName := fmt.Sprintf("q_%d_vec", vecSize)
// Check if table already exists
exists, err := e.TableExists(ctx, tableName)
if err != nil {
return fmt.Errorf("Failed to check if table exists: %w", err)
}
var table *infinity.Table
if exists {
return fmt.Errorf("table '%s' already exists", tableName)
}
// Build column definitions (preserving JSON order)
var columns infinity.TableSchema
for _, fieldName := range schema.Keys {
fieldInfo := schema.Fields[fieldName]
col := infinity.ColumnDefinition{
Name: fieldName,
DataType: fieldInfo.Type,
Default: fieldInfo.Default,
// Comment: fieldInfo.Comment,
// Table exists, open it and check if vector column needs to be added
logger.Info("Table already exists, checking for vector column", zap.String("tableName", tableName))
table, err = db.GetTable(tableName)
if err != nil {
return fmt.Errorf("Failed to open existing table %s: %w", tableName, err)
}
columns = append(columns, &col)
}
// Add vector column
vectorColName := fmt.Sprintf("q_%d_vec", vecSize)
columns = append(columns, &infinity.ColumnDefinition{
Name: vectorColName,
DataType: fmt.Sprintf("vector,%d,float", vecSize),
})
// Check if vector column exists (for embedding model changes)
colExists, err := e.columnExists(table, vectorColName)
if err != nil {
logger.Warn("Failed to check column existence", zap.String("column", vectorColName), zap.Error(err))
}
// Add chunk_data column for table parser
if parserID == "table" {
// Add new vector column if it doesn't exist (handles embedding model change)
if !colExists {
logger.Info("Adding new vector column for embedding model change", zap.String("column", vectorColName), zap.Int("size", vecSize))
addColSchema := infinity.TableSchema{
&infinity.ColumnDefinition{
Name: vectorColName,
DataType: fmt.Sprintf("vector,%d,float", vecSize),
},
}
if _, err := table.AddColumns(addColSchema); err != nil {
logger.Error("Failed to add vector column "+vectorColName, err)
return fmt.Errorf("Failed to add vector column %s: %w", vectorColName, err)
}
logger.Info("Successfully added vector column", zap.String("column", vectorColName))
}
} else {
// Table doesn't exist, create it with vector column in the initial schema
logger.Info(fmt.Sprintf("Creating table with vector column: %s with dimension %d", vectorColName, vecSize))
// Build column definitions (preserving JSON order)
var columns infinity.TableSchema
for _, fieldName := range schema.Keys {
fieldInfo := schema.Fields[fieldName]
col := infinity.ColumnDefinition{
Name: fieldName,
DataType: fieldInfo.Type,
Default: fieldInfo.Default,
// Comment: fieldInfo.Comment,
}
columns = append(columns, &col)
}
// Add vector column
columns = append(columns, &infinity.ColumnDefinition{
Name: "chunk_data",
DataType: "json",
Default: "{}",
Name: vectorColName,
DataType: fmt.Sprintf("vector,%d,float", vecSize),
})
// Add chunk_data column for table parser
if parserID == "table" {
columns = append(columns, &infinity.ColumnDefinition{
Name: "chunk_data",
DataType: "json",
Default: "{}",
})
}
// Create table
table, err = db.CreateTable(tableName, columns, infinity.ConflictTypeIgnore)
if err != nil {
return fmt.Errorf("Failed to create table: %w", err)
}
logger.Debug("Infinity created table", zap.String("tableName", tableName))
}
// Create table
table, err := db.CreateTable(tableName, columns, infinity.ConflictTypeIgnore)
if err != nil {
return fmt.Errorf("Failed to create table: %w", err)
}
logger.Debug("Infinity created table", zap.String("tableName", tableName))
// Create HNSW index on vector column
// Create HNSW index on vector column with unique name based on vector size
// Use unique index name to avoid conflict when embedding model changes
vectorIndexName := fmt.Sprintf("q_%d_vec_idx", vecSize)
_, err = table.CreateIndex(
"q_vec_idx",
vectorIndexName,
infinity.NewIndexInfo(vectorColName, infinity.IndexTypeHnsw, map[string]string{
"M": "16",
"ef_construction": "50",
@@ -119,8 +169,9 @@ func (e *infinityEngine) CreateDataset(ctx context.Context, indexName, datasetID
"",
)
if err != nil {
return fmt.Errorf("Failed to create HNSW index: %w", err)
return fmt.Errorf("Failed to create HNSW index %s: %w", vectorIndexName, err)
}
logger.Info("Created vector index", zap.String("indexName", vectorIndexName), zap.String("column", vectorColName))
// Create full-text indexes for varchar fields with analyzers
for _, fieldName := range schema.Keys {

View File

@@ -0,0 +1,239 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package infinity
import (
"context"
"fmt"
"strings"
"go.uber.org/zap"
"ragflow/internal/logger"
)
// IndexDocument indexes a single document
// For skill index (tableName starts with "skill_"), uses InsertSkill
// For regular document index, returns not implemented error
func (e *infinityEngine) IndexDocument(ctx context.Context, tableName, docID string, doc interface{}) error {
// Check if this is a skill index
if strings.HasPrefix(tableName, "skill_") {
return e.InsertSkill(ctx, tableName, docID, doc)
}
return fmt.Errorf("infinity insert not implemented for regular documents: waiting for official Go SDK")
}
// InsertSkill inserts a skill document into skill index
// Auto-creates the table if it doesn't exist
func (e *infinityEngine) InsertSkill(ctx context.Context, tableName, docID string, doc interface{}) error {
db, err := e.client.conn.GetDatabase(e.client.dbName)
if err != nil {
return fmt.Errorf("failed to get database: %w", err)
}
table, err := db.GetTable(tableName)
if err != nil {
// Table doesn't exist, try to create it
errMsg := strings.ToLower(err.Error())
if !strings.Contains(errMsg, "not found") && !strings.Contains(errMsg, "doesn't exist") {
return fmt.Errorf("failed to get table %s: %w", tableName, err)
}
// Cannot auto-create skill table without knowing the vector dimension
// The table should be created by SkillIndexerService.EnsureIndex before calling this
return fmt.Errorf("skill table %s does not exist, please ensure index is initialized first", tableName)
}
// Transform doc to map
docMap, ok := doc.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid doc type, expected map[string]interface{}")
}
// Prepare insert data
insertDoc := make(map[string]interface{})
for k, v := range docMap {
insertDoc[k] = v
}
// Ensure skill_id is set (schema uses skill_id, not id)
insertDoc["skill_id"] = docID
// Delete existing document with same skill_id
// Escape single quotes to prevent filter injection
docIDEscaped := strings.ReplaceAll(docID, "'", "''")
filter := fmt.Sprintf("skill_id = '%s'", docIDEscaped)
delResp, delErr := table.Delete(filter)
if delErr != nil {
logger.Warn(fmt.Sprintf("Failed to delete existing skill document: %v", delErr))
} else if delResp.DeletedRows > 0 {
logger.Debug(fmt.Sprintf("Deleted %d existing skill document(s)", delResp.DeletedRows))
}
// Insert the document
_, err = table.Insert([]map[string]interface{}{insertDoc})
if err != nil {
return fmt.Errorf("failed to insert skill document into %s: %w", tableName, err)
}
return nil
}
// BulkIndex indexes documents in bulk
// For skill index (tableName starts with "skill_"), uses BulkInsertSkill
// For regular document index, returns not implemented error
func (e *infinityEngine) BulkIndex(ctx context.Context, tableName string, docs []interface{}) (interface{}, error) {
// Check if this is a skill index
if strings.HasPrefix(tableName, "skill_") {
inserted, err := e.BulkInsertSkill(ctx, tableName, docs)
return &BulkResponse{Inserted: inserted}, err
}
return nil, fmt.Errorf("infinity bulk insert not implemented for regular documents: waiting for official Go SDK")
}
// BulkInsertSkill inserts multiple skill documents in bulk with upsert semantics.
// For each document, deletes existing rows with the same skill_id before inserting,
// matching the behavior of InsertSkill. Creates shallow copies of input maps to
// avoid mutating caller data.
func (e *infinityEngine) BulkInsertSkill(ctx context.Context, tableName string, docs []interface{}) (int, error) {
db, err := e.client.conn.GetDatabase(e.client.dbName)
if err != nil {
return 0, fmt.Errorf("failed to get database: %w", err)
}
table, err := db.GetTable(tableName)
if err != nil {
return 0, fmt.Errorf("failed to get table %s: %w", tableName, err)
}
// Collect skill_ids for upsert and create shallow copies of docs
skillIDs := make([]string, 0, len(docs))
insertDocs := make([]map[string]interface{}, 0, len(docs))
for _, doc := range docs {
docMap, ok := doc.(map[string]interface{})
if !ok {
logger.Warn("Invalid doc type in bulk insert, expected map[string]interface{}")
continue
}
// Create shallow copy to avoid mutating caller's map
insertDoc := make(map[string]interface{})
for k, v := range docMap {
insertDoc[k] = v
}
// Ensure skill_id is set if id or skill_id exists in doc
var skillID string
if id, hasID := docMap["id"]; hasID {
skillID = fmt.Sprintf("%v", id)
insertDoc["skill_id"] = skillID
} else if sid, hasSkillID := docMap["skill_id"]; hasSkillID {
skillID = fmt.Sprintf("%v", sid)
}
if skillID != "" {
skillIDs = append(skillIDs, skillID)
}
insertDocs = append(insertDocs, insertDoc)
}
if len(insertDocs) == 0 {
logger.Warn("No valid documents to bulk insert", zap.String("tableName", tableName))
return 0, nil
}
// Upsert: delete existing documents with same skill_ids before inserting
for _, skillID := range skillIDs {
// Escape single quotes to prevent filter injection
docIDEscaped := strings.ReplaceAll(skillID, "'", "''")
filter := fmt.Sprintf("skill_id = '%s'", docIDEscaped)
delResp, delErr := table.Delete(filter)
if delErr != nil {
logger.Warn("Failed to delete existing skill document before bulk insert",
zap.String("tableName", tableName),
zap.String("skill_id", skillID),
zap.Error(delErr))
} else if delResp.DeletedRows > 0 {
logger.Debug("Deleted existing skill document before bulk insert",
zap.String("tableName", tableName),
zap.String("skill_id", skillID),
zap.Int64("deletedRows", delResp.DeletedRows))
}
}
// Insert the documents
_, err = table.Insert(insertDocs)
if err != nil {
return 0, fmt.Errorf("failed to bulk insert skill documents: %w", err)
}
logger.Debug("Bulk upserted skill documents",
zap.String("tableName", tableName),
zap.Int("count", len(insertDocs)),
zap.Int("skillIDs", len(skillIDs)))
return len(insertDocs), nil
}
// BulkResponse bulk operation response
type BulkResponse struct {
Inserted int
}
// GetDocument gets a document
func (e *infinityEngine) GetDocument(ctx context.Context, tableName, docID string) (interface{}, error) {
return nil, fmt.Errorf("infinity get document not implemented: waiting for official Go SDK")
}
// DeleteDocument deletes a document by ID
func (e *infinityEngine) DeleteDocument(ctx context.Context, tableName, docID string) error {
if tableName == "" {
return fmt.Errorf("table name cannot be empty")
}
if docID == "" {
return fmt.Errorf("document id cannot be empty")
}
db, err := e.client.conn.GetDatabase(e.client.dbName)
if err != nil {
return fmt.Errorf("failed to get database: %w", err)
}
table, err := db.GetTable(tableName)
if err != nil {
return fmt.Errorf("failed to get table: %w", err)
}
// Use filter to delete document by ID
// Skill index uses 'skill_id', regular indices use 'id'
idField := "id"
if strings.HasPrefix(tableName, "skill_") {
idField = "skill_id"
}
// Escape single quotes to prevent filter injection
docIDEscaped := strings.ReplaceAll(docID, "'", "''")
filter := fmt.Sprintf("%s = '%s'", idField, docIDEscaped)
resp, err := table.Delete(filter)
if err != nil {
return fmt.Errorf("failed to delete document: %w", err)
}
logger.Debug("Deleted document from Infinity",
zap.String("tableName", tableName),
zap.String("docID", docID),
zap.String("idField", idField),
zap.Int64("deletedRows", resp.DeletedRows))
return nil
}

View File

@@ -40,7 +40,7 @@ import (
// It supports three matching types: MatchTextExpr (full-text), MatchDenseExpr (vector), and FusionExpr (combined).
// If no match expressions are provided, Search relies solely on filter (e.g., doc_id, available_int) to find results.
func (e *infinityEngine) Search(ctx context.Context, req *types.SearchRequest) (*types.SearchResult, error) {
logger.Info("Search in Infinity started", zap.Any("indexNames", req.IndexNames))
logger.Debug("Search in Infinity started", zap.Any("indexNames", req.IndexNames))
if logger.IsDebugEnabled() {
// Format match expressions for logging
var matchExprsStr string
@@ -88,16 +88,27 @@ func (e *infinityEngine) Search(ctx context.Context, req *types.SearchRequest) (
}
isMetadataTable := false
isSkillIndex := false
for _, idx := range req.IndexNames {
if strings.HasPrefix(idx, "ragflow_doc_meta_") {
isMetadataTable = true
break
}
if strings.HasPrefix(idx, "skill_") {
isSkillIndex = true
break
}
}
var outputColumns []string
if isMetadataTable {
outputColumns = []string{"id", "kb_id", "meta_fields"}
} else if isSkillIndex {
outputColumns = []string{
"skill_id", "space_id", "folder_id", "name", "tags", "description", "content",
"version", "status", "create_time", "update_time",
}
outputColumns = convertSelectFields(outputColumns, true)
} else {
outputColumns = []string{
"id", "doc_id", "kb_id", "content_ltks", "content_with_weight",
@@ -119,12 +130,24 @@ func (e *infinityEngine) Search(ctx context.Context, req *types.SearchRequest) (
continue
}
switch e := expr.(type) {
case string:
if e != "" {
hasTextMatch = true
matchText = &types.MatchTextExpr{
MatchingText: e,
TopN: pageSize,
}
}
case *types.MatchTextExpr:
hasTextMatch = true
matchText = e
if e.MatchingText != "" {
hasTextMatch = true
matchText = e
}
case *types.MatchDenseExpr:
hasVectorMatch = true
matchDense = e
if len(e.EmbeddingData) > 0 {
hasVectorMatch = true
matchDense = e
}
}
}
}
@@ -132,14 +155,21 @@ func (e *infinityEngine) Search(ctx context.Context, req *types.SearchRequest) (
if hasTextMatch || hasVectorMatch {
if hasTextMatch {
outputColumns = append(outputColumns, "score()")
} else if hasVectorMatch {
}
// similarity() is only allowed by Infinity when there is ONLY MATCH VECTOR.
// When both text and vector matches exist (hybrid search with Fusion),
// only score() is valid — Fusion produces a unified SCORE column.
if hasVectorMatch && !hasTextMatch {
outputColumns = append(outputColumns, "similarity()")
}
if !slices.Contains(outputColumns, common.PAGERANK_FLD) {
outputColumns = append(outputColumns, common.PAGERANK_FLD)
}
if !slices.Contains(outputColumns, common.TAG_FLD) {
outputColumns = append(outputColumns, common.TAG_FLD)
// Skill index does not have pagerank_fea and tag_feas columns
if !isSkillIndex {
if !slices.Contains(outputColumns, common.PAGERANK_FLD) {
outputColumns = append(outputColumns, common.PAGERANK_FLD)
}
if !slices.Contains(outputColumns, common.TAG_FLD) {
outputColumns = append(outputColumns, common.TAG_FLD)
}
}
}
@@ -147,7 +177,7 @@ func (e *infinityEngine) Search(ctx context.Context, req *types.SearchRequest) (
outputColumns = append(outputColumns, "row_id()")
}
outputColumns = convertSelectFields(outputColumns)
outputColumns = convertSelectFields(outputColumns, isSkillIndex)
if hasVectorMatch && matchDense != nil && matchDense.VectorColumnName != "" {
outputColumns = append(outputColumns, matchDense.VectorColumnName)
}
@@ -167,11 +197,21 @@ func (e *infinityEngine) Search(ctx context.Context, req *types.SearchRequest) (
if req.Filter != nil {
if availInt, ok := req.Filter["available_int"]; ok {
filterParts = append(filterParts, fmt.Sprintf("available_int=%v", availInt))
} else if status, ok := req.Filter["status"]; ok {
filterParts = append(filterParts, fmt.Sprintf("status='%s'", status))
} else {
if isSkillIndex {
filterParts = append(filterParts, "status='1'")
} else {
filterParts = append(filterParts, "available_int=1")
}
}
} else {
if isSkillIndex {
filterParts = append(filterParts, "status='1'")
} else {
filterParts = append(filterParts, "available_int=1")
}
} else {
filterParts = append(filterParts, "available_int=1")
}
}
@@ -257,6 +297,13 @@ func (e *infinityEngine) Search(ctx context.Context, req *types.SearchRequest) (
var textFields []string
if matchText != nil && len(matchText.Fields) > 0 {
textFields = matchText.Fields
} else if isSkillIndex {
textFields = []string{
"name^10",
"tags^5",
"description^3",
"content^1",
}
} else {
textFields = []string{
"title_tks^10",
@@ -339,12 +386,16 @@ func (e *infinityEngine) Search(ctx context.Context, req *types.SearchRequest) (
vectorTopN = int(matchDense.TopN)
}
denseFilterStr := filterStr
if denseFilterStr == "" {
denseFilterStr := filterStr
if denseFilterStr == "" {
if isSkillIndex {
denseFilterStr = "status='1'"
} else {
denseFilterStr = "available_int=1"
}
}
if hasTextMatch {
if hasTextMatch && fusionExpr == nil {
fieldsStr := strings.Join(convertedFields, ",")
filterFulltext := fmt.Sprintf("filter_fulltext('%s', '%s')", fieldsStr, questionText)
denseFilterStr = fmt.Sprintf("(%s) AND %s", denseFilterStr, filterFulltext)
@@ -354,13 +405,11 @@ func (e *infinityEngine) Search(ctx context.Context, req *types.SearchRequest) (
"filter": denseFilterStr,
}
logger.Debug(fmt.Sprintf(
"MatchDenseExpr:\n"+
" field=%s\n"+
" topn=%d\n"+
" extra_options=%v",
fieldName, vectorTopN, extraOptions,
))
logger.Debug("MatchDense for hybrid search",
zap.String("fieldName", fieldName),
zap.String("distanceType", distanceType),
zap.Int("topN", vectorTopN),
zap.Bool("hasFusion", fusionExpr != nil))
table = table.MatchDense(fieldName, vectorData, dataType, distanceType, vectorTopN, extraOptions)
}
@@ -380,13 +429,11 @@ func (e *infinityEngine) Search(ctx context.Context, req *types.SearchRequest) (
fusionParams[k] = v
}
}
logger.Debug(fmt.Sprintf(
"FusionExpr:\n"+
" method=%s\n"+
" topn=%d\n"+
" fusion_params=%v",
fusionMethod, fusionTopK, fusionParams,
))
logger.Debug("Applying Fusion for hybrid search",
zap.String("method", fusionMethod),
zap.Int("topN", fusionTopK),
zap.Any("params", fusionParams))
table = table.Fusion(fusionMethod, fusionTopK, fusionParams)
}
@@ -422,6 +469,12 @@ func (e *infinityEngine) Search(ctx context.Context, req *types.SearchRequest) (
// Execute query
df, err := table.ToDataFrame()
if err != nil {
logger.Warn("Infinity query failed",
zap.String("tableName", tableName),
zap.Bool("hasTextMatch", hasTextMatch),
zap.Bool("hasVectorMatch", hasVectorMatch),
zap.Bool("hasFusion", fusionExpr != nil),
zap.Error(err))
continue
}
@@ -437,7 +490,19 @@ func (e *infinityEngine) Search(ctx context.Context, req *types.SearchRequest) (
}
// Apply field name mapping and row_id handling
GetFields(chunks, nil)
// Skill index uses different schema
// so we skip the document-specific field mappings
if !isSkillIndex {
GetFields(chunks, nil)
} else {
// For skill index, only handle ROW_ID -> row_id() mapping
for _, chunk := range chunks {
if val, ok := chunk["ROW_ID"]; ok {
chunk["row_id()"] = val
delete(chunk, "ROW_ID")
}
}
}
// Parse total_hits_count from ExtraInfo
var tableTotal int64
@@ -462,12 +527,19 @@ func (e *infinityEngine) Search(ctx context.Context, req *types.SearchRequest) (
if hasTextMatch || hasVectorMatch {
scoreColumn := ""
if hasTextMatch {
if hasTextMatch && hasVectorMatch {
scoreColumn = "SCORE"
} else if hasTextMatch {
scoreColumn = "SCORE"
} else if hasVectorMatch {
scoreColumn = "SIMILARITY"
}
allResults = calculateScores(allResults, scoreColumn)
pagerankField := common.PAGERANK_FLD
if isSkillIndex {
pagerankField = "" // Skill index has no pagerank field
}
allResults = calculateScores(allResults, scoreColumn, pagerankField)
allResults = sortByScore(allResults, len(allResults))
}
@@ -475,7 +547,7 @@ func (e *infinityEngine) Search(ctx context.Context, req *types.SearchRequest) (
allResults = allResults[:pageSize]
}
logger.Info("Search in Infinity completed", zap.Any("indexNames", req.IndexNames), zap.Int("returnedRows", len(allResults)), zap.Int64("totalHits", totalHits))
logger.Debug("Search in Infinity completed", zap.Int("returnedRows", len(allResults)), zap.Int64("totalHits", totalHits))
return &types.SearchResult{
Chunks: allResults,
@@ -483,9 +555,9 @@ func (e *infinityEngine) Search(ctx context.Context, req *types.SearchRequest) (
}, nil
}
// convertSelectFields converts RAG field names to Infinity column names for SELECT (output_columns).
// Example: docnm_kwd → docnm, content_ltks → content
func convertSelectFields(output []string) []string {
// convertSelectFields converts field names to Infinity format
// isSkillIndex indicates if this is a skill index (uses skill_id instead of id)
func convertSelectFields(output []string, isSkillIndex ...bool) []string {
fieldMapping := map[string]string{
"docnm_kwd": "docnm",
"title_tks": "docnm",
@@ -501,6 +573,11 @@ func convertSelectFields(output []string) []string {
"authors_sm_tks": "authors",
}
skillIndex := false
if len(isSkillIndex) > 0 {
skillIndex = isSkillIndex[0]
}
needEmptyCount := false
for i, field := range output {
if field == "important_kwd" {
@@ -522,15 +599,20 @@ func convertSelectFields(output []string) []string {
}
// Add id and empty count if needed
// For skill index, use skill_id instead of id
hasID := false
idField := "id"
if skillIndex {
idField = "skill_id"
}
for _, f := range result {
if f == "id" {
if f == idField {
hasID = true
break
}
}
if !hasID {
result = append([]string{"id"}, result...)
result = append([]string{idField}, result...)
}
if needEmptyCount {
@@ -540,8 +622,10 @@ func convertSelectFields(output []string) []string {
return result
}
// convertMatchingField converts RAG field names to Infinity full-text index names for MATCH expressions.
// Example: docnm_kwd → docnm@ft_docnm_rag_coarse, content_ltks → content@ft_content_rag_coarse
// convertMatchingField converts field names for matching
// For regular document indices: maps _tks/_kwd fields to column@index_name format
// For skill indices: maps raw field names to column@index_name format
// Infinity requires column@index_name when a column has multiple full-text indexes
func convertMatchingField(fieldWeightStr string) string {
// Split on ^ to get field name
parts := strings.Split(fieldWeightStr, "^")
@@ -562,6 +646,11 @@ func convertMatchingField(fieldWeightStr string) string {
"authors_tks": "authors@ft_authors_rag_coarse",
"authors_sm_tks": "authors@ft_authors_rag_fine",
"tag_kwd": "tag_kwd@ft_tag_kwd_whitespace__",
// Skill index fields
"name": "name@ft_name_rag_coarse",
"tags": "tags@ft_tags_rag_coarse",
"description": "description@ft_description_rag_coarse",
"content": "content@ft_content_rag_coarse",
}
if newField, ok := fieldMapping[field]; ok {
@@ -728,8 +817,8 @@ func equivalentConditionToStr(condition map[string]interface{}) string {
return strings.Join(cond, " AND ")
}
// calculateScores calculates _score = score_column + pagerank_fld
func calculateScores(chunks []map[string]interface{}, scoreColumn string) []map[string]interface{} {
// calculateScores calculates _score = score_column + pagerank
func calculateScores(chunks []map[string]interface{}, scoreColumn, pagerankField string) []map[string]interface{} {
for i := range chunks {
score := 0.0
if scoreVal, ok := chunks[i][scoreColumn]; ok {
@@ -737,9 +826,11 @@ func calculateScores(chunks []map[string]interface{}, scoreColumn string) []map[
score += f
}
}
if prVal, ok := chunks[i][common.PAGERANK_FLD]; ok {
if f, ok := utility.ToFloat64(prVal); ok {
score += f
if pagerankField != "" {
if prVal, ok := chunks[i][pagerankField]; ok {
if f, ok := utility.ToFloat64(prVal); ok {
score += f
}
}
}
chunks[i]["_score"] = score
@@ -1008,4 +1099,4 @@ func (e *infinityEngine) GetHighlight(chunks []map[string]interface{}, keywords
}
return result
}
}