Skip to content

Commit b1c998d

Browse files
committed
fix: prune orphaned service data when service is renamed in config.yaml (#75)
When a service is renamed or removed from config.yaml, the old service name's points remain in both the code symbols and git commits Qdrant collections, showing up in unfiltered searches. Add a shared prune_orphaned_services() utility (via PrunableStore protocol) that compares indexed service names against the configured set and deletes orphaned data. Wire it into both IndexPipeline.index_all and GitHistoryPipeline.index_all so it runs automatically on every full reindex (POST /reindex and POST /reindex-history).
1 parent f482b40 commit b1c998d

5 files changed

Lines changed: 50 additions & 2 deletions

File tree

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
# AI
55
.ai/
6-
.tokensave/
76

87
# Python
98
.venv/
@@ -32,4 +31,4 @@ config.yaml
3231
# Claude Code local settings
3332
.claude/settings.local.json
3433

35-
memory/
34+
memory/

server/indexer/git_history.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
fetch_commits_with_diffs,
1616
list_commits,
1717
)
18+
from server.indexer.cleanup import prune_orphaned_services
1819
from server.indexer.pipeline import ProgressEvent
1920
from server.store.commit_store import CommitStore
2021

@@ -197,6 +198,13 @@ async def index_all(
197198
progress_callback: Callable[[ProgressEvent], Awaitable[None]] | None = None,
198199
) -> dict[str, Any]:
199200
services = settings.load_services()
201+
configured_names = {s.name for s in services}
202+
orphaned = await prune_orphaned_services(
203+
self._store, configured_names, label="git commits"
204+
)
205+
if orphaned:
206+
logger.info("Pruned %d orphaned service(s): %s", len(orphaned), orphaned)
207+
200208
results: dict[str, Any] = {}
201209
for svc in services:
202210
logger.info("Indexing git history for: %s", svc.name)

server/indexer/pipeline.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from server.embeddings.base import EmbeddingProvider
1515
from server.embeddings.bm25 import BM25SparseProvider, get_sparse_embedding_provider
1616
from server.embeddings import get_embedding_provider
17+
from server.indexer.cleanup import prune_orphaned_services
1718
from server.indexer.github_source import fetch_blob_content, list_github_files
1819
from server.parser.base import CodeSymbol, ParseError
1920
from server.parser.registry import parse_file
@@ -281,6 +282,13 @@ async def index_all(
281282
progress_callback: Callable[[ProgressEvent], Awaitable[None]] | None = None,
282283
) -> dict[str, Any]:
283284
services = settings.load_services()
285+
configured_names = {s.name for s in services}
286+
orphaned = await prune_orphaned_services(
287+
self._store, configured_names, label="code symbols"
288+
)
289+
if orphaned:
290+
logger.info("Pruned %d orphaned service(s): %s", len(orphaned), orphaned)
291+
284292
results: dict[str, Any] = {}
285293
for svc in services:
286294
logger.info("Indexing service: %s", svc.name)

server/store/commit_store.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,5 +227,33 @@ async def _update_one(p: dict[str, Any]) -> None:
227227

228228
await asyncio.gather(*(_update_one(p) for p in payloads))
229229

230+
async def get_indexed_services(self) -> list[str]:
231+
"""Return distinct service names that have indexed commits."""
232+
services: set[str] = set()
233+
offset = None
234+
while True:
235+
results, offset = await self._client.scroll(
236+
collection_name=self._collection,
237+
limit=1000,
238+
offset=offset,
239+
with_payload=["service"],
240+
with_vectors=False,
241+
)
242+
for point in results:
243+
svc = point.payload.get("service")
244+
if svc:
245+
services.add(svc)
246+
if offset is None:
247+
break
248+
return sorted(services)
249+
250+
async def delete_by_service(self, service: str) -> None:
251+
await self._client.delete(
252+
collection_name=self._collection,
253+
points_selector=Filter(
254+
must=[FieldCondition(key="service", match=MatchValue(value=service))]
255+
),
256+
)
257+
230258
async def close(self) -> None:
231259
await self._client.close()

server/store/qdrant.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,11 @@ async def get_service_stats(self) -> list[dict[str, Any]]:
309309
)
310310
return result
311311

312+
async def get_indexed_services(self) -> list[str]:
313+
"""Return distinct service names that have indexed code symbols."""
314+
stats = await self.get_service_stats()
315+
return sorted(s["service"] for s in stats)
316+
312317
async def collection_info(self) -> dict[str, Any]:
313318
info = await self._client.get_collection(self._collection)
314319
return {

0 commit comments

Comments
 (0)