diff --git a/go/internal/api/routes/graph.go b/go/internal/api/routes/graph.go index 69aef3d..ea61277 100644 --- a/go/internal/api/routes/graph.go +++ b/go/internal/api/routes/graph.go @@ -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: 双向 BFS(source + 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 { diff --git a/go/internal/storage/recall.go b/go/internal/storage/recall.go index ce9c361..5e838e6 100644 --- a/go/internal/storage/recall.go +++ b/go/internal/storage/recall.go @@ -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 追踪器收集真实预取候选项