Skip to content

feat(embedding): apply asymmetric query/document prefixes to embed calls - #1858

Open
NullSense wants to merge 2 commits into
repowise-dev:mainfrom
NullSense:feat/asymmetric-embedding-query-doc-prefix
Open

feat(embedding): apply asymmetric query/document prefixes to embed calls#1858
NullSense wants to merge 2 commits into
repowise-dev:mainfrom
NullSense:feat/asymmetric-embedding-query-doc-prefix

Conversation

@NullSense

Copy link
Copy Markdown

Summary

  • The embedder protocol has one embed() method, called identically for indexing and for querying — nothing says which side a text is on, and no provider prefixes anything. Adds kind: str = "query" | "document" through Embedder.embed(), VectorStore.embed_texts()/search()/search_many(), and every store/provider that calls them, plus resolve_embed_prefix() reading REPOWISE_EMBED_QUERY_PREFIX / REPOWISE_EMBED_DOC_PREFIX (empty by default, so existing deployments are byte-identical).
  • Measured locally (Nemotron-3-Embed-1B, ~1,200 wiki pages, 11 queries): hit@5 went from 3/11 (27.3%) bare to 7/11 (63.6%) with query: / passage: prefixes; hit@10 4/11 → 7/11; MRR 0.236 → 0.332.
  • The three decision-dedup call sites (find_duplicate_decision, find_related_decisions, find_related_decisions_many) now pass kind="document" explicitly, so they keep embedding symmetrically once an asymmetric prefix is configured — left alone, they'd have silently picked up the new default kind="query" through the same VectorStore.search() real document search uses, breaking near-duplicate matching.

Related Issues

None.

Test Plan

  • uv run pytest tests/unit/test_persistence/test_openai_embedder.py tests/unit/server/mcp/test_answer_question_embedding.py tests/unit/server/mcp/test_why_search_payload.py tests/unit/server/mcp/test_keyless_vector_leg.py tests/unit/server/mcp/test_retrieval_leg_visibility.py -q — 92 passed (every file touched to fix a kind-signature break, plus the new prefix tests)
  • uv run ruff check .
  • uv run pytest tests/unit/ -q (full suite) — not verified. A second commit on this branch (test: accept kind= in every embed()/embed_texts() test double) fixes embed()/embed_texts() test doubles in tests/unit/cli, tests/unit/persistence, and tests/unit/server/mcp whose signature didn't accept the new kind parameter, per that commit's own message; I have not independently re-run the full suite to confirm it's green afterward, so I'm not claiming that result here. No production code changes in that second commit.
  • uv run repowise risk main..HEAD:
    Change risk for main..HEAD: touches files that have broken before · 5th percentile of this repo's fix-bearing files
      test: accept kind= in every embed()/embed_texts() test double
      +490 / -54 lines · 25 files · 8 dirs · 2 subsystems · entropy 3.22 · author exp 0
    
    File                                                                   Lines   Prior fixes
    packages/server/src/repowise/server/mcp_server/tool_why.py                2            8.4
    packages/server/src/repowise/server/mcp_server/_answer_pipeline.py        4            4.9
    tests/unit/server/mcp/test_keyless_vector_leg.py                          3            2.9
    tests/unit/cli/test_embedder_resolution.py                                6            2.9
    tests/unit/server/mcp/test_embedder_resolution.py                         3            2.8
    
    Diff shape: Elevated · 68th percentile of recent commits by size and spread
      Riskier than most commits in this repo.
    
    risk flags this as elevated on diff shape (25 files, 8 dirs) — expected for a protocol-signature change that touches every caller, and consistent with the full-suite verification gap noted above rather than a new finding.
    (impacted-tests / health --file need a completed repowise init against this clone; that indexing run did not finish in time and is omitted rather than faked.)

The wrinkle: decision dedup

