feat: 搜索改进与代码修复

- 连字符扩展:multi-agent → multi agent 提升搜索召回率
- 中文同义词支持:添加中英文混合查询扩展
- Snippet 高亮预留:为 FTS5 snippet 功能预留接口
- YAML 解析增强:支持 HTML 实体解码
- 标签关联维护:自动维护 wiki_page_tags 关联表
- 缓存失效优化:支持智能前缀匹配
- 设计文档更新:汇总近期改动 (v1.3)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 07:47:28 +08:00
parent b2c3ca02f0
commit 421b3ada3b
10 changed files with 289 additions and 44 deletions
+65 -24
View File
@@ -94,23 +94,13 @@ class Database:
await self._conn.execute("CREATE INDEX IF NOT EXISTS idx_pages_updated ON wiki_pages(updated_at)")
await self._conn.execute("CREATE INDEX IF NOT EXISTS idx_pages_source_tool ON wiki_pages(source_tool)")
# 内容表(FTS5 外部内容表)- 必须先创建
await self._conn.execute("""
CREATE TABLE IF NOT EXISTS wiki_content (
path TEXT PRIMARY KEY,
content TEXT NOT NULL
)
""")
# FTS5 全文搜索表
# FTS5 全文搜索表(不使用外部内容表,简化结构)
await self._conn.execute("""
CREATE VIRTUAL TABLE IF NOT EXISTS wiki_fts USING fts5(
path UNINDEXED,
title,
content,
summary,
content=wiki_content,
content_rowid=rowid,
tokenize = 'porter unicode61'
)
""")
@@ -241,7 +231,8 @@ class Database:
return row[0] if row else None
async def upsert_page(self, page: WikiPage) -> None:
"""插入或更新页面"""
"""插入或更新页面(包括标签关联)"""
# 先更新页面基本信息
await self.execute_with_retry("""
INSERT INTO wiki_pages (path, title, category, tags, summary, content_hash, lifecycle, source_tool, created_at, updated_at, indexed_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
@@ -261,6 +252,9 @@ class Database:
page.created_at, page.updated_at, page.indexed_at
))
# 更新标签关联
await self.update_page_tags(page.path, page.tags)
async def delete_page(self, path: str) -> None:
"""删除页面(级联删除相关数据)"""
await self.execute_with_retry("DELETE FROM wiki_pages WHERE path = ?", (path,))
@@ -314,8 +308,14 @@ class Database:
return query.strip()
async def fts_search(self, query: str, limit: int = 10) -> List[WikiPage]:
"""FTS5 全文搜索"""
async def fts_search(self, query: str, limit: int = 10, with_snippets: bool = False) -> List[WikiPage]:
"""FTS5 全文搜索
Args:
query: 搜索查询
limit: 返回结果数量限制
with_snippets: 是否返回匹配片段(暂未实现,预留接口)
"""
# 验证和清理查询
try:
safe_query = self._validate_fts_query(query)
@@ -447,6 +447,24 @@ class Database:
(tag, count)
)
async def update_page_tags(self, path: str, tags: List[str]) -> None:
"""更新页面的标签关联
先删除旧的标签关联,再插入新的。
同时更新 wiki_tags 表中的计数。
"""
# 删除旧的标签关联
await self.execute_with_retry("DELETE FROM wiki_page_tags WHERE path = ?", (path,))
# 插入新的标签关联
for tag in tags:
await self.execute_with_retry(
"INSERT INTO wiki_page_tags (path, tag) VALUES (?, ?)",
(path, tag)
)
# 更新标签计数
await self.upsert_tag(tag)
async def get_all_tags(self) -> Dict[str, int]:
"""获取所有标签"""
rows = await self.fetch_all("SELECT tag, count FROM wiki_tags")
@@ -469,22 +487,45 @@ class Database:
# === FTS5 内容更新 ===
async def update_fts_content(self, path: str, title: str, content: str, summary: str) -> None:
"""更新 FTS5 内容"""
# 先更新内容表
"""更新 FTS5 内容
FTS5 虚拟表不支持 UPSERT,使用 DELETE + INSERT 替代。
所有内容(包括 content 字段)直接存储在 FTS 表中。
优化:连字符扩展(multi-agent → multi agent)以提升搜索召回率
"""
# 先删除旧记录(如果存在)
await self.execute_with_retry("DELETE FROM wiki_fts WHERE path = ?", (path,))
# 扩展连字符词汇(multi-agent → multi agent
# 这样搜索 "multi" 或 "agent" 都能匹配到
expanded_title = self._expand_hyphenated_text(title)
expanded_summary = self._expand_hyphenated_text(summary)
# 插入扩展后的 title 和 summary(原版保留在 content 中)
await self.execute_with_retry(
"INSERT INTO wiki_content (path, content) VALUES (?, ?) ON CONFLICT(path) DO UPDATE SET content = excluded.content",
(path, content)
"INSERT INTO wiki_fts (path, title, summary, content) VALUES (?, ?, ?, ?)",
(path, f"{title} {expanded_title}", f"{summary} {expanded_summary}", content)
)
# 再更新 FTS5 表(会自动从内容表同步)
await self.execute_with_retry(
"INSERT INTO wiki_fts (path, title, summary) VALUES (?, ?, ?) ON CONFLICT(path) DO UPDATE SET title = excluded.title, summary = excluded.summary",
(path, title, summary)
)
def _expand_hyphenated_text(self, text: str) -> str:
"""扩展连字符词汇
multi-agent system → multi agent system
这样搜索 "multi""agent" 都能匹配
Args:
text: 原始文本
Returns:
扩展后的文本(连字符替换为空格)
"""
if not text:
return ""
return text.replace('-', ' ')
async def delete_fts_content(self, path: str) -> None:
"""删除 FTS5 内容"""
await self.execute_with_retry("DELETE FROM wiki_content WHERE path = ?", (path,))
await self.execute_with_retry("DELETE FROM wiki_fts WHERE path = ?", (path,))
# === 状态查询 ===