1 Commits
Author SHA1 Message Date
Sainikhil JuluriandGeorge Panchuk 69f0a9c02a fix: support FieldCondition.is_empty/is_null in local mode (#1308)
* fix: support FieldCondition.is_empty/is_null in local mode

`FieldCondition` carries `is_empty` and `is_null` as the shorthand syntax for
`IsEmptyCondition` / `IsNullCondition`, and both are wired up for REST and gRPC.
Local mode's `check_condition()` never inspected them, so a `FieldCondition`
carrying `is_empty=`/`is_null=` matched none of the `if` branches and fell
through to the trailing `return False`.

The condition was therefore False for every point, silently: `must` matched
nothing and `must_not` matched everything, with no warning and no
`NotImplementedError`. That affects scroll, count, query_points, facet and
delete(filter=...).

Behaviour was established by running the queries against qdrant/qdrant:dev
rather than by reading core, and is pinned by a congruence test:

- a value is empty when it is null or an empty array; a key holding no value
  counts as empty but not null
- a value is null when it is null or an array containing a null
- for a key resolving to several values, any one of them satisfying the
  condition is a match, so one point can satisfy both `is_empty=True` and
  `is_empty=False`

A condition that also carries `values_count` is left to the existing
`values_count` branch, so its behaviour is unchanged.

Note that on a field without a payload index the server does not treat
`is_null` and `IsNullCondition` as interchangeable, even though the generated
models describe them as alternative syntax: the verbose condition tests the
values a key resolves to, so an array holding a null is not itself null, while
the shorthand looks inside it. Local mode does not model payload indexes, so it
mirrors the unindexed behaviour here, as the surrounding branches already do.

Values are extracted with `flat=False`, like the neighbouring
`IsEmptyCondition`/`IsNullCondition` branches. It is load-bearing:
`{"field": []}` flattens to `None`, which would otherwise collapse the
empty-array case into the no-value case.

* test: compare count() as well as scroll() for is_empty/is_null

The conditions reach every filtered operation, so compare a second one that
shares the filter path rather than only scroll.

* refactor: share the is_empty/is_null value checks in local mode

* fix: make is empty in local mode work as indexed is empty in remote

---------

Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
2026-09-16 00:28:11 +07:00