fix: delete Qdrant points by ID so memory deletions don't orphan vectors (#25495)

The Qdrant backends implemented delete(ids=...) as a payload filter on
metadata.id, but points are stored with the item id as the Qdrant point id
(see _create_points), and not every point carries an id in its payload.
Memory points store only {created_at} in metadata (KB metadata embeddings
likewise), so deleting a single memory matched nothing and left an orphaned
vector that kept being injected into RAG context.

Delete by point id instead: PointIdsList for the standard backend, and a
tenant-scoped HasIdCondition for multitenancy (point ids are unique, so tenant
isolation is preserved). Filter-based deletion is unchanged.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Classic298
2026-06-01 23:13:46 +02:00
committed by GitHub
parent eebbc48f80
commit eb38389636
2 changed files with 20 additions and 23 deletions
@@ -216,28 +216,23 @@ class QdrantClient(VectorDBBase):
ids: Optional[list[str]] = None,
filter: Optional[dict] = None,
):
# Delete the items from the collection based on the ids.
field_conditions = []
# Delete by point ID: the point ID is the item's id (see _create_points).
# Filtering on metadata.id silently misses points whose payload omits an
# id (e.g. memories), leaving orphaned vectors behind.
if ids:
for id_value in ids:
(
field_conditions.append(
models.FieldCondition(
key='metadata.id',
match=models.MatchValue(value=id_value),
),
),
)
elif filter:
return self.client.delete(
collection_name=f'{self.collection_prefix}_{collection_name}',
points_selector=models.PointIdsList(points=ids),
)
field_conditions = []
if filter:
for key, value in filter.items():
(
field_conditions.append(
models.FieldCondition(
key=f'metadata.{key}',
match=models.MatchValue(value=value),
),
),
field_conditions.append(
models.FieldCondition(
key=f'metadata.{key}',
match=models.MatchValue(value=value),
)
)
return self.client.delete(
@@ -228,15 +228,17 @@ class QdrantClient(VectorDBBase):
return None
must_conditions = [_tenant_filter(tenant_id)]
should_conditions = []
if ids:
should_conditions = [_metadata_filter('id', id_value) for id_value in ids]
# Delete by point ID within the tenant. The point ID is the item's id
# (see _create_points); filtering on metadata.id silently misses points
# whose payload omits an id (e.g. memories), leaving orphaned vectors.
must_conditions.append(models.HasIdCondition(has_id=ids))
elif filter:
must_conditions += [_metadata_filter(k, v) for k, v in filter.items()]
return self.client.delete(
collection_name=mt_collection,
points_selector=models.FilterSelector(filter=models.Filter(must=must_conditions, should=should_conditions)),
points_selector=models.FilterSelector(filter=models.Filter(must=must_conditions)),
)
def search(