Skip to content

Fix match: {except: []} returning zero results with payload index#9055

Merged
timvisee merged 3 commits into
devfrom
fix/empty-except-integer-index
May 15, 2026
Merged

Fix match: {except: []} returning zero results with payload index#9055
timvisee merged 3 commits into
devfrom
fix/empty-except-integer-index

Conversation

@qdrant-cloud-bot
Copy link
Copy Markdown
Contributor

Summary

Fixes #9050

  • match: {"except": []} (NOT IN empty list) incorrectly returns zero results when a payload index exists on the field
  • The bug affects both integer and keyword indexes
  • Root cause: serde deserializes except: [] as AnyVariants::Strings([]) (first variant of untagged enum); the map index filter_impl then returns iter::empty() for the empty cross-type case instead of delegating to the condition checker
  • Fix: return None for cross-type Except variants unconditionally (not just when non-empty), so the fallback condition checker handles the query correctly
  • Same fix applied to estimate_cardinality_impl which had the same CardinalityEstimation::exact(0) bug

Test plan

  • First commit adds integration (OpenAPI) test covering:
    • except: [] without index (baseline, was working)
    • except: [] with integer index (was broken)
    • except: [] with keyword index (was broken)
  • Second commit contains the fix
  • CI: cargo check -p segment passes locally
  • CI: OpenAPI integration tests pass

Made with Cursor

Cursor Agent and others added 2 commits May 15, 2026 09:53
Regression test for #9050

An empty `except` list (NOT IN []) should always match all points that
have the field, both with and without a payload index. Currently the
integer (and keyword) indexed path incorrectly returns zero results
because:

1. serde deserializes `except: []` as `AnyVariants::Strings([])` (first
   variant of the untagged enum)
2. The map index filter_impl returns `iter::empty()` for empty
   cross-type variant, instead of matching everything

The test covers:
- except: [] without any index (baseline, currently works)
- except: [] with an integer index (currently broken)
- except: [] with a keyword index (currently broken)

Co-authored-by: Cursor <[email protected]>
Fixes #9050

Root cause: `except: []` deserializes as `AnyVariants::Strings([])`
due to the untagged serde enum trying `Strings` first. On an integer
index, the filter_impl matched `Except + Strings(empty)` and returned
`iter::empty()` (zero results). The same issue existed symmetrically
on keyword indexes with `Integers(empty)` and on UUID indexes with
`Integers(empty)`.

The fix: when the cross-type variant reaches the Except branch (e.g.
Strings on an integer index, Integers on a keyword/UUID index), return
`None` unconditionally — regardless of whether the set is empty. This
delegates to the fallback condition checker, which already handles
`except: []` correctly by matching all values.

The `estimate_cardinality_impl` functions had the same bug (returning
`CardinalityEstimation::exact(0)` for the empty cross-type case) and
are fixed the same way.

Affected index types: integer, keyword (str), UUID.
Bool index is NOT affected — it already returns `None` for all
Any/Except conditions.

Co-authored-by: Cursor <[email protected]>
@qdrant-cloud-bot qdrant-cloud-bot force-pushed the fix/empty-except-integer-index branch from 1cfc2a5 to 9f7038c Compare May 15, 2026 10:01
@qdrant qdrant deleted a comment from coderabbitai Bot May 15, 2026
Extends the regression test for #9050 to comprehensively cover:

- Integer, keyword, and UUID field indexes
- Empty except list (the original bug) for each index type
- Non-empty except list with matching types (normal filtering)
- Cross-type except values (e.g. strings on integer index, integers on
  keyword/UUID index) — type mismatch should exclude nothing → all match
- Exclude-all: listing every value should return zero results
- All scenarios tested both with and without a payload index

