fix(qdrant): deduplicate $or/$not keys and add icontains debug warning
Memory._process_metadata_filters() renames OR→$or and NOT→$not, but
effective_filters also retains the original OR/NOT keys from the
deepcopy of input_filters. Without dedup, the same sub-conditions were
evaluated twice in the Qdrant filter.
Normalize the filter dict upfront so $or/$not/$and map to OR/NOT/AND
and only the first occurrence is kept. Also replace the fragile
value.get("contains") or value.get("icontains") pattern with explicit
key lookup, and add a logger.debug for icontains explaining that Qdrant
MatchText case sensitivity depends on full-text index configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
b1dbd08c63
commit
b38f2ef45c
@@ -698,8 +698,13 @@ class TestQdrantEnhancedFilters(unittest.TestCase):
|
||||
self.assertEqual(len(result.must_not), 1)
|
||||
|
||||
def test_memory_search_or_shape(self):
|
||||
"""Simulate exact shape Memory.search() sends for OR filters."""
|
||||
# Memory.search() preserves original OR AND adds $or
|
||||
"""Simulate exact shape Memory.search() sends for OR filters.
|
||||
|
||||
effective_filters keeps the original OR key (via deepcopy of
|
||||
input_filters) and _process_metadata_filters adds $or with the
|
||||
same content. _create_filter should deduplicate so only the
|
||||
first occurrence (OR) is used — exactly 2 should entries.
|
||||
"""
|
||||
filters = {
|
||||
"OR": [{"category": "programming"}, {"category": "data"}],
|
||||
"user_id": "test_user",
|
||||
@@ -707,12 +712,15 @@ class TestQdrantEnhancedFilters(unittest.TestCase):
|
||||
}
|
||||
result = self.qdrant._create_filter(filters)
|
||||
self.assertIsInstance(result, Filter)
|
||||
# OR and $or both contribute to should — duplicates are harmless
|
||||
self.assertIsNotNone(result.should)
|
||||
self.assertGreaterEqual(len(result.should), 2)
|
||||
# Deduplicated: OR wins, $or is skipped — exactly 2 entries
|
||||
self.assertEqual(len(result.should), 2)
|
||||
|
||||
def test_memory_search_not_shape(self):
|
||||
"""Simulate exact shape Memory.search() sends for NOT filters."""
|
||||
"""Simulate exact shape Memory.search() sends for NOT filters.
|
||||
|
||||
Same deduplication as OR: NOT wins, $not is skipped.
|
||||
"""
|
||||
filters = {
|
||||
"NOT": [{"category": "spam"}],
|
||||
"user_id": "test_user",
|
||||
@@ -721,7 +729,8 @@ class TestQdrantEnhancedFilters(unittest.TestCase):
|
||||
result = self.qdrant._create_filter(filters)
|
||||
self.assertIsInstance(result, Filter)
|
||||
self.assertIsNotNone(result.must_not)
|
||||
self.assertGreaterEqual(len(result.must_not), 1)
|
||||
# Deduplicated: NOT wins, $not is skipped — exactly 1 entry
|
||||
self.assertEqual(len(result.must_not), 1)
|
||||
|
||||
def tearDown(self):
|
||||
del self.qdrant
|
||||
|
||||
Reference in New Issue
Block a user