Skip to content

fix(search): fall back to episode vector when no fact in scope - #509

Merged
gloryfromca merged 3 commits into
mainfrom
fix/vector-route-reads-episode-vector
Oct 16, 2026
Merged

gloryfromca merged 3 commits into
mainfrom
fix/vector-route-reads-episode-vector

Conversation

@gloryfromca

Copy link
Copy Markdown
Member

Summary

Fixes #502.

method="vector" over episodes recalls through atomic_fact: ANN over facts, max-pool by parent episode, fetch the episodes (SearchManager._maxsim_atomic_recall). When no fact in scope pointed at an episode (extraction disabled, not caught up, or failing), it returned an empty list with HTTP 200 and no log line, while every episode carries its own vector that was never consulted. Keyword kept working, so the store looked half-alive rather than broken.

This PR makes that one exit fall back to ANN over the episode's own vector, with radius applied as on the other dense routes, and logs vector_recall_fact_pool_empty at info with the hit count. Info rather than warning: it also fires in ordinary states, such as a new user or a session whose facts are not extracted yet. The fallback is the same code as the vector_strategy = "episode" branch that existed before 1.1.1.

When any fact is in scope, the MaxSim path is unchanged.

Out of scope, on purpose:

Area

  • Architecture method
  • Benchmark
  • Use case
  • Documentation
  • Developer experience
  • CI, build, or release

Verification

Symptom before and after the fix, on the same store and query. The store was built in an isolated root the way the issue describes: episodes written with EpisodeWriter, then everos cascade rebuild --yes, with a real embedding provider (Qwen3-Embedding-4B). Requests went through create_app() via TestClient, with the lifespan running.

episode      rows = 16  (vector non-null: 16)
atomic_fact  rows = 0
query: 'Which animal did the user bring home recently?'

before (origin/main ec32f4b)
keyword  HTTP 200  episodes=5
vector   HTTP 200  episodes=0  []
hybrid   HTTP 200  episodes=5

after (this branch)
keyword  HTTP 200  episodes=5
vector   HTTP 200  episodes=5  [('Pet adoption', 0.696), ('Tax filing', 0.575), ...]
hybrid   HTTP 200  episodes=5
log: [info] vector_recall_fact_pool_empty  hits=10 owner_id=u502   (hits = recalled before the top_k cut)
atomic_fact rows after the run = 0
pytest tests/unit/test_memory/test_search/ \
       tests/integration/test_tiers/test_upgrade_path.py \
       tests/integration/test_tiers/test_tier2_no_rerank.py      273 passed
pytest -m live_llm tests/e2e/test_search_endpoint_e2e.py::test_search_radius_filters_low_similarity
                                                              1 passed (live embedding)
  (tests/integration/search/test_search_e2e.py: 6 deselected by default markers, not run)
make lint docs-check check-assets check-file-sizes check-deprecated-names
     check-github-docs check-datetime check-openapi check-commits   all pass
make check-cjk   fails on 10 pre-existing hits in 7 files this PR does not touch

Mutation checks:

The full make ci was not run locally; CI runs it.

Checklist

  • I kept the change scoped to the relevant area.
  • I am opening this from a separate branch, not pushing directly to main.
  • I updated docs, examples, or setup notes when behavior changed.
  • I added or updated tests when the change affects behavior.
  • I did not commit secrets, .env files, dependency folders, or generated output.
  • Active relative links in Markdown files resolve.

Notes for Reviewers

  • test_vector_returns_empty_when_no_facts pinned the empty result, which is the defect method=vector over episodes never reads the episode vector column — it depends entirely on atomic_fact #502 reports. It is replaced by test_vector_falls_back_to_episode_vector_when_no_facts, which also pins radius on the fallback.
  • filters.parent_id means different things in the two paths. On atomic_fact it is the episode entry_id; on episode it is the memcell id. With the fallback, a parent_id filter on vector matches episodes by memcell when no fact is in scope. keyword and hybrid already apply the same filter against the episode table this way.
  • No user-facing doc changes. docs/api.md already describes vector as "Dense vector ANN only" and never mentions atomic_fact.
  • Seen while reproducing, not changed here: with no facts, hybrid drops the top dense hit. Pet adoption is first by vector (0.696) but is absent from hybrid's top 5. This looks like the hierarchy ranking when episode_to_facts is empty, which is a separate question from method=vector over episodes never reads the episode vector column — it depends entirely on atomic_fact #502.

By submitting this pull request, I agree that my contribution is licensed under
the Apache License 2.0.

🤖 Generated with Claude Code

gloryfromca and others added 2 commits October 16, 2026 15:57
method="vector" over episodes recalls through atomic_fact (MaxSim) and
returned an empty list with HTTP 200 whenever no fact in scope pointed
at an episode: extraction disabled, not caught up, or failing. Every
episode carries its own vector that was never consulted, so vector
search went silently dead while keyword kept working.

Fall back to ANN over the episode vector in that case, with radius
applied as on the other dense routes, and log a warning. When any fact
is in scope the MaxSim path is unchanged.

The unit test that pinned the empty result is replaced: returning
nothing was the defect reported in #502, not a requirement.

Refs: #502

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fallback fires in ordinary states too (a new user, a session whose
facts are not extracted yet), so a per-request warning would be noise.
Log at info with the number of hits instead.

Make test_search_radius_filters_low_similarity the end-to-end pin for
#502: it seeds episodes only, so it now asserts that vector search
finds them. Before the fallback it could not fail (0 <= 0).

Correct comments that still said VECTOR never reads the episode
vector, and the docstring that said MaxSim pools by memcell (it pools
by parent episode).

Refs: #502

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs: #502

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gloryfromca
gloryfromca merged commit 736b821 into main Oct 16, 2026
11 checks passed
@gloryfromca
gloryfromca deleted the fix/vector-route-reads-episode-vector branch October 16, 2026 08:47
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.

method=vector over episodes never reads the episode vector column — it depends entirely on atomic_fact

2 participants