Skip to content

Commit efcb748

Browse files
committed
Improve section 3 and remove section 4
1 parent ae88a4b commit efcb748

1 file changed

Lines changed: 14 additions & 35 deletions

File tree

blog.md

Lines changed: 14 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -180,45 +180,24 @@ How does **semcode** build the sparse input?
180180

181181
Building BM25 text input is minimal — it concatenates only the signature, docstring, and raw source, with no metadata.
182182
It splits camelCase and snake_case identifiers into their component words while keeping the original form alongside. A
183-
token like `PlaceOrderRequest`becomes `Place Order Request` — so BM25 can match the exact identifier *and* a
183+
token like `PlaceOrderRequest` becomes `Place Order Request` — so BM25 can match the exact identifier *and* a
184184
natural-language query like "place order request" that doesn't use the original casing.
185185

186+
Why does sparse matter when the dense input is already rich? Dense embeddings excel at intent — a query like "find
187+
the method that retries payments" can surface `retryWithBackoff` even if no query word appears in the source — but that
188+
power trades precision for meaning, and rare or project-specific identifiers like `PlaceOrderRequest` get smoothed
189+
toward neighboring concepts in the model's vector space. BM25 fills exactly that gap: it matches tokens literally with
190+
no compression, and **semcode's** code-aware tokenization splits `PlaceOrderRequest` into `Place Order Request` alongside
191+
the original, so it handles both exact identifier lookups and natural-language queries that dense alone would miss.
192+
186193
So the full picture is:
187194
Every `CodeSymbol` produces two inputs. The dense input is wide and context-rich — it tells the model the symbol's
188195
place in the system. The sparse input is narrow and literal — it gives BM25 the exact tokens to match against. Both
189196
are computed in the same pipeline step and stored together as a single point in Qdrant.
190197

191198
---
192199

193-
## Section 4 — The sparse side: BM25 with code-aware tokenization
194-
195-
- BM25 input is intentionally coarser: signature + docstring + source only
196-
- Reference: `server/indexer/pipeline.py:94-101`
197-
- Identifier expansion: `CamelCase` and `snake_case` are split so BM25 can match partial queries
198-
- Both original and split forms kept → "PlaceOrderRequest" matches exact lookups *and* "place order"
199-
- Reference: `server/embeddings/code_tokenizer.py:6-16`
200-
- Implementation: fastembed's `Bm25("Qdrant/bm25")`, stored as a native sparse vector in Qdrant
201-
- Reference: `server/embeddings/bm25.py`
202-
- What BM25 solves that dense doesn't:
203-
- Exact symbol-name lookups
204-
- Rare tokens (vocabulary mismatch — domain jargon, project-specific names)
205-
- Queries that are *literal* references rather than intent descriptions
206-
207-
---
208-
209-
## Section 5 — The dense side: pluggable embedding providers
210-
211-
- Five providers, all behind one interface: Jina API (hosted), self-hosted Jina via TEI, OpenAI, Voyage, Ollama
212-
- Reference: `server/embeddings/{jina_api,jina,openai,voyage,ollama}.py`
213-
- Why pluggable matters for code: dimensions vary (768 → 3072), code-tuned models (jina-code-embeddings, voyage-code-3)
214-
outperform general-purpose ones
215-
- Optional callout: the factory pattern refactor (commit `cd778ee`) — each provider self-registers on import, so adding
216-
a new one doesn't touch `factory.py` (OCP)
217-
- Reference: `server/embeddings/__init__.py`, `server/embeddings/factory.py`
218-
219-
---
220-
221-
## Section 6 — What goes into Qdrant: the named-vector schema
200+
## Section 4 — What goes into Qdrant: the named-vector schema
222201

223202
- One collection (`code_symbols`) with **two named vectors per point**:
224203
- `text-dense` — cosine, provider-dependent dims
@@ -236,7 +215,7 @@ are computed in the same pipeline step and stored together as a single point in
236215

237216
---
238217

239-
## Section 7 — Hybrid retrieval at query time (RRF in one Qdrant call)
218+
## Section 5 — Hybrid retrieval at query time (RRF in one Qdrant call)
240219

241220
- The query goes through *both* encoders: dense (full model) and sparse (tokenizer + BM25)
242221
- One Qdrant `query_points` call does the fusion server-side:
@@ -255,7 +234,7 @@ are computed in the same pipeline step and stored together as a single point in
255234

256235
---
257236

258-
## Section 8 — Indexing flow: incremental, content-addressed
237+
## Section 6 — Indexing flow: incremental, content-addressed
259238

260239
- Walk the repo (GitHub API or local), apply excludes
261240
- For each file: compute blob SHA → compare against payload's `file_hash` → skip if unchanged
@@ -266,7 +245,7 @@ are computed in the same pipeline step and stored together as a single point in
266245

267246
---
268247

269-
## Section 9 — Bonus: indexing git history as a second RAG corpus
248+
## Section 7 — Bonus: indexing git history as a second RAG corpus
270249

271250
- Separate pipeline embeds **commit messages + file deltas** into the `git_commits` collection
272251
- Dense-only (commit messages are short, sparse adds little)
@@ -275,7 +254,7 @@ are computed in the same pipeline step and stored together as a single point in
275254

276255
---
277256

278-
## Section 10 — What I'd do differently / open questions
257+
## Section 8 — What I'd do differently / open questions
279258

280259
- Re-ranker on top of RRF (cross-encoder) — worth the latency?
281260
- Per-language collections vs single collection — when does the trade-off flip?
@@ -284,7 +263,7 @@ are computed in the same pipeline step and stored together as a single point in
284263

285264
---
286265

287-
## Section 11 — Takeaways
266+
## Section 9 — Takeaways
288267

289268
- Symbol-level chunking + rich, language-aware embedding inputs are the foundation
290269
- Hybrid dense+sparse with RRF gives you both "intent" and "exact name" search for free, server-side

0 commit comments

Comments
 (0)