G1: Recall 管线完整化 — 搜索缓存 + 双向 BFS 导航

改动:
- G1.3: graph/navigate API 新增 source+target 参数,调用 NavigateBiDir 双向 BFS
- G1.4: recall pipeline 新增 Step 0 搜索缓存检查 + Step 6.5 缓存写入
  缓存 TTL 1h,相同 query+namespace 跳过完整管线直接返回
- MMR 多样性去重(G1.1)+ 多跳图谱扩展(G1.2)已在上次提交中存在

测试:双向 BFS 返回 9 条路径,bidirectional=true;缓存命中测试通过
This commit is contained in:
xiaowei 2026-05-30 02:03:01 +08:00
parent f6f4c832fb
commit 4541b0165a
2 changed files with 42 additions and 4 deletions

View File

@ -65,9 +65,14 @@ func (ga *GraphAPI) Query(w http.ResponseWriter, r *http.Request) {
}
// POST /api/v1/graph/navigate
// 支持两种模式:
// 1. 单实体 BFS: {"entity": "...", "max_hops": 2}
// 2. 双向 BFS: {"source": "...", "target": "...", "max_hops": 3}(设计文档 §2.5.4
func (ga *GraphAPI) Navigate(w http.ResponseWriter, r *http.Request) {
var req struct {
Entity string `json:"entity"`
Source string `json:"source"`
Target string `json:"target"`
MaxHops int `json:"max_hops"`
Namespace string `json:"namespace"`
}
@ -75,14 +80,31 @@ func (ga *GraphAPI) Navigate(w http.ResponseWriter, r *http.Request) {
respondError(w, 400, "invalid body")
return
}
if req.Entity == "" {
respondError(w, 400, "entity required")
return
}
if req.MaxHops <= 0 {
req.MaxHops = 2
}
// 模式 2: 双向 BFSsource + target
if req.Source != "" && req.Target != "" {
source := normalizeEntity(req.Source)
target := normalizeEntity(req.Target)
paths, err := ga.Graph.NavigateBiDir(source, target, req.MaxHops, req.Namespace)
if err != nil {
respondError(w, 500, "navigate failed: "+err.Error())
return
}
respond(w, 200, map[string]interface{}{
"paths": paths, "source": req.Source, "target": req.Target,
"bidirectional": true, "count": len(paths),
})
return
}
// 模式 1: 单实体 BFS兼容旧格式
if req.Entity == "" {
respondError(w, 400, "entity (or source+target) required")
return
}
entity := normalizeEntity(req.Entity)
paths, err := ga.Graph.Navigate(entity, req.MaxHops, req.Namespace)
if err != nil {

View File

@ -2,6 +2,7 @@
package storage
import (
"encoding/json"
"fmt"
"math"
"time"
@ -50,6 +51,16 @@ func (p *RecallPipeline) Recall(query, namespace string, topK int, diversity flo
topK = 10
}
// Step 0: 搜索缓存§2.6 — 相同 query hash → TTL 1h → 命中直接返回)
if SearchCacheInstance != nil {
if cached, ok := SearchCacheInstance.Get(query, namespace); ok {
var results []models.RecallResult
if err := json.Unmarshal(cached, &results); err == nil {
return results, nil
}
}
}
// Step 1: Encode query
queryVec, err := p.embedder.EncodeSingle(query)
if err != nil {
@ -158,6 +169,11 @@ func (p *RecallPipeline) Recall(query, namespace string, topK int, diversity flo
}
results = deduped
// Step 6.5: 写搜索缓存§2.6 — TTL 1h下次相同 query 直接返回)
if SearchCacheInstance != nil {
SearchCacheInstance.Set(query, namespace, results)
}
// Step 7: 预取推送CO_OCCURS 权重 > 0.6 的配套记忆 → WebSocket
if p.prefetch != nil {
// 使用 CO_OCCURS 追踪器收集真实预取候选项