Skip to content

Commit fb82e21

Browse files
Raj725eyurtsev
andauthored
Add support for the Negation('$not') operator (#62)
Add support for the Negation(`$not`) operator in PGVector --------- Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
1 parent f724ab3 commit fb82e21

File tree

3 files changed

+70
-12
lines changed

3 files changed

+70
-12
lines changed

langchain_postgres/vectorstores.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ class DistanceStrategy(str, enum.Enum):
8888
"$ilike",
8989
}
9090

91-
LOGICAL_OPERATORS = {"$and", "$or"}
91+
LOGICAL_OPERATORS = {"$and", "$or", "$not"}
9292

9393
SUPPORTED_OPERATORS = (
9494
set(COMPARISONS_TO_NATIVE)
@@ -1248,26 +1248,25 @@ def _create_filter_clause(self, filters: Any) -> Any:
12481248
"""
12491249
if isinstance(filters, dict):
12501250
if len(filters) == 1:
1251-
# The only operators allowed at the top level are $AND and $OR
1251+
# The only operators allowed at the top level are $AND, $OR, and $NOT
12521252
# First check if an operator or a field
12531253
key, value = list(filters.items())[0]
12541254
if key.startswith("$"):
12551255
# Then it's an operator
1256-
if key.lower() not in ["$and", "$or"]:
1256+
if key.lower() not in ["$and", "$or", "$not"]:
12571257
raise ValueError(
1258-
f"Invalid filter condition. Expected $and or $or "
1258+
f"Invalid filter condition. Expected $and, $or or $not "
12591259
f"but got: {key}"
12601260
)
12611261
else:
12621262
# Then it's a field
12631263
return self._handle_field_filter(key, filters[key])
12641264

1265-
# Here we handle the $and and $or operators
1266-
if not isinstance(value, list):
1267-
raise ValueError(
1268-
f"Expected a list, but got {type(value)} for value: {value}"
1269-
)
12701265
if key.lower() == "$and":
1266+
if not isinstance(value, list):
1267+
raise ValueError(
1268+
f"Expected a list, but got {type(value)} for value: {value}"
1269+
)
12711270
and_ = [self._create_filter_clause(el) for el in value]
12721271
if len(and_) > 1:
12731272
return sqlalchemy.and_(*and_)
@@ -1279,6 +1278,10 @@ def _create_filter_clause(self, filters: Any) -> Any:
12791278
"but got an empty dictionary"
12801279
)
12811280
elif key.lower() == "$or":
1281+
if not isinstance(value, list):
1282+
raise ValueError(
1283+
f"Expected a list, but got {type(value)} for value: {value}"
1284+
)
12821285
or_ = [self._create_filter_clause(el) for el in value]
12831286
if len(or_) > 1:
12841287
return sqlalchemy.or_(*or_)
@@ -1289,9 +1292,29 @@ def _create_filter_clause(self, filters: Any) -> Any:
12891292
"Invalid filter condition. Expected a dictionary "
12901293
"but got an empty dictionary"
12911294
)
1295+
elif key.lower() == "$not":
1296+
if isinstance(value, list):
1297+
not_conditions = [
1298+
self._create_filter_clause(item) for item in value
1299+
]
1300+
not_ = sqlalchemy.and_(
1301+
*[
1302+
sqlalchemy.not_(condition)
1303+
for condition in not_conditions
1304+
]
1305+
)
1306+
return not_
1307+
elif isinstance(value, dict):
1308+
not_ = self._create_filter_clause(value)
1309+
return sqlalchemy.not_(not_)
1310+
else:
1311+
raise ValueError(
1312+
f"Invalid filter condition. Expected a dictionary "
1313+
f"or a list but got: {type(value)}"
1314+
)
12921315
else:
12931316
raise ValueError(
1294-
f"Invalid filter condition. Expected $and or $or "
1317+
f"Invalid filter condition. Expected $and, $or or $not "
12951318
f"but got: {key}"
12961319
)
12971320
elif len(filters) > 1:

tests/unit_tests/fixtures/filtering_test_cases.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181

8282
TYPE_2_FILTERING_TEST_CASES = [
8383
# These involve equality checks and other operators
84-
# like $ne, $gt, $gte, $lt, $lte, $not
84+
# like $ne, $gt, $gte, $lt, $lte
8585
(
8686
{"id": 1},
8787
[1],
@@ -168,7 +168,7 @@
168168
]
169169

170170
TYPE_3_FILTERING_TEST_CASES = [
171-
# These involve usage of AND and OR operators
171+
# These involve usage of AND, OR and NOT operators
172172
(
173173
{"$or": [{"id": 1}, {"id": 2}]},
174174
[1, 2],
@@ -185,6 +185,39 @@
185185
{"$or": [{"id": 1}, {"id": 2}, {"id": 3}]},
186186
[1, 2, 3],
187187
),
188+
# Test for $not operator
189+
(
190+
{"$not": {"id": 1}},
191+
[2, 3],
192+
),
193+
(
194+
{"$not": [{"id": 1}]},
195+
[2, 3],
196+
),
197+
(
198+
{"$not": {"name": "adam"}},
199+
[2, 3],
200+
),
201+
(
202+
{"$not": [{"name": "adam"}]},
203+
[2, 3],
204+
),
205+
(
206+
{"$not": {"is_active": True}},
207+
[2],
208+
),
209+
(
210+
{"$not": [{"is_active": True}]},
211+
[2],
212+
),
213+
(
214+
{"$not": {"height": {"$gt": 5.0}}},
215+
[3],
216+
),
217+
(
218+
{"$not": [{"height": {"$gt": 5.0}}]},
219+
[3],
220+
),
188221
]
189222

190223
TYPE_4_FILTERING_TEST_CASES = [

tests/unit_tests/test_vectorstore.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,7 @@ async def test_async_pgvector_with_with_metadata_filters_5(
992992
{"$eq": {}},
993993
{"$exists": {}},
994994
{"$exists": 1},
995+
{"$not": 2},
995996
],
996997
)
997998
def test_invalid_filters(pgvector: PGVector, invalid_filter: Any) -> None:
@@ -1016,5 +1017,6 @@ def test_validate_operators() -> None:
10161017
"$lte",
10171018
"$ne",
10181019
"$nin",
1020+
"$not",
10191021
"$or",
10201022
]

0 commit comments

Comments
 (0)