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() }