From 5a2be87f658d01bc5b3b7ecdba92ac69bf09dcf7 Mon Sep 17 00:00:00 2001
From: ai_xiaopei <xiaopei@aisim.cn>
Date: Thu, 03 Sep 2026 15:15:37 +0800
Subject: [PATCH] fix(search): 搜索准确性四连修——FTS5 MATCH 引号包裹(连字符不再被解析成 MINUS);KeywordSearch 截断按命中关键词数排序(高频 bigram 不再挤出强相关文档);原词奖励档(完整短语压过泛化 bigram);tags 字段真实参与评分(此前误用 Section)

---
 internal/index/unresolved_test.go |  130 +++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 130 insertions(+), 0 deletions(-)

diff --git a/internal/index/unresolved_test.go b/internal/index/unresolved_test.go
new file mode 100644
index 0000000..4d15230
--- /dev/null
+++ b/internal/index/unresolved_test.go
@@ -0,0 +1,130 @@
+package index
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+)
+
+func TestRetryUnresolved(t *testing.T) {
+	dir := t.TempDir()
+	os.MkdirAll(filepath.Join(dir, "FAQ"), 0755)
+	os.WriteFile(filepath.Join(dir, "FAQ", "001-旧文档.md"),
+		[]byte("---\ntitle: 旧文档\n---\n提到 [[补气失败]]\n"), 0644)
+
+	store, _ := Open(filepath.Join(t.TempDir(), "kb.db"))
+	defer store.Close()
+
+	Reconcile(store, dir)
+
+	// 验证悬空链接入表
+	var n int
+	store.db.QueryRow(`SELECT COUNT(*) FROM unresolved_links WHERE link_text='补气失败'`).Scan(&n)
+	if n != 1 {
+		t.Fatalf("悬空链接未入表: %d", n)
+	}
+
+	// 新文档入库:标题正好是 "补气失败"
+	os.WriteFile(filepath.Join(dir, "FAQ", "002-补气失败.md"),
+		[]byte("---\ntitle: 补气失败\n---\n补气失败排查\n"), 0644)
+
+	res, err := Reconcile(store, dir)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if res.Added != 1 {
+		t.Fatalf("新增: %+v", res)
+	}
+
+	// 悬空链接应被解析:行删除 + 边建立
+	store.db.QueryRow(`SELECT COUNT(*) FROM unresolved_links WHERE link_text='补气失败'`).Scan(&n)
+	if n != 0 {
+		t.Errorf("悬空行未清除: %d", n)
+	}
+	store.db.QueryRow(`SELECT COUNT(*) FROM edges WHERE relation='wikilink' AND label='补气失败'`).Scan(&n)
+	if n != 1 {
+		t.Errorf("wikilink 边未建立: %d", n)
+	}
+	var prov string
+	store.db.QueryRow(`SELECT provenance FROM edges WHERE relation='wikilink' AND label='补气失败'`).Scan(&prov)
+	if prov != "exact" {
+		t.Errorf("补全边应为 exact: %s", prov)
+	}
+}
+
+func TestResolveWikilinkEmptyLink(t *testing.T) {
+	dir := t.TempDir()
+	os.MkdirAll(filepath.Join(dir, "FAQ"), 0755)
+	os.WriteFile(filepath.Join(dir, "FAQ", "001-文档.md"),
+		[]byte("---\ntitle: 文档\n---\n提到 [[|别名]]\n"), 0644)
+
+	store, _ := Open(filepath.Join(t.TempDir(), "kb.db"))
+	defer store.Close()
+	Reconcile(store, dir)
+
+	// 空 link 不应 fuzzy 误建边(Contains 空串恒真)
+	var n int
+	store.db.QueryRow(`SELECT COUNT(*) FROM edges WHERE relation='wikilink' AND label='|别名'`).Scan(&n)
+	if n != 0 {
+		t.Errorf("空 link 不应建 wikilink 边: %d", n)
+	}
+	// 悬空行应入表且只入一行
+	store.db.QueryRow(`SELECT COUNT(*) FROM unresolved_links WHERE link_text='|别名'`).Scan(&n)
+	if n != 1 {
+		t.Errorf("悬空行应为 1: %d", n)
+	}
+}
+
+func TestTagEntityEdgeProvenance(t *testing.T) {
+	dir := t.TempDir()
+	os.MkdirAll(filepath.Join(dir, "FAQ"), 0755)
+	os.WriteFile(filepath.Join(dir, "FAQ", "001-文档.md"),
+		[]byte("---\ntitle: 文档\ntags: [t1]\nentities: [张三]\n---\n内容\n"), 0644)
+
+	store, _ := Open(filepath.Join(t.TempDir(), "kb.db"))
+	defer store.Close()
+	Reconcile(store, dir)
+
+	// tag/entity 边的 provenance 应为 relation 本身(Minor 1 修复)
+	var prov string
+	store.db.QueryRow(`SELECT provenance FROM edges WHERE relation='tag' AND label='t1'`).Scan(&prov)
+	if prov != "tag" {
+		t.Errorf("tag 边 provenance: %q", prov)
+	}
+	store.db.QueryRow(`SELECT provenance FROM edges WHERE relation='entity' AND label='张三'`).Scan(&prov)
+	if prov != "entity" {
+		t.Errorf("entity 边 provenance: %q", prov)
+	}
+}
+
+// TestGetUnresolvedLinks 覆盖 GetUnresolvedLinks 查询(explore 命令悬空链接提示用)
+func TestGetUnresolvedLinks(t *testing.T) {
+	dir := t.TempDir()
+	os.MkdirAll(filepath.Join(dir, "FAQ"), 0755)
+	// 文档 A 有两条悬空链接;文档 B 无
+	os.WriteFile(filepath.Join(dir, "FAQ", "001-文档A.md"),
+		[]byte("---\ntitle: 文档A\n---\n提到 [[悬空甲]] 和 [[悬空乙]]\n"), 0644)
+	os.WriteFile(filepath.Join(dir, "FAQ", "002-文档B.md"),
+		[]byte("---\ntitle: 文档B\n---\n无链接\n"), 0644)
+
+	store, _ := Open(filepath.Join(t.TempDir(), "kb.db"))
+	defer store.Close()
+	Reconcile(store, dir)
+
+	links, err := store.GetUnresolvedLinks("FAQ/001-文档A.md")
+	if err != nil {
+		t.Fatalf("GetUnresolvedLinks failed: %v", err)
+	}
+	if len(links) != 2 {
+		t.Fatalf("文档A 悬空链接 = %d, want 2: %v", len(links), links)
+	}
+
+	// 无悬空链接的文档返回空(非 nil 错误)
+	links, err = store.GetUnresolvedLinks("FAQ/002-文档B.md")
+	if err != nil {
+		t.Fatalf("GetUnresolvedLinks(文档B) failed: %v", err)
+	}
+	if len(links) != 0 {
+		t.Errorf("文档B 悬空链接 = %d, want 0: %v", len(links), links)
+	}
+}

--
Gitblit v1.10.0