The product path pulled every atom into Python and rebuilt a BM25Okapi
index per query: p50 20.16s over 165,973 atoms. It now runs one SQLite
statement with LIMIT/OFFSET.
- text_index.py: one normalizer for both sides of the index. CJK runs are
split per character (unicode61 cannot segment them), camelCase and
separators are split so "search atoms" finds SearchAtoms, latin tokens
get a prefix match. Rows written before this existed hold raw text in
atom_fts, so schema v3 rebuilds the index on upgrade.
- retrieval.py: filters (holder / scenario / source / modality / since /
until / min_trust), a base64 cursor, and reciprocal rank fusion in place
of the 0.55/0.35/0.10 blend whose graph term was always zero.
- An empty result is {status: "no_match"}, not a bare array.
- Go: search_atoms and compile_context rank through POST /api/search. The
second Go tokenizer is deleted; two implementations could drift, which
is what the MCP/HTTP parity requirement forbids.
- db.py passed 500 lines, so the row codec moved to row_codec.py.
- Tooling: installed the repo's own pyright. pyrightconfig.json had no
exclude and swept the downloaded corpus (1171 errors); with excludes the
count is 62, all pre-existing (evals 48, profiles 4, training 4,
ingestion 3, runtime 2, cartridge_assets 1). backend/pyproject.toml
carried a second [tool.pyright] that shadowed the root config; deleted.
- bench/scale_check.py measures the product search at corpus size. The
100k-atom p50 number is not written until it is measured on an idle
machine.
Verification: 112 pytest passed; go build/vet/test clean, gofmt clean;
ruff clean on every touched file; mypy clean on the 14 touched Python
files; repo pyright 62 errors, unchanged per-file ledger.
76 lines
3.1 KiB
Python
76 lines
3.1 KiB
Python
"""Checks for the FTS normalizer, run against a real FTS5 table.
|
|
|
|
String equality alone would pass a normalizer SQLite cannot use, so every acceptance case
|
|
here inserts indexed text and asks the engine the question the user would ask.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
|
|
import pytest
|
|
|
|
from ely_eye.text_index import fts_query, index_text
|
|
|
|
|
|
def _fts(rows: list[str]) -> sqlite3.Connection:
|
|
connection = sqlite3.connect(":memory:")
|
|
connection.execute("CREATE VIRTUAL TABLE fts USING fts5(original UNINDEXED, text, tokenize = 'unicode61')")
|
|
connection.executemany("INSERT INTO fts(original, text) VALUES (?, ?)", [(row, index_text(row)) for row in rows])
|
|
return connection
|
|
|
|
|
|
def _hits(connection: sqlite3.Connection, query: str) -> list[str]:
|
|
return [row[0] for row in connection.execute("SELECT original FROM fts WHERE fts MATCH ?", (fts_query(query),))]
|
|
|
|
|
|
def test_index_text_splits_a_chinese_sentence_into_characters() -> None:
|
|
assert index_text("我们的钥匙放在玄关抽屉里") == "我 们 的 钥 匙 放 在 玄 关 抽 屉 里"
|
|
|
|
|
|
def test_index_text_splits_identifiers_a_user_cannot_guess_the_casing_of() -> None:
|
|
assert index_text("func SearchAtoms") == "func search atoms"
|
|
assert index_text("def search_atoms(self) -> None:") == "def search atoms self none"
|
|
assert index_text("HTTPServer2 listens on 8765") == "http server 2 listens on 8765"
|
|
|
|
|
|
def test_index_text_folds_full_width_letters_that_search_would_miss() -> None:
|
|
assert index_text("Search Atoms") == "search atoms"
|
|
|
|
|
|
def test_fts_query_phrases_cjk_and_prefixes_latin() -> None:
|
|
assert fts_query("钥匙") == '"钥 匙"'
|
|
assert fts_query("search atoms") == '"search"* "atoms"*'
|
|
|
|
|
|
def test_fts_query_refuses_a_query_with_nothing_to_match() -> None:
|
|
for empty in ("", " ", "?!。"):
|
|
with pytest.raises(ValueError, match="needs at least one searchable word"):
|
|
fts_query(empty)
|
|
|
|
|
|
def test_a_chinese_question_finds_the_chinese_memory() -> None:
|
|
connection = _fts(["我们的钥匙放在玄关抽屉里", "The keys are in the kitchen drawer"])
|
|
assert _hits(connection, "钥匙") == ["我们的钥匙放在玄关抽屉里"]
|
|
|
|
|
|
def test_a_cjk_phrase_will_not_match_its_characters_scattered_apart() -> None:
|
|
connection = _fts(["钥匙在抽屉", "匙 在 钥"])
|
|
assert _hits(connection, "钥匙") == ["钥匙在抽屉"]
|
|
|
|
|
|
def test_an_english_query_finds_the_identifier_it_names() -> None:
|
|
connection = _fts(["func SearchAtoms(ctx context.Context)", "func ListCartridges()"])
|
|
assert _hits(connection, "search atoms") == ["func SearchAtoms(ctx context.Context)"]
|
|
|
|
|
|
def test_a_prefix_reaches_the_word_it_grew_into() -> None:
|
|
connection = _fts(["the atom was forgotten", "unrelated text"])
|
|
assert _hits(connection, "atom") == ["the atom was forgotten"]
|
|
assert _hits(connection, "forg") == ["the atom was forgotten"]
|
|
|
|
|
|
def test_a_mixed_query_needs_both_halves_in_the_same_memory() -> None:
|
|
connection = _fts(["钥匙放在 drawer 里", "钥匙在玄关", "the drawer is empty"])
|
|
assert _hits(connection, "钥匙 drawer") == ["钥匙放在 drawer 里"]
|