@@ -172,9 +172,9 @@ layers in framework-specific metadata — Spring stereotypes, HTTP method and ro
172172docstring and the full signature. Finally, the raw source body is appended, capped at ~ 6,000 characters (~ 1,500
173173tokens). The goal is to give the embedding model everything it would need to understand the symbol's role, not just
174174its 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
179179How 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