Files
md-vector-db/src/core/search.py
T
Serendipity 832201186d fix: 修复 11 个代码架构审计问题
H1: AppConfig 自定义 __init__ 改用 from_dict() 类方法
H2: list_collections_with_stats 改为从 ChromaDB 直接查询
H3: RateLimiter 过期 key 自动清理, 防止内存泄漏
M1: delete_by_source 区分 ValueError 与真实异常, 记日志
M2: VectorDB 新增 list_collections() 封装方法
M3: _remove_by_source 异常记日志, 不再静默吞掉
M4: CLI 集合回退支持 MD_VECTOR_DB_COLLECTION 环境变量
L1: DEFAULT_CONFIG_PATH 自动从项目根目录解析
L2: ingest 命令内重复 import 移至模块顶部
L3: 新增死循环回归测试 + 密集分隔符分块测试
L4: 统一 logger 名称为 md-vector-db
删除旧版审计文档

测试: 48 passed
2026-07-06 13:16:26 +08:00

95 lines
3.2 KiB
Python

"""语义检索模块."""
import logging
from src.core.db import VectorDB
from src.core.embedder import Embedder
logger = logging.getLogger("md-vector-db")
class Searcher:
"""向量检索器."""
def __init__(self, db: VectorDB, embedder: Embedder, collection_name: str):
self.db = db
self.embedder = embedder
self.collection_name = collection_name
@property
def collection(self):
return self.db.get_or_create_collection(self.collection_name)
def search(
self,
query: str,
top_k: int = 10,
source_file: str | None = None,
) -> list[dict]:
"""语义检索, 返回格式化结果列表."""
query_embedding = self.embedder.embed([query])[0]
where_filter = None
if source_file:
where_filter = {"source_file": source_file}
results = self.collection.query(
query_embeddings=[query_embedding],
n_results=top_k,
where=where_filter,
include=["documents", "metadatas", "distances"],
)
formatted = []
if results["ids"] and results["ids"][0]:
for i, doc_id in enumerate(results["ids"][0]):
metadata = results["metadatas"][0][i] if results["metadatas"] else {}
distance = results["distances"][0][i] if results["distances"] else 0.0
# ChromaDB 默认用余弦距离, 转为相似度分数 (0~1)
score = round(1.0 - distance, 4)
formatted.append({
"id": doc_id,
"content": results["documents"][0][i] if results["documents"] else "",
"source_file": metadata.get("source_file", ""),
"section_title": metadata.get("section_title", ""),
"heading_level": metadata.get("heading_level", 0),
"chunk_index": metadata.get("chunk_index", 0),
"score": max(0.0, score),
})
return formatted
def get_collection_info(self) -> dict:
"""获取 collection 信息."""
return {
"name": self.collection_name,
"count": self.collection.count(),
}
def list_sources(self) -> list[str]:
"""列出所有已入库的源文件."""
if self.collection.count() == 0:
return []
result = self.collection.get(include=["metadatas"])
sources = set()
if result and result["metadatas"]:
for m in result["metadatas"]:
if m and "source_file" in m:
sources.add(m["source_file"])
return sorted(sources)
def delete_by_source(self, file_name: str) -> bool:
"""按文件名删除文档 (线程安全)."""
try:
with self.db.write_lock:
existing = self.collection.get(
where={"source_file": file_name}
)
if existing and existing["ids"]:
self.collection.delete(ids=existing["ids"])
return True
except ValueError:
pass # collection 为空时 ChromaDB 抛 ValueError
except Exception:
logger.exception("删除文档失败: %s", file_name)
return False