·15 min read

Finding the Right Page

My equipment-manual RAG put the right page in its top eight for 40% of paraphrased questions. I benchmarked four embedding models and four answer models on the plant's own manuals. The biggest gain came from neither: a three-line header that tells every chunk which machine it belongs to. Recall is now 78%.

In April I wrote about the search engine I built over the equipment manuals at the cement plant where I work: 876 PDFs, about 40,000 pages, one search box. It worked. Six people use it.

Then the complaints got specific. Ask for the motor size of a filter fan and you got a VFD manual, a bucket elevator and a mill. The answer read well and cited a page. The page was about the wrong machine.

So this month I stopped adding features and measured it. The number that mattered: for a question worded the way a technician actually talks, the right page made it into the top eight sources 40% of the time.

40 → 78 %right page in the top 8
44 %of chunks were duplicates
1,446drawings recovered
$0.001per answer
Swapping in a better embedding model was worth 8 points. Telling each chunk which machine it came from was worth 21. The data was the problem, not the model.
Hand-drawn diagram of the pipeline. Ingest lane: manuals share and browser uploads, page fingerprint to skip pages already indexed, text extraction or vision for drawings with Claude Haiku 5.5, chunking with an Equipment / Section / Document header, embedding with voyage-4-large. Both lanes meet at PostgreSQL with pgvector. Search lane: the technician asks in English or Spanish, the query is rewritten, vector search plus part-number keywords, rank fusion with one copy per page, and an answer from Claude Haiku 5.5 cited to the page.
Every box in the top lane changed this month. The bottom lane mostly got its weights corrected. The page fingerprint and the header are the two ideas worth stealing; neither needs a new model.

What was actually wrong, measured on the live database

I had assumptions. I replaced them with queries against the production database before touching any code.

  • 44% of the index was duplicates. 72,748 chunks, but only 40,877 distinct texts. The same manual lived on the share as a full binder, as individual tab files and as _GREY copies, and all of them were indexed. A top-eight result routinely held four distinct pages.
  • No chunk knew which machine it described. The equipment name lived in the folder path, never in the text that was embedded. A paragraph about "the motor" was just a paragraph about a motor.
  • 3,344 drawings had silently vanished. Pages with no text layer need a vision model to describe them. That call was failing and the failure was logged as a warning, so ingestion carried on and the page was simply never indexed.
  • 260 files were never seen at all. The scanner looked for *.pdf. They were *.PDF.

None of these is a model problem. Every one of them would have survived a model upgrade.

Building a benchmark from the plant's own manuals

Leaderboards rank embedding models on general text. My corpus is OEM manuals full of torque tables, part numbers and drawings. The only ranking that matters is the one on these documents, so I built one.

Five manuals, every gold page checked by hand, searched against the whole deduplicated corpus so the other 30,000+ chunks act as distractors.

  • 31 direct questions, worded close to the page.
  • 40 paraphrased questions, worded the way someone on the floor would ask, with the manual's own terms deliberately avoided. A third of them in Spanish, because that is how half the crew asks.
  • Recall@8: does the right page make it into the eight sources the answer model sees? With 40 questions the margin is about ±8 points, so anything closer than that is a tie.
RECALL@8, FOUR EMBEDDING MODELS, WHOLE CORPUS (40,877 CHUNKS)31 direct questions40 paraphrased questionsvoyage-4-large0.970.55qwen3-embedding-8b0.870.53text-embedding-3-large (was live)0.900.47gemini-embedding-20.840.42
The direct set flatters everyone. All four models find a page worded like the question. The paraphrased set is where they separate, and where voyage-4-large and Qwen3 are a statistical tie. Gemini was last on both sets and the second most expensive.

voyage-4-large won, Qwen3-Embedding-8B tied it on the hard set at a tenth of the price, and the model I had in production was eight points behind. A real gain, but not the 40-to-78 story.

The header was worth more than the model

The fix for "no chunk knows its machine" is three lines. Every chunk is now embedded behind a header built from its path on the share. For example (names changed):

Equipment: Vertical Roller Mill
Section: Volume 3 / Tab 4
Document: lubrication-system.pdf, page 12

The stored text does not change, so citations and snippets look exactly as before. Only the vector moves, and it moves toward the right machine.

PARAPHRASED QUESTIONS: SHARE WHOSE RIGHT PAGE REACHED THE TOP 8Before: old model, hybrid0.40MRR 0.22voyage-4-large, keywords × 0.50.57MRR 0.37+ context header on every chunk0.78MRR 0.53 · keywords only for part numbers
+21 points from metadata the system already had. MRR, where the right page lands in the list, rose from 0.37 to 0.53, so it is not just inside the eight; it is near the top of them. Re-embedding all 72,748 chunks took 16 minutes while the old index kept serving.

Four answer models, and the question that separates them

Retrieval decides what the model sees. The answer model decides what it does when what it sees is wrong. I gave four models the same eight sources for all 71 questions and had a fifth model grade them blind against the gold page.

ANSWER QUALITY, 71 QUESTIONS, JUDGED BLIND (0 TO 1)claude-haiku-5.50.92$0.0010 per answer · 5.6 sclaude-sonnet-5.50.92$0.016 per answer · 6.5 sgpt-6-luna0.87$0.0005 per answer · 3.7 sgpt-5.6-luna (was live)0.82$0.0009 per answer · 3.2 s
Sonnet costs 16 times more and does not beat Haiku. The only thing it did better was always answering in Spanish when asked in Spanish, which is a sentence in the system prompt, not a model upgrade.

When the right page was among the sources, all four scored the same, about 1.8 out of 2. The gap is entirely in the other case: the 19 questions where retrieval failed and the right page was not there.

RIGHT PAGE NOT RETRIEVED (19 QUESTIONS): ANSWERS JUDGED WRONGgpt-5.6-luna (was live)7 of 19gpt-6-luna4 of 19claude-sonnet-5.51 of 19claude-haiku-5.51 of 19
This is the chart I would show anyone choosing a model for a plant. The model I had in production answered confidently from the wrong section: ask for a motor rating and get the rating of a different motor, cited and formatted. Haiku was wrong once.

A wrong motor rating with a citation is worse than no answer, because it looks checked. That is why the answer model changed to Claude Haiku 5.5, at the same cost per answer as before: about a tenth of a cent.

The drawings were never there

The April article said a vision model described every diagram. It did not. The root cause turned out to be one parameter: the vision call had a budget of 1,000 output tokens, and the reasoning model spent all of it thinking. It returned an empty description, the page had no other text, and the page was dropped without an error.

Vision now runs on Claude Haiku 5.5 with thinking off and a 4,000-token budget. On an A0 outline drawing it transcribed the legend, dimensions and loads correctly in 12 seconds for about $0.0013. The old model, even at low effort, took 41 seconds and four times the price.

Before re-reading three thousand drawings I fixed the duplicates, because there is no point paying to describe the same page twice:

  • Every page now carries a fingerprint: its normalized text, or a small grayscale render when it has no text. Ingestion skips any page already indexed anywhere, before vision and before embedding.
  • Binders go first, then tab files, then the grey copies, so the complete document wins.
  • The cleanup removed 11,653 duplicate pages from the existing index without losing any content: 72,748 rows in 452 documents became 45,042 in 282.

The incremental ingest after that found 1,446 pages that needed vision and 539 new text pages across 217 PDFs, ran for 1 hour 26 minutes and finished with no errors. Drawings now show up among the sources of real searches.

Letting the crew add what is missing

The manuals are not the whole story; the knowledge in the plant is also in photos, marked-up prints and hand sketches. So the app now takes uploads:

  • PDF, JPG or PNG. A photo becomes a one-page PDF, upright, so the viewer and citations work the same way.
  • An equipment field, which becomes the folder and therefore the Equipment: line in the header. The header pays off twice.
  • A transcribe-first vision prompt for uploads: write down every word on the page, handwriting included, then describe it.
  • A shared upload key for the people I trust with it, on top of the sign-in. Deleting and re-scanning stay admin-only.

The first real upload was a scanned sheet of hand sketches for a counterweight and its guides. It came out as 12 searchable chunks.

Screenshot of the demo instance answering, in Spanish, which bearing the drive end of a boiler feed pump motor uses and how often it is greased, cited to an uploaded nameplate photo.
A phone photo, answered in Spanish. The nameplate is a demo image uploaded as a JPG under Boiler Feed Pump 2. The answer takes the bearing and the grease interval off the plate and then says what the plate does not tell it, which is the behaviour I chose the model for.
Screenshot of the document library on a demo instance: an upload form with a file picker, an equipment field and an upload key, above a table of indexed manuals with page and chunk counts.
Demo instance, public manuals. The plant's library stays private, so every screenshot in this post comes from the same code running on fourteen public OEM manuals. The equipment field is the one people skip and the one the search depends on.

Moving it into the cluster

The app also moved out of a Docker Compose VM and into my k3s cluster, and the rule was to reuse what already exists:

  • The database went into the CloudNativePG cluster that already serves another app, as its own database and role, instead of a new Postgres. The vector index is 413 MB after a reindex.
  • The PDFs stay where they were, on the storage container's NFS export; the pods mount it directly.
  • Sign-in is an emailed six-digit PIN for the company's two email domains, the same pattern as my other work apps. No public page, nothing to guess.
  • A weekly CronJob dumps the database to the backup share, and the nightly restic job carries it off site.
Screenshot of a search on the demo instance: a question about a pump, the answer with its citation, and the ranked source pages below it.
One answer, eight sources, every claim pinned to a page. The percentage on each source card is a fused rank score, not a confidence. The answer quotes the page it used, and the card links to it.
Screenshot of the PDF viewer opened from a citation, showing the cited manual page.
The citation is the product. A technician does not have to trust the answer; one click puts the manual page in front of them.

What it cost

ItemCost
Model pilot: four embedding models over 41,000 chunks, four answer models, a blind judge~$6.55
Voyage tokens for the header experiments~$3.15
Re-embedding production with voyage-4-large~$2.80
Vision on 1,446 drawings with Claude Haiku 5.5~$2.00
Total~$14.50
Each search afterwards, answer included~$0.001

What I'd actually take from this

  • Measure on your own documents. Leaderboards rank models on general text. Mine is torque tables and part lists, and the order changed.
  • Fix the data before the model. Duplicates and missing context cost more than any model choice, and a model swap would have hidden both.
  • Test the failure case on purpose. Every answer model looks the same when retrieval works. Choose the one that admits it did not find the page.
  • Keep the old setup as the control. "Better" means nothing without the number it beat.
  • Log failures as failures. Three thousand drawings disappeared behind a warning.

The search box looks exactly the same as it did in April. What changed is how often the page behind the answer is the right one.

// share