Skip to content

Commit 491e098

Browse files
committed
chore: Add section 4: what goes into Qdrant
1 parent efcb748 commit 491e098

1 file changed

Lines changed: 49 additions & 16 deletions

File tree

blog.md

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,9 @@ layers in framework-specific metadata — Spring stereotypes, HTTP method and ro
172172
docstring and the full signature. Finally, the raw source body is appended, capped at ~6,000 characters (~1,500
173173
tokens). The goal is to give the embedding model everything it would need to understand the symbol's role, not just
174174
its implementation.
175-
The fields that are useful for *displaying or filtering* results (like `start_line`,
176-
`file_path`, or `parent_name`, `package`) are stored separately as the Qdrant **payload** they sit next to the vector
177-
but are never embedded.
175+
The fields that are useful for *displaying* results (like `start_line`, `end_line`, `file_path`, `signature`, `source`)
176+
or *filtering* them (like `language`, `service`, `symbol_type`) are stored separately as the Qdrant **payload**
177+
they sit next to the vector but are never embedded.
178178

179179
How does **semcode** build the sparse input?
180180

@@ -199,19 +199,52 @@ are computed in the same pipeline step and stored together as a single point in
199199

200200
## Section 4 — What goes into Qdrant: the named-vector schema
201201

202-
- One collection (`code_symbols`) with **two named vectors per point**:
203-
- `text-dense` — cosine, provider-dependent dims
204-
- `text-sparse` — Qdrant native BM25 sparse index
205-
- Reference: `server/store/qdrant.py:47-62`
206-
- The payload (the underappreciated half of every vector DB):
207-
- Identity: `symbol_name`, `symbol_type`, `language`, `service`, `file_path`, `package`, `parent_name`
208-
- Display: `signature`, `source`, `docstring`, `start_line`, `end_line`
209-
- Filtering: `annotations`, `chunk_tier`, framework `extras` (HTTP method, route, Spring stereotype)
210-
- Bookkeeping: `file_hash` (for incremental reindex), `indexed_at`
211-
- Reference: `server/indexer/pipeline.py:104-125`
212-
- Keyword payload indexes on the high-cardinality filter fields → fast `language=python AND service=catalog` style
213-
filters
214-
- Separate `git_commits` collection — dense-only, message + diff metadata
202+
In Section 3 it's explained that we have two inputs per symbol — dense and sparse — stored together in Qdrant.
203+
This section explains *how* they are stored: the shape of a single stored point and why that shape matters at query time.
204+
205+
### Named vectors: two vectors, one point
206+
207+
Qdrant lets a single point carry multiple vectors under distinct names, each with its own distance metric and index.
208+
**semcode** uses this directly: the `code_symbols` collection defines two named vectors per point.
209+
210+
- `text-dense` — cosine distance, dimensionality set by the embedding provider.
211+
- `text-sparse` — Qdrant's native BM25 sparse index.
212+
213+
The advantage of named vectors over two parallel collections is that one point ID identifies one symbol everywhere.
214+
Dense and sparse retrievers always agree on what "document 42" means, which is what makes server-side fusion (next
215+
section) possible in a single round-trip.
216+
217+
### Anatomy of a stored point
218+
219+
Alongside the two vectors, there is the payload — the non-embedded half of the point.
220+
Payload is a JSON object with the following fields:
221+
222+
- **Identity & filtering**`symbol_name`, `symbol_type`, `language`, `service`,
223+
`file_path`, `package`, `parent_name`. These uniquely place the symbol in
224+
the repo, and three of them — `language`, `service`, `symbol_type` — are
225+
wired as active query-time filters.
226+
- **Display**`signature`, `source`, `docstring`, `start_line`, `end_line`,
227+
`annotations`, `extras` (HTTP method, route, Spring stereotype). These are
228+
what the MCP client renders back to the user — they are never filtered on,
229+
just returned alongside the score (`server/tools/search.py:60-71`).
230+
- **Bookkeeping**`file_hash`, `indexed_at`. Not exposed at query time, but
231+
critical for the incremental reindex flow: the hash is how the pipeline
232+
decides a file hasn't changed and can be skipped (`server/indexer/pipeline.py:122-123`).
233+
234+
### Payload indexes: filters before vectors
235+
236+
By default, when you search Qdrant, it scores vectors first and filters results afterward. That means if you ask for
237+
"OAuth 2.0 implementation in payment-service", Qdrant would still compare your query vector against *every* stored
238+
symbol — then throw away the ones that don't match.
239+
240+
Payload indexes flip this order. **semcode** indexes six fields — `language`, `service`, `symbol_type`, `chunk_tier`,
241+
`parent_name`, `file_path` — so Qdrant can narrow the candidate set *before* any vector math happens. The
242+
vector search then runs only over the matching symbols, not the whole collection.
243+
244+
### A second, simpler collection
245+
246+
Code symbols aren't the only RAG corpus in **semcode**. A separate `git_commits` collection stores commit messages and
247+
diff metadata as dense-only points.
215248

216249
---
217250

0 commit comments

Comments
 (0)