Chat reads its confirmed definitions with confirmed(kb, 30): the first thirty by concept name. The seeded upper bound on the wide corpus, 27 definitions, scored 17/18 (#520) — three short of the cap. A real base with a hundred definitions leaves seventy of them, the ones later in the alphabet, outside the prompt on every question. “Twenty-seven works” does not extrapolate.
The other wall is already visible at 27: the one miss in the seeded run was the model asking which of three adjacent names (gross profit, gross margin, gross merchandise value) the person meant. More definitions in the prompt means more of that.
Shape
Definitions become retrievable, in the shape the ontology index already has: concept_mappings.embedding / embedded_model / embedded_text (migration 0048), re-embedded when the model or the text changes, refreshed lazily before a lookup so nobody has to remember to.
Two channels, RRF-fused, top k = 8 into the prompt:
- vector — “name: summary (unit)” embedded; the question embedded once; pgvector orders. Absent when no embedding model is configured.
- lexical — every confirmed definition pulled back and scored in Rust by how many of the question’s tokens it contains (ASCII words; CJK bigrams). A base has tens to a few hundred definitions; this is cheaper than Chinese tokenisation in SQL and needs no
pg_trgm.
Degradation: vector + lexical → lexical only → the first k by name when nothing matches (today’s behaviour). No step is “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.
Measured by
recall@k, without asking anything. The bench already knows which definition each question needs (by number, #520), so ask.mjs --recall 8 calls the endpoint per question and checks whether a correct row is in the top 8 — seconds per round, no chat model. Misses are annotated with the truth file’s near field: if they cluster on near-name pairs, that is the case for a reranker (a separate cut, with a third model slot and this same degradation); if they scatter, the embedded text is the thing to fix.
A base that exceeds the cap: mappings.mjs --fresh --corpus wide --also tpch mounts both sources into one base, seeding both truths gives 45 definitions across two sources. Then ask.mjs on each corpus checks that 17/18 survives selection.
Chat reads its confirmed definitions with
confirmed(kb, 30): the first thirty by concept name. The seeded upper bound on the wide corpus, 27 definitions, scored 17/18 (#520) — three short of the cap. A real base with a hundred definitions leaves seventy of them, the ones later in the alphabet, outside the prompt on every question. “Twenty-seven works” does not extrapolate.The other wall is already visible at 27: the one miss in the seeded run was the model asking which of three adjacent names (gross profit, gross margin, gross merchandise value) the person meant. More definitions in the prompt means more of that.
Shape
Definitions become retrievable, in the shape the ontology index already has:
concept_mappings.embedding/embedded_model/embedded_text(migration0048), re-embedded when the model or the text changes, refreshed lazily before a lookup so nobody has to remember to.Two channels, RRF-fused, top
k = 8into the prompt:pg_trgm.Degradation: vector + lexical → lexical only → the first
kby name when nothing matches (today’s behaviour). No step is “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.Measured by
recall@k, without asking anything. The bench already knows which definition each question needs (by number, #520), so
ask.mjs --recall 8calls the endpoint per question and checks whether a correct row is in the top 8 — seconds per round, no chat model. Misses are annotated with the truth file’snearfield: if they cluster on near-name pairs, that is the case for a reranker (a separate cut, with a third model slot and this same degradation); if they scatter, the embedded text is the thing to fix.A base that exceeds the cap:
mappings.mjs --fresh --corpus wide --also tpchmounts both sources into one base, seeding both truths gives 45 definitions across two sources. Thenask.mjson each corpus checks that 17/18 survives selection.