2.3.0 — the index train: local-first recall index, incremental sweep, stemming + alias expansion
Build and Push Docker Image / build (push) Successful in 14s
Build and Push Docker Image / build (push) Successful in 14s
One schema-bump release (recall-index schema 3, entity-index schema 2; old recall indexes rebuild automatically, no vault migration): - Local-first recall index: the live BM25 index lives in the machine state dir (keyed by endpoint hash); capture's upkeep is a zero-network atomic file write, no advisory lock — replacing the O(vault) GET/PUT-whole-index round trip per write. The vault copy is a snapshot (sweep + session-end) used to seed fresh machines / catch up after another client swept (ECHO_RECALL_SYNC_HOURS, default 24). The read path never PUTs the vault. - Incremental sweep: entity + recall meta carry 16-char content hashes; `sweep --fast` fetches only new/gone/hash-missing notes plus a rotating weekday shard (--all-shards forces everything); deletions drop from both indexes; index-only so no --apply gate. `load` auto-runs it past ECHO_FAST_SWEEP_DAYS (7); doctor reports index freshness. Obsidian-side edits now reach the indexes without manual maintenance. - Stemming + alias expansion: echo_stem.py (conservative Porter-lite, unit- tested families + over-stemming guards) applied at index AND query time; a query fuzzy-matching an entity folds its title/alias vocabulary into the BM25 query at half weight — expansion can only boost docs containing the terms. capture hashes the note's FINAL content (auto-link reordered first). Eval gold set 8 -> 14 queries (6 paraphrases): recall@5/MRR 1.00/1.00 vs keyword baseline 0.75/0.79. +3 unit tests, +10 e2e; test harnesses now isolate ECHO_STATE_DIR. All seven suites green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -215,6 +215,42 @@ def test_links_create_related_section_when_absent() -> None:
|
||||
assert changed and "## Related" in new and "[[resources/concepts/beta]]" in new
|
||||
|
||||
|
||||
def test_stemmer_conflates_inflection_families() -> None:
|
||||
import echo_stem
|
||||
families = [
|
||||
["deploy", "deploys", "deployed", "deploying", "deployment"],
|
||||
["navigate", "navigation", "navigating", "navigational"],
|
||||
["renew", "renews", "renewing"],
|
||||
["prefer", "prefers", "preferring", "preferred"],
|
||||
["penalty", "penalties"],
|
||||
["saturate", "saturation", "saturated"],
|
||||
["frequency", "frequencies"],
|
||||
["expand", "expanded", "expanding"],
|
||||
["meet", "meeting", "meetings"],
|
||||
["service", "services"],
|
||||
]
|
||||
for fam in families:
|
||||
stems = {echo_stem.stem(w) for w in fam}
|
||||
assert len(stems) == 1, f"{fam} -> {stems}"
|
||||
|
||||
|
||||
def test_stemmer_leaves_short_and_risky_tokens_alone() -> None:
|
||||
import echo_stem
|
||||
# over-stemming merges unrelated words — worse than a missed match
|
||||
for w in ("sing", "thing", "was", "is", "bus", "this", "echo", "vault", "mpm"):
|
||||
assert echo_stem.stem(w) == w, f"{w} mangled to {echo_stem.stem(w)}"
|
||||
# documented non-conflations (the -al / d~s irregulars full Porter also skips)
|
||||
assert echo_stem.stem("renewal") != echo_stem.stem("renew")
|
||||
assert echo_stem.stem("expansion") != echo_stem.stem("expand")
|
||||
|
||||
|
||||
def test_content_hash_stable_and_short() -> None:
|
||||
import echo_index as ix
|
||||
a = ix.content_hash("hello world")
|
||||
assert a == ix.content_hash("hello world") and len(a) == 16
|
||||
assert a != ix.content_hash("hello world!")
|
||||
|
||||
|
||||
def _run_all() -> int:
|
||||
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
|
||||
failed = 0
|
||||
|
||||
Reference in New Issue
Block a user