Files
Agent-System-Backend/internal/store/vectors.go
T
weichen 4fdbfbc9a2 Add the vector RAG pipeline: paragraph chunker
Details (original message):
Add the vector RAG pipeline: paragraph chunker, OpenAI-compatible embeddings and rerank clients, dual-tier vector store with cosine search, and RRF hybrid retrieval
2026-09-30 02:39:50 +00:00

113 lines
3.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package store
import (
"context"
"encoding/binary"
"math"
core "agent-system-core"
)
// ---- 向量索引(4.12 向量管线)----
// 存储形态双档一致:BLOB(little-endian float32 数组),检索为进程内余弦扫描。
// 规模注记:当前实现面向 ≤ 数万块/实例;PG 档 pgvector+HNSW 是既定优化路径
//(换实现不动管线与契约,见设计 D13)。
func encodeVector(vec []float32) []byte {
buf := make([]byte, 4*len(vec))
for i, v := range vec {
binary.LittleEndian.PutUint32(buf[i*4:], math.Float32bits(v))
}
return buf
}
func decodeVector(blob []byte) []float32 {
n := len(blob) / 4
out := make([]float32, n)
for i := 0; i < n; i++ {
out[i] = math.Float32frombits(binary.LittleEndian.Uint32(blob[i*4:]))
}
return out
}
// cosine 余弦相似度(零向量返回 0)。
func cosine(a, b []float32) float64 {
if len(a) != len(b) {
return 0
}
var dot, na, nb float64
for i := range a {
dot += float64(a[i]) * float64(b[i])
na += float64(a[i]) * float64(a[i])
nb += float64(b[i]) * float64(b[i])
}
if na == 0 || nb == 0 {
return 0
}
return dot / (math.Sqrt(na) * math.Sqrt(nb))
}
func (s *Store) UpsertKnowledgeVector(ctx context.Context, owner, docID, ref string, vec []float32) error {
_, err := s.db.ExecContext(ctx, s.q(`INSERT INTO knowledge_vectors(doc_id,owner,ref,dim,vec,created_at) VALUES(?,?,?,?,?,?)
ON CONFLICT(doc_id) DO UPDATE SET owner=excluded.owner, ref=excluded.ref, dim=excluded.dim, vec=excluded.vec, created_at=excluded.created_at`),
docID, owner, ref, len(vec), encodeVector(vec), nowISO())
return err
}
func (s *Store) DeleteKnowledgeVectors(ctx context.Context, owner, ref string) error {
_, err := s.db.ExecContext(ctx, s.q(`DELETE FROM knowledge_vectors WHERE ref=? AND owner=?`), ref, owner)
return err
}
// SearchKnowledgeVectors 余弦扫描:候选 = 租户本人 + 共享(owner=”),dim 匹配,
// 再按 RetrievalQuery 的项目/来源过滤。
func (s *Store) SearchKnowledgeVectors(ctx context.Context, owner string, vec []float32, topK int, filter core.RetrievalQuery) ([]core.RetrievalHit, error) {
if topK <= 0 {
topK = 5
}
rows, err := s.db.QueryContext(ctx, s.q(`
SELECT k.id,k.owner,k.project_id,k.source,k.title,k.ref,k.chunk,k.created_at, v.vec
FROM knowledge_vectors v JOIN knowledge k ON k.id = v.doc_id
WHERE (v.owner=? OR v.owner='') AND v.dim=?`), owner, len(vec))
if err != nil {
return nil, err
}
defer rows.Close()
type cand struct {
hit core.RetrievalHit
sim float64
}
var cands []cand
for rows.Next() {
var d core.KnowledgeDoc
var own, ca string
var blob []byte
if err := rows.Scan(&d.ID, &own, &d.ProjectID, &d.Source, &d.Title, &d.Ref, &d.Chunk, &ca, &blob); err != nil {
return nil, err
}
d.Owner = own
d.CreatedAt, _ = parseTime(ca)
if !knowledgeVisible(owner, filter, d) {
continue
}
sim := cosine(vec, decodeVector(blob))
cands = append(cands, cand{hit: core.RetrievalHit{Doc: d, Score: sim, Kind: "vector"}, sim: sim})
}
if err := rows.Err(); err != nil {
return nil, err
}
// 简单选择排序取 TopK(候选规模有限;HNSW 归 PG 档优化)。
out := make([]core.RetrievalHit, 0, topK)
for len(out) < topK && len(cands) > 0 {
best := 0
for i := 1; i < len(cands); i++ {
if cands[i].sim > cands[best].sim {
best = i
}
}
out = append(out, cands[best].hit)
cands = append(cands[:best], cands[best+1:]...)
}
return out, nil
}