Skip to content

Add optional plain-language search terms for each passage - #157

Open
obalak3 wants to merge 3 commits into
georgia-tech-db:mainfrom
obalak3:plain-language-terms
Open

obalak3 wants to merge 3 commits into
georgia-tech-db:mainfrom
obalak3:plain-language-terms

Conversation

@obalak3

@obalak3 obalak3 commented Oct 12, 2026

Copy link
Copy Markdown

Summary

Students ask in everyday words and course material answers in its own terms, so the passage that holds the answer never reaches the window. keyword_terms_for_query also drops any query term whose document frequency is zero, so a casual word that appears nowhere in the collection is discarded before BM25 runs.

Preparation can now write one everyday-language sentence for each passage and store it as that chunk's search terms. The sentence is indexed for keyword search and embedded with the passage. chunk_text is never modified, so the stored source, the source panel and the answer window are unchanged, and no chunk is added or displaced. Off by default.

This is the follow-up to the closed bridge-chunks PR, built the way that review suggested: write into the existing passage instead of adding a generated one. The earlier approach failed structurally, because a generated passage competes for a window slot and the embedder cannot tell the right one from the wrong one (admitted on 120 of 128 questions, off topic on 92).

What changed

  • tokensmith_preparation.py: plain_language_terms writes one sentence per source unit from that unit's own text, through the existing completion_client with a JSON schema, cached by passage text under the job directory like boundaries. Sentences that describe the passage instead of rephrasing it are dropped.
  • tokensmith_preparation_job.py: runs the step in both Basic and AI mode when the option is on, before the report is written.
  • tokensmith_store.py / tokensmith_engine.py: the sentence is persisted in the existing chunks.keywords column (already indexed by the chunks_fts triggers, never selected into source rows) and joins the embedding input.
  • Library settings: one checkbox, a preparation model requirement when it is on, and the generated sentence shown under its passage in the preparation report.

Measured

