10ac5a5277
Obsidian 化继续:笔记能编辑/新建,文档间 [[双链]] 连成可点关系图。
按 doc 重索引(编辑不重复累积):
- Milvus 加 doc 字段(旧 schema 自动重建);insert 带 doc;deleteDoc(kb,doc) 重入库前清旧块。
- Bleve 索引 id 含 doc + deleteDoc 按 kb+doc 清旧块。
- rag.Ingest(kb, doc, text):写入前按 doc 删旧块再写(Neo4j MERGE 仍幂等,附加式)。
- kb_ingest 工具加 doc 参数;gateway runIngest 把 doc 透传,forceDoc 支持编辑保持笔记名稳定。
编辑/新建:
- gateway POST /kb/note {kb,name,content}:落库 + 以 name 为 doc 重入库(替换旧块,搜索/图谱同步)。
- 前端 VaultPanel:阅读/编辑切换(textarea 预填原文,保存调 saveNote)、新建笔记、乐观更新。
笔记关系图:
- GraphView 加 onNode(节点可点);VaultPanel 阅读/关系图切换,关系图 = 文档间 [[双链]] 三元组
力导向(点节点跳转该笔记)。
验证:curl 编辑 笔记B → 检索只返编辑后内容(旧块已清,不重复)。Preview:关系图渲染
笔记B—链接→项目A概述/模块X 且节点可点;编辑器预填原文可改可存。tsc+vite+后端 build 通过;重建 .app。
注:Milvus 加 doc 字段会触发集合重建(旧向量丢,文库原文在 PG 可重灌);Neo4j 图谱按附加式合并,
编辑删除的实体不会自动消失(图谱倾向增长)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
101 lines
2.4 KiB
Go
101 lines
2.4 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, doc, texts) 写入全文索引(id 含 kb+doc+文本哈希,幂等)。
|
|
func (b *bleveStore) index(kb, doc string, texts []string) error {
|
|
if !b.ready() {
|
|
return nil
|
|
}
|
|
batch := b.idx.NewBatch()
|
|
for _, t := range texts {
|
|
id := fmt.Sprintf("%s:%s:%x", kb, doc, fnvHash(t))
|
|
if err := batch.Index(id, map[string]any{"text": t, "kb": kb, "doc": doc}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return b.idx.Batch(batch)
|
|
}
|
|
|
|
// deleteDoc 删除某 (kb, doc) 的全部全文块(笔记重入库前清旧块)。
|
|
func (b *bleveStore) deleteDoc(kb, doc string) {
|
|
if !b.ready() || doc == "" {
|
|
return
|
|
}
|
|
kq := bleve.NewTermQuery(kb)
|
|
kq.SetField("kb")
|
|
dq := bleve.NewTermQuery(doc)
|
|
dq.SetField("doc")
|
|
req := bleve.NewSearchRequest(bleve.NewConjunctionQuery(kq, dq))
|
|
req.Size = 1000
|
|
res, err := b.idx.Search(req)
|
|
if err != nil {
|
|
return
|
|
}
|
|
batch := b.idx.NewBatch()
|
|
for _, h := range res.Hits {
|
|
batch.Delete(h.ID)
|
|
}
|
|
_ = 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()
|
|
}
|