Skip to content

test(search): pin that /search never answers with a blank content (pm-0918-c-02) - #1705

Merged
arkash20 merged 1 commit into
mainfrom
fix/pm-0918-c-02-search-content
Sep 23, 2026
Merged

arkash20 merged 1 commit into
mainfrom
fix/pm-0918-c-02-search-content

Conversation

@arkash20

Copy link
Copy Markdown
Contributor

What this is

pm-0918-c-02 reported that for ~75 minutes after a 3,500-chunk bulk write, POST /api/v1/search returned rows whose content was empty — 17% of the top-50 — on a fresh store written and queried immediately. The stored rows were fine (0 empty-content rows across 104,437 local memories), so the claim was that the emptiness was in the response.

The previous investigation cleared the recall path and explicitly flagged one thing it had not traced: "a serialiser on the REST /search route rather than the recall route I traced". This PR is that trace, and its outcome: the OSS /search route is clear, plus the coverage that was missing for the state the report describes.

What was traced, and how it was cleared

Path Verdict Evidence
POST /api/v1/searchSearchResponse(items: list[MemoryOut]) clear MemoryOut.content is str with no default — a row without content raises, it does not degrade to ""
/search vs /recall field set identical both build rows with the same _memory_to_out; a live run of both routes returns the same 35 keys and the same content. Recall's only difference is stripping platform keys from metadata (C25), which never touches content
scored-search projection clear storage projects MEMORY_LIST_FIELDS (MEMORY_FIELDS minus embedding/search_vector); the outer query selects the whole Memory entity
embedding-state branches traced, no blanking passes_relevance_filter short-circuits on has_embedding is False — an un-embedded row bypasses the similarity floor and is admitted on its full-text match alone. That changes admission, not serialisation. It is the mechanism that links this row to ax-0917-h-06
successor injection traced LoadAndSerialize appends find_successors rows that never pass through the scored-search projection; storage returns them with MEMORY_FIELDS, so content is present
entity-lookup short-circuit (/load-by-ids) clear MEMORY_FIELDS
truncation / summarisation knobs none on this route CAURA_SOURCE_ONLY / CAURA_SHOW_TITLE do not exist anywhere in this repo — they are the caller's own adapter
"the other search router" not it the only other search route is /documents/search, whose rows carry data, not content at all

The tests

Three serialisation paths can produce a /search row, and only the first had a route-level test:

  1. scored search with a vector;
  2. scored search without one — the post-bulk-write window, the state in the report;
  3. successor injection — rows that skip the scored-search projection entirely.

Four cases drive the real route (httpx against the app, real Postgres) in all three, and assert content is non-blank in every returned row. The fourth is the guard: it feeds injection a row with content removed — the shape a narrowed storage projection produces, since orm_to_dict reads every field with getattr(obj, f, None).

The guard is mutation-verified in both directions. Giving MemoryOut.content a "" default does not make it pass (_memory_to_out passes the key explicitly, so None still fails validation). What does make it pass is content=_mem_attr(memory, "content") or "" — the or "" idiom used on eight other content reads in this codebase, just not on this one. That single-token edit is the realistic defect, and it is what this case catches.

Two harness facts the tests compensate for

Both are why path 2 had never been covered, and both are worth knowing beyond this PR:

  • full-text match is dead suite-wide. The test database is built from the ORM models, so migration 001's search_vector trigger does not exist and the column is NULL on every row. Since FTS is the only admission route an un-embedded row has, nothing could reach that path. _index_for_fts rebuilds the vector for one tenant with migration 034's own expression.
  • embedding_pending is not evidence of the state. track_task runs the deferred re-embed in-process, so rows written by a deployment_mode="deferred" bulk are embedded again before the next await — measured: vectors NULL in the database, present by the time the search ran one statement later. The flag stays True regardless (only core-worker clears it), so the cases assert on has_embedding from the diagnostic instead.

What this does NOT settle

