Files
ragflow/internal/service/graph/retrieval.go
Jin Hai 1880e65e99 Go: refactor (#16602)
### Summary

1. update doc
2. refactor route code

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-07-03 17:00:43 +08:00

265 lines
8.3 KiB
Go

package graph
import (
"context"
"encoding/json"
"fmt"
"ragflow/internal/common"
"ragflow/internal/engine"
"ragflow/internal/engine/types"
modelModule "ragflow/internal/entity/models"
)
// Retrieval performs a full knowledge graph retrieval and returns
// a synthetic chunk. Convenience wrapper around Pipeline.
func Retrieval(
ctx context.Context,
docEngine engine.DocEngine,
chatModel *modelModule.ChatModel,
embModel *modelModule.EmbeddingModel,
kbIDs []string,
tenantIDs []string,
question string,
) (map[string]interface{}, error) {
p := &Pipeline{
docEngine: docEngine,
chatModel: chatModel,
embModel: embModel,
kbIDs: kbIDs,
idxnms: makeIndexNames(tenantIDs),
question: question,
entSimThreshold: defaultSimThreshold,
relSimThreshold: defaultSimThreshold,
denseTopK: defaultDenseTopK,
entTopN: 6,
relTopN: 6,
commTopN: 1,
maxToken: 8196,
}
return p.Retrieval(ctx)
}
// makeIndexNames converts tenant IDs to search index names.
func makeIndexNames(tenantIDs []string) []string {
idxnms := make([]string, len(tenantIDs))
for i, tid := range tenantIDs {
idxnms[i] = indexName(tid)
}
return idxnms
}
// indexName builds the search index name from a tenant ID.
func indexName(tenantID string) string {
return "ragflow_" + tenantID
}
// searchTypeSamples searches for ty2ents data.
func searchTypeSamples(ctx context.Context, docEngine engine.DocEngine, idxnms []string, kbIDs []string) (map[string][]string, error) {
req := &types.SearchRequest{
IndexNames: idxnms,
KbIDs: kbIDs,
SelectFields: []string{"content_with_weight"},
Limit: 10000,
Filter: map[string]interface{}{"knowledge_graph_kwd": "ty2ents"},
}
result, err := docEngine.Search(ctx, req)
if err != nil {
return nil, err
}
typeMap := make(map[string][]string)
for _, chunk := range result.Chunks {
content, ok := chunk["content_with_weight"].(string)
if !ok || content == "" {
continue
}
var parsed map[string][]string
if err := json.Unmarshal([]byte(content), &parsed); err != nil {
continue
}
for typ, entities := range parsed {
typeMap[typ] = append(typeMap[typ], entities...)
}
}
return typeMap, nil
}
// searchCommunityContent searches for community reports and formats them.
func searchCommunityContent(ctx context.Context, docEngine engine.DocEngine, idxnms []string, kbIDs []string, scoredEnts []ScoredEntity, topN int, maxToken *int) string {
if maxToken == nil || len(scoredEnts) == 0 || *maxToken <= 0 {
return ""
}
entityNames := make([]string, len(scoredEnts))
for i, e := range scoredEnts {
entityNames[i] = e.Entity
}
req := &types.SearchRequest{
IndexNames: idxnms,
KbIDs: kbIDs,
SelectFields: []string{"docnm_kwd", "content_with_weight", "weight_flt", "entities_kwd"},
Limit: topN,
Filter: map[string]interface{}{"knowledge_graph_kwd": "community_report"},
OrderBy: (&types.OrderByExpr{}).Desc("weight_flt"),
}
if len(entityNames) > 0 {
filters := make([]interface{}, len(entityNames))
for i, name := range entityNames {
filters[i] = name
}
req.Filter["entities_kwd"] = filters
}
result, err := docEngine.Search(ctx, req)
if err != nil || len(result.Chunks) == 0 || *maxToken <= 0 {
return ""
}
var bld string
for idx, chunk := range result.Chunks {
title, _ := chunk["docnm_kwd"].(string)
raw, _ := chunk["content_with_weight"].(string)
if title == "" && raw == "" {
continue
}
report := raw
evidence := ""
var parsed map[string]interface{}
if err := json.Unmarshal([]byte(raw), &parsed); err == nil {
if r, ok := parsed["report"].(string); ok {
report = r
}
if e, ok := parsed["evidences"].(string); ok {
evidence = e
}
}
section := fmt.Sprintf("\n# %d. %s\n## Content\n%s\n## Evidences\n%s\n", idx+1, title, report, evidence)
tokens := NumTokensFromString(section)
if *maxToken-tokens <= 0 {
break
}
bld += section
*maxToken -= tokens
}
return bld
}
// entityFromChunk parses a single entity chunk into a KGEntity.
func entityFromChunk(name string, chunk map[string]interface{}) KGEntity {
e := KGEntity{}
if v, ok := chunk["_score"].(float64); ok {
e.Similarity = v
} else if v, ok := chunk["score"].(float64); ok {
e.Similarity = v
}
if v, ok := chunk["rank_flt"].(float64); ok {
e.PageRank = v
}
e.Description, _ = chunk["content_with_weight"].(string)
if raw, ok := chunk["n_hop_with_weight"].(string); ok && raw != "" {
var nhopData []struct {
Path []string `json:"path"`
Weights []float64 `json:"weights"`
}
if err := json.Unmarshal([]byte(raw), &nhopData); err == nil {
for _, item := range nhopData {
e.NhopEnts = append(e.NhopEnts, NhopEntity{
Path: item.Path,
Weights: item.Weights,
})
}
}
}
return e
}
// relationFromChunk parses a single relation chunk into a KGRelation.
func relationFromChunk(chunk map[string]interface{}) (Edge, KGRelation) {
r := KGRelation{}
r.Description, _ = chunk["content_with_weight"].(string)
if v, ok := chunk["_score"].(float64); ok {
r.Sim = v
} else if v, ok := chunk["score"].(float64); ok {
r.Sim = v
}
if v, ok := chunk["weight_int"].(float64); ok {
r.PageRank = float64(v)
} else if v, ok := chunk["weight_int"].(int); ok {
r.PageRank = float64(v)
}
from, _ := chunk["from_entity_kwd"].(string)
to, _ := chunk["to_entity_kwd"].(string)
return Edge{From: from, To: to}, r
}
// buildSearchExprs constructs MatchExprs for KG entity/relation search.
// When embModel is nil, returns text-only match expression.
// When embModel is non-nil, embeds the question and returns hybrid
// (text + dense + fusion) expressions for vector+keyword search.
func buildSearchExprs(embModel *modelModule.EmbeddingModel, matchText *types.MatchTextExpr, simThreshold float64, denseTopK int) []interface{} {
if embModel == nil || embModel.ModelDriver == nil {
return []interface{}{matchText}
}
embeddingConfig := &modelModule.EmbeddingConfig{Dimension: 0}
embeddings, err := embModel.ModelDriver.Embed(embModel.ModelName, []string{matchText.MatchingText}, embModel.APIConfig, embeddingConfig)
if err != nil || len(embeddings) == 0 {
return []interface{}{matchText}
}
denseExpr := buildMatchDenseExpr(embeddings[0].Embedding, denseTopK, simThreshold)
fusionExpr := buildFusionExpr(defaultTextWeight, defaultVectorWeight, matchText.TopN)
return []interface{}{matchText, denseExpr, fusionExpr}
}
// buildMatchDenseExpr constructs a MatchDenseExpr from an embedding vector.
func buildMatchDenseExpr(vector []float64, topN int, similarity float64) *types.MatchDenseExpr {
vectorColumnName := fmt.Sprintf("q_%d_vec", len(vector))
return &types.MatchDenseExpr{
VectorColumnName: vectorColumnName,
EmbeddingData: vector,
EmbeddingDataType: "float",
DistanceType: "cosine",
TopN: topN,
ExtraOptions: map[string]interface{}{"similarity": similarity},
}
}
// buildFusionExpr constructs a FusionExpr for weighted-sum hybrid search.
func buildFusionExpr(textWeight, vectorWeight float64, topN int) *types.FusionExpr {
return &types.FusionExpr{
Method: "weighted_sum",
TopN: topN,
FusionParams: map[string]interface{}{
"weights": fmt.Sprintf("%.2f,%.2f", textWeight, vectorWeight),
},
}
}
// queryRewrite attempts LLM-based query rewrite, falling back to raw question.
func queryRewrite(chatModel *modelModule.ChatModel, question string, ty2entsJSON string) (typeKeywords, entities []string) {
if question == "" {
return nil, nil
}
if chatModel != nil && chatModel.ModelName != nil && chatModel.APIConfig != nil {
prompt := common.BuildQueryRewritePrompt(question, ty2entsJSON)
messages := []modelModule.Message{
{Role: "system", Content: prompt},
{Role: "user", Content: "Output:"},
}
response, err := chatModel.ModelDriver.ChatWithMessages(*chatModel.ModelName, messages, chatModel.APIConfig, nil)
if err == nil && response != nil && response.Answer != nil {
result, parseErr := common.ParseQueryRewriteResponse(*response.Answer)
if parseErr == nil && result != nil {
return result.TypeKeywords, result.Entities
}
}
}
return nil, []string{question}
}
// Python alignment defaults
const (
defaultSimThreshold = 0.3
defaultDenseTopK = 1024
// defaultTextWeight / defaultVectorWeight are fusion weights for hybrid search (equal by default).
defaultTextWeight = 0.5
defaultVectorWeight = 0.5
)