Generated BuzzDB textbook, nomic-embed-text, K=4 hybrid, gemma4:e4b writing the sentences from the passage text alone. 56 casual student phrasings over 14 concepts, scored strictly (one retrieved chunk must carry at least two of the concept's answer keywords), plus 28 textbook-worded controls. Every index rebuilt from empty.

passages given terms retrieval (56) won / lost controls (28)
none 40 28
every prose passage 45 +5 / 0 28
the 13 that answer the test concepts 46 +6 / 0 28

Answers with gemma4:e4b reading the same windows: 41 of 56 without terms, 47 with them on those 13 passages. On a broader set (150 passages sampled across the book, one casual question each, written by a different model) giving every prose passage a sentence raised passage recall from 98 to 112 of 150.

Costs: about 3 seconds per passage once, cached; index time unchanged (62 to 75 s for this book either way); search latency unchanged; answer window unchanged because the sentence stays out of the passage text.

Limits, including the negative results, are in docs/plain-language-search-terms.md: the gain is retrieval rather than reading, a question only benefits when it shares words with the sentence, two sentences per passage reached further on the broad set but was the only configuration that lost human questions, and a second textbook gained far less because its baseline was already 89%.

Note on an existing check

tests/ui/library-preparation.jsx asserted that Basic preparation must never submit a preparation model. The terms step needs one in either mode, so that invariant is now conditional and the opposite case is asserted too.

Validation

  • 116 Python unit tests (6 new), 292 TypeScript unit tests (1 new), integration tests, npm run typecheck and npm run build pass on 0.1.15.
  • Real run through the engine on one chapter (220 chunks): chunk count identical with and without the option, stored passage text byte-identical, 133 passages given terms, preparation 8 s to 420 s. Words that appear in no passage text ("trick", "forth") match in chunks_fts only in the index built with the option on, and never appear in a shown source.

🤖 Generated with Claude Code

Students ask in everyday words and course material answers in its own terms, so the
passage that holds the answer never reaches the window. keyword_terms_for_query also
drops any query term whose document frequency is zero, so a casual word that appears
nowhere in the collection is discarded before BM25 runs.

Preparation can now write one everyday-language sentence for each passage and store it
as that chunk's search terms. The sentence is indexed for keyword search and embedded
with the passage; chunk text is never modified, so the stored source, the source panel
and the answer window are unchanged and no chunk is added or displaced. The option is
off by default, works in Basic and AI preparation, needs a preparation model, caches by
passage text so an unchanged document makes no model call again, and never fails
preparation when the model declines.

Measured on a generated database textbook with gemma4:e4b writing the sentences and 56
casual student phrasings scored strictly: retrieval 40 to 45 of 56 with every prose
passage given a sentence, 28 of 28 textbook-worded controls unchanged, and answers 41 to
47 of 56. Preparation costs about 3 seconds per passage once. See
docs/plain-language-search-terms.md for the limits and the negative results.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jarulraj

Copy link
Copy Markdown
Member

I ran an independent holdout evaluation of this PR at d628f8322c09911306c923d655db1139aab035e0. The results support a modest retrieval benefit and keeping the feature optional, but the end-to-end answer benefit needs stronger evidence before making a broader recommendation.

What the independent test found

  • 72 newly authored synthetic student-style questions across 24 concepts: two casual formulations and one technical control per concept. Questions, target evidence, and answer criteria were frozen before enrichment or retrieval.
  • Four complete BuzzDB chapters (2, 8, 11, 15), 416 original passages, with all 251 eligible passages enriched, including distractors. Preparation never received the questions or expected answers.
  • Exact PR engine, Basic preparation, Nomic embeddings, Gemma 4 E4B, hybrid retrieval with four sources, fresh isolated indexes. The answer test used the production source packer and identical fixed generation settings for 24 predetermined casual questions.
Measure Off On
At least one target passage retrieved, casual questions 28/48 32/48
All predeclared evidence groups retrieved, casual questions 20/48 22/48
Answers meeting both frozen criteria with source support 11/24 12/24
Initial preparation, Apple M1 Max / 32 GB 18.1 s 741.3 s
Unchanged cached preparation 1.10 s 1.47 s

The casual retrieval gain (+8.3 percentage points) is close to the PR's full-enrichment result (+8.9 points). The answer test had two wins and one regression, a net +4.2 points; its paired bootstrap interval was wide (-8.3 to +16.7 points). Partial-or-complete supported answers were 19/24 versus 21/24, so the strict complete-answer metric should not be read as saying every other answer was useless. Original passage text stayed identical, and all 48 answers completed without truncation.

Limitations: these were assistant-authored questions and assistant scoring of blinded A/B answers, not actual student submissions or an independent human panel. This is one textbook and one local model pair. We checked for exact overlap with existing local benchmark strings, but could not check your private question sets or model pretraining. The samples and grading rules differ, so the answer-rate difference is not a controlled comparison between the two evaluations.

Suggested improvements to this PR

  1. Make all-passage enrichment the primary answer-quality experiment. The current 41-to-47 answer result enriches only the 13 passages already known to answer the test concepts. That is a useful targeted diagnostic, but it gives relevant passages extra search terms while leaving distractors unchanged. Please label it accordingly and add an answer evaluation with every eligible passage enriched, on questions frozen before inspecting enrichment/results. Include regressions as well as wins.

  2. Make the evaluation reproducible and auditable. Include the frozen questions, evidence/answer criteria, runner and settings, retrieved source IDs, answers, and per-question scores. Report independent concepts separately from paraphrase counts; blind arm labels during grading and have a human spot-check the changed answers. Our improvements were text-storage tradeoffs and blocking sort; the regression was a less complete answer about hash indexes and range queries.

  3. Lead with total preparation cost. Your reported roughly three seconds per passage agrees with this run (2.80 seconds median description call). Separating unchanged indexing time from generation is technically valid, but students experience their sum. Show total initial preparation and cached preparation prominently, with hardware/model details. Keep the option off by default and make the added preparation cost clear when enabling it. We measured an extra 12 minutes for this corpus; whether that is worthwhile depends on repeated use, not just a retrieval percentage.

  4. Compare against a larger source budget before attributing the benefit to enrichment's extra work. As a diagnostic, baseline hybrid retrieval at eight sources found a target on 33/48 casual questions and complete target evidence on 27/48, versus 32/48 and 22/48 for four enriched sources. We did not test eight-source answer quality, so this is a candidate comparison, not a demonstrated replacement. Please compare answer quality and latency as well as retrieval, since more context can also distract the reader.

My recommendation is to retain the optional design, clarify the targeted answer result and end-to-end cost, and strengthen the full-corpus holdout evaluation. The independent result supports “this can help”; it does not yet establish that the usual answer improvement repays the preparation time.

@jarulraj

Copy link
Copy Markdown
Member

The complete independent evaluation is now available for reproduction and regrading:

Evaluation package and step-by-step README

It is published on codex/pr157-independent-evaluation, based on the tested PR head d628f8322c09911306c923d655db1139aab035e0. The commit adds evaluation artifacts only; it does not modify this PR's application code.

  • Questions and criteria: all 72 cases, frozen corpus, original passage IDs, protocol, and pre-run hashes.
  • Scripts: preparation/retrieval runner, answer runner, the exact TypeScript loading helper used, blind-review generator, fresh-run setup, and a generic statistics script. Original runners are retained alongside the documented portability adaptations.
  • Results: summary, all 576 searches, all 48 answers with inference requests/responses, generated descriptions, stored vectors, per-call preparation logs, model digests, and the complete blind scores. Download and open report.html locally for expandable per-question evidence.

To start:

git clone --single-branch --branch codex/pr157-independent-evaluation https://github.com/georgia-tech-db/TokenSmith.git TokenSmith-pr157-eval
cd TokenSmith-pr157-eval
python3 tests/experiments/pr157-holdout-2026-10-14/verify.py --source .

That audit needs only standard Python and makes no model calls. The README then covers dependencies, exact model digests, and new_run.py, which creates fresh isolated indexes and prints the rerun commands without overwriting these results. After grading, summarize.py computes new statistics without reusing the original report's narrative.

The publication checks passed: all frozen input hashes match, the reported statistics reproduce, and all 48 recorded prompts, source counts, and budget limits recreate offline. Local machine paths and unrelated model inventory were removed from metadata; the changes are documented. For an independent regrade, use blind-review.json before opening blind-key.json. These remain synthetic questions with assistant blind grading, so a human review of the changed answers would be valuable.

@obalak3

obalak3 commented Oct 27, 2026 •

Copy link
Copy Markdown
Author

Follow-up to the review. I kept the feature as it was (one plain-language sentence per eligible passage, stored in chunks.keywords, off by default) and looked at where the sentence is actually used.

select_source_rows re-sorts the hybrid candidates by how many query words each passage's text contains. It reads the passage text, section header and titles, so the sentence never reached it. The new commit adds two lines so the final selection sees the same column BM25 already indexes: ch.keywords AS keywords in source_row_select, and "keywords" in the metadata keys of source_row_text. With the option off the column is NULL and results are identical (72/72 identical source sets on your holdout, 240/240 on a larger phrasing set). The sentence still never enters the sources sent to the model or shown in chat answers (it was already visible in the Library preview under the collapsed "Plain-language search terms" details, unchanged here). One behaviour to state plainly: a passage whose only match for a query word is in its sentence now gets the same single credit for that word as a passage whose header or title matches it. Coverage is counted per unique term, so a passage whose text already contains the word gains nothing.

Retrieval on your holdout (fresh profiles, same models, my machine), casual questions, target / complete:

4 sources 8 sources
baseline 28/48 / 20/48 33/48 / 27/48
PR as reviewed 33/48 / 20/48 39/48 / 30/48
PR + this change 37/48 / 25/48 42/48 / 33/48

Paired against the PR as reviewed: +5/-1 target at four sources, +3/-0 at eight. Bootstrap by concept 0 to 9, so the interval touches zero at 24 concepts. Technical controls unchanged. Query latency unchanged (median 15 ms either way).

Retrieval on a larger set. 120 new casual phrasings, five per concept, generated from the original passages with the same gold groups and criteria:

target: 4 complete: 4 target: 8 complete: 8
baseline 58 42 73 56
PR as reviewed 73 51 80 63
PR + this change 79 59 91 70

Paired against the PR at four sources: +8/-2 target and +11/-3 complete, bootstrap intervals by concept 0 to 13 and 1 to 15.

Answers on the larger set, gemma4:e4b at four sources, 360 answers graded blind against the frozen criteria with arm labels hidden (0 wrong or unsupported, 1 partial, 2 complete):

baseline PR as reviewed PR + this change
complete 30 39 40
partial 48 48 54
wrong or unsupported 42 33 26

Every row moves the right way. Against the plain app the feature with this change raises complete answers from 30 to 40 and cuts wrong answers from 42 to 26. What the change itself adds on top of the PR is at the bottom of the table: seven wrong answers become partial ones (33 to 26), and the count of complete answers holds (39 to 40, paired +9/-8). So the extra passages the change brings into the window stop the model from answering wrongly more often than they produce a fully complete answer, which is the expected shape when retrieval improves and the answering step stays the same. Twenty answers were identical across arms and were scored by different graders, and all twenty scores matched.

On your original 24 pairs the picture is consistent but too small to separate from grader noise: my first grading gave 16 / 18 / 20 complete at four sources and 17 / 22 / 22 at eight, second formulation 14 / 13 / 17, and a second blind pass over all 288 answers kept the direction in every cell with net differences of two or less.

Things I tested and did not adopt: three sentences per passage, three student-style questions, a more distinctive vocabulary, half a sentence, a separate embedding for the sentence, BM25 column weights, a larger keyword-term budget. All within the one-to-two question noise of regenerating the sentence, several worse. The one stable regression is the page I/O question, where a distractor's sentence contains two of the query's words.

Baseline reproduced your numbers exactly. The PR arm differs by one or two because the sentences were regenerated (4 of 251 identical, Ollama 0.35.1 vs 0.31.1). Full packet, patch, blind packets and scores are in the holdout folder on my machine and I can push them to the evaluation branch if useful.

Baseline reproduced your numbers exactly. The PR arm differs by one or two because the sentences were regenerated (4 of 251 identical, Ollama 0.35.1 vs 0.31.1). The 120 phrasings, the sentences the on profile held, the retrieval results, all 360 answers, the blind mapping and scores, the grader prompt, the second grading of your 288 holdout answers, and the scripts to rerun it are in tests/experiments/pr157-extended-2026-10-27 on https://github.com/obalak3/TokenSmith/tree/pr157-extended-evaluation/tests/experiments/pr157-extended-2026-10-27, branched from your evaluation branch so it sits next to the holdout package.

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