fix(graph_sqlite): fix ExpandFromResults - wrong field access + Chinese entity extraction
Root causes fixed:
1. ExpandFromResults accessed p["to"] but SQLiteGraphStore.Navigate
returns {"source","target","relation","weight"} - field name mismatch
caused graph expansion to always return empty
2. Original logic checked seen[r.ID] (memory ID) against entity name
(neighbor string), which never matched - removed stale logic
Also improved extractPotentialEntities:
- Now handles Chinese continuous character sequences (2-8 chars)
- Chinese entities extracted as whole words, not split by spaces
- English capitalized words preserved with original casing
Changes:
- ExpandFromResults: correct field access (source/target/relation),
bidirectional neighbor detection (forward + reverse edges),
separate seenEntities map for entity dedup
- extractPotentialEntities: full rewrite for Chinese + better English
word tokenization without space-based splitting
This commit is contained in:
parent
04057637e0
commit
31712986de
|
|
@ -556,10 +556,11 @@ func (gs *SQLiteGraphStore) GetGraph(namespace string, limit int) ([]map[string]
|
|||
|
||||
func (gs *SQLiteGraphStore) ExpandFromResults(results []models.RecallResult, namespace string, maxHops int) []models.RecallResult {
|
||||
// 从 recall 结果提取实体,展开图谱邻居
|
||||
expanded := make([]models.RecallResult, len(results))
|
||||
copy(expanded, results)
|
||||
expanded := make([]models.RecallResult, 0, len(results)*2)
|
||||
expanded = append(expanded, results...)
|
||||
|
||||
seen := make(map[string]bool)
|
||||
seen := make(map[string]bool) // 追踪已访问的记忆 ID
|
||||
seenEntities := make(map[string]bool) // 追踪已访问的实体(用于去重扩展结果)
|
||||
for _, r := range results {
|
||||
if seen[r.ID] {
|
||||
continue
|
||||
|
|
@ -569,53 +570,93 @@ func (gs *SQLiteGraphStore) ExpandFromResults(results []models.RecallResult, nam
|
|||
// 从内容中提取可能作为实体的关键词
|
||||
entities := extractPotentialEntities(r.Content)
|
||||
for _, entity := range entities {
|
||||
if seenEntities[entity] {
|
||||
continue
|
||||
}
|
||||
seenEntities[entity] = true
|
||||
|
||||
nodeID := normalizeEntityID(entity)
|
||||
paths, _ := gs.Navigate(nodeID, maxHops, namespace)
|
||||
for _, p := range paths {
|
||||
if target, ok := p["to"].(string); ok && !seen[target] {
|
||||
seen[target] = true
|
||||
expanded = append(expanded, models.RecallResult{
|
||||
Content: fmt.Sprintf("[graph] %s --[%s]--> %s", entity, p["relation"], p["to"]),
|
||||
Score: r.Score * 0.5,
|
||||
})
|
||||
// Navigate 返回的是单条边 (source/target/relation/weight)
|
||||
// 有两种情况:
|
||||
// 1. source == entity(正向边):target 是下游邻居
|
||||
// 2. target == entity(反向边):source 是上游邻居
|
||||
var neighbor, rel string
|
||||
src, _ := p["source"].(string)
|
||||
tgt, _ := p["target"].(string)
|
||||
relVal, _ := p["relation"].(string)
|
||||
if src == entity && tgt != "" {
|
||||
neighbor = tgt
|
||||
rel = relVal
|
||||
} else if tgt == entity && src != "" {
|
||||
neighbor = src
|
||||
rel = "↩ " + relVal
|
||||
}
|
||||
if neighbor == "" {
|
||||
continue
|
||||
}
|
||||
if seenEntities[neighbor] {
|
||||
continue
|
||||
}
|
||||
seenEntities[neighbor] = true
|
||||
expanded = append(expanded, models.RecallResult{
|
||||
Content: fmt.Sprintf("[graph] %s --[%s]--> %s", entity, rel, neighbor),
|
||||
Score: r.Score * 0.5,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return expanded
|
||||
}
|
||||
|
||||
// extractPotentialEntities 从文本中提取可能作为图谱实体的关键词
|
||||
// extractPotentialEntities 从文本中提取可能作为图谱实体的关键词(支持中文连续字符)
|
||||
func extractPotentialEntities(text string) []string {
|
||||
var entities []string
|
||||
seen := make(map[string]bool)
|
||||
for _, w := range strings.Fields(text) {
|
||||
runes := []rune(text)
|
||||
for i := 0; i < len(runes); {
|
||||
r := runes[i]
|
||||
// 中文字符:收集连续的中文字符序列(2-8字)
|
||||
if r >= 0x4E00 && r <= 0x9FFF {
|
||||
start := i
|
||||
i++
|
||||
for i < len(runes) && runes[i] >= 0x4E00 && runes[i] <= 0x9FFF {
|
||||
i++
|
||||
}
|
||||
chinese := string(runes[start:i])
|
||||
// 不等式:2 <= len(chinese) <= 8
|
||||
if len(chinese) >= 2 && len(chinese) <= 8 && !seen[chinese] {
|
||||
seen[chinese] = true
|
||||
entities = append(entities, chinese)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// 非中文字符,收集整个单词
|
||||
start := i
|
||||
for i < len(runes) {
|
||||
r2 := runes[i]
|
||||
if r2 >= 0x4E00 && r2 <= 0x9FFF {
|
||||
break // 遇到汉字则停止
|
||||
}
|
||||
i++
|
||||
}
|
||||
if i-start < 2 {
|
||||
continue
|
||||
}
|
||||
w := string(runes[start:i])
|
||||
// 去除首尾标点
|
||||
w = strings.Trim(w, ",.;:!?,。;:!?、\"'()()[]【】")
|
||||
if len(w) < 2 {
|
||||
continue
|
||||
}
|
||||
// 大写开头(英文命名实体)
|
||||
runes := []rune(w)
|
||||
if len(runes) >= 2 && runes[0] >= 'A' && runes[0] <= 'Z' {
|
||||
if !seen[strings.ToLower(w)] {
|
||||
seen[strings.ToLower(w)] = true
|
||||
entities = append(entities, w)
|
||||
}
|
||||
}
|
||||
// 纯中文字符 2-8 字
|
||||
chineseOnly := true
|
||||
chineseRunes := 0
|
||||
for _, r := range runes {
|
||||
if r >= 0x4E00 && r <= 0x9FFF {
|
||||
chineseRunes++
|
||||
} else {
|
||||
chineseOnly = false
|
||||
}
|
||||
}
|
||||
if chineseOnly && chineseRunes >= 2 && chineseRunes <= 8 {
|
||||
if !seen[w] {
|
||||
seen[w] = true
|
||||
entities = append(entities, w)
|
||||
// 英文大写字母开头的词
|
||||
first := []rune(w)
|
||||
if len(first) > 0 && first[0] >= 'A' && first[0] <= 'Z' {
|
||||
lower := strings.ToLower(w)
|
||||
if !seen[lower] {
|
||||
seen[lower] = true
|
||||
entities = append(entities, w) // 保留原始大小写
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue