"""检索模块测试.""" import tempfile import pytest from src.core.config import EmbedConfig from src.core.db import VectorDB from src.core.embedder import create_embedder from src.core.ingest import DocumentIngestor from src.core.search import Searcher @pytest.fixture def searcher(): """创建带测试数据的 Searcher.""" tmpdir = tempfile.mkdtemp() db = VectorDB(persist_dir=tmpdir) embedder = create_embedder(EmbedConfig(mode="local")) ingestor = DocumentIngestor(db, embedder, "test_search") # 入库一些测试文档 content = """# Python 入门 Python 是一种解释型编程语言。 ## 安装 Python 从 python.org 下载安装包。 # 向量数据库 ChromaDB 是一个轻量级向量数据库。 ## ChromaDB 安装 使用 pip install chromadb 安装。""" ingestor.ingest_content(content, "guide.md") return Searcher(db, embedder, "test_search") class TestSearcher: """检索器测试.""" def test_search_returns_results(self, searcher): """搜索返回至少一条结果.""" results = searcher.search("Python 编程", top_k=3) assert len(results) > 0 for r in results: assert r["content"] assert r["source_file"] assert "score" in r def test_search_scores_are_descending(self, searcher): """搜索结果按相似度降序排列.""" results = searcher.search("向量数据库", top_k=5) scores = [r["score"] for r in results] assert scores == sorted(scores, reverse=True) def test_search_respects_top_k(self, searcher): """top_k 参数限制返回数量.""" results = searcher.search("安装", top_k=2) assert len(results) <= 2 def test_search_returns_all_fields(self, searcher): """搜索结果包含完整字段.""" results = searcher.search("ChromaDB", top_k=1) if results: r = results[0] assert "content" in r assert "source_file" in r assert "section_title" in r assert "heading_level" in r assert "chunk_index" in r assert "score" in r def test_search_no_results(self, searcher): """无语义匹配时不崩溃.""" results = searcher.search("xyzxyz不存在的内容abcabc", top_k=3) assert isinstance(results, list) def test_list_sources(self, searcher): """list_sources 返回已入库的源文件列表.""" sources = searcher.list_sources() assert isinstance(sources, list) def test_get_collection_info(self, searcher): """get_collection_info 返回 collection 信息.""" info = searcher.get_collection_info() assert info["name"] == "test_search" assert info["count"] > 0 def test_delete_by_source(self, searcher): """delete_by_source 删除源文件的所有 chunks.""" sources_before = searcher.list_sources() if sources_before: target = sources_before[0] result = searcher.delete_by_source(target) assert result is True sources_after = searcher.list_sources() assert target not in sources_after def test_search_with_source_filter(self, searcher): """带 source_file 过滤的搜索.""" sources = searcher.list_sources() if sources: results = searcher.search("测试", top_k=3, source_file=sources[0]) assert isinstance(results, list) for r in results: assert r["source_file"] == sources[0] def test_delete_by_source_nonexistent(self, searcher): """删除不存在的源文件返回 False.""" result = searcher.delete_by_source("nonexistent_file_xyz.md") assert result is False def test_list_sources_empty_collection(self, tmp_path): """空 collection 的 list_sources 返回空列表.""" from src.core.config import EmbedConfig from src.core.db import VectorDB from src.core.embedder import create_embedder from src.core.search import Searcher db = VectorDB(persist_dir=str(tmp_path)) embedder = create_embedder(EmbedConfig(mode="local")) searcher = Searcher(db, embedder, "empty_coll") sources = searcher.list_sources() assert sources == [] def test_list_sources_empty(): """空 collection 的 list_sources 返回空列表.""" from src.core.search import Searcher class EmptyColl: def count(self): return 0 def get(self, **kwargs): return {"ids": [], "documents": [], "metadatas": []} class EmptyDB: def get_or_create_collection(self, name): return EmptyColl() def list_collections(self): return [] class FakeEmb: @property def dimension(self): return 4 def embed(self, texts): return [[0.0] * 4] s = Searcher(EmptyDB(), FakeEmb(), "empty") assert s.list_sources() == []