""" Knowledge Base 語意搜尋整合測試 ================================= KB Phase 2: pgvector semantic search E2E 建立時間: 2026-04-04 (台北時區) 建立者: Claude Code (KB Phase 2 整合測試) 規則: - 使用真實 awoooi_dev DB + Ollama embedding - 每個測試後 rollback - 禁止 Mock """ import pytest from src.models.knowledge import EntryType, KnowledgeEntryCreate from src.repositories.knowledge_repository import KnowledgeDBRepository # ============================================================================= # Tests # ============================================================================= @pytest.mark.asyncio async def test_save_embedding(db_session): """save_embedding 正確寫入 pgvector 欄位""" repo = KnowledgeDBRepository(db_session) data = KnowledgeEntryCreate( title="K8s Pod OOMKilled 處理流程", content="當 Pod 因記憶體不足被 OOMKilled 時,應先增加 memory limit 再滾動重啟。", entry_type=EntryType.RUNBOOK, category="基礎設施", source="human", ) entry = await repo.create(data) # 手動產生假 embedding (768 維) fake_embedding = [0.01] * 768 success = await repo.save_embedding(entry.id, fake_embedding) assert success is True @pytest.mark.asyncio async def test_semantic_search_returns_results(db_session): """semantic_search 能查到相關條目""" repo = KnowledgeDBRepository(db_session) # 建立一筆條目 data = KnowledgeEntryCreate( title="SLA 監控與告警設定", content="設定 SLA 告警:當 API 回應時間超過 500ms 時發送 Telegram 告警。", entry_type=EntryType.BEST_PRACTICE, category="應用層", source="human", ) entry = await repo.create(data) # 存入 embedding (模擬向量) embedding_a = [0.5] * 768 await repo.save_embedding(entry.id, embedding_a) # 用相近向量搜尋,應能找到 query_embedding = [0.5] * 768 # 完全相同 → cosine similarity = 1.0 results = await repo.semantic_search(query_embedding, limit=5, threshold=0.9) assert len(results) >= 1 ids = [e.id for e, _ in results] assert entry.id in ids @pytest.mark.asyncio async def test_semantic_search_threshold_filters(db_session): """threshold 過濾:低相似度條目不出現在結果 使用兩個方向幾乎相反的向量(正負交替)確保 cosine similarity < threshold """ repo = KnowledgeDBRepository(db_session) data = KnowledgeEntryCreate( title="Redis 連線逾時排查", content="Redis 連線逾時通常由網路抖動或 Redis 記憶體滿導致。", entry_type=EntryType.RUNBOOK, category="基礎設施", source="human", ) entry = await repo.create(data) # 存入正交向量 A:奇數維 1.0,偶數維 0.0 emb_a = [1.0 if i % 2 == 0 else 0.0 for i in range(768)] await repo.save_embedding(entry.id, emb_a) # 查詢向量 B:奇數維 0.0,偶數維 1.0 → 與 A 完全正交,cosine similarity = 0 emb_b = [0.0 if i % 2 == 0 else 1.0 for i in range(768)] # threshold=0.5 → 正交向量 (score=0) 應被過濾掉 results = await repo.semantic_search(emb_b, limit=5, threshold=0.5) ids = [e.id for e, _ in results] assert entry.id not in ids @pytest.mark.asyncio async def test_semantic_search_archived_excluded(db_session): """archived 條目不出現在語意搜尋結果""" repo = KnowledgeDBRepository(db_session) data = KnowledgeEntryCreate( title="已封存的測試文章", content="此文章已被封存,不應出現在搜尋結果。", entry_type=EntryType.BEST_PRACTICE, category="應用層", source="human", ) entry = await repo.create(data) embedding = [0.8] * 768 await repo.save_embedding(entry.id, embedding) # 封存 await repo.delete(entry.id) # 搜尋不應包含已封存條目 results = await repo.semantic_search(embedding, limit=10, threshold=0.5) ids = [e.id for e, _ in results] assert entry.id not in ids @pytest.mark.asyncio async def test_list_unembedded_entries(db_session): """list_unembedded_entries 正確列出未 embed 條目""" repo = KnowledgeDBRepository(db_session) # 建立兩筆條目,一筆有 embedding、一筆沒有 data_a = KnowledgeEntryCreate( title="有 Embedding 的條目", content="已完成向量化。", entry_type=EntryType.RUNBOOK, category="基礎設施", source="human", ) data_b = KnowledgeEntryCreate( title="沒有 Embedding 的條目", content="尚未向量化。", entry_type=EntryType.RUNBOOK, category="基礎設施", source="human", ) entry_a = await repo.create(data_a) entry_b = await repo.create(data_b) # 只給 entry_a embedding await repo.save_embedding(entry_a.id, [0.1] * 768) rows = await repo.list_unembedded_entries() row_ids = [r[0] for r in rows] assert entry_b.id in row_ids assert entry_a.id not in row_ids