From a930603c12ccd65a6bb8f63ddfa76dcfe849e149 Mon Sep 17 00:00:00 2001 From: kartik-mem0 Date: Sat, 21 Mar 2026 18:05:48 +0530 Subject: [PATCH] fix(qdrant): add wildcard * support and update type hints Wildcard '*' values now return None from _build_field_condition (match any / field exists semantics), and _create_filter skips None conditions so a filter containing only wildcards returns None. Updated return type annotations to Optional[FieldCondition] and Optional[Filter] accordingly. Co-Authored-By: Claude Sonnet 4.6 --- mem0/vector_stores/qdrant.py | 16 ++++++++++++---- tests/vector_stores/test_qdrant.py | 17 +++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/mem0/vector_stores/qdrant.py b/mem0/vector_stores/qdrant.py index 47d29c5c8..5f9051b22 100644 --- a/mem0/vector_stores/qdrant.py +++ b/mem0/vector_stores/qdrant.py @@ -1,6 +1,7 @@ import logging import os import shutil +from typing import Optional from qdrant_client import QdrantClient from qdrant_client.models import ( @@ -141,7 +142,7 @@ class Qdrant(VectorStoreBase): ] self.client.upsert(collection_name=self.collection_name, points=points) - def _build_field_condition(self, key: str, value) -> FieldCondition: + def _build_field_condition(self, key: str, value) -> Optional[FieldCondition]: """ Build a single FieldCondition from a key-value filter pair. @@ -153,9 +154,14 @@ class Qdrant(VectorStoreBase): value: A scalar for simple equality, or a dict with one operator key. Returns: - FieldCondition: The Qdrant field condition. + Optional[FieldCondition]: The Qdrant field condition, or None if the + value is the wildcard '*' (match any / field exists — skip filter). """ if not isinstance(value, dict): + if value == "*": + # Wildcard: match any value. Qdrant has no direct "field exists" + # condition via FieldCondition, so we skip this filter (match all). + return None # Simple equality: {"field": "value"} return FieldCondition(key=key, match=MatchValue(value=value)) @@ -192,7 +198,7 @@ class Qdrant(VectorStoreBase): f"Supported operators: {supported}" ) - def _create_filter(self, filters: dict) -> Filter: + def _create_filter(self, filters: dict) -> Optional[Filter]: """ Create a Filter object from the provided filters. @@ -230,7 +236,9 @@ class Qdrant(VectorStoreBase): if built: must_not.append(built) else: - must.append(self._build_field_condition(key, value)) + condition = self._build_field_condition(key, value) + if condition is not None: + must.append(condition) if not any([must, should, must_not]): return None diff --git a/tests/vector_stores/test_qdrant.py b/tests/vector_stores/test_qdrant.py index ca20a0eb5..b85134bf2 100644 --- a/tests/vector_stores/test_qdrant.py +++ b/tests/vector_stores/test_qdrant.py @@ -551,5 +551,22 @@ class TestQdrantEnhancedFilters(unittest.TestCase): with self.assertRaises(ValueError): self.qdrant._build_field_condition("score", {"gt": 0.5, "eq": 1.0}) + def test_wildcard_returns_none(self): + """Wildcard '*' should return None (skip filter — match any).""" + result = self.qdrant._build_field_condition("category", "*") + self.assertIsNone(result) + + def test_create_filter_with_wildcard_skips_it(self): + """Wildcard fields should be skipped in the final filter.""" + result = self.qdrant._create_filter({"category": "*", "user_id": "alice"}) + self.assertIsInstance(result, Filter) + self.assertEqual(len(result.must), 1) + self.assertEqual(result.must[0].key, "user_id") + + def test_create_filter_only_wildcard_returns_none(self): + """Filter with only wildcard should return None.""" + result = self.qdrant._create_filter({"category": "*"}) + self.assertIsNone(result) + def tearDown(self): del self.qdrant