"What does Pip like to eat?" and "She likes carrots" share no words, but they're about the same thing. To find a chunk by meaning, you turn text into an embedding: a list of numbers where texts that mean similar things point in similar directions.
Verified agents 0.33.0wrangler 4.157.0
Start from e-03 on GitHub. Starting here, or behind? Get the code at that tag:
git clone https://github.com/ryanyogan/klippy cd klippy git checkout e-03
1. Meaning as numbers
The embedding model here, @cf/baai/bge-base-en-v1.5 on Workers AI, turns any text into 768 numbers. You embed every chunk once, and each question when it's asked. Then you compare directions with cosine similarity: 1 means the same direction (the same meaning), and lower means less related.
A vector database stores the vectors and finds the closest ones fast. On Cloudflare that's Vectorize.
2. Create the index
In apps/agent, create an index that matches the model: 768 dimensions, compared by cosine. Neither can change later.
Then a metadata index on owner, so a search can ask for one Klippy's vectors only. Create it now, before any vectors go in: Vectorize only indexes metadata for vectors written after the index exists.
Bind it in wrangler.jsonc, then run pnpm run cf-typegen:
There's no local Vectorize, so "remote": true: wrangler dev uses the real index. The tests use a stand-in.
3. Embed and store
Create apps/agent/src/retrieval/vectors.ts:
embedsends a batch of texts in one Workers AI call. A source has at most 50 chunks, so that is one call per source.Each vector's id is
chunk-<D1 id>, so a match leads back to its row.metadata.owneris the wall between people. Every search will filter on it.upsert, notinsert: a retried step overwrites instead of failing.
4. A step in the Workflow
Import the new helper and type at the top of workflow.ts, under the other imports:
After 'chunk', read the chunks back and embed them, with retries:
At most 50 chunks, so it's one Workers AI call per source.
5. Run it
Send two files, then wait. Vectorize is eventually consistent: the upsert returns at once, and the vectors became searchable about a minute later for us. If a query finds nothing right away, it hasn't failed. Wait and ask again.
chunk-11 is the Pip note. Queried with its own vector, it matches itself at about 1.0. The UI brief's chunks are only loosely related. Now ask as someone else:
That's the owner wall working.
6. Prove it, and when not to build this
Copy test/vectors.spec.ts and the updated test/ingest.spec.ts: every chunk is embedded in one call and stored under chunk-<id> with its owner, and nothing is called for a source with no chunks. Ours: 17 files, 78 tests passed.
When AI Search is the better call. Cloudflare's AI Search is a managed search service: point it at your content, and it handles indexing and search (hybrid included) behind one binding. If you want "ask my docs" running this afternoon and don't need your own ranking, start there. This course builds the parts by hand so you know what's inside, and so Klippy can apply per-person walls and locks of its own.
Your code matches tag e-04 (changes from e-03).