Skip to content

Python: Bug: vector store filters fail when the constant is on the left, including chained comparisons #14779

Open

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, 3

Qdrant 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 Field as Field op' Constant, flipping </> and <=/>= and keeping ==/!=
  • split a chained comparison a < b < c into a < 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    pythonPull requests for the Python Semantic Kerneltriage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions