Repository navigation
Description
Describe the bug
Vector store filters fail when the constant is on the left of a comparison, and that includes Python's chained range comparison. QdrantCollection and ChromaCollection raise for all three of these, while InMemoryCollection and FaissCollection return the expected records:
await collection.search(vector=[1.0, 1.0], filter="lambda x: 2 < x.num") # expected num 3, 4, 5
await collection.search(vector=[1.0, 1.0], filter="lambda x: 3 == x.num") # expected num 3
await collection.search(vector=[1.0, 1.0], filter="lambda x: 1 <= x.num < 4") # expected num 1, 2, 3Qdrant fails with a pydantic ValidationError (FieldCondition gets key=3, or Range gets lt="num"). Chroma fails with Expected where key to be a str, got 2.
The chained form is the one users are most likely to write. QdrantCollection._lambda_parser already splits chains into an AND (the comment there gives 1 < x < 3 as the example), but the first pair of a chain always has the constant on the left, so that path can't succeed today.
To Reproduce
A record type with an int data field num, six records with num 0 to 5, then the searches above on QdrantCollection(location=":memory:") and an ephemeral ChromaCollection. Rewriting each filter with the field on the left (x.num > 2, x.num >= 1 and x.num < 4) works on both.
Expected behavior
The same records as the field-on-the-left form, as InMemoryCollection returns today.
Additional context
Checked by running: Qdrant and Chroma fail. In-memory and FAISS work.
Read in the code but not run: the Weaviate, Pinecone, MongoDB and Azure Cosmos DB parsers also take the left operand as the field name and have no case for a constant there. Redis and Azure AI Search handle the reversed form explicitly. The SQL-based connectors may work by accident, since 2 < num is valid SQL.
Suggested fix: Normalize the tree once in VectorSearch._build_filter, next to _FoldUnaryNumericConstants from #14572, so the connector parsers stay unchanged:
- rewrite
Constant op FieldasField op' Constant, flipping</>and<=/>=and keeping==/!= - split a chained comparison
a < b < cintoa < b and b < c
in and not in with the constant on the left mean "the list field contains the value", which is a different operation, so I'd leave them as they are.
I'm happy to open a PR for this if the approach sounds right.
Platform
- Language: Python
- Source: main at b714e97
- Python 3.12, qdrant-client 1.20.0, chromadb 1.5.9
AI disclosure: found with Claude Code while comparing filter results across the Python connectors.
Activity