No defect was found, and that is a real result rather than a stalled one — but it is scoped to this repo. The residual gap is unchanged from the original investigation and is the same one cheap ask:

  • the raw /search response from run caura-bulk-2k-top50-sess showing one empty-content row WITH ITS MEMORY ID. With an id we can say whether the row is a fan-out child, a source chunk, or something else, and whether its stored content was ever empty.
  • the deployed read surface is wider than this repo — requests reach OSS /search through the enterprise gateway, which is not traced here.

🤖 Generated with Claude Code

@arkash20
arkash20 requested a review from a team as a code owner September 23, 2026 11:41
@arkash20
arkash20 force-pushed the fix/pm-0918-c-02-search-content branch from 0e52a83 to 1666f0f Compare September 23, 2026 11:41
@github-actions

Copy link
Copy Markdown
Contributor

Claude Code Review — skipped: PR author 'arkash20' is not a public member of the 'caura-ai' org

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

Claude Code Review — skipped: PR author 'arkash20' is not a public member of the 'caura-ai' org

@arkash20

Copy link
Copy Markdown
Contributor Author

@Eldad-Caura please approve

@arkash20
arkash20 force-pushed the fix/pm-0918-c-02-search-content branch from 1666f0f to 4cc5d4c Compare September 23, 2026 20:35
@github-actions

Copy link
Copy Markdown
Contributor

Claude Code Review — skipped: PR author 'arkash20' is not a public member of the 'caura-ai' org

@arkash20
arkash20 force-pushed the fix/pm-0918-c-02-search-content branch from 4cc5d4c to eefc904 Compare September 23, 2026 20:47
@github-actions

Copy link
Copy Markdown
Contributor

Claude Code Review — skipped: PR author 'arkash20' is not a public member of the 'caura-ai' org

pm-0918-c-02 reported that for ~75 minutes after a 3,500-chunk bulk write,
POST /api/v1/search returned rows whose `content` was empty — 17% of the
top-50 — while the stored rows were fine. The previous investigation cleared
the recall path and flagged one thing it had not traced: a serialiser on the
REST /search route itself.

Traced, and the route is clear. /search and /recall build their rows with the
same `_memory_to_out` into the same `MemoryOut`, and the two responses carry a
byte-identical 35-key field set (verified against a live route, not by
reading). `MemoryOut.content` is `str` with no default, so a row that reaches
the serialiser without content raises rather than degrading to "". There is no
projection, no embedding-state branch and no truncation knob on this route that
can blank the field.

What there was no coverage for is the state the report describes. Three
serialisation paths can produce a search row — scored search with a vector,
scored search WITHOUT one (the post-bulk-write window, where
`passes_relevance_filter` short-circuits on `has_embedding is False` and skips
the similarity floor entirely), and successor injection, whose rows never pass
through the scored-search projection at all. Only the first had a route-level
test. These four cases drive the real route in all three, plus a guard that
feeds injection a row with `content` removed — the shape a narrowed storage
projection would produce.

The guard is mutation-verified in both directions: giving `MemoryOut.content` a
`""` default does NOT make it pass (`_memory_to_out` passes the key explicitly,
so `None` still fails validation), while
`content=_mem_attr(memory, "content") or ""` does — the `or ""` idiom used on
eight other content reads in this codebase, just not on this one. That
single-token edit is what the case exists to catch.

Two harness facts the tests compensate for, both of which are why path 2 had
never been covered: the test database is built from the ORM models, so
migration 001's `search_vector` trigger does not exist and full-text match is
dead suite-wide; and `track_task` runs the deferred re-embed in-process, so a
"deferred" bulk write is embedded again before the next await, which makes
`embedding_pending` useless as evidence of the state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Signed-off-by: Arkady Mankovsky <arkash20@gmail.com>
@arkash20
arkash20 force-pushed the fix/pm-0918-c-02-search-content branch from eefc904 to d99e505 Compare September 23, 2026 20:58
@github-actions

Copy link
Copy Markdown
Contributor

Claude Code Review — skipped: PR author 'arkash20' is not a public member of the 'caura-ai' org

@arkash20
arkash20 merged commit d3965c0 into main Sep 23, 2026
14 checks passed
@arkash20
arkash20 deleted the fix/pm-0918-c-02-search-content branch September 23, 2026 21:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants