Skip to content

Commit b9a34af

Browse files
Merge pull request #47 from GoodbyePlanet/feat/filtering-reconsidered
feat: Filter only on service name when searching code
2 parents 4386ad8 + 5a4f6e5 commit b9a34af

3 files changed

Lines changed: 16 additions & 29 deletions

File tree

blog.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ which captures everything needed to search, understand, and locate it without re
8383
What a `CodeSymbol` carries:
8484

8585
**name / symbol_type / language** — These uniquely describe what kind of thing this is (save,
86-
method, java) so retrieval can filter by language or type before even looking at embeddings.
86+
method, java), and are stored on the point so results can be displayed and grouped by language or type.
8787

8888
**signature** — The declaration line only, e.g. *def save(self, db: Session) -> User*. This is what you'd see in an
8989
IDE's autocomplete popup — compact enough to show in search results without including the full body.
@@ -173,7 +173,7 @@ docstring and the full signature. Finally, the raw source body is appended, capp
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.
175175
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**
176+
or *filtering* them (like `service`) are stored separately as the Qdrant **payload**
177177
they sit next to the vector but are never embedded.
178178

179179
How does **semcode** build the sparse input?
@@ -223,8 +223,9 @@ Payload is a JSON object with the following fields:
223223

224224
- **Identity & filtering**`symbol_name`, `symbol_type`, `language`, `service`,
225225
`file_path`, `package`, `parent_name`. These uniquely place the symbol in
226-
the repo, and three of them — `language`, `service`, `symbol_type` — are
227-
wired as active query-time filters.
226+
the repo. Only one of them — `service` — is wired as an active query-time
227+
filter on semantic search; the others are kept on the payload for display,
228+
scoped lookups (e.g. exact-name search), and future use.
228229
- **Display**`signature`, `source`, `docstring`, `start_line`, `end_line`,
229230
`annotations`, `extras` (HTTP method, route, Spring stereotype). These are
230231
what the MCP client renders back to the user — they are never filtered on,
@@ -241,7 +242,9 @@ symbol — then throw away the ones that don't match.
241242

242243
Payload indexes flip this order. **semcode** indexes six fields — `language`, `service`, `symbol_type`, `chunk_tier`,
243244
`parent_name`, `file_path` — so Qdrant can narrow the candidate set *before* any vector math happens. The
244-
vector search then runs only over the matching symbols, not the whole collection.
245+
vector search then runs only over the matching symbols, not the whole collection. In practice the semantic search
246+
path only filters on `service`; the other indexes still pay off for direct symbol lookups and the incremental
247+
reindex flow, which scrolls the collection by `service` and `file_path`.
245248

246249
### A second, simpler collection
247250

@@ -324,8 +327,8 @@ it requires rethinking every layer of the pipeline, from how you chunk (by symbo
324327
to how you embed (rich context for dense vectors, exact tokens for sparse vectors) to how you store
325328
(named vectors with a payload that carries as much signal as the vectors themselves). Hybrid
326329
dense+sparse retrieval with server-side RRF bridges the gap between intent-based queries and exact identifier lookups,
327-
giving you both in a single round-trip. The payload is half the system: without language, service, and type fields
328-
indexed as filters, every search scans the entire collection regardless of how good the vectors are. And without
330+
giving you both in a single round-trip. The payload is half the system: without a `service` filter indexed on the
331+
payload, every search scans the entire collection regardless of how good the vectors are. And without
329332
incremental indexing via blob SHAs, the embedding cost alone would make continuous reindexing impractical at any serious
330333
repository scale. Together these choices form a pipeline that stays accurate, stays fast, and stays affordable as the
331334
codebase grows.

server/store/qdrant.py

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -182,23 +182,15 @@ async def search(
182182
dense_vector: list[float],
183183
sparse_vector: SparseVector,
184184
limit: int = 10,
185-
language: str | None = None,
186185
service: str | None = None,
187-
symbol_type: str | None = None,
188186
) -> list[ScoredPoint]:
189-
must = []
190-
if language:
191-
must.append(
192-
FieldCondition(key="language", match=MatchValue(value=language))
193-
)
194-
if service:
195-
must.append(FieldCondition(key="service", match=MatchValue(value=service)))
196-
if symbol_type:
197-
must.append(
198-
FieldCondition(key="symbol_type", match=MatchValue(value=symbol_type))
187+
query_filter = (
188+
Filter(
189+
must=[FieldCondition(key="service", match=MatchValue(value=service))]
199190
)
200-
201-
query_filter = Filter(must=must) if must else None
191+
if service
192+
else None
193+
)
202194

203195
result = await self._client.query_points(
204196
collection_name=self._collection,
@@ -217,7 +209,6 @@ async def search(
217209
),
218210
],
219211
query=FusionQuery(fusion=Fusion.RRF),
220-
query_filter=query_filter,
221212
limit=limit,
222213
with_payload=True,
223214
)

server/tools/search.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,14 @@ def register_search_tools(mcp: FastMCP) -> None:
1919
@mcp.tool()
2020
async def search_code(
2121
query: str,
22-
language: str | None = None,
2322
service: str | None = None,
24-
symbol_type: str | None = None,
2523
limit: int = 10,
2624
) -> str:
2725
"""Semantically search code across indexed services using natural language.
2826
2927
Args:
3028
query: Natural language description of what you're looking for.
31-
language: Filter by language: java, python, typescript
3229
service: Filter by service name
33-
symbol_type: Filter by type: class, method, interface, enum, record, function,
34-
react_component, react_hook, type, pydantic_model
3530
limit: Maximum number of results (default 10)
3631
"""
3732
embedder = get_embedding_provider()
@@ -44,9 +39,7 @@ async def search_code(
4439
dense_vector=dense_vector,
4540
sparse_vector=sparse_vector,
4641
limit=limit,
47-
language=language,
4842
service=service,
49-
symbol_type=symbol_type,
5043
)
5144

5245
if not results:

0 commit comments

Comments
 (0)