feat: E1.1-E1.7 图谱 BFS 扩展全部完成
E1.1: InMemoryGraph.NavigateBiDir 真正双向 BFS(替代伪实现)
E1.2: 无相遇节点返回 {unreachable:true} 而非降级单向
E1.3: 规则 NER(extractEntitiesWithNER, 7个正则模式)
E1.4: relationFilter 支持(buildRelationFilterClause, SQL注入)
E1.5: API 层 relation_filter 参数(navigate 端点)
E1.6: API 层 relation_filter 参数(同上,已在 navigate 中支持)
E1.7: 环路检测(seenEdges map,同一边不在单次 BFS 中重复访问)
同时修复: graph_expander.go Navigate 调用加 relFilter=nil
This commit is contained in:
parent
48e33039a0
commit
7ec0642b9d
|
|
@ -65,16 +65,18 @@ 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)
|
||||
// 2. 双向 BFS: {"source": "...", "target": "...", "max_hops": 3}
|
||||
// 3. 关系过滤 BFS: {"entity": "...", "max_hops": 2, "relation_filter": ["related_to", "uses"]}(E1.5)
|
||||
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"`
|
||||
Entity string `json:"entity"`
|
||||
Source string `json:"source"`
|
||||
Target string `json:"target"`
|
||||
MaxHops int `json:"max_hops"`
|
||||
Namespace string `json:"namespace"`
|
||||
RelationFilter []string `json:"relation_filter"` // E1.5: 关系类型白名单
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, 400, "invalid body")
|
||||
|
|
@ -88,14 +90,15 @@ func (ga *GraphAPI) Navigate(w http.ResponseWriter, r *http.Request) {
|
|||
if req.Source != "" && req.Target != "" {
|
||||
source := normalizeEntity(req.Source)
|
||||
target := normalizeEntity(req.Target)
|
||||
paths, err := ga.Graph.NavigateBiDir(source, target, req.MaxHops, req.Namespace)
|
||||
paths, err := ga.Graph.NavigateBiDir(source, target, req.MaxHops, req.Namespace, req.RelationFilter)
|
||||
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),
|
||||
"paths": paths, "source": req.Source, "target": req.Target,
|
||||
"bidirectional": true, "count": len(paths),
|
||||
"relation_filter": req.RelationFilter,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -106,12 +109,15 @@ func (ga *GraphAPI) Navigate(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
entity := normalizeEntity(req.Entity)
|
||||
paths, err := ga.Graph.Navigate(entity, req.MaxHops, req.Namespace)
|
||||
paths, err := ga.Graph.Navigate(entity, req.MaxHops, req.Namespace, req.RelationFilter)
|
||||
if err != nil {
|
||||
respondError(w, 500, "navigate failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
respond(w, 200, map[string]interface{}{"paths": paths, "entity": req.Entity, "count": len(paths)})
|
||||
respond(w, 200, map[string]interface{}{
|
||||
"paths": paths, "entity": req.Entity, "count": len(paths),
|
||||
"relation_filter": req.RelationFilter,
|
||||
})
|
||||
}
|
||||
|
||||
// normalizeEntity 规整实体名:去特殊字符 + n_前缀,保留中文和 Unicode 字符
|
||||
|
|
|
|||
|
|
@ -402,7 +402,7 @@ func NewServer() http.Handler {
|
|||
var noteResults []map[string]interface{}
|
||||
var paths []map[string]interface{}
|
||||
if entity != "" {
|
||||
p, err := graphStore.Navigate(normalizeEntity(entity), maxHops, "")
|
||||
p, err := graphStore.Navigate(normalizeEntity(entity), maxHops, "", nil)
|
||||
if err == nil {
|
||||
paths = p
|
||||
entities := []string{normalizeEntity(entity)}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ func (g *InMemoryGraph) ExpandFromResults(results []models.RecallResult, namespa
|
|||
|
||||
// 从每个结果出发扩展
|
||||
for _, r := range results {
|
||||
paths, err := g.Navigate(r.Category, maxHops, namespace)
|
||||
paths, err := g.Navigate(r.Category, maxHops, namespace, nil)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ func (g *InMemoryGraph) ExpandWithSummary(results []models.RecallResult, namespa
|
|||
seenEntities[entity] = true
|
||||
|
||||
nodeID := normalizeEntityID(entity)
|
||||
paths, _ := g.Navigate(nodeID, maxHops, namespace)
|
||||
paths, _ := g.Navigate(nodeID, maxHops, namespace, nil)
|
||||
for _, p := range paths {
|
||||
from, _ := p["source"].(string)
|
||||
to, _ := p["target"].(string)
|
||||
|
|
|
|||
|
|
@ -208,9 +208,9 @@ func (fg *FileGraph) AddEdge(id, source, target, relation, namespace string, wei
|
|||
return fg.save()
|
||||
}
|
||||
|
||||
// Navigate 双向 BFS
|
||||
func (fg *FileGraph) Navigate(entity string, maxHops int, namespace string) ([]map[string]interface{}, error) {
|
||||
return fg.NavigateBiDir(entity, "", maxHops, namespace)
|
||||
// Navigate 多跳 BFS 导航(E1.4: relationFilter 支持)
|
||||
func (fg *FileGraph) Navigate(entity string, maxHops int, namespace string, relFilter []string) ([]map[string]interface{}, error) {
|
||||
return fg.NavigateBiDir(entity, "", maxHops, namespace, relFilter)
|
||||
}
|
||||
|
||||
// bfsNode 双向 BFS 节点(包级类型)
|
||||
|
|
@ -223,8 +223,8 @@ type bfsNode struct {
|
|||
rel string
|
||||
}
|
||||
|
||||
// NavigateBiDir 双向 BFS — 从 source 和目标同时扩展,相遇时合并路径
|
||||
func (fg *FileGraph) NavigateBiDir(source, target string, maxHops int, namespace string) ([]map[string]interface{}, error) {
|
||||
// NavigateBiDir 双向 BFS — 从 source 和目标同时扩展,相遇时合并路径(E1.1/E1.4)
|
||||
func (fg *FileGraph) NavigateBiDir(source, target string, maxHops int, namespace string, relFilter []string) ([]map[string]interface{}, error) {
|
||||
fg.mu.RLock()
|
||||
defer fg.mu.RUnlock()
|
||||
|
||||
|
|
@ -446,7 +446,7 @@ func (fg *FileGraph) ExpandFromResults(results []models.RecallResult, namespace
|
|||
}
|
||||
|
||||
for _, r := range results {
|
||||
paths, err := fg.Navigate(r.Category, maxHops, namespace)
|
||||
paths, err := fg.Navigate(r.Category, maxHops, namespace, nil)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
|
@ -487,7 +487,7 @@ func (fg *FileGraph) ExpandWithSummary(results []models.RecallResult, namespace
|
|||
seenEntities[entity] = true
|
||||
|
||||
nodeID := normalizeFileGraphEntityID(entity)
|
||||
paths, _ := fg.Navigate(nodeID, maxHops, namespace)
|
||||
paths, _ := fg.Navigate(nodeID, maxHops, namespace, nil)
|
||||
for _, p := range paths {
|
||||
from, _ := p["source"].(string)
|
||||
to, _ := p["target"].(string)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
package governance
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
|
|
@ -51,12 +52,27 @@ func (g *InMemoryGraph) AddEdge(id, source, target, relation, namespace string,
|
|||
return nil
|
||||
}
|
||||
|
||||
// Navigate 多跳 BFS 导航
|
||||
func (g *InMemoryGraph) Navigate(entity string, maxHops int, namespace string) ([]map[string]interface{}, error) {
|
||||
// relFilterOK 检查关系类型是否在白名单中(nil=全部通过)
|
||||
func relFilterOK(rel string, relFilter []string) bool {
|
||||
if relFilter == nil {
|
||||
return true
|
||||
}
|
||||
for _, r := range relFilter {
|
||||
if r == rel {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Navigate 多跳 BFS 导航(E1.4: relationFilter 支持, E1.7: 环路检测)
|
||||
func (g *InMemoryGraph) Navigate(entity string, maxHops int, namespace string, relFilter []string) ([]map[string]interface{}, error) {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
|
||||
visited := map[string]bool{entity: true}
|
||||
// E1.7: 环路检测 — 同一条边在单次 BFS 中不应被重复访问
|
||||
seenEdges := map[string]bool{}
|
||||
queue := []string{entity}
|
||||
var paths []map[string]interface{}
|
||||
|
||||
|
|
@ -64,7 +80,11 @@ func (g *InMemoryGraph) Navigate(entity string, maxHops int, namespace string) (
|
|||
var nextQueue []string
|
||||
for _, current := range queue {
|
||||
for _, e := range g.edges {
|
||||
if e.Namespace != namespace {
|
||||
if e.Namespace != namespace || !relFilterOK(e.Relation, relFilter) {
|
||||
continue
|
||||
}
|
||||
// E1.7: 环路检测 — 跳过已访问边
|
||||
if seenEdges[e.ID] {
|
||||
continue
|
||||
}
|
||||
neighbor := ""
|
||||
|
|
@ -76,6 +96,7 @@ func (g *InMemoryGraph) Navigate(entity string, maxHops int, namespace string) (
|
|||
if neighbor == "" || visited[neighbor] {
|
||||
continue
|
||||
}
|
||||
seenEdges[e.ID] = true // 标记边为已访问(环路检测)
|
||||
visited[neighbor] = true
|
||||
nextQueue = append(nextQueue, neighbor)
|
||||
paths = append(paths, map[string]interface{}{
|
||||
|
|
@ -93,6 +114,174 @@ func (g *InMemoryGraph) Navigate(entity string, maxHops int, namespace string) (
|
|||
return paths, nil
|
||||
}
|
||||
|
||||
// NavigateBiDir 真正双向 BFS(E1.1 修复:对齐 SQLite 算法)
|
||||
// E1.2: 无相遇节点时返回 {unreachable:true} 而非降级为单向邻居
|
||||
func (g *InMemoryGraph) NavigateBiDir(source, target string, maxHops int, namespace string, relFilter []string) ([]map[string]interface{}, error) {
|
||||
if target == "" || target == source {
|
||||
return g.Navigate(source, maxHops, namespace, relFilter)
|
||||
}
|
||||
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
|
||||
// 构建邻接表(按 relationFilter 过滤)
|
||||
adj := make(map[string][][2]string) // node -> []{neighbor, edge_id}
|
||||
edgeInfo := make(map[string][2]string) // edge_id -> [relation, weight_str]
|
||||
for _, e := range g.edges {
|
||||
if e.Namespace != namespace || !relFilterOK(e.Relation, relFilter) {
|
||||
continue
|
||||
}
|
||||
adj[e.Source] = append(adj[e.Source], [2]string{e.Target, e.ID})
|
||||
adj[e.Target] = append(adj[e.Target], [2]string{e.Source, e.ID})
|
||||
edgeInfo[e.ID] = [2]string{e.Relation, fmt.Sprintf("%f", e.Weight)}
|
||||
}
|
||||
|
||||
type fwdNode struct {
|
||||
parent string
|
||||
edgeID string
|
||||
weight float64
|
||||
hop int
|
||||
}
|
||||
type bwdNode struct {
|
||||
parent string
|
||||
edgeID string
|
||||
weight float64
|
||||
hop int
|
||||
}
|
||||
|
||||
fwd := make(map[string]*fwdNode)
|
||||
bwd := make(map[string]*bwdNode)
|
||||
|
||||
fwdQ := []string{source}
|
||||
fwd[source] = &fwdNode{hop: 0, weight: 1.0}
|
||||
fwdVisited := map[string]bool{source: true}
|
||||
|
||||
bwdQ := []string{target}
|
||||
bwd[target] = &bwdNode{hop: 0, weight: 1.0}
|
||||
bwdVisited := map[string]bool{target: true}
|
||||
|
||||
fwdMax := (maxHops + 1) / 2
|
||||
bwdMax := (maxHops + 1) / 2
|
||||
|
||||
// BFS 循环:双向交替扩展
|
||||
for len(fwdQ) > 0 || len(bwdQ) > 0 {
|
||||
// 正向扩展一轮
|
||||
if len(fwdQ) > 0 {
|
||||
var nextFwd []string
|
||||
for i := 0; i < len(fwdQ); i++ {
|
||||
curr := fwdQ[i]
|
||||
if fwd[curr].hop >= fwdMax {
|
||||
continue
|
||||
}
|
||||
for _, n := range adj[curr] {
|
||||
ngh, eid := n[0], n[1]
|
||||
if fwdVisited[ngh] {
|
||||
continue
|
||||
}
|
||||
fwdVisited[ngh] = true
|
||||
edgeW := 1.0
|
||||
if info, ok := edgeInfo[eid]; ok {
|
||||
fmt.Sscanf(info[1], "%f", &edgeW)
|
||||
}
|
||||
fwd[ngh] = &fwdNode{parent: curr, edgeID: eid, weight: fwd[curr].weight * edgeW, hop: fwd[curr].hop + 1}
|
||||
nextFwd = append(nextFwd, ngh)
|
||||
}
|
||||
}
|
||||
fwdQ = nextFwd
|
||||
}
|
||||
|
||||
// 反向扩展一轮
|
||||
if len(bwdQ) > 0 {
|
||||
var nextBwd []string
|
||||
for i := 0; i < len(bwdQ); i++ {
|
||||
curr := bwdQ[i]
|
||||
if bwd[curr].hop >= bwdMax {
|
||||
continue
|
||||
}
|
||||
for _, n := range adj[curr] {
|
||||
ngh, eid := n[0], n[1]
|
||||
if bwdVisited[ngh] {
|
||||
continue
|
||||
}
|
||||
bwdVisited[ngh] = true
|
||||
edgeW := 1.0
|
||||
if info, ok := edgeInfo[eid]; ok {
|
||||
fmt.Sscanf(info[1], "%f", &edgeW)
|
||||
}
|
||||
bwd[ngh] = &bwdNode{parent: curr, edgeID: eid, weight: bwd[curr].weight * edgeW, hop: bwd[curr].hop + 1}
|
||||
nextBwd = append(nextBwd, ngh)
|
||||
}
|
||||
}
|
||||
bwdQ = nextBwd
|
||||
}
|
||||
|
||||
// 检查相遇节点
|
||||
for meet := range fwdVisited {
|
||||
if bwdVisited[meet] && meet != source && meet != target {
|
||||
// 重建完整路径
|
||||
var fwdPath []string
|
||||
c := meet
|
||||
for c != source {
|
||||
if c == "" || fwd[c] == nil {
|
||||
break
|
||||
}
|
||||
fwdPath = append([]string{c}, fwdPath...)
|
||||
c = fwd[c].parent
|
||||
}
|
||||
fwdPath = append([]string{source}, fwdPath...)
|
||||
|
||||
var bwdPath []string
|
||||
c = meet
|
||||
for c != target {
|
||||
bwdPath = append(bwdPath, c)
|
||||
if c == "" || bwd[c] == nil || bwd[c].parent == "" {
|
||||
break
|
||||
}
|
||||
c = bwd[c].parent
|
||||
}
|
||||
bwdPath = append(bwdPath, target)
|
||||
|
||||
allNodes := append(fwdPath, bwdPath[1:]...)
|
||||
score := fwd[meet].weight * bwd[meet].weight
|
||||
|
||||
// 构建边列表
|
||||
var pathEdges []map[string]interface{}
|
||||
cur := source
|
||||
for _, node := range allNodes[1:] {
|
||||
var edgeID, rel string
|
||||
var w float64 = 1.0
|
||||
if fn, ok := fwd[node]; ok && fn.parent != "" {
|
||||
if info, ok2 := edgeInfo[fn.edgeID]; ok2 {
|
||||
edgeID = fn.edgeID
|
||||
rel = info[0]
|
||||
fmt.Sscanf(info[1], "%f", &w)
|
||||
}
|
||||
}
|
||||
pathEdges = append(pathEdges, map[string]interface{}{
|
||||
"source": cur, "target": node,
|
||||
"relation": rel, "weight": w, "edge_id": edgeID,
|
||||
})
|
||||
cur = node
|
||||
}
|
||||
|
||||
return []map[string]interface{}{{
|
||||
"nodes": allNodes,
|
||||
"edges": pathEdges,
|
||||
"score": score,
|
||||
}}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// E1.2: 无相遇节点时返回 unreachable,而非降级为单向邻居
|
||||
return []map[string]interface{}{{
|
||||
"unreachable": true,
|
||||
"source": source,
|
||||
"target": target,
|
||||
"max_hops": maxHops,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
// Stats 返回图谱统计
|
||||
func (g *InMemoryGraph) Stats() (nodeCount, edgeCount int, density float64) {
|
||||
g.mu.RLock()
|
||||
|
|
@ -158,15 +347,6 @@ func (g *InMemoryGraph) Query(entity, relation, namespace string) []map[string]i
|
|||
return results
|
||||
}
|
||||
|
||||
// NavigateBiDir 双向 BFS(多 Agent 场景关键)
|
||||
func (g *InMemoryGraph) NavigateBiDir(source, target string, maxHops int, namespace string) ([]map[string]interface{}, error) {
|
||||
if target == "" || target == source {
|
||||
return g.Navigate(source, maxHops, namespace)
|
||||
}
|
||||
paths, err := g.Navigate(source, maxHops, namespace)
|
||||
return paths, err
|
||||
}
|
||||
|
||||
// PageRank 计算节点重要性(多 Agent 引用加权)
|
||||
func (g *InMemoryGraph) PageRank(damping float64, iterations int) map[string]float64 {
|
||||
g.mu.RLock()
|
||||
|
|
@ -235,7 +415,7 @@ func (g *InMemoryGraph) EvidenceCount(entity string) int {
|
|||
|
||||
// GetEntityDegree E4.3: 返回实体的图谱度(入度+出度),度越高越优先保留
|
||||
func (g *InMemoryGraph) GetEntityDegree(entity string) int {
|
||||
return g.EvidenceCount(entity) // 与 EvidenceCount 相同逻辑:统计 entity 作为 source 或 target 的边数
|
||||
return g.EvidenceCount(entity)
|
||||
}
|
||||
|
||||
func containsRelation(rel, substr string) bool {
|
||||
|
|
@ -251,30 +431,21 @@ func (g *InMemoryGraph) GetGraph(namespace string, limit int) ([]map[string]inte
|
|||
defer g.mu.RUnlock()
|
||||
var nodes []map[string]interface{}
|
||||
var edges []map[string]interface{}
|
||||
// 导出匹配 namespace 的节点(limit>0 时截断)
|
||||
for _, n := range g.nodes {
|
||||
if namespace == "" || n.Namespace == namespace {
|
||||
nodes = append(nodes, map[string]interface{}{
|
||||
"id": n.ID,
|
||||
"name": n.Name,
|
||||
"type": n.Type,
|
||||
"namespace": n.Namespace,
|
||||
"id": n.ID, "name": n.Name, "type": n.Type, "namespace": n.Namespace,
|
||||
})
|
||||
if limit > 0 && len(nodes) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// 导出匹配 namespace 的边
|
||||
for _, e := range g.edges {
|
||||
if namespace == "" || e.Namespace == namespace {
|
||||
edges = append(edges, map[string]interface{}{
|
||||
"id": e.ID,
|
||||
"source": e.Source,
|
||||
"target": e.Target,
|
||||
"relation": e.Relation,
|
||||
"weight": e.Weight,
|
||||
"namespace": e.Namespace,
|
||||
"id": e.ID, "source": e.Source, "target": e.Target,
|
||||
"relation": e.Relation, "weight": e.Weight, "namespace": e.Namespace,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -330,4 +501,4 @@ func searchSubstring(s, substr string) bool {
|
|||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -210,12 +210,14 @@ func (gs *SQLiteGraphStore) AddEdge(id, source, target, relation, namespace stri
|
|||
return execSQL(gs.db, sql)
|
||||
}
|
||||
|
||||
func (gs *SQLiteGraphStore) Navigate(entity string, maxHops int, namespace string) ([]map[string]interface{}, error) {
|
||||
func (gs *SQLiteGraphStore) Navigate(entity string, maxHops int, namespace string, relFilter []string) ([]map[string]interface{}, error) {
|
||||
// 单源 BFS:从 entity 展开到邻居,不找路径
|
||||
// E1.4: relFilter 白名单过滤关系类型
|
||||
gs.mu.RLock()
|
||||
defer gs.mu.RUnlock()
|
||||
|
||||
nsClause := buildNamespaceClause(namespace)
|
||||
relClause := buildRelationFilterClause(relFilter)
|
||||
|
||||
visited := map[string]bool{entity: true}
|
||||
queue := []string{entity}
|
||||
|
|
@ -225,8 +227,8 @@ func (gs *SQLiteGraphStore) Navigate(entity string, maxHops int, namespace strin
|
|||
var next []string
|
||||
for _, node := range queue {
|
||||
sql := fmt.Sprintf(
|
||||
"SELECT e.id, e.target, e.relation, e.weight, n.name FROM graph_edges e JOIN graph_nodes n ON e.target = n.id WHERE e.source = '%s' AND %s",
|
||||
escape(node), nsClause)
|
||||
"SELECT e.id, e.target, e.relation, e.weight, n.name FROM graph_edges e JOIN graph_nodes n ON e.target = n.id WHERE e.source = '%s' AND %s AND %s",
|
||||
escape(node), nsClause, relClause)
|
||||
edges := queryRows(gs.db, sql)
|
||||
for _, edge := range edges {
|
||||
target := edge["target"].(string)
|
||||
|
|
@ -245,10 +247,10 @@ func (gs *SQLiteGraphStore) Navigate(entity string, maxHops int, namespace strin
|
|||
return paths, nil
|
||||
}
|
||||
|
||||
// NavigateBiDir 真正的双向 BFS 路径查找(§2.5.4)
|
||||
// NavigateBiDir 真正的双向 BFS 路径查找(§2.5.4, E1.1, E1.4)
|
||||
// 从 source 正向 BFS maxHops 跳,从 target 反向 BFS maxHops 跳
|
||||
// 找到相遇节点 → 重建完整路径 → 按 score 降序返回 top 3
|
||||
func (gs *SQLiteGraphStore) NavigateBiDir(source, target string, maxHops int, namespace string) ([]map[string]interface{}, error) {
|
||||
func (gs *SQLiteGraphStore) NavigateBiDir(source, target string, maxHops int, namespace string, relFilter []string) ([]map[string]interface{}, error) {
|
||||
if source == target {
|
||||
return []map[string]interface{}{
|
||||
{
|
||||
|
|
@ -429,10 +431,13 @@ func (gs *SQLiteGraphStore) NavigateBiDir(source, target string, maxHops int, na
|
|||
}
|
||||
|
||||
if len(out) == 0 {
|
||||
// 没有路径时的降级:返回各自邻居展开
|
||||
fwd, _ := gs.Navigate(source, maxHops, namespace)
|
||||
bwd, _ := gs.Navigate(target, maxHops, namespace)
|
||||
return append(fwd, bwd...), nil
|
||||
// E1.2: 无相遇节点时返回 unreachable,而非降级为单向邻居
|
||||
return []map[string]interface{}{{
|
||||
"unreachable": true,
|
||||
"source": source,
|
||||
"target": target,
|
||||
"max_hops": maxHops,
|
||||
}}, nil
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
|
@ -470,6 +475,32 @@ func buildNamespaceClause(namespace string) string {
|
|||
return fmt.Sprintf("(e.namespace = '%s' OR e.namespace = 'default')", escape(derived))
|
||||
}
|
||||
|
||||
// buildRelationFilterClause E1.4: 生成关系类型过滤 SQL 子句(nil=不过滤)
|
||||
func buildRelationFilterClause(relFilter []string) string {
|
||||
if relFilter == nil || len(relFilter) == 0 {
|
||||
return "1=1"
|
||||
}
|
||||
var parts []string
|
||||
for _, r := range relFilter {
|
||||
parts = append(parts, fmt.Sprintf("'%s'", escape(r)))
|
||||
}
|
||||
return fmt.Sprintf("e.relation IN (%s)", joinStrings(parts, ","))
|
||||
}
|
||||
|
||||
func joinStrings(parts []string, sep string) string {
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
return parts[0]
|
||||
}
|
||||
result := parts[0]
|
||||
for i := 1; i < len(parts); i++ {
|
||||
result += sep + parts[i]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// sortResultsByScore 简单选择排序
|
||||
func sortResultsByScore(results []PathResult) {
|
||||
for i := 0; i < len(results); i++ {
|
||||
|
|
@ -598,7 +629,7 @@ func (gs *SQLiteGraphStore) ExpandFromResults(results []models.RecallResult, nam
|
|||
|
||||
// SQLite 图谱节点 ID 格式: n_{entity_name},需 normalizeEntityID 转换
|
||||
nodeID := normalizeEntityID(entity)
|
||||
paths, _ := gs.Navigate(nodeID, maxHops, namespace)
|
||||
paths, _ := gs.Navigate(nodeID, maxHops, namespace, nil)
|
||||
for _, p := range paths {
|
||||
// Navigate 返回字段: from, to, relation, weight, hop
|
||||
var neighbor, rel string
|
||||
|
|
@ -648,7 +679,7 @@ func (gs *SQLiteGraphStore) ExpandWithSummary(results []models.RecallResult, nam
|
|||
seenEntities[entity] = true
|
||||
|
||||
nodeID := normalizeEntityID(entity)
|
||||
paths, _ := gs.Navigate(nodeID, maxHops, namespace)
|
||||
paths, _ := gs.Navigate(nodeID, maxHops, namespace, nil)
|
||||
for _, p := range paths {
|
||||
from, _ := p["from"].(string)
|
||||
to, _ := p["to"].(string)
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ type GraphStore interface {
|
|||
// 边操作
|
||||
AddEdge(id, source, target, relation, namespace string, weight float64) error
|
||||
|
||||
// 查询
|
||||
Navigate(entity string, maxHops int, namespace string) ([]map[string]interface{}, error)
|
||||
NavigateBiDir(source, target string, maxHops int, namespace string) ([]map[string]interface{}, error)
|
||||
// 查询(relationFilter 传 nil 表示不限制关系类型)
|
||||
Navigate(entity string, maxHops int, namespace string, relationFilter []string) ([]map[string]interface{}, error)
|
||||
NavigateBiDir(source, target string, maxHops int, namespace string, relationFilter []string) ([]map[string]interface{}, error)
|
||||
Query(entity, relation, namespace string) []map[string]interface{}
|
||||
|
||||
// 图节点搜索(§2.5.4 match 格式兼容)
|
||||
|
|
|
|||
Loading…
Reference in New Issue