DEV Community

Cover image for "Money we gave back to shoppers" matches no table name. Four searches find it anyway.
Ashish sinha
Ashish sinha

Posted on

"Money we gave back to shoppers" matches no table name. Four searches find it anyway.

Someone asks your text-to-SQL agent: "money we gave back to shoppers."

The table that answers it is called billing_credit_note.

Those two strings share no word. Not a low score — zero overlap. Any retriever that works on table names is blind here, and the schema has 42 tables, so guessing is not an option either.

Here is what four separate searches do with that question, measured on a real run rather than described.

One question, four rankings

names — BM25 over table and column names only.

nothing matched
Enter fullscreen mode Exit fullscreen mode

Zero. "money", "gave", "back", "shoppers" appear in no identifier in the schema. This is the channel that scores 100% recall@6 when people phrase questions in the schema's own vocabulary, and it contributes exactly nothing to this one.

prose — BM25 over the generated descriptions.

1  v_supplier_spend
2  billing_credit_note      <- the answer
3  v_monthly_revenue
4  sup_supplier
Enter fullscreen mode Exit fullscreen mode

Second place. The description reads "Records refunds or corrections issued against a specific invoice." The word refunds is the bridge from "gave back" to credit_note — a bridge that exists nowhere in the DDL.

vector — embedding similarity.

1  v_monthly_revenue
2  v_supplier_spend
3  ship_carrier
...
22 billing_credit_note
Enter fullscreen mode Exit fullscreen mode

Twenty-second. It got the neighbourhood right — revenue, spend, money-shaped things — and the specific table wrong. That is the characteristic dense-retrieval failure, and the reason it is hard to debug: there is no per-term number to inspect. You just get a ranking that feels slightly off.

body — BM25 over everything concatenated: names, columns, types, comments, descriptions.

1  billing_credit_note
2  v_supplier_spend
3  v_monthly_revenue
4  sup_supplier
Enter fullscreen mode Exit fullscreen mode

First. Best of the four here — and also the exact configuration that collapsed on a 1,245-object schema, because with everything in one bag the descriptions dilute the identifiers. More on that below.

Merging them: rank, not score

You cannot add these scores together. A cosine distance and a BM25 score are different units; 0.83 plus 14.2 is not a number that means anything.

Positions are comparable, so positions are what get added. Reciprocal Rank Fusion:

contribution = weight / (K + position)        K = 60
Enter fullscreen mode Exit fullscreen mode

First place pays 1/61, second 1/62, twenty-second 1/82, absent pays nothing. For billing_credit_note:

names    absent    0.00000
prose    2nd       0.01613
vector   22nd      0.01220
body     1st       0.01639
                   -------
                   0.04472   -> top 6
Enter fullscreen mode Exit fullscreen mode

The important line is the first one. A channel that is completely blind costs the answer nothing. It contributes zero and the other three carry it. That is the entire argument for having four: they are not redundant, they fail on different questions, and it is rare for all four to miss at once.

The cost of rank-based fusion is the mirror image, and a commenter on my last post caught it: a channel that ranked things for a meaningless reason still votes at full strength. If every description contains the word "about", every object ties in the prose channel, and the tie is broken by whatever order the catalog was built in. I reproduced that — shuffling the insertion order changes which tables come back on 1 to 2 questions out of 12. Recall does not move; the prompt does. Both fixes he proposed are right, and both are now filed.

Why not just concatenate everything

This is the question I got wrong first, and it cost me a public retraction.

Putting names and descriptions in one bag seems obviously better — more text, more to match on. Measured on the same fixtures, by someone else, from the published wheel:

identifiers only          29/52   55.8%
one flat bag              41/52
prose as its own channel  48/52
shipped configuration     49/52   94.2%
Enter fullscreen mode Exit fullscreen mode

The flat bag beats identifiers alone. It is also seven questions worse than keeping prose in its own index. Inside one bag, the sentences compete with the identifiers for term frequency, and the identifiers lose because there are more words of prose than there are words of name.

At 1,245 objects it stops being a seven-question gap and becomes a failure. Every description written by the same model in the same voice put the token contact into roughly 1,072 of 1,245 documents. Its IDF went from 4.27 — computed over names, where it appears in 17 of 1,245 — to 0.15. The word users actually type had become a stopword. The table literally called contacts fell from rank 3 to below rank 40 for the question "show the contacts of xmagnet".

The descriptions were accurate. I read them. Accurate text can still be index poison, because IDF is a property of the corpus, not of the sentence.

Fielded scoring fixes it for a structural reason rather than a tuning reason: in the names index, contact is still 17 of 1,245 and still worth 4.27. The prose can be as repetitive as it likes over in its own index without touching that.

What it costs and what it buys

That same question, end to end on the 42-object schema: 6 ranked tables plus 3 pulled in by foreign-key closure, 490 tokens instead of 2,205 for the whole schema.

And the honest limit, because the comments on the last post will find it otherwise: 100% recall@6 when questions use the schema's vocabulary, 46–86% depending on schema when they do not. Fielded fusion narrows that gap. It does not close it.


schemagate is Apache-2.0. Every number above is from tests/run_paraphrase_eval.py and the checked-in fixtures.

Top comments (0)