5.8 KiB
5.8 KiB
P5b task 02: cached embeddings and BM25-first retrieval for long documents
Branch: p5b (git switch p5b; git status --short must be empty, otherwise stop)
Commit subject: digest: cache chunk embeddings; long documents answer from BM25 while embedding in the background
Goal
Today selectText embeds every chunk of a document for every question, inline, inside the
digest's 45 s timeout, and never uses the embedding cache. After this task:
- chunk embeddings are cached;
- a document with few uncached chunks is embedded inline and scored hybrid (as today);
- a long one is answered at once from BM25, with no embedding call on the request path, while its chunks are embedded by a background worker;
- once they are, the next question is hybrid.
Files
- Copy:
internal/digest/retrieval_test.go - Create:
internal/digest/retrieval.go(selection),internal/digest/background.go(the worker) - Modify:
internal/digest/digest.go(Resultfields,Run,selectText's call site),docs/implementer-log.md
Interfaces
package digest
const (
EmbedSyncMax = 48 // uncached chunks a question may embed inline; more go to the background
MaxDocChunks = 5000 // background embedding cap per document
)
// in Result:
Retrieval string // "whole", "head", "hybrid", "bm25" or "cached" (see rule 2)
Embedded int // chunks that had an embedding when the answer was built
// WaitBackground returns when the background worker is idle with nothing queued, or ctx.Err().
func (e *Engine) WaitBackground(ctx context.Context) error
What already exists and stays:
Split,SplitPages,NewBM25(task 01);EstTokens,SmallDoc,ChunkTokens,EmbedBatch,lexicalOverlap,cosine;e.embed(ctx, inputs)(batches ofEmbedBatch, halves on "too large");e.Gate.Acquire/Release;dcache.EmbedKey(contentHash string, chunkIndex int, embedModel, callerClass string),e.Cache.GetEmbed(key)/PutEmbed(key, vec)(a nil*DBis a no-op).
Rules
- Chunks and keys.
- Chunk with
SplitPages(cleaned, ChunkTokens). - Embedding keys are
dcache.EmbedKey(contentHash+"/sp1200", i, e.embedModel(), callerClass): the suffix binds the key to this chunker, so a future chunking change cannot reuse wrong vectors. contentHashandcallerClasscome fromRun(pass them to the selection).
- Chunk with
- Selection (
retrieval.go), in order:EstTokens(cleaned) <= SmallDoc→ the whole text,Retrieval "whole".- Empty question → the leading chunks within
SmallDoc, as today,Retrieval "head". - No cache (
e.Cache == nil):len(chunks) <= EmbedSyncMax→ embed everything inline, as today,"hybrid";- otherwise
"bm25", with no embedding and nothing queued (there is nowhere to keep the vectors).
- Cache: look up every chunk's vector.
- uncached
<= EmbedSyncMax→ embed the question and the uncached chunks inline (the question first, then the missing chunks, throughe.embed), store each new chunk vector,"hybrid". - otherwise →
"bm25": rank byNewBM25(chunks).Scores(question)normalised by its maximum. Make no embedding call, and queue the document for the background (rule 4).
- uncached
- Hybrid score =
0.7·cosine(q, v_i) + 0.3·lexicalOverlap(question, chunk), as today. - Both modes then use today's greedy budget: chunk 0 first, then best-scoring chunks while the total stays within
SmallDoc, emitted in document order. Embedded= the chunks with a vector at that moment.
Run:- Set
r.Retrievalandr.Embedded. - On a digest-cache hit:
Retrieval "cached". - A digest made in
"bm25"mode is not stored in the digest cache. It is provisional; once the embeddings exist the same question must get a hybrid answer.
- Set
- Background worker (
background.go):- One goroutine per
Engine, started on first use (sync.Once), with a queue of at most 4 documents, deduplicated bycontentHash+"|"+callerClass. A full queue drops the document, logsWarn(with no text) and a later question re-queues it. - For each document: embed the first
MaxDocChunkschunks that lack a vector,EmbedBatchat a time. For each batch:e.Gate.Acquire(context.Background()), so interactive digests get turns; on a refusal (queue full or wait), sleep 1 s and try again;e.embedwith ae.timeout()context;Release;- store the vectors.
- On an embedding error: log
Warn, drop the rest of that document, go on to the next. WaitBackgroundis true-idle: the queue is empty and no document is in progress. Poll every 20 ms or use a condition variable; either is fine.
- One goroutine per
- Never log document text. The worker never holds the gate between batches.
Facts
- The given test's fake dixie counts every embedded input. It can hold
/embedshut, which is how it proves the"bm25"answer made no embedding call on the request path: if yours does, the firstRunblocks and the test fails on time. sseHandlerandchatReqcome fromdigest_test.go/digest.goin the same package.- Existing tests (
TestChunkOrderAndBudget,TestEmbedTooLargeRetriesAndCompletes, …) run with no cache and must stay green.
Steps
- 1.
git switch p5b; copy the given test. 2. See it fail. - 3.
retrieval.go, 4.background.go, 5. theRun/Resultchanges. - 6.
gofmt -w;go test -race -count=3 ./internal/digest/→ok. 7.make gate. - 8. Row
p5b/02-retrieval; commit.
git add internal/digest docs/implementer-log.md
git commit -F .state/commit-msg.txt
Done when
- The given and existing digest tests pass under
-race -count=3;make gateok; given file byte-identical.
Stop and report if
- A given test's timing cannot be met without sleeping on the request path.