find_duplicate_decision / find_related_decisions / find_related_decisions_many compare decision text to other decision text, and their own docstring already states the invariant this depends on: "the stored vector and the query side always embed the same shape." Decision vectors are stored via upsert_decision_vectors, which calls embed_texts() at its "document" default, so the dedup lookup has to search at "document" too — but those three functions look up existing decisions through the same generic VectorStore.search() / search_many() real document search uses, whose default kind is "query". All three call sites now pass kind="document" explicitly.

Checklist

  • My code follows the project's code style
  • I have added tests for new functionality
  • All existing tests still pass
  • I have updated documentation if needed

Written with AI assistance; measurements and tests were run and verified locally.

Indexing and querying call the same embed() the same way, so a provider
has no way to tell which side of retrieval a text is on. Models trained
with asymmetric prefixes (E5, BGE, Nomic, NVIDIA Nemotron) are meant to
see "query: " on the search side and "passage: " (or similar) on the
stored side; repowise sends both bare. Measured A/B against a live
corpus (Nemotron-3-Embed-1B, ~1,200 wiki pages, 11 queries): hit@5 went
3/11 -> 7/11, MRR 0.236 -> 0.332.

Adds kind: str = "query" | "document" through Embedder.embed(),
VectorStore.embed_texts()/search()/search_many(), and every provider and
store that implements them, plus resolve_embed_prefix() reading
REPOWISE_EMBED_QUERY_PREFIX / REPOWISE_EMBED_DOC_PREFIX (empty by
default, so an unconfigured deployment's requests stay byte-identical).

Decision near-duplicate matching (find_duplicate_decision and its two
siblings) compares decision text to other decision text and needs both
sides on the same prefix -- its own docstring already says so. Those
three call sites now pass kind="document" explicitly rather than
inheriting search()'s new "query" default, which would have silently
broken dedup for exactly the kind of asymmetric-prefix deployment this
PR is meant to support.
The full unit suite surfaced test doubles across tests/unit/cli,
tests/unit/persistence, and tests/unit/server/mcp whose embed() or
embed_texts() signature didn't accept the new kind parameter. Two
classes of breakage: a bare TypeError swallowed by a broad except
Exception (crud/decisions.py's bulk_upsert_decisions caught it and
silently fell back to per-decision embedding -- 48 embedder round
trips for 48 decisions instead of one batch, and semantic dedup/
supersession stopped merging paraphrases entirely), and a direct
TypeError surfaced through lancedb_store.search().

Fixed by accepting kind (unused, deleted) in each double. No production
code changed in this commit.
@repowise-bot

repowise-bot Bot commented Aug 22, 2026

Copy link
Copy Markdown

✅ Health of changed files: 5.5 → 5.6 (+0.1)
⚠️ Change risk: moderate, riskier than 62% of this repo's commits.

📋 At a glance
5 hotspots touched · 5 files with recent fix history · 1 dead-code finding.

Files & modules (2)
  • packages (4 files)
    • .../vector_store/_base.py
    • .../embedding/ollama.py
    • .../mcp_server/tool_why.py
    • .../mcp_server/_answer_pipeline.py
  • tests (4 files)
    • .../mcp/test_keyless_vector_leg.py
    • .../cli/test_embedder_resolution.py
    • .../persistence/test_vector_store.py
    • .../mcp/test_embedder_resolution.py

✅ Health gate: passed

📌 Before you merge

  • Run .../analysis/test_ff_mcp_payload_decisions.py, .../cli/test_doctor_vector_decisions.py, .../persistence/test_coordinator_health.py, .../persistence/test_embed_recipe.py (+22 more): they import the changed files

🎯 Blast radius (symbols whose signature this PR changed, and who calls them)

  • embed in .../embedding/ollama.py signature changed. Called by 7 symbols outside this PR: .../test_persistence/test_ollama_embedder.py::test_embed_empty_returns_empty, .../test_persistence/test_ollama_embedder.py::test_embed_posts_batch_to_native_endpoint, .../test_persistence/test_ollama_embedder.py::test_embed_raises_when_server_returns_wrong_width_explicit (+4 more)
  • embed in .../embedding/openrouter.py signature changed. Called by 7 symbols outside this PR: .../test_persistence/test_openrouter_embedder.py::test_embed_batch_returns_correct_count, .../test_persistence/test_openrouter_embedder.py::test_embed_empty_returns_empty, .../test_persistence/test_openrouter_embedder.py::test_embed_passes_model_and_input (+4 more)
  • embed in .../embedding/edenai.py signature changed. Called by 6 symbols outside this PR: .../test_persistence/test_edenai_embedder.py::test_embed_empty_returns_empty, .../test_persistence/test_edenai_embedder.py::test_embed_passes_model_and_input, .../test_persistence/test_edenai_embedder.py::test_embed_raises_when_api_returns_wrong_width (+3 more)
  • embed in .../embedding/gemini.py signature changed. Called by 6 symbols outside this PR: .../test_persistence/test_gemini_embedder.py::test_embed_batch_returns_correct_count, .../test_persistence/test_gemini_embedder.py::test_embed_empty_returns_empty, .../test_persistence/test_gemini_embedder.py::test_embed_raises_when_api_returns_wrong_width (+3 more)
  • ...and 1 more changed symbol with outside callers
🔎 More signals (3)

🗺️ Change map

flowchart LR
  subgraph PR ["Changed in this PR (2 modules)"]
    m_packages["packages (13 files)"]:::changed
    m_tests["tests (1 file)"]:::changed
  end
  d_packages["packages"]
  m_tests -->|2 files| d_packages
  classDef changed fill:#dbeafe,stroke:#1d4ed8,color:#1e3a5f
  classDef warn fill:#fef3c7,stroke:#b45309,color:#78350f
  classDef guard fill:#dcfce7,stroke:#15803d,color:#14532d
Loading

Solid arrows: code that imports the changed files (36 direct dependents, from the last indexed snapshot). Dashed: history/tests.

🔥 Hotspots touched (5)

  • .../vector_store/_base.py: 8 commits/90d, 8 dependents · primary owner: Swati Ahuja (52%)
  • .../mcp/test_keyless_vector_leg.py: 5 commits/90d, 0 dependents · primary owner: Raghav Chamadiya (100%)
  • .../embedding/ollama.py: 4 commits/90d, 6 dependents · primary owner: biggiesmallcap-blip (80%)
2 more
  • .../cli/test_embedder_resolution.py: 7 commits/90d, 2 dependents · primary owner: Raghav Chamadiya (99%)
  • .../persistence/test_vector_store.py: 2 commits/90d, 1 dependents · primary owner: Raghav Chamadiya (100%)

💀 Dead code (1 finding)

  • 💀 .../mcp_server/tool_why.py _path_decision_sort_key (confidence 0.65)

👀 Suggested reviewers @RaghavChamadiya


📊 See the full report for this PR
Your repo map with this PR's blast radius lit up, every caller of the contracts it changes, and health before and after. No sign-in. · ⭐ Star Repowise · 📥 Install bot · Silence on a single PR with [skip repowise] in the title · Per-repo toggle on repowise.dev/settings?tab=bot · Updated 2026-08-22 15:51 UTC

@Ayush7614 Ayush7614 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified subset: 107 passed (test_openai_embedder, test_answer_question_embedding, test_why_search_payload, test_keyless_vector_leg, test_retrieval_leg_visibility, test_embed_kind_prefix).\n\nChange is valid and working: adds kind='query'|'document' through Embedder.embed()VectorStore.embed_texts/search/search_many → all providers, with resolve_embed_prefix() reading REPOWISE_EMBED_QUERY_PREFIX/REPOWISE_EMBED_DOC_PREFIX (empty by default → byte-identical). Decision dedup call sites correctly pass kind='document' to keep symmetric embedding. Blast-radius flagged (25 files, 8 dirs) is expected for a protocol-signature change — no new issues spotted, handling of empty text and invalid kind looks correct. LGTM. Note: full tests/unit not re-verified in CI yet due to prior test-double fixes, recommend confirming full suite green before merge.

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