Co-authored-by: Cursor <[email protected]>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/openapi/test_match_except_empty.py`:
- Around line 162-166: The test suite currently relies on test_create_indexes()
to create indexes via _create_index(COLLECTION_NAME, ...), which fails under
pytest-xdist because tests with indexes may run in different workers; move the
index creation into a module-scoped fixture (e.g., define a
`@pytest.fixture`(scope="module") create_indexes that calls
_create_index(COLLECTION_NAME, "n", "integer"), _create_index(COLLECTION_NAME,
"tag", "keyword"), and _create_index(COLLECTION_NAME, "uid", "uuid")) and have
the indexed tests (those named like test_match_except_empty_*_with_index) depend
on that fixture, or alternatively perform the same index calls in the module
setup fixture (setup_module) so all workers use the same module-scoped setup
instead of relying on test_create_indexes().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c85ba8eb-e0a4-46b8-bfdd-897d70631192

📥 Commits

Reviewing files that changed from the base of the PR and between 9f7038c and 5e5df85.

📒 Files selected for processing (1)
  • tests/openapi/test_match_except_empty.py

Comment on lines +162 to +166
def test_create_indexes():
_create_index(COLLECTION_NAME, "n", "integer")
_create_index(COLLECTION_NAME, "tag", "keyword")
_create_index(COLLECTION_NAME, "uid", "uuid")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether pytest order-randomization plugins/config are present,
# which would make current order-coupled tests flaky.

set -euo pipefail

echo "== pytest-related config files =="
fd -i 'pytest\.ini|pyproject\.toml|tox\.ini|setup\.cfg'

echo
echo "== search for random-order plugins/config =="
rg -n -C2 'pytest-randomly|random-order|randomly|--random|addopts' \
  pyproject.toml pytest.ini tox.ini setup.cfg requirements*.txt **/conftest.py 2>/dev/null || true

echo
echo "== confirm indexed tests depend on explicit index-creation test =="
rg -n -C2 'def test_create_indexes|def test_match_except_.*with_index|def test_match_except_.*on_.*index' \
  tests/openapi/test_match_except_empty.py

Repository: qdrant/qdrant

Length of output: 2906


🏁 Script executed:

cat tests/pyproject.toml

Repository: qdrant/qdrant

Length of output: 1185


🏁 Script executed:

head -70 tests/openapi/test_match_except_empty.py

Repository: qdrant/qdrant

Length of output: 2032


🏁 Script executed:

rg -n '@pytest.mark.xdist_group|xdist_group' tests/openapi/test_match_except_empty.py | head -20

Repository: qdrant/qdrant

Length of output: 39


🏁 Script executed:

sed -n '162,210p' tests/openapi/test_match_except_empty.py

Repository: qdrant/qdrant

Length of output: 1856


Move index creation to a fixture to avoid worker isolation issues with pytest-xdist.

Tests test_match_except_empty_*_with_index and other indexed cases at lines 172–305 depend on test_create_indexes() (line 162) running first. With pytest-xdist's --dist=loadgroup strategy configured in tests/pyproject.toml, indexed tests can be distributed to a different worker than test_create_indexes(), causing them to fail silently (each worker runs the module fixture independently, creating an empty collection without the required indexes). Move index creation into a module-scoped fixture that indexed tests depend on, or create indexes directly in the module setup fixture.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/openapi/test_match_except_empty.py` around lines 162 - 166, The test
suite currently relies on test_create_indexes() to create indexes via
_create_index(COLLECTION_NAME, ...), which fails under pytest-xdist because
tests with indexes may run in different workers; move the index creation into a
module-scoped fixture (e.g., define a `@pytest.fixture`(scope="module")
create_indexes that calls _create_index(COLLECTION_NAME, "n", "integer"),
_create_index(COLLECTION_NAME, "tag", "keyword"), and
_create_index(COLLECTION_NAME, "uid", "uuid")) and have the indexed tests (those
named like test_match_except_empty_*_with_index) depend on that fixture, or
alternatively perform the same index calls in the module setup fixture
(setup_module) so all workers use the same module-scoped setup instead of
relying on test_create_indexes().

@qdrant qdrant deleted a comment from coderabbitai Bot May 15, 2026
@timvisee timvisee merged commit 50e9c7b into dev May 15, 2026
15 checks passed
@timvisee timvisee deleted the fix/empty-except-integer-index branch May 15, 2026 10:51
generall pushed a commit that referenced this pull request May 22, 2026
…9055)

* Add integration test for `match: {except: []}` with integer index

Regression test for #9050

An empty `except` list (NOT IN []) should always match all points that
have the field, both with and without a payload index. Currently the
integer (and keyword) indexed path incorrectly returns zero results
because:

1. serde deserializes `except: []` as `AnyVariants::Strings([])` (first
   variant of the untagged enum)
2. The map index filter_impl returns `iter::empty()` for empty
   cross-type variant, instead of matching everything

The test covers:
- except: [] without any index (baseline, currently works)
- except: [] with an integer index (currently broken)
- except: [] with a keyword index (currently broken)

Co-authored-by: Cursor <[email protected]>

* Fix `match: {except: []}` returning zero results with payload index

Fixes #9050

Root cause: `except: []` deserializes as `AnyVariants::Strings([])`
due to the untagged serde enum trying `Strings` first. On an integer
index, the filter_impl matched `Except + Strings(empty)` and returned
`iter::empty()` (zero results). The same issue existed symmetrically
on keyword indexes with `Integers(empty)` and on UUID indexes with
`Integers(empty)`.

The fix: when the cross-type variant reaches the Except branch (e.g.
Strings on an integer index, Integers on a keyword/UUID index), return
`None` unconditionally — regardless of whether the set is empty. This
delegates to the fallback condition checker, which already handles
`except: []` correctly by matching all values.

The `estimate_cardinality_impl` functions had the same bug (returning
`CardinalityEstimation::exact(0)` for the empty cross-type case) and
are fixed the same way.

Affected index types: integer, keyword (str), UUID.
Bool index is NOT affected — it already returns `None` for all
Any/Except conditions.

Co-authored-by: Cursor <[email protected]>

* Expand match-except test to cover all index types and cross-type filters

Extends the regression test for #9050 to comprehensively cover:

- Integer, keyword, and UUID field indexes
- Empty except list (the original bug) for each index type
- Non-empty except list with matching types (normal filtering)
- Cross-type except values (e.g. strings on integer index, integers on
  keyword/UUID index) — type mismatch should exclude nothing → all match
- Exclude-all: listing every value should return zero results
- All scenarios tested both with and without a payload index

Co-authored-by: Cursor <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Cursor <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants