Skip to content

A question picks its definitions instead of taking the first thirty - #575

Merged
WaylandYang merged 7 commits into
devfrom
feat/a-question-picks-its-definitions
Sep 9, 2026
Merged

A question picks its definitions instead of taking the first thirty#575
WaylandYang merged 7 commits into
devfrom
feat/a-question-picks-its-definitions

Conversation

@WaylandYang

Copy link
Copy Markdown
Contributor

Closes #574. Stacked on #571 (both touch the prompt block in chat.rs); retarget to dev once the stack lands.

Chat read its definitions with confirmed(kb, 30) — the first thirty by concept name. The seeded upper bound (27 definitions, 17/18 on wide) sat three short of that cap, and a base with a hundred definitions would leave seventy outside the prompt on every question.

What changes

  • Definitions are retrievable. concept_mappings gains embedding / embedded_model / embedded_text (migration 0048), the ontology index’s shape: re-embedded when the model or the text changes, refreshed lazily before a lookup.
  • Two channels, RRF-fused, top 8 into the prompt (mapping_index): vector (“name: summary (unit)”, pgvector) and lexical (every confirmed definition scored in Rust by the question’s tokens — ASCII words, CJK bigrams; no pg_trgm). Degradation: both → lexical only → the first k by name when nothing matches. Never “no definitions”.
  • GET /kbs/{id}/mappings/relevant?q=&k= exposes the same lookup, so the bench can score it and a page can later show which definitions a question used.
  • ask.mjs --recall K measures recall@k without asking anything (seconds); mappings.mjs --also <corpus> mounts a second source so a base can exceed the old cap.

Measured

Two sources in one base, both truths seeded, 45 confirmed definitions:

recall@8 chat right single-source seeded, for reference
tpch, 24 questions 23/24 24/24 24/24
wide, 18 questions 13/18 15/18 17/18

Selection kept the tpch upper bound with 45 definitions in play (the old cap would have dropped fifteen of them). Wide lost two questions, and the recall misses are not near-name pairs: they are Chinese questions against English seeded definitions whose summaries are bench placeholders (Paid orders — bench truth). The lever is the embedded text, not a reranker — details on #574.

Verified

  • Unit: tokenizer (both languages, single ASCII characters dropped), lexical ranking (most shared tokens first, no-match empty).
  • DB test: only changed or unembedded rows are re-embedded; vector search orders by distance and skips mismatched dimensions; by_ids keeps the requested order.
  • The bench rows above on a base migrated by this branch; all 45 rows embedded on first lookup.

🤖 Generated with Claude Code

@WaylandYang

Copy link
Copy Markdown
Contributor Author

Re-measured after afe152e6 (seeded definitions carry a real summary): wide recall@8 18/18, wide chat 18/18; tpch recall 23/24 (the same paraphrase miss, answered correctly by chat). Table and reading on #574.

@WaylandYang

WaylandYang commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Review before merge. Two fixes:

  • The refresh had no bound on the request path. relevant awaits refresh before the turn is answered, and needing_embedding returned every stale row, so the first question after a change of embedding model (or after this migration, on a base with many confirmed definitions) embedded all of them serially before speaking, and concurrent turns repeated the work. A turn now embeds at most 64 rows (REFRESH_PER_TURN); the rest are picked up by the following turns, and the lexical channel covers the gap meanwhile. The store function takes a limit, and the test asserts it.
  • The vector channel compared across models. vector_search filtered on dimension only, so after a model switch to one of equal dimension, rows not yet re-embedded were ranked against the new model's query. It now takes the model and compares only rows embedded by it; a stale row falls back to the lexical channel instead of a false distance. Test case added.

Noted, not changed: the lexical channel matches tokens as substrings, so short English words ("is", "in") hit inside longer ones; it is noisy alone, but it only stands alone when no embedding model is configured, and RRF with the vector channel washes it out otherwise.

Not re-measured after these two changes. At 45 rows the bound does not bite and the base has one model, so the numbers on #574 should stand; I will re-run --recall 8 on the merged dev before closing #574.

@WaylandYang
WaylandYang force-pushed the feat/exploration-describes-the-data branch from fcf3844 to c933025 Compare September 9, 2026 23:34
Base automatically changed from feat/exploration-describes-the-data to dev September 9, 2026 23:37
WaylandYang and others added 6 commits September 10, 2026 07:38
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
…k in

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
… own model

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
@WaylandYang
WaylandYang force-pushed the feat/a-question-picks-its-definitions branch from a864948 to 774d99c Compare September 9, 2026 23:38
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
@WaylandYang
WaylandYang merged commit 0fc44a7 into dev Sep 9, 2026
4 checks passed
@WaylandYang
WaylandYang deleted the feat/a-question-picks-its-definitions branch September 9, 2026 23:43
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.

A question picks its definitions instead of taking the first thirty

1 participant