85a5c2c1e7
检索从向量单路升级为混合:向量(Milvus) + 全文(Bleve BM25) → RRF 融合 → 可选 rerank(DashScope gte-rerank)。 - rag/bleve.go: Bleve 全文索引(内存,随 ingest 写入;kb 过滤);ingest 同步写 Milvus+Bleve - rag/fuse.go: RRF(Reciprocal Rank Fusion, k=60, 按文本去重)融合多路排序 - rag/rerank.go: DashScope gte-rerank 客户端(可选,env 配置,失败降级 RRF) - rag/rag.go: Search 改混合(向量+全文→RRF→可选rerank→topK);main 读 RERANK_* env - 验证: 全模块 build✓ + e2e PASS; live——入库写双索引;查'NATS'→全文精确命中#1+向量 →RRF NATS 排首(向量=4 全文=1);接 DashScope gte-rerank(百炼 key 有权限)→relevance score 0.19 真重排;retriever 节点端到端→DeepSeek 答 Milvus - 边界: Neo4j 图路(GraphRAG,需实体抽取)推迟;Bleve 内存索引重启重建;rerank 走 env (TODO 同 embedding 搬控制面 kind=rerank) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
79 lines
1.8 KiB
Go
79 lines
1.8 KiB
Go
package rag
|
|
|
|
import (
|
|
"fmt"
|
|
"hash/fnv"
|
|
"log"
|
|
|
|
"github.com/blevesearch/bleve/v2"
|
|
"github.com/blevesearch/bleve/v2/search/query"
|
|
)
|
|
|
|
// bleveStore 是全文(BM25)检索路。内存索引:随 ingest 写入,进程重启重建。
|
|
// 真实生产应落盘(bleve.New(path,...));此处内存优先求简。
|
|
type bleveStore struct {
|
|
idx bleve.Index
|
|
}
|
|
|
|
func openBleve() *bleveStore {
|
|
idx, err := bleve.NewMemOnly(bleve.NewIndexMapping())
|
|
if err != nil {
|
|
log.Printf("[rag] bleve 初始化失败,全文路降级: %v", err)
|
|
return &bleveStore{}
|
|
}
|
|
return &bleveStore{idx: idx}
|
|
}
|
|
|
|
func (b *bleveStore) ready() bool { return b != nil && b.idx != nil }
|
|
|
|
// index 把 (kb, texts) 写入全文索引(按 kb+文本哈希做幂等 ID)。
|
|
func (b *bleveStore) index(kb string, texts []string) error {
|
|
if !b.ready() {
|
|
return nil
|
|
}
|
|
batch := b.idx.NewBatch()
|
|
for _, t := range texts {
|
|
id := fmt.Sprintf("%s:%x", kb, fnvHash(t))
|
|
if err := batch.Index(id, map[string]any{"text": t, "kb": kb}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return b.idx.Batch(batch)
|
|
}
|
|
|
|
// search 全文检索(可按 kb 过滤),返回 BM25 排序的命中。
|
|
func (b *bleveStore) search(kb, q string, topK int) []Hit {
|
|
if !b.ready() || q == "" {
|
|
return nil
|
|
}
|
|
mq := bleve.NewMatchQuery(q)
|
|
mq.SetField("text")
|
|
var qy query.Query = mq
|
|
if kb != "" {
|
|
tq := bleve.NewTermQuery(kb)
|
|
tq.SetField("kb")
|
|
qy = bleve.NewConjunctionQuery(mq, tq)
|
|
}
|
|
req := bleve.NewSearchRequest(qy)
|
|
req.Size = topK
|
|
req.Fields = []string{"text"}
|
|
res, err := b.idx.Search(req)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var hits []Hit
|
|
for _, h := range res.Hits {
|
|
text, _ := h.Fields["text"].(string)
|
|
if text != "" {
|
|
hits = append(hits, Hit{Text: text, Score: float32(h.Score)})
|
|
}
|
|
}
|
|
return hits
|
|
}
|
|
|
|
func fnvHash(s string) uint64 {
|
|
h := fnv.New64a()
|
|
_, _ = h.Write([]byte(s))
|
|
return h.Sum64()
|
|
}
|