tfpdev
02 / 09Part 1 · Retrieval

Words or Meaning

Searching by matching words finds exact codes but misses the same idea said differently. Searching by meaning catches that, even in another language. Switch between them and see the answers change.

01Step 1

Try it

This is the bot from the lab's front page with its parts on switches. Start with the quick version, change one thing, ask again. Hover a number to see where it came from.

Start fromThis setup got 77% of our 65 test questions right.
Cut documents into
Find pages by
Second look
Out-of-date pages
The 5 pieces it read
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
What it answered
Pick a question or type your own. Try the quick version first, then the careful one, and compare.
Questions from the list get checked against an answer key.
02Step 2

What actually mattered

Matching words

Great for exact things like product codes and names. Misses the same idea in different words.

Matching meaning

Turns text into numbers so similar ideas land close together, even in another language. Matching words found 1 of 5 questions asked in Dutch, German or Russian. Matching meaning found all of them.

Use both, then double-check

Merge both lists, then a second model reads each candidate next to the question and keeps the best five. A third of a second, and it fixed the last misses.

03Step 3

The code

What the two versions do, in one sentence. The code itself is one click away.

The quick version looks for pages that share words with the question. The careful version also looks for pages that mean the same thing, merges both lists, and has a second model re-check the best ones.

Show the code anyway
BreaksBreaks
Keyword only, the usual first build.
rag-first-try.tsts
// The 5 windows sharing the most words with the question.const hits = await sql(  `SELECT text FROM rag.chunks    WHERE tsv @@ $1 ORDER BY ts_rank_cd(tsv, $1) DESC LIMIT 5`,  [anyWord(question)]);const answer = await model({ system: SYSTEM, user: sources(hits) + question });
The build most tutorials end on. Word overlap finds product codes and nothing phrased differently. A question in German shares no words with an English price list.
HoldsHolds
Hybrid with rerank, running behind the switches.
lib/rag/search.tsts
// Words (Postgres full-text) and meaning (pgvector), 20 candidates each.const [kw, vec] = await Promise.all([keyword(q, cfg), vector(await embed(q), cfg)]);// Reciprocal rank fusion: high on either list rises.kw.forEach((c, i) => (score[c.id] += 1 / (60 + i + 1)));vec.forEach((c, i) => (score[c.id] += 1 / (60 + i + 1)));// A cross-encoder re-reads each candidate with the question, keeps 5.const top = await rerank(q, fused, 5);              // Voyage rerank-3
Two searches in parallel, fused by rank, then a reranker reads every candidate against the question and keeps five. Archived files can be filtered out, or left in with a label the prompt knows about.
04Step 4

Still curious?

A few more questions people ask. Open what you need.

Why not just give the AI every file?

It would fit today, but each question would cost about 40 times more and take longer. A real company has thousands of files, not 140. Looking up the right 5 pieces first keeps working as the pile grows.

Does this need special software?

No. It runs in the ordinary database the rest of the site uses. One place for the files and the search, nothing extra to keep in sync.

Why the second look, if both searches already work?

The last few misses are the ones clients notice: a fact that's only in one email. The two searches find it somewhere in the top 20. The second look moves it into the 5 the AI reads.