From eb3838963651d08700f2bfbcb4bd083d544448fe Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Mon, 1 Jun 2026 23:13:46 +0200 Subject: [PATCH] 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) --- .../open_webui/retrieval/vector/dbs/qdrant.py | 35 ++++++++----------- .../vector/dbs/qdrant_multitenancy.py | 8 +++-- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/backend/open_webui/retrieval/vector/dbs/qdrant.py b/backend/open_webui/retrieval/vector/dbs/qdrant.py index acf4e61993..7156a16c53 100644 --- a/backend/open_webui/retrieval/vector/dbs/qdrant.py +++ b/backend/open_webui/retrieval/vector/dbs/qdrant.py @@ -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( diff --git a/backend/open_webui/retrieval/vector/dbs/qdrant_multitenancy.py b/backend/open_webui/retrieval/vector/dbs/qdrant_multitenancy.py index 15e72f1d16..9b717644c5 100644 --- a/backend/open_webui/retrieval/vector/dbs/qdrant_multitenancy.py +++ b/backend/open_webui/retrieval/vector/dbs/qdrant_multitenancy.py @@ -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(