tests/test_fts_recall.py
Rama main · réplica de lectura del repositorio original.
"""BM25 召回腿(SQLite FTS5),取代那條審了十輪的識別碼特例腿。
量過:BM25 identifier hit@3 = 9/12(實際 10-11,兩個是測試 needle 寫錯),
跟識別碼腿與 zvec-grep 打平,真 IDF、~0ms、零 bespoke code。
"""
from __future__ import annotations
import sqlite3
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import fts_recall
import xkb_text
def _mk_index(rows):
"""rows: list of (path, title, kind, body_source_text)."""
tmp = Path(tempfile.mkdtemp()) / "fts_index.db"
db = sqlite3.connect(tmp)
db.execute("CREATE VIRTUAL TABLE docs USING fts5("
"path UNINDEXED, title UNINDEXED, kind UNINDEXED, body, tokenize='unicode61')")
for path, title, kind, body in rows:
db.execute("INSERT INTO docs VALUES (?,?,?,?)",
(path, title, kind, " ".join(xkb_text.tokenize(body))))
db.commit()
db.close()
return tmp
class BM25Leg(unittest.TestCase):
def setUp(self):
self.db_path = _mk_index([
("cards/github_star-ItusiAI-Open-Magiviz.md", "Open-Magiviz", "card",
"Open-Magiviz 開源 AI 影片創作引擎 github ItusiAI"),
("cards/2045420631295242340.md", "GBrain Minions", "card",
"2045420631295242340 GBrain v0.11 Minions BullMQ 任務佇列"),
("cards/2099999999999999999.md", "無關卡片", "card",
"這是一張完全無關的卡片 講別的東西"),
("wiki/topics/ai-agent-memory-systems.md#四層架構", "四層架構", "wiki",
"記憶系統 四層架構 分工 wiki �知識 卡片 對話 聯想"),
])
self._orig_db = fts_recall.FTS_DB
self._orig_conn = fts_recall._conn
fts_recall.FTS_DB = self.db_path
fts_recall._conn = None
def tearDown(self):
if fts_recall._conn:
fts_recall._conn.close()
fts_recall.FTS_DB = self._orig_db
fts_recall._conn = self._orig_conn
def test_repo_slug_hits(self):
out = fts_recall.fts_search("Open-Magiviz", 3)
self.assertTrue(out)
self.assertIn("Open-Magiviz", out[0]["relative_path"])
self.assertEqual(out[0]["score_scale"], "card_bm25")
def test_bare_tweet_id_hits(self):
# zvec-grep 的 FTS 漏掉純數字 ID;識別碼腿特例處理它。BM25 帶進去就有。
out = fts_recall.fts_search("2045420631295242340", 3)
self.assertTrue(out)
self.assertIn("2045420631295242340", out[0]["relative_path"])
def test_cjk_query_hits_via_ngrams(self):
out = fts_recall.fts_search("記憶系統的四層架構", 3)
self.assertTrue(out)
self.assertEqual(out[0]["score_scale"], "wiki_bm25")
def test_wiki_path_strips_the_section_anchor(self):
out = fts_recall.fts_search("四層架構分工", 3)
self.assertTrue(out)
self.assertNotIn("#", out[0]["relative_path"])
def test_empty_or_stopword_query_returns_nothing(self):
self.assertEqual(fts_recall.fts_search("", 3), [])
self.assertEqual(fts_recall.fts_search("的 了 嗎", 3), [])
def test_missing_index_returns_empty_not_crash(self):
fts_recall._conn = None
fts_recall.FTS_DB = Path("/nonexistent/fts_index.db")
self.assertFalse(fts_recall.available())
self.assertEqual(fts_recall.fts_search("anything", 3), [])
def test_scores_are_higher_is_better(self):
out = fts_recall.fts_search("Open-Magiviz 開源 影片", 3)
self.assertGreater(len(out), 0)
self.assertEqual(out, sorted(out, key=lambda r: r["score"], reverse=True))
if __name__ == "__main__":
unittest.main()