mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-12 11:43:39 +08:00
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:
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
239
internal/engine/infinity/document.go
Normal file
239
internal/engine/infinity/document.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user