mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-08 20:34:48 +08:00
### Summary 1. update doc 2. refactor route code --------- Signed-off-by: Jin Hai <haijin.chn@gmail.com>
287 lines
8.9 KiB
Go
287 lines
8.9 KiB
Go
//
|
|
// 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 graph
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
"go.uber.org/zap"
|
|
"ragflow/internal/common"
|
|
"ragflow/internal/engine"
|
|
"ragflow/internal/engine/types"
|
|
modelModule "ragflow/internal/entity/models"
|
|
)
|
|
|
|
// Pipeline encapsulates the knowledge graph retrieval pipeline.
|
|
// Matches Python: rag/graphrag/search.py::KGSearch
|
|
type Pipeline struct {
|
|
docEngine engine.DocEngine
|
|
chatModel *modelModule.ChatModel
|
|
embModel *modelModule.EmbeddingModel
|
|
kbIDs []string
|
|
idxnms []string
|
|
question string
|
|
|
|
// Configurable parameters (defaults match Python)
|
|
entSimThreshold float64
|
|
relSimThreshold float64
|
|
denseTopK int
|
|
entTopN int
|
|
relTopN int
|
|
commTopN int
|
|
maxToken int
|
|
}
|
|
|
|
// Option configures a Pipeline.
|
|
type Option func(*Pipeline)
|
|
|
|
// WithSimThreshold sets the similarity threshold for entity and relation search.
|
|
// Default: 0.3 (matches Python ent_sim_threshold, rel_sim_threshold).
|
|
func WithSimThreshold(v float64) Option {
|
|
return func(p *Pipeline) { p.entSimThreshold = v; p.relSimThreshold = v }
|
|
}
|
|
|
|
// WithDenseTopK sets the TopK for dense vector search.
|
|
// Default: 1024 (matches Python get_vector topk).
|
|
func WithDenseTopK(v int) Option {
|
|
return func(p *Pipeline) { p.denseTopK = v }
|
|
}
|
|
|
|
// NewPipeline creates a KG search pipeline with the given dependencies.
|
|
//
|
|
// docEngine: search engine backend
|
|
// kbIDs: knowledge base IDs to search
|
|
// tenantIDs: tenant IDs (converted to index names internally)
|
|
// question: user query string
|
|
// opts: optional configuration (WithSimThreshold, WithDenseTopK)
|
|
//
|
|
// chatModel and embModel should be set via WithChatModel/WithEmbModel setters
|
|
// or passed directly after construction.
|
|
func NewPipeline(
|
|
docEngine engine.DocEngine,
|
|
kbIDs []string,
|
|
tenantIDs []string,
|
|
question string,
|
|
opts ...Option,
|
|
) *Pipeline {
|
|
idxnms := make([]string, len(tenantIDs))
|
|
for i, tid := range tenantIDs {
|
|
idxnms[i] = indexName(tid)
|
|
}
|
|
p := &Pipeline{
|
|
docEngine: docEngine,
|
|
kbIDs: kbIDs,
|
|
idxnms: idxnms,
|
|
question: question,
|
|
|
|
entSimThreshold: defaultSimThreshold,
|
|
relSimThreshold: defaultSimThreshold,
|
|
denseTopK: defaultDenseTopK,
|
|
entTopN: 6,
|
|
relTopN: 6,
|
|
commTopN: 1,
|
|
maxToken: 8196,
|
|
}
|
|
for _, opt := range opts {
|
|
opt(p)
|
|
}
|
|
return p
|
|
}
|
|
|
|
// SetChatModel sets the chat model for LLM-based query rewrite.
|
|
func (p *Pipeline) SetChatModel(chatModel *modelModule.ChatModel) {
|
|
p.chatModel = chatModel
|
|
}
|
|
|
|
// SetEmbModel sets the embedding model for dense/hybrid search.
|
|
func (p *Pipeline) SetEmbModel(embModel *modelModule.EmbeddingModel) {
|
|
p.embModel = embModel
|
|
}
|
|
|
|
// Retrieval runs the full KG retrieval pipeline and returns a synthetic chunk.
|
|
func (p *Pipeline) Retrieval(ctx context.Context) (map[string]interface{}, error) {
|
|
// 1. Query rewrite via LLM, or fall back to raw question
|
|
ty2entsJSON := ""
|
|
if p.chatModel != nil {
|
|
typeSamples, err := searchTypeSamples(ctx, p.docEngine, p.idxnms, p.kbIDs)
|
|
if err != nil {
|
|
common.Warn("KG type samples search failed", zap.String("kbIDs", fmt.Sprint(p.kbIDs)))
|
|
}
|
|
if typeSamples == nil {
|
|
typeSamples = make(map[string][]string)
|
|
}
|
|
data, _ := json.Marshal(typeSamples)
|
|
ty2entsJSON = string(data)
|
|
}
|
|
typeKeywords, entities := queryRewrite(p.chatModel, p.question, ty2entsJSON)
|
|
|
|
// 2-4. Search entities, types, and relations in parallel (mutually independent)
|
|
var (
|
|
entsFromQuery map[string]*KGEntity
|
|
entsFromTypes map[string]struct{}
|
|
relsFromText map[Edge]*KGRelation
|
|
entsErr error
|
|
)
|
|
var wg sync.WaitGroup
|
|
wg.Add(3)
|
|
go func() {
|
|
defer wg.Done()
|
|
entsFromQuery, entsErr = p.searchEntities(ctx, entities)
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
entsFromTypes = p.searchEntityTypes(ctx, typeKeywords)
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
relsFromText = p.searchRelations(ctx, entities)
|
|
}()
|
|
wg.Wait()
|
|
if entsErr != nil {
|
|
return nil, entsErr
|
|
} // 5. N-hop analysis + score fusion
|
|
nhopPathes := AnalyzeNHopPaths(entsFromQuery)
|
|
DoubleHitBoost(entsFromQuery, entsFromTypes)
|
|
FuseRelationScores(relsFromText, entsFromTypes, nhopPathes)
|
|
|
|
// 6. Sort and trim
|
|
scoredEnts := SortAndTrimEntities(entsFromQuery, p.entTopN)
|
|
scoredRels := SortAndTrimRelations(relsFromText, p.relTopN)
|
|
|
|
// 7. Build KG content with token budget
|
|
entsRelsContent := BuildContent(scoredEnts, scoredRels, p.maxToken)
|
|
used := NumTokensFromString(entsRelsContent)
|
|
remaining := p.maxToken - used
|
|
// 8. Search community reports with remaining token budget
|
|
communityContent := searchCommunityContent(ctx, p.docEngine, p.idxnms, p.kbIDs, scoredEnts, p.commTopN, &remaining)
|
|
|
|
// 9. Build synthetic chunk
|
|
return map[string]interface{}{
|
|
"chunk_id": "",
|
|
"content_ltks": "",
|
|
"content_with_weight": entsRelsContent + communityContent,
|
|
"doc_id": "",
|
|
"docnm_kwd": "Related content in Knowledge Graph",
|
|
"kb_id": p.kbIDs,
|
|
"important_kwd": []string{},
|
|
"image_id": "",
|
|
"similarity": 1.0,
|
|
"vector_similarity": 1.0,
|
|
"term_similarity": 0,
|
|
"vector": []float64{},
|
|
"positions": []interface{}{},
|
|
}, nil
|
|
}
|
|
|
|
// searchEntities searches KG entities by keyword text and optional dense vector.
|
|
func (p *Pipeline) searchEntities(ctx context.Context, entities []string) (map[string]*KGEntity, error) {
|
|
entsReq := &types.SearchRequest{
|
|
IndexNames: p.idxnms,
|
|
KbIDs: p.kbIDs,
|
|
SelectFields: []string{"entity_kwd", "entity_type_kwd", "rank_flt", "content_with_weight", "n_hop_with_weight"},
|
|
Limit: 50,
|
|
Filter: map[string]interface{}{"knowledge_graph_kwd": "entity"},
|
|
}
|
|
if len(entities) > 0 {
|
|
entsReq.MatchExprs = buildSearchExprs(p.embModel, &types.MatchTextExpr{
|
|
Fields: []string{"entity_kwd^10", "content_ltks^2"},
|
|
MatchingText: strings.Join(entities, " "),
|
|
TopN: 50,
|
|
}, p.entSimThreshold, p.denseTopK)
|
|
}
|
|
entsResult, err := p.docEngine.Search(ctx, entsReq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("KG entity search failed: %w", err)
|
|
}
|
|
result := make(map[string]*KGEntity)
|
|
for _, chunk := range FilterChunksByScore(entsResult.Chunks, p.entSimThreshold) {
|
|
name, _ := chunk["entity_kwd"].(string)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
e := entityFromChunk(name, chunk)
|
|
result[name] = &e
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// searchEntityTypes searches KG entities by type keywords.
|
|
func (p *Pipeline) searchEntityTypes(ctx context.Context, typeKeywords []string) map[string]struct{} {
|
|
typesReq := &types.SearchRequest{
|
|
IndexNames: p.idxnms,
|
|
KbIDs: p.kbIDs,
|
|
SelectFields: []string{"entity_kwd", "entity_type_kwd"},
|
|
Limit: 10000,
|
|
Filter: map[string]interface{}{"knowledge_graph_kwd": "entity"},
|
|
}
|
|
if len(typeKeywords) > 0 {
|
|
typeFilters := make([]interface{}, len(typeKeywords))
|
|
for i, t := range typeKeywords {
|
|
typeFilters[i] = t
|
|
}
|
|
typesReq.Filter["entity_type_kwd"] = typeFilters
|
|
}
|
|
typesResult, err := p.docEngine.Search(ctx, typesReq)
|
|
result := make(map[string]struct{})
|
|
if err != nil {
|
|
common.Warn("KG types search failed", zap.String("kbIDs", fmt.Sprint(p.kbIDs)))
|
|
} else {
|
|
for _, chunk := range typesResult.Chunks {
|
|
if name, ok := chunk["entity_kwd"].(string); ok {
|
|
result[name] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// searchRelations searches KG relations by entity text and optional dense vector.
|
|
func (p *Pipeline) searchRelations(ctx context.Context, entities []string) map[Edge]*KGRelation {
|
|
relsReq := &types.SearchRequest{
|
|
IndexNames: p.idxnms,
|
|
KbIDs: p.kbIDs,
|
|
SelectFields: []string{"from_entity_kwd", "to_entity_kwd", "weight_int", "content_with_weight"},
|
|
Limit: 50,
|
|
Filter: map[string]interface{}{"knowledge_graph_kwd": "relation"},
|
|
}
|
|
if len(entities) > 0 {
|
|
relsReq.MatchExprs = buildSearchExprs(p.embModel, &types.MatchTextExpr{
|
|
Fields: []string{"content_ltks", "from_entity_kwd", "to_entity_kwd"},
|
|
MatchingText: strings.Join(entities, " "),
|
|
TopN: 50,
|
|
}, p.relSimThreshold, p.denseTopK)
|
|
}
|
|
relsResult, err := p.docEngine.Search(ctx, relsReq)
|
|
result := make(map[Edge]*KGRelation)
|
|
if err != nil {
|
|
common.Warn("KG relations search failed", zap.String("kbIDs", fmt.Sprint(p.kbIDs)))
|
|
} else {
|
|
for _, chunk := range FilterChunksByScore(relsResult.Chunks, p.relSimThreshold) {
|
|
edge, rel := relationFromChunk(chunk)
|
|
if edge.From == "" || edge.To == "" {
|
|
continue
|
|
}
|
|
result[edge] = &rel
|
|
}
|
|
}
|
|
return result
|
|
}
|