Keep the local index fresh from the change feed and improve ranking
- Track a feed cursor in the local index: reindex takes the head cursor before listing and commits it with the snapshot; reads refresh from /changes (bounded, throttled) and apply creates, updates, archives, restores and hard deletes atomically with the cursor. Existing cursorless indexes bootstrap tracking on first read. Failures answer locally with explicit freshness warnings; 400 triggers a full reindex, 404 falls back to timestamp sync. - Rank by a weighted score: exact identity (including aliases and paths), term coverage including body, intent, bm25, authority, a status penalty for superseded/completed records and mild recency. - Return query-centred FTS5 snippets (never for E2E nodes). - FTS schema v2: porter stemming, prefix index, title column, rowid deletes, two-character terms, domain words no longer stopwords; v1 indexes upgrade in place. - Hyphenated alias/path literals match as adjacent-token phrases and rank as exact identity. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7eaaffb63f
commit
0938b3026e
@@ -153,6 +153,18 @@ def indexed_node(path, *, node_type="note", tags=None, status="active", content=
|
||||
}
|
||||
|
||||
|
||||
def feed_provider(calls):
|
||||
"""Synthetic server: empty change feed and no newer nodes."""
|
||||
|
||||
def request(method, path, payload=None, **_kwargs):
|
||||
calls.append((method, path))
|
||||
if "/changes?" in path:
|
||||
return {"items": [], "next_cursor": "c0", "has_more": False}
|
||||
return {"items": [], "next_cursor": None}
|
||||
|
||||
return request
|
||||
|
||||
|
||||
def configure_local_index(mod, tmp_path):
|
||||
mod.AUTH_DIR = tmp_path
|
||||
mod.INDEX_PATH = tmp_path / "index.sqlite"
|
||||
@@ -208,11 +220,14 @@ def test_complete_local_compact_browse_uses_keyset_cursor_without_http(tmp_path)
|
||||
item["updated_at"] = f"2026-08-2{index}T00:00:00Z"
|
||||
mod.index_node(item)
|
||||
mod.set_local_graph_index_complete("project-1", True)
|
||||
calls = []
|
||||
mod.TOKEN = "disposable-test-token" # synthetic login; requests go to feed_provider only
|
||||
mod.request_json = feed_provider(calls)
|
||||
# The first read starts change-feed tracking for this cursorless index.
|
||||
mod.discover_memory({"folder_path": "/plans", "limit": 1, "view": "paths"})
|
||||
assert [path.split("?")[0].rsplit("/", 1)[-1] for _method, path in calls] == ["changes", "nodes"]
|
||||
calls.clear()
|
||||
|
||||
def unexpected_request(*_args, **_kwargs):
|
||||
raise AssertionError("complete local compact browse must not make an HTTP request")
|
||||
|
||||
mod.request_json = unexpected_request
|
||||
first = mod.discover_memory(
|
||||
{
|
||||
"folder_path": "/plans",
|
||||
@@ -241,6 +256,9 @@ def test_complete_local_compact_browse_uses_keyset_cursor_without_http(tmp_path)
|
||||
)
|
||||
assert not ({item["node_id"] for item in first["items"]} & {item["node_id"] for item in second["items"]})
|
||||
assert all(item["node_id"] for item in first["items"] + second["items"])
|
||||
# Steady state inside the refresh throttle window: no HTTP at all.
|
||||
assert calls == []
|
||||
assert first["freshness"]["feed_sync"] == "throttled"
|
||||
|
||||
|
||||
def test_local_compact_browse_cursor_is_bound_to_filters(tmp_path):
|
||||
@@ -482,11 +500,13 @@ def test_complete_filtered_local_search_stays_off_network(tmp_path):
|
||||
)
|
||||
)
|
||||
mod.set_local_graph_index_complete("project-1", True)
|
||||
calls = []
|
||||
mod.TOKEN = "disposable-test-token" # synthetic login; requests go to feed_provider only
|
||||
mod.request_json = feed_provider(calls)
|
||||
mod.search_memory({"q": "facturación", "tag": "billing", "limit": 1, "view": "summary"})
|
||||
assert len(calls) == 2 # one-time change-feed bootstrap: head + catch-up
|
||||
calls.clear()
|
||||
|
||||
def unexpected_request(*_args, **_kwargs):
|
||||
raise AssertionError("a complete filtered local result must not use the network")
|
||||
|
||||
mod.request_json = unexpected_request
|
||||
result = mod.search_memory(
|
||||
{
|
||||
"q": "facturación",
|
||||
@@ -500,6 +520,8 @@ def test_complete_filtered_local_search_stays_off_network(tmp_path):
|
||||
assert [item["full_path"] for item in result["items"]] == [
|
||||
"/documentation/facturacion-electronica"
|
||||
]
|
||||
assert calls == [] # a complete, tracked, recently refreshed index stays off the network
|
||||
assert not result.get("warnings")
|
||||
|
||||
|
||||
def test_discovery_schema_documents_dynamic_sort_and_intent():
|
||||
|
||||
Reference in New Issue
Block a user