Skip to content

fix(recall): embed the QUESTION as a query, not as a memory - #319

Merged
jayesh-keychain merged 2 commits into
mainfrom
fix/asymmetric-query-embedding
Aug 1, 2026
Merged

fix(recall): embed the QUESTION as a query, not as a memory#319
jayesh-keychain merged 2 commits into
mainfrom
fix/asymmetric-query-embedding

Conversation

@jayesh-keychain

Copy link
Copy Markdown
Collaborator

Asking "What is my name?" answered "I don't have a memory about that" while "My name is Jayesh" sat in the corpus, encoded and indexed.

Cause: mxbai-embed-large is trained asymmetrically — documents are embedded bare, but a retrieval query must carry the prompt "Represent this sentence for searching relevant passages: ". ask() called embed(), the same method the encoder uses for memory text, so every query vector landed in a systematically different place from the corpus it was compared against. Cosine was depressed for all queries, not one.

Measured on the live corpus, "What is my name?" vs "My name is Jayesh":

top cosine confidence result
without the prompt 0.5337 0.4410 miss
with the prompt 0.5663 0.4695 answers

It missed the §05.6 floor by 0.009. Retrieval was never broken — the question was being asked in the wrong vector space.

Changes

  • EmbeddingProvider gains an optional embedQuery. Symmetric providers (OpenAI, Voyage, fake) omit it and the shared embedQueries helper falls back to embed — nothing changes for them, and no call site needs to know which providers are asymmetric.
  • Ollama implements it via queryPrefixFor(model), keyed by model-name prefix so a :latest tag or quantisation suffix still matches. Documents deliberately do not get the prefix — the asymmetry is the trained behaviour.
  • Only the recall path changes; encode still calls embed, so stored vectors keep their meaning and no re-encode is required.

Verification — same questions that failed before, against the live stack:

  • "What is my name?" → "My name is Jayesh" (1 citation)
  • "Who am I?" → "My name is Jayesh." (1 citation)
  • "What is Priya allergic to?" → "peanuts" (1 citation)
  • type-check, lint, build green; 82 embedding/LLM/model-router unit tests pass unchanged

Deliberately not included: the §05.6 confidence bands are still calibrated for a different model's cosine distribution (relevant pairs land ~0.5–0.6 here against a 0.45 miss floor). Retuning is eval-gated (#257), not a guess. This PR only fixes the query encoding that was plainly wrong.

🤖 Generated with Claude Code

Asking "What is my name?" answered "I don't have a memory about that" while
the memory "My name is Jayesh" sat in the corpus, encoded and indexed.

Cause: `mxbai-embed-large` is trained ASYMMETRICALLY. Documents are embedded
bare, but a retrieval query must carry the prompt "Represent this sentence for
searching relevant passages: ". `ask()` called `embed()` for the question, the
same method the encoder uses for memory text, so every query vector landed in a
systematically different place from the corpus it was being compared against.
Cosine was depressed across the board — not for one query, for all of them.

Measured on the live corpus, "What is my name?" vs "My name is Jayesh":

    without the prompt   top cosine 0.5337 → confidence 0.4410 → MISS
    with the prompt      top cosine 0.5663 → confidence 0.4695 → answers

It missed the §05.6 floor by 0.009. The retrieval was never broken; the
question was simply being asked in the wrong vector space.

  - `EmbeddingProvider` gains an OPTIONAL `embedQuery`. Symmetric providers
    (OpenAI, Voyage, the fake) omit it and the shared `embedQueries` helper
    falls back to `embed`, so nothing changes for them and no call site needs
    to know which providers are asymmetric.
  - The Ollama provider implements it via `queryPrefixFor(model)`, keyed by
    model-name prefix so a `:latest` tag or a quantisation suffix still matches
    (mxbai and nomic use different schemes). Documents deliberately do NOT get
    the prefix — the asymmetry IS the trained behaviour, and prefixing both
    sides would be exactly as wrong as prefixing neither.
  - Only the recall path changes. The encode path still calls `embed`, so
    stored vectors keep their meaning and no re-encode is required.

Verified against the live stack (Ollama mxbai-embed-large + gemma3:4b), same
questions that failed before:

    "What is my name?"          → "My name is Jayesh"   1 citation
    "Who am I?"                 → "My name is Jayesh."  1 citation
    "What is Priya allergic to?" → "peanuts"            1 citation

type-check, lint, build green; the 82 embedding/LLM/model-router unit tests
pass unchanged.

NOT included, deliberately: the §05.6 confidence bands are still calibrated for
a different model's cosine distribution (relevant pairs land ~0.5-0.6 here, and
the miss floor is 0.45). Retuning them is an eval-gated change (#257), not a
guess — this commit only fixes the query encoding that was plainly wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI `test (3)` failed on two suites with:

  [vitest] No "embedQueries" export is defined on the
  "../src/modules/memory/embedding/index.js" mock

Both suites `vi.mock` the embedding module with an explicit factory, so adding
a new export to the real module leaves the mock incomplete — `ask()` now calls
`embedQueries`, which the factory did not provide. A mock factory replaces the
module wholesale; it does not inherit new exports.

The stub mirrors the real helper's fallback (no `embedQuery` on the provider →
plain `embed`), which is exactly the behaviour these suites depend on: they
assert recall wiring, not query-prefixing, so the vectors they get are
unchanged and neither assertion moves.

Both suites pass locally; lint green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jayesh-keychain
jayesh-keychain merged commit f8d07cb into main Aug 1, 2026
6 checks passed
@jayesh-keychain
jayesh-keychain deleted the fix/asymmetric-query-embedding branch August 1, 2026 05: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.

1 participant