@@ -180,45 +180,24 @@ How does **semcode** build the sparse input?
180180
181181Building BM25 text input is minimal — it concatenates only the signature, docstring, and raw source, with no metadata.
182182It 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
184184natural-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+
186193So the full picture is:
187194Every ` CodeSymbol ` produces two inputs. The dense input is wide and context-rich — it tells the model the symbol's
188195place in the system. The sparse input is narrow and literal — it gives BM25 the exact tokens to match against. Both
189196are 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