diff --git a/mem0/vector_stores/turbopuffer.py b/mem0/vector_stores/turbopuffer.py index bef4b40b2..76281fc96 100644 --- a/mem0/vector_stores/turbopuffer.py +++ b/mem0/vector_stores/turbopuffer.py @@ -131,6 +131,18 @@ class TurbopufferDB(VectorStoreBase): )) return results + # Maps mem0 filter operators to their Turbopuffer equivalents. + OPERATOR_MAP = { + "eq": "Eq", + "ne": "NotEq", + "gt": "Gt", + "gte": "Gte", + "lt": "Lt", + "lte": "Lte", + "in": "In", + "nin": "NotIn", + } + def _convert_filters(self, filters: Optional[Dict]): """ Convert mem0 filters to Turbopuffer filter format. @@ -143,10 +155,14 @@ class TurbopufferDB(VectorStoreBase): conditions = [] for key, value in filters.items(): if isinstance(value, dict): - if "gte" in value: - conditions.append((key, "Gte", value["gte"])) - if "lte" in value: - conditions.append((key, "Lte", value["lte"])) + for op, operand in value.items(): + tpuf_op = self.OPERATOR_MAP.get(op) + if tpuf_op is None: + raise ValueError( + f"Unsupported filter operator '{op}' for field '{key}'. " + f"Supported operators: {sorted(self.OPERATOR_MAP)}" + ) + conditions.append((key, tpuf_op, operand)) else: conditions.append((key, "Eq", value)) diff --git a/tests/vector_stores/test_turbopuffer.py b/tests/vector_stores/test_turbopuffer.py index d1915b94e..e1b9233bb 100644 --- a/tests/vector_stores/test_turbopuffer.py +++ b/tests/vector_stores/test_turbopuffer.py @@ -276,6 +276,40 @@ class TestConvertFilters: assert ("user_id", "Eq", "u1") in conditions assert ("score", "Gte", 0.5) in conditions + def test_gt_operator_not_dropped(self, db): + """Regression: {"gt": ...} was silently dropped, returning unfiltered results.""" + result = db._convert_filters({"age": {"gt": 18}}) + assert result == ("age", "Gt", 18) + + @pytest.mark.parametrize( + "op,expected_token", + [ + ("eq", "Eq"), + ("ne", "NotEq"), + ("gt", "Gt"), + ("gte", "Gte"), + ("lt", "Lt"), + ("lte", "Lte"), + ("in", "In"), + ("nin", "NotIn"), + ], + ) + def test_all_operators_mapped(self, db, op, expected_token): + operand = [1, 2] if op in ("in", "nin") else 5 + result = db._convert_filters({"age": {op: operand}}) + assert result == ("age", expected_token, operand) + + def test_multiple_operators_on_one_field(self, db): + result = db._convert_filters({"age": {"gt": 18, "lt": 65}}) + assert result[0] == "And" + conditions = result[1] + assert ("age", "Gt", 18) in conditions + assert ("age", "Lt", 65) in conditions + + def test_unknown_operator_raises(self, db): + with pytest.raises(ValueError, match="Unsupported filter operator"): + db._convert_filters({"age": {"between": [1, 2]}}) + # ── search ───────────────────────────────────────────────────────────