Files
sanguo_llmwiki/mcp_server/services/query.py
T
claude_dev 421b3ada3b 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>
2026-06-28 07:47:28 +08:00

193 lines
6.3 KiB
Python

"""
Service Layer - QueryService(查询服务)
负责所有查询逻辑,包括 FTS5 全文搜索、标签搜索、页面查询等。
参考设计文档:第 2.3 节
"""
import logging
import re
from typing import List, Set, Optional, Dict
from ..storage import Database, WikiPage
from .cache import CacheService
logger = logging.getLogger(__name__)
# 中文同义词映射(用于扩展搜索查询)
CHINESE_SYNONYMS: Dict[str, List[str]] = {
"多智能体": ["multi-agent", "多agent", "multi agent"],
"编排": ["orchestration", "调度"],
"协作": ["collaboration", "协同"],
"工具": ["tool", "工具调用"],
"模式": ["pattern", "设计模式"],
"实践": ["practice", "最佳实践"],
"架构": ["architecture", "系统架构"],
"代理": ["agent", "智能体"],
"大模型": ["llm", "language model", "语言模型"],
"向量": ["vector", "embedding", "嵌入"],
"检索": ["retrieval", "search", "搜索"],
"生成": ["generation", "生成式"],
"推理": ["inference", "reasoning"],
}
class QueryService:
"""查询服务 - 负责所有查询逻辑"""
def __init__(self, db: Database, cache: CacheService):
self.db = db
self.cache = cache
def _expand_query_with_synonyms(self, query: str) -> str:
"""使用中文同义词扩展查询
例如:"多智能体编排""多智能体编排 multi-agent orchestration"
"""
expanded_terms = []
query_lower = query.lower()
# 检查每个中文词是否有对应的英文同义词
for chinese_term, english_synonyms in CHINESE_SYNONYMS.items():
if chinese_term in query:
expanded_terms.extend(english_synonyms)
if expanded_terms:
# 构建扩展查询(OR 连接)
expanded_query = f"{query} OR {' OR '.join(expanded_terms)}"
logger.debug(f"Expanded query: {query}{expanded_query}")
return expanded_query
return query
async def search(self, query: str, limit: int = 10, use_synonyms: bool = True) -> List[WikiPage]:
"""FTS5 全文搜索
Args:
query: 搜索查询
limit: 返回结果数量限制
use_synonyms: 是否使用中文同义词扩展
"""
# 1. 检查缓存
cache_key = f"search:{query}:{limit}:{use_synonyms}"
cached = await self.cache.get(cache_key)
if cached:
logger.debug(f"Cache hit for search: {query}")
return cached
# 2. 查询扩展(中文同义词)
search_query = query
if use_synonyms:
search_query = self._expand_query_with_synonyms(query)
# 3. FTS5 搜索
results = await self.db.fts_search(search_query, limit)
# 4. 缓存结果
await self.cache.set(cache_key, results, ttl=3600)
return results
async def search_by_tags(self, tags: List[str]) -> List[WikiPage]:
"""按标签搜索"""
cache_key = f"tags:{','.join(tags)}"
cached = await self.cache.get(cache_key)
if cached:
return cached
results = await self.db.search_by_tags(tags)
await self.cache.set(cache_key, results, ttl=3600)
return results
async def search_by_source_tool(self, tool_name: str, limit: int = 50) -> List[WikiPage]:
"""按来源工具搜索(memory_bridge 使用)"""
cache_key = f"tool:{tool_name}:{limit}"
cached = await self.cache.get(cache_key)
if cached:
return cached
results = await self.db.search_by_source_tool(tool_name, limit)
await self.cache.set(cache_key, results, ttl=1800) # 30 分钟 TTL
return results
async def get_page(self, path: str) -> Optional[WikiPage]:
"""获取单个页面"""
cache_key = f"page:{path}"
cached = await self.cache.get(cache_key)
if cached:
return cached
page = await self.db.get_page(path)
if page:
await self.cache.set(cache_key, page, ttl=3600)
return page
async def get_links(self, path: str) -> Set[str]:
"""获取页面的出链"""
cache_key = f"links:{path}"
cached = await self.cache.get(cache_key)
if cached:
return cached
links = await self.db.get_links(path)
await self.cache.set(cache_key, links, ttl=3600)
return links
async def get_backlinks(self, path: str) -> Set[str]:
"""获取页面的反向链接"""
cache_key = f"backlinks:{path}"
cached = await self.cache.get(cache_key)
if cached:
return cached
backlinks = await self.db.get_backlinks(path)
await self.cache.set(cache_key, backlinks, ttl=3600)
return backlinks
async def find_orphans(self) -> Set[str]:
"""查找孤立页面(无反向链接)"""
cache_key = "orphans"
cached = await self.cache.get(cache_key)
if cached:
return cached
all_pages = await self.db.get_all_pages()
orphans = set()
for page in all_pages:
backlinks = await self.db.get_backlinks(page.path)
if not backlinks and page.path != "index.md":
orphans.add(page.path)
await self.cache.set(cache_key, orphans, ttl=1800)
return orphans
async def get_stats(self) -> dict:
"""获取索引统计信息"""
return await self.db.get_stats()
async def invalidate_cache(self, pattern: str = "*") -> int:
"""使缓存失效
如果 pattern 是具体路径(不是通配符),自动尝试匹配所有缓存前缀:
- page:{pattern}
- links:{pattern}
- search:{pattern}
"""
# 清除所有缓存
if pattern == "*":
return await self.cache.invalidate(pattern)
# 对于具体路径,尝试所有可能的前缀
prefixes = ["page:", "links:", "search:"]
total_invalidated = 0
for prefix in prefixes:
count = await self.cache.invalidate(f"{prefix}{pattern}")
total_invalidated += count
# 也尝试原始模式(兼容直接传入完整 key 的情况)
total_invalidated += await self.cache.invalidate(pattern)
return total_invalidated