@@ -250,16 +250,54 @@ diff metadata as dense-only points.
250250
251251---
252252
253- ## Section 5 — Hybrid retrieval at query time
253+ ## Section 5 — Indexing flow: incremental, content-addressed
254+
255+ Embedding API calls are the dominant cost in any indexing run, and re-embedding an entire repository on every push would
256+ be expensive at scale. ** semcode** avoids this by treating indexing as a diff operation: it uses git blob
257+ SHAs as content fingerprints to identify which files have changed, and only those files are parsed, embedded, and
258+ upserted. A service with 1,000 files where 10 changed sends 10 embedding requests, not 1,000. This section describes
259+ the full indexing pipeline.
260+
261+ ### Step 1 — Discovery via the Git Trees API
262+
263+ The pipeline opens by calling GitHub's Trees API. One request returns every file in the repository tree. Crucially,
264+ each entry already includes the git ` blob_sha ` — git's own content hash for that file
265+ — without downloading a single byte of source code.
266+
267+ ### Step 2 — Hash comparison before any network I/O
268+
269+ Before fetching any file content, the pipeline loads the ` file_hash ` values stored in the Qdrant payload for all
270+ already-indexed symbols in this service. It then compares each file's ` blob_sha `
271+ against that map. If the hashes match, the file is skipped entirely — no HTTP download, no parsing, no embedding call.
272+ This is the core of the incremental design — instead of re-embedding every symbol on every run, only files whose content
273+ actually changed are embedded again.
274+
275+ ### Step 3 — Fetch, parse, embed, upsert
276+
277+ For every file that is new or has a changed blob SHA, the pipeline fetches the content by SHA,
278+ parses it into ` CodeSymbol ` objects, builds both dense and sparse inputs as described in Section 3,
279+ and calls both embedding providers in a batch.
280+
281+ The upsert is a ** delete-then-insert at the file level** : all existing points whose ` file_path ` matches are removed
282+ first, then the freshly embedded points are inserted. This keeps the index clean when a file loses methods,
283+ gains new ones, or is restructured.
284+
285+ ### Step 4 — Cleanup pass for deleted files
286+
287+ After the main loop, the pipeline diffs the current repo file set against every ` file_path ` that exists in Qdrant.
288+ Any path no longer present in the repo is deleted.
289+
290+ ---
291+
292+ ## Section 6 — Hybrid retrieval at query time
254293
255294At query time, the same two-track split like in the ingestion phase runs in reverse. The query string goes through both
256295encoders — the dense model turns it into a floating-point vector, the BM25 turns it into a sparse vector.
257296Both are sent to Qdrant in a single call, which runs each retriever independently, ranks the top K×2 candidates
258297from each, and produces two separate ranked lists.
259298
260299Qdrant then uses ** Reciprocal Rank Fusion (RRF)** to merge those two ranked lists into one before returning the
261- final top K results. The merge looks like this step by step, using the query _ "find the method that retries
262- failed payments"_ as an example:
300+ final top K results. For example, using the query _ "find the method that retries failed payments"_ merge looks like this:
263301
2643021 . Dense retriever returns its ranked list:
265303 ` [retryWithBackoff (rank 1), processPayment (rank 2), PlaceOrderRequest (rank 3), ...] `
@@ -278,36 +316,7 @@ RRF rewards consistent rank across retrievers. The score it produces answers a s
278316"how consistently did this result appear near the top across both dense and sparse retrievers?"
279317---
280318
281- ## Section 6 — Indexing flow: incremental, content-addressed
282-
283- - Walk the repo (GitHub API or local), apply excludes
284- - For each file: compute blob SHA → compare against payload's ` file_hash ` → skip if unchanged
285- - Parse → build dense + sparse inputs → batch-embed → upsert (delete-then-insert per file path)
286- - Cleanup pass removes stale symbols for files no longer in the repo
287- - Reference: ` server/indexer/pipeline.py:128-249 `
288- - Why this matters: embedding API costs amortize across reindexes; large monorepos stay tractable
289-
290- ---
291-
292- ## Section 7 — Bonus: indexing git history as a second RAG corpus
293-
294- - Separate pipeline embeds ** commit messages + file deltas** into the ` git_commits ` collection
295- - Dense-only (commit messages are short, sparse adds little)
296- - Enables "when was retry logic introduced?" style queries
297- - Reference: ` server/indexer/git_history.py:24-63 ` , ` server/tools/history.py `
298-
299- ---
300-
301- ## Section 8 — What I'd do differently / open questions
302-
303- - Re-ranker on top of RRF (cross-encoder) — worth the latency?
304- - Per-language collections vs single collection — when does the trade-off flip?
305- - Embedding the * call graph* (cross-symbol relationships), not just symbols in isolation
306- - Tuning the 6000-char source cap per language
307-
308- ---
309-
310- ## Section 9 — Takeaways
319+ ## Section 7 — Takeaways
311320
312321- Symbol-level chunking + rich, language-aware embedding inputs are the foundation
313322- Hybrid dense+sparse with RRF gives you both "intent" and "exact name" search for free, server-side
0 commit comments