* Fix flaky shard snapshot API CI readiness race
Run the prebuilt binary and poll /readyz instead of cargo run + fixed sleep, which can miss startup when cargo recompiles.
* Move shard snapshot API CI runner into a dedicated script
Keep workflow YAML thin by starting Qdrant, waiting for /readyz, and invoking shard-snapshot-api.sh from tests/shard-snapshot-api-tests.sh.
Add stop checks during the pre-HNSW build setup phase so cancellation
is observed promptly, and widen timing tolerance for noisy Windows debug
builds where post-stop delays can exceed 1s.
Mirror the fix from #9938 for test_upload_snapshot: assert every shard
has n_replicas active replicas across local+remote shards instead of
assuming peer 0 always sees exactly 2*n_replicas remote shards.
Mirror the internal gRPC server setting to avoid GOAWAY/ENHANCE_YOUR_CALM
errors when clients multiplex many short-lived streams (e.g. coach
high_concurrency drill). See #1907.
Use a warmup baseline and adaptive timeout so the search is cancelled
mid-flight on both fast macos ARM runners and overloaded hosts, instead
of relying on a fixed 350ms cutoff.
Extend the Windows-only ignore pattern to the remaining long-pole
unit/integration tests that dominate the Windows nextest phase:
- custom_query_scorer_equivalency::compare_scoring_equivalency
(~130s from product_x4 cases alone; rstest via test_attr)
- test_appendable_multi_turbo_vector_storage::{congruent_upsert_read_all_distances,
congruent_random_ops_{dot,cosine}} (~80s)
- test_appendable_turbo_vector_storage::{upsert_flush_reload_in_ram_matches_independent_oracle,
turbo_model_test_random_ops_{dot,cosine}} (~55s)
- scroll_filtering_test::test_filtering_context_consistency (~28s)
None of these exercise Windows-specific behavior; Linux/macOS keep
full coverage. Roughly ~300s of Windows test time removed on top of
the multivector rstest fix.
`multivector_filtrable_hnsw_test::test_multi_filterable_hnsw` and
`multivector_quantization_test::test_multivector_quantization_hnsw`
were meant to be ignored on Windows, but the
#[cfg_attr(target_os = "windows", ignore = "...")]
#[rstest]
pattern places the attribute BEFORE `#[rstest]`, so it never reaches
the per-case functions rstest generates — the cases were still running
on Windows CI (visible in the streaming test log).
Switch to the pattern that already works for
`byte_storage_quantization_test.rs`:
#[rstest]
#[cfg_attr(target_os = "windows",
test_attr(ignore = "..."))]
which uses rstest's `test_attr(...)` forwarding, so the `#[ignore]` is
applied to each generated per-case test.
Removes ~160 s of Windows test time (multivector_filtrable_hnsw ~100 s,
multivector_quantization ~64 s), which with ~3.6× test parallelism
should trim the Windows job wall-clock by ~45 s. Coverage on
Linux/macOS is unchanged.
Tolerate not-yet-ready collection upserts in test_rejoin_cluster and give
JWT snapshot uploads more headroom while still bounding auth-rejection hangs.
* fix: stop underestimating is_empty / not-null cardinality by 1/3
Re-introduce #10128 after its revert so CI can exercise
payload_index_test::test_read_operations / test_is_empty_conditions.
* test: stop requiring is_empty struct exp to beat plain
NullIndex complement estimates use an indexed upper bound (may include
soft-deletes); that is not guaranteed to be closer to truth than plain's
available/2 guess. Assert upper-bound semantics instead.
* test: drop is_empty exp==max assertion
That locked in NullIndex implementation detail. Keep result parity and
min/max bounds only; document why exp-vs-plain is not checked.
NullIndex used an arbitrary `exp = 2/3 * estimated` heuristic for
complement conditions (`is_empty=true`, `is_null=false`), which caused
steady-state approximate counts to under-report by ~35% even with no
deletes (see #10120). Use the indexed upper bound as the expected count
instead.
* feat(edge): add query_batch for batched planned queries
Expose the planned-query batch path as a public API so multiple
independent queries can share one planning pass over leaf searches
and scrolls. Wired through EdgeShardRead, FFI, and Python bindings.
Co-authored-by: Cursor <cursoragent@cursor.com>
* perf(edge): push batched query vectors down to segments
`query_batch` planned the whole batch at once but then executed every leaf
search on its own: one query context, one fan-out over all segments, and one
single-vector `Segment::search_batch` call per leaf.
Execute the batch as a batch instead:
- `EdgeReadView::search_batch` builds the query context once, visits the
segments once, and hands each segment the leaves that agree on everything
but their query vector as a single multi-vector `search_batch` call.
`search` is now a thin wrapper over a one-element batch.
- Move `SearchType`/`BatchSearchParams` from `collection`'s segments searcher
into `shard`, next to `CoreSearchRequest`, and add `group_search_batches`
so both the collection and the edge read path share one grouping
implementation. Edge computes the grouping once and reuses it per segment.
- `search_matrix` now issues its per-sample nearest queries through
`query_batch`; they share filter, limit and vector name, so the whole
sample is scored in one batched search per segment instead of one full
segment pass per sampled point.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: generall <andrey@vasnetsov.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Security release addressing CVE-2026-59884, CVE-2026-59885, and CVE-2026-59886.
Equivalent to #9973 for the dev branch.
Co-authored-by: Cursor <cursoragent@cursor.com>
POST /cluster/recover can return 200 while raft silently drops the
snapshot request when no leader is known yet, leaving the test stuck
on a missing collection until timeout.
Co-authored-by: Cursor <cursoragent@cursor.com>
Wait for green on write (and read after recover_read) so collection and
partial snapshots are not taken mid-indexing. Otherwise a leftover
appendable segment survives partial merge and breaks manifest equality.
Co-authored-by: Cursor <cursoragent@cursor.com>
#9013 skipped the manual replicate_shard when a transfer was already
visible, but the recovery loop can still start one between that check
and the POST. Accept 400 "already involved in transfer" as success so
the remaining wait assertions still cover recovery.
Co-authored-by: Cursor <cursoragent@cursor.com>
Right after the no_sync snapshot recovery, the recovered replica serves local
reads immediately, but a remote read to it can transiently fail for a short
window. The read path then falls back to the other replica in hash order on
just the requesting peer, so a single routing token momentarily resolves to
different replicas across peers (observed as {A, B, B}), failing the
determinism assertion.
Wait until token-routed reads are stable across all peers for every token the
test asserts on before measuring, so the transient post-recovery fallback
window is passed. Pure test-side change; routing behaviour is unchanged.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: make test_upload_snapshot robust to shard placement balance
The final assertion in recover_from_uploaded_snapshot assumed a perfectly
balanced shard placement (peer 0 having exactly 2*n_replicas remote shards).
Shard placement across peers is not guaranteed to be balanced, so this made
the test flaky (e.g. peer 0 ended up hosting all shards locally, leaving only
3 remote replicas instead of 4).
Instead, verify the full replica layout is healthy: peer 0 observes every
replica through its local + remote shards, so assert that all replicas are
Active and every shard has exactly n_replicas copies across the cluster.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: fetch cluster info once for shard validation
Read local and remote shards from a single /cluster response so both lists
come from the same cluster revision, per review feedback.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(model_testing): add slice matcher to generated scroll filter
Extend ScrollFilter with a Slice variant so paginated scroll exercises
Condition::Slice. The generator draws small totals (1/2/3/4/5/8) and a
valid index; the model verifier mirrors membership via Slice::check —
the same hash contract the engine uses — so the existing paged-scroll
id-set assertion covers sliced scroll under soak (optimizer, WAL reload,
multi-shard, mixed UUID/numeric ids).
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(model_testing): add CountBySlice verification op
Exercise Condition::Slice through the exact count API under soak.
Shares the slice generator with ScrollPaged; the model oracle uses
Slice::check so engine and in-memory counts must agree.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(model_testing): compose slice with num on scroll and delete-by-filter
- ScrollFilter::NumAndSlice: indexed num drives candidates; slice is a
per-candidate check via Filter::merge.
- DeleteByFilter { num, slice: Option<Slice> }: half the deletes also
restrict by slice so submit-time filter resolution and WAL-replayed
id lists exercise Condition::Slice.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Align the workspace dependency pin with the already-resolved
Cargo.lock version (includes GHSA-7gcf-g7xr-8hxj fix).
Co-authored-by: Cursor <cursoragent@cursor.com>
Skip the three slowest Windows rust-tests (>60s each): turbo multi
random-ops model tests and the turbo Manhattan HNSW quantization case.
Use rstest test_attr for the parametrized case since a top-level ignore
does not apply to all generated tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
An empty update channel only means the last operation was received,
not that it finished applying in spawn_blocking. Use plunge_async as
a barrier before asserting point counts.
Fixes#9831
Co-authored-by: Cursor <cursoragent@cursor.com>
Drain the update worker queue after setup upserts with WaitUntil::Wal.
Wal only waits for the WAL write, so on slow CI (notably Windows) B could
time out while queued behind the setup backlog rather than because the
worker was blocked on A's deferred wait.
Fixes#9814
Co-authored-by: Cursor <cursoragent@cursor.com>
Collect per-segment point ids into a BTreeSet before counting so points
visible in multiple segments (e.g. during proxy/merge optimization) are
counted once, matching the canonical collection count path.
Fixes#9789
Co-authored-by: Cursor <cursoragent@cursor.com>
Wait for the collection metadata to propagate to the newly added extra
peer before querying its collection cluster info. Being online and
present in consensus does not guarantee the peer has already applied the
collection-creation Raft entry locally, so get_collection_cluster_info
could race and return 404.
Co-authored-by: Cursor <cursoragent@cursor.com>
Streamable snapshots wrap each segment's files in a nested `files/`
directory. Make recovery tolerate segments where files are placed directly
in the segment directory by only hoisting `files/` when it is present.
Snapshot creation is unchanged; this only relaxes recovery to accept both
layouts (with and without the `files/` wrapper).
The recover-remote-concurrent step can hang indefinitely with S3 storage;
cap CI steps at 15 minutes and curl requests at 120 seconds so stuck runs
do not consume the full 6-hour job timeout.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add prefix keyword index coverage to model_testing soak harness.
Exercise prefix filters on a new `url` payload field through count/scroll,
paginated scroll, facet, search, and query ops, with CreateIndex/DropIndex
toggling a prefix-enabled keyword index to stress index lifecycle and fallback.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Apply rustfmt to model_testing prefix index changes.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address review: preserve keyword-equality coverage, reuse Filter::merge_opts.
- Arms 8/9 now randomly toggle either the plain keyword index on `tag`
(exact-match path) or the prefix-enabled keyword index on `url`, keeping
coverage of both index paths.
- optional_read_filter now delegates to Filter::merge_opts over the existing
match_num_filter / match_url_prefix_filter constructors instead of
hand-building conditions.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix optimizer recreation dropping sibling shard restarts on error
Replace try_join_all with join_all in recreate_optimizers so a failing
shard does not cancel non-cancel-safe on_optimizer_config_update futures
that have already stopped their update workers.
Fixes#9670
Co-authored-by: Cursor <cursoragent@cursor.com>
* Apply cargo fmt
Co-authored-by: Cursor <cursoragent@cursor.com>
* Apply suggestion from @timvisee
Co-authored-by: Tim Visée <tim+github@visee.me>
* Move optimizer config update tests into dedicated file
Extract worker restart test hooks and unit tests from updaters.rs into
optimizer_config_update_tests.rs, following the snapshot_tests pattern.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Andrey Vasnetsov <andrey@vasnetsov.com>
Co-authored-by: Tim Visée <tim+github@visee.me>
Replace try_join_all so a failing shard does not cancel sibling
on_strict_mode_config_update futures before they apply rate limiters.
Co-authored-by: Cursor <cursoragent@cursor.com>
Optimizer-on runs without restarts were the slowest no-restart cell (~98s on
ubuntu CI). Halving OP_NUM matches the existing harness() trimming pattern;
coverage is recovered across many seeded CI runs.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: add serverless feature flag
Introduce a composite `serverless` feature flag that automatically enables
`write_segment_manifest` and `append_only_mutations` during initialization.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor: rename serverless flag to serverless_compatible
Rename the composite feature flag for clarity and consistency with
serverless deployment terminology.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: satisfy clippy field_reassign_with_default in flag tests
Use struct update syntax instead of mutating Default::default() fields.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Track whether optimization ran at any point during the soak run and skip
the post-run wait when the counter was reset by a close+reopen. Also
trigger optimizers explicitly while waiting when the wait is still needed.
Fixes#9626
Co-authored-by: Cursor <cursoragent@cursor.com>
* perf(edge): load ReadOnlyEdgeShard segments in parallel
Open each segment on a dedicated thread during initial open and refresh,
reducing follower startup time for shards with many segments.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(edge): resolve clippy type_complexity in parallel segment load
Co-authored-by: Cursor <cursoragent@cursor.com>
* perf(edge): run per-segment reads on a configurable thread pool
Replace the per-segment sequential read loops and the spawn-a-thread-per-segment
loader with a single fixed-size rayon thread pool owned by each shard.
- Add EdgeConfig::max_search_threads (Option<usize>, None = CPU-derived default
matching the core search runtime via common::defaults::search_thread_count).
- Build a long-lived pool in EdgeShard and ReadOnlyEdgeShard; reuse it for
parallel segment loading on open/refresh instead of std::thread::spawn.
- Add EdgeReadView::par_map_segments as the single seam that runs per-segment
work on the pool; use it in search, scroll, count, facet and rescore-formula.
Each task mints its own HardwareCounterCell from the shared accumulator.
- Expose max_search_threads through the builder and the Python binding (+ stub).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(edge): return error instead of panicking on search pool creation
ThreadPoolBuilder::build() can fail (e.g. thread spawn / resource exhaustion).
This runs during EdgeShard open/load and ReadOnlyEdgeShard follower open, so a
transient failure must not abort the process. Propagate it as an OperationError
through the existing OperationResult-returning constructors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: generall <andrey@vasnetsov.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci(windows): skip slow facet sampling filter iter test
The 200k-point fixture is too slow for Windows CI runners.
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci(windows): gate N_FILTER_ITER behind same cfg as test
Avoid dead_code warning when the sampling filter iter test is skipped.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: add openapi regression test for values_count on missing fields
Reproduces https://github.com/qdrant/qdrant/issues/9586 where points with
a missing payload field are not matched by values_count filters that should
treat the count as 0.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: use module fixture for values_count missing field test
Follow the standard openapi test structure with setup/teardown fixture
instead of explicit drop_collection calls in the test body.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: treat missing field as count 0 for values_count filter (#9607)
`FieldCondition::check_empty()` ignored the `values_count` condition, so a
point with a missing payload field was never matched by a `values_count`
filter. The desired semantics (and the existing `ValuesCount::check_empty`)
treat a missing field as having a value count of 0.
Forward the empty check to `values_count.check_empty()` so bounds like
`lt: 1`, `gte: 0` and `lte: 0` correctly match points whose field is absent.
Fixes#9586
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Andrey Vasnetsov <andrey@vasnetsov.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After `--package qdrant`, cargo scopes `--bin` to the qdrant crate only.
Pass `--package edge-shard-query --bin edge-shard-query` so the edge tool
is actually produced for collection/upload.
Co-authored-by: Cursor <cursoragent@cursor.com>
Without an explicit qdrant package, cargo applies --features service_debug
to every -p target including edge-shard-query, which does not define it.
Co-authored-by: Cursor <cursoragent@cursor.com>
edge-shard-query does not define service_debug; use qdrant/service_debug
when building qdrant bins and edge tools in one cargo invocation.
Co-authored-by: Cursor <cursoragent@cursor.com>
Use the perf profile instead of release, merge qdrant and edge tool builds
into one cargo invocation, and add sccache so shared deps compile once and
rustc outputs are cached across runs.
Co-authored-by: Cursor <cursoragent@cursor.com>
The Debug requirement on UserData (#9588) left two impl blocks without
the trait bound, breaking compilation on dev.
Co-authored-by: Cursor <cursoragent@cursor.com>
Apply the existing "model testing" label alongside "bug" when the nightly
workflow opens or updates a failure issue.
Co-authored-by: Cursor <cursoragent@cursor.com>
DeleteByFilter remains broken: WAL replay can resurrect filter-deleted
points (#9575). Re-mask the op in FORCE_OFF and document the issue.
Co-authored-by: Cursor <cursoragent@cursor.com>
The flushing/reload-durability bug that caused post-reload count
mismatches for live filtered deletes has been fixed, so DeleteByFilter
no longer needs to be masked off in the model tester swarm config.
Remove it from FORCE_OFF and update the related doc comments.
Co-authored-by: Cursor <cursoragent@cursor.com>
Follow-up to #9530 and #9558.
The shard-level segment manifest was written inside the `segments/`
directory (`segments/manifest.json`). Older versions of Qdrant choke on
an unknown file inside `segments/`, so move it next to the directory as
`segments_manifest.json` instead.
- `SEGMENT_MANIFEST_FILE` is now `segments_manifest.json` and
`segment_manifest_path()` points at the shard root.
- The manifest is added to `ShardDataFiles` so clear/move handle it.
- Snapshots write the manifest to the snapshot root (next to
`segments/`), and restore/partial-snapshot loaders no longer need to
skip it inside `segments/`.
Co-authored-by: Cursor <cursoragent@cursor.com>
Follow-up to #9530. When the `write_segment_manifest` feature flag is
enabled, a shard maintains a `segments/manifest.json` listing its
segments so out-of-process readers can discover them without scanning
the filesystem. That manifest was not included in shard snapshots.
Include the segment manifest in the snapshot when the shard maintains
one, capturing it from the live segment holder before proxying (proxies
preserve the wrapped segments' UUIDs, which are the directories written
into the snapshot, so the manifest matches the snapshot contents).
On restore, the snapshot's `segments/manifest.json` is skipped while
restoring segment directories in place; the manifest is regenerated from
the loaded segments when the segment holder is built. The partial
snapshot manifest loader also skips this file.
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: fix rust-cache running before checkout in shard-snapshot job
The `test-shard-snapshot-api-s3-minio` job ran `Swatinem/rust-cache`
before `actions/checkout`, so there was no `Cargo.lock`/source present
when the cache action ran. As a result the action no-opped and the job
never actually restored or saved a Rust build cache. Move the cache
step to run after checkout.
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: remove dead rust-cache steps from docker-only jobs
These jobs only build Docker images via `docker buildx`/docker build
and never invoke cargo directly, so the `Swatinem/rust-cache` step
never populates or restores anything useful:
- integration-tests.yml: `test-consistency` (docker buildx + shell
consistency checks only)
- docker-image.yml: `build` and `build-gpu` (docker buildx only)
Drop the unused cache steps.
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: drop rust-cache from coverage and release workflows
These workflows run on rare triggers (coverage: nightly schedule;
release-artifacts: on release publish), so by the time they run again
their cache is almost always already evicted from the 10 GB Actions
cache budget. They mostly just consume cache space that would otherwise
be useful to the frequently-run PR builds, so remove the rust-cache
steps entirely:
- coverage.yml: unit-coverage and integration-coverage
- release-artifacts.yml: build-windows-binaries
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: drop explicit clippy cache key in rust-lint
The lint job has no compatible cache peer to share with: clippy
produces clippy-driver metadata that cargo fingerprints separately
from normal build/test artifacts, and its `--all-features` invocation
pulls in the GPU dependency crates, so it matches neither the
rust-tests nor the gpu group. With nothing to share, the explicit
`key: clippy` is equivalent to the default per-job key, so drop it for
consistency with the rest of the workflows.
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: dedicated rust-cache keys with single writer per group
Give related jobs explicit cache keys and let only the broadest build
in each group save the cache (`save-if: "false"` on the others), so a
narrower build can't overwrite it with fewer compiled dependencies
first.
- `rust-tests` (rust.yml): explicit `shared-key: rust-tests`. It builds
`--workspace --tests`, which pulls in dev-dependency features that the
integration-tests builds (plain `cargo build`) do not, so it is kept
as its own dependency cache rather than merged with integration-tests.
- `integration-tests`: writer = integration-tests; readers =
integration-tests-consensus, test-shard-snapshot-api-s3-minio.
- `edge`: writer = edge-test (clippy + examples build); reader =
edge-rust-check. Both build the amalgamated qdrant-edge `examples`
package.
rust-gpu keeps its own default cache (the `gpu` feature adds the
ash/gpu-allocator/shaderc dependency crates).
Co-authored-by: Cursor <cursoragent@cursor.com>
* Reuse `integration-tests` build cache for `io-bridge-object-store-tests` workflow
* Cleanup Docker cache in GHA workflows
* ci: only save rust-cache on the integration branch
Under the 10 GB Actions cache budget there's no practical benefit to
saving feature-branch build caches: they'd be evicted before being
reused. Scope every rust-cache *writer* to save only on `dev` (the
branch PRs target and restore from) via `save-if`:
- rust.yml, rust-lint.yml, edge-test.yml, integration-tests.yml
Reader jobs already have `save-if: "false"` and are unchanged.
Also drop rust-cache from rust-gpu.yml entirely: it only runs on
`master` pushes (releases, a few times a month at most), so any cache
would always be evicted between runs.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Roman Titov <ffuugoo@users.noreply.github.com>
* feat(bm25): add explicit Disabled stemmer; deprecate language hack
Adds a `Disabled` variant to `StemmingAlgorithm` (`stemmer: {"type": "none"}`)
so stemming can be turned off explicitly in both the main engine and Edge,
instead of relying on the undocumented `language: "none"` footgun that
silently disabled both stemming and stopwords.
For language-neutral text processing the supported setup is now:
1. set the stemmer to disabled, and
2. configure an empty stopword set.
The main engine still tolerates unsupported languages (so existing
`language: "none"` configs keep working on upgrade) but now logs a
deprecation warning pointing users to the explicit setup. Edge continues
to reject unsupported languages, and now has a real way to disable stemming.
Refs: https://github.com/qdrant/qdrant/issues/9289
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(edge-py): handle Disabled stemmer in python bindings; fix openapi schema
- Handle the new StemmingAlgorithm::Disabled variant in the qdrant-edge-py
bindings (FromPyObject/IntoPyObject/Repr) and add a DisabledStemmer pyclass
plus its .pyi stub entry.
- Match generator output for the StemmingAlgorithm OpenAPI schema (plain $ref
in anyOf) so docs/redoc/master/openapi.json stays consistent.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(openapi): regenerate StemmingAlgorithm schema with generator output
Ran tools/generate_openapi_models.sh so docs/redoc/master/openapi.json
exactly matches generator output: DisabledStemmerParams/NoStemmer are placed
after SnowballLanguage, and the StemmingAlgorithm anyOf entry is a plain $ref
(the schema2openapi step flattens the allOf+description wrapper).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(test): avoid wildcard enum match arm in bm25 sparse_len helper
clippy --all-targets flags `other => panic!()` as wildcard_enum_match_arm;
match the Dense/MultiDense variants explicitly instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: issues
* fix: log::warn as call once
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Daniel Boros <dancixx@gmail.com>
* test(model_testing): add basic Query API coverage
Add an Op::Query variant that exercises the unified Query API with a
plain Nearest scoring query (ScoringQuery::Vector(QueryEnum::Nearest)),
routed through collection.query. Verification mirrors the existing
Search op: exact dense / multi-dense scans must return exactly the
top-k, sparse and approximate paths are an upper bound, and every
returned id must exist in the model with the queried vector populated
and matching the optional num filter.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(model_testing): unify Search/Query invariant check
Extract the shared candidate set, strict-vs-upper-bound decision,
per-result membership checks, and the rich diagnostic probes
(retrieve/count/scroll/retry) into helpers reused by both apply_search
and apply_query. apply_query now gets the same strict top-k check plus
failure diagnostics it previously lacked.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Extend ScrollFilter with a HasVector variant so paginated scroll
exercises Condition::HasVector. The matcher targets an active vector
name, and the model verifier checks it against each point's populated
vector set (which varies via DeleteVectors / partial UpdateVectors),
restricting results to a known, model-checkable subset.
Co-authored-by: Cursor <cursoragent@cursor.com>
Previously all three Kubernetes health endpoints shared the same generic
description ("An endpoint for health checking used in Kubernetes."), which did
not convey what each one actually guarantees.
- /healthz and /livez: clarify they are pure liveness checks (200 once the HTTP
API is up), do not inspect data/shards/consensus, and are identical to each
other.
- /readyz: clarify it is a readiness probe that waits out pending data
operations (consensus catch-up + shard health in distributed mode) before
reporting ready, and document the real 200 ("all shards are ready") and 503
("some shards are not ready") responses.
Documentation-only change (OpenAPI descriptions/examples + added 503 response
doc). No runtime behavior changes.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(model_testing): add has_id matcher to generated scroll filter
Extend the ScrollPaged filter selector with a HasId variant so the
generated filter exercises a has_id matcher (restrict to an explicit
point-id set), in addition to the existing none / num / tag filters.
The id set mixes ids present in the model with ids drawn from the id pool
that may not be, so the matcher meaningfully restricts. The model mirrors
it with a set-membership predicate, and the existing paged-scroll
id-set assertion validates the engine result.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: fix clippy lints (wildcard_enum_match_arm, from_iter_instead_of_collect)
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(model_testing): clamp has_id sample count to id_pool
Addresses CodeRabbit review: random_distinct_ids could spin forever when
the requested distinct count (up to 15) exceeds id_pool. Clamp to
id_pool.min(15). Only relevant for tiny --id-pool values; the default
pool (500) is far above 15.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Add a ScrollPaged verification op that scrolls the (optionally filtered)
collection in pages of a small limit, following next_page_offset until
exhausted. The other scroll ops always read everything in one page
(offset: None, limit: usize::MAX), so the offset cursor and a real
(non-MAX) limit had no coverage.
Asserts each page holds at most `limit` points, no id repeats across
pages, and the union of all pages equals the model's expected id set for
the chosen filter (none / num == X / tag == X). Includes a page-count
guard against a stuck cursor.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add a RetrieveSelective verification op that exercises the
with_payload/with_vector selector forms, which the soak test previously
never used (every read passed Bool only).
- with_payload covers Bool, Fields, Selector::Include, Selector::Exclude.
- with_vector covers both Bool forms and Selector(names) over a subset of
active vector names.
The verifier asserts the engine's returned payload equals the model entry
filtered by the engine's own PayloadSelector::process, and the returned
vectors equal the requested name subset that the point actually has
populated.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add a SetPayloadByKey op to the model_testing soak test so the keyed
set-payload path (SetPayloadOp.key) is exercised. Previously every
set-payload op passed key: None, leaving the key-scoped assignment path
(merge_by_key / JsonPath::value_set) uncovered.
The op samples existing point ids, a random payload, and a single
top-level schema field as the key. The model mirrors the engine via
Payload::merge_by_key (the same JsonPath::value_set the engine's
set_by_key uses), keeping model and engine in lockstep.
Co-authored-by: Cursor <cursoragent@cursor.com>
The test asserts that creating a partial snapshot between two in-sync peers
returns 304 (empty diff). It only waited for the write peer to become green,
but read the read peer's manifest for the comparison. An async optimization
reshaping the read peer's segments after collection-snapshot recovery makes
its manifest diverge from the write peer's files, producing 200 instead of
304 (assert 200 == 304).
Wait for the read peer to become green as well before comparing manifests,
mirroring the earlier flaky-test fixes (#7358, #7360).
Co-authored-by: Cursor <cursoragent@cursor.com>
Add a new named dense vector "i" to the model_testing fixture configured
with HNSW inline_storage backed by scalar quantization, so the soak harness
exercises the inline-storage index layout. It behaves like a normal dense
vector to the model; only the on-disk HNSW layout differs.
Co-authored-by: Cursor <cursoragent@cursor.com>
The 6 turbo vector storage model tests are flagged SLOW by nextest on
Windows CI (>60s, some >240s):
- vector_storage::turbo::tests::turbo_model_test_random_ops_{dot,cosine}
- vector_storage::turbo::multi::tests::turbo_multi_model_test_random_ops_{dot,cosine}
- vector_storage::turbo::test::congruent_random_ops_{dot,cosine}
These are dim x seed x ops sweeps; Windows runners are several times slower.
Lower SEEDS_PER_CELL on Windows only via #[cfg(windows)] so the runs stay
within the slow-timeout, while keeping full coverage on Linux/macOS.
Co-authored-by: Cursor <cursoragent@cursor.com>
Reduce the default in-RAM update worker queue size to limit memory
held by pending operations and provide faster backpressure.
Co-authored-by: Cursor <cursoragent@cursor.com>
A plain upsert could fail with a spurious "No point with id ... found"
(HTTP 404) when it raced a `prevent_unoptimized` optimization. This
surfaced as flaky CI failures of
`test_shard_transfer_includes_deferred_points[snapshot]`.
Root cause: `apply_points_with_conditional_move` reads the source point's
vectors and payload before relocating it into an appendable segment. Those
reads used the default `DeferredBehavior::VisibleOnly` accessors
(`all_vectors`/`payload`). When the source point is deferred — invisible to
ordinary reads, e.g. a point whose internal id is beyond the deferred
threshold under `prevent_unoptimized` while an optimization wraps its
segment in a proxy — `VisibleOnly` cannot resolve it: `payload`/`vector`
raise `PointIdError`, which propagates out as the user-facing 404 on a plain
upsert that internally takes the copy-on-write move path.
Fix: add deferred-aware read accessors (`vector_with_behavior`,
`all_vectors_with_behavior`, `payload_with_behavior`) to `ReadSegmentEntry`,
implemented on both `Segment` and `ProxySegment` (the proxy forwards the
behavior to its wrapped segment). The existing accessors delegate with
`VisibleOnly`, preserving current behavior. The CoW move path now reads with
`WithDeferred`, so deferred points are relocated with their real data
instead of failing.
Adds a deterministic regression test asserting `VisibleOnly` hides a
deferred point while `WithDeferred` resolves its real vectors/payload.
Co-authored-by: Cursor <cursoragent@cursor.com>
`TurboVectorStorage::update_from` references `AtomicBool` and `Range`
without importing them, breaking `cargo clippy --workspace` on the `dev`
branch (introduced in #9346). Import both types to restore the build.
Co-authored-by: Cursor <cursoragent@cursor.com>
The test relied on concurrent background upserts adding *new* matching
points to the destination shard during the streamed transfer, asserting
the destination count was strictly greater than the original snapshot.
Whether new matching points land in the destination before the transfer
completes is timing-dependent (especially under pytest-xdist load), so
the strict `>` assertion is racy and occasionally fails with equal
counts (e.g. `assert 5031 > 5031`).
Relax to `>=` and keep the strict `dest == src` consistency check, which
is the actual invariant being verified.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: reproduce MatchAny(any=[]) strict-mode rejection on integer index
Adds an openapi test that demonstrates the strict-mode bug where
`match: {"any": []}` on an integer-indexed payload field is rejected with:
Bad request: Index required but not found for "<field>" of one of
the following types: [keyword, uuid]
even though the field is indexed as integer. Root cause is that an empty
`any` list deserializes as `AnyVariants::Strings(empty)` (untagged enum;
Strings variant listed first), and strict mode infers a keyword/uuid
index requirement from the variant tag — ignoring that the list is
empty (i.e. a no-op condition).
The test covers:
- Baseline no-op semantics with and without indexes (passes today).
- Reproduction under strict mode for `must`, `must_not`, and a
FormulaQuery FieldCondition (currently failing; will pass once
empty `any`/`except` no longer requires an index).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: treat empty MatchAny/MatchExcept as no-op in strict-mode index check
An empty `match: {"any": []}` (or `{"except": []}`) is a no-op: `any: []`
matches nothing and `except: []` excludes nothing, regardless of the
field's data type. Because an empty list cannot carry type information it
deserializes as the keyword `AnyVariants::Strings(empty)` variant, which
previously caused strict mode to demand a keyword/uuid index and reject
the request with:
Index required but not found for "<field>" of one of the following
types: [keyword, uuid]
even on a field indexed as integer.
Fix:
- `infer_index_from_any_variants` returns no required index for an empty
variant set.
- `Extractor::update_from_condition` skips a condition whose required
index set is empty, so a no-op condition is never reported as needing
an index (this also covers the FormulaQuery condition path).
This makes the openapi reproduction in test_match_any_empty.py pass.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* Strict mode: add `max_disk_usage_percent`
Mirrors `max_resident_memory_percent`: rejects disk-consuming update ops
(upsert, set/overwrite payload, update vectors) when the filesystem hosting
Qdrant storage is filled above the configured percentage. Delete-style ops
remain allowed so callers can free disk.
Disk usage is sampled via `statvfs` and TTL-cached for 5s (same cadence as
the resident-memory reader) so high-RPS request paths don't hammer the
syscall. Reader is keyed by path in `common::disk_usage` and returns `None`
on stat failure — callers (the strict-mode check) treat `None` as "skip",
matching the memory-check behaviour.
Plumbing follows the existing pattern: field on `StrictModeConfig` (+
output/diff/Hash), gRPC proto field `22`, validation 1..=100, REST/proto
conversions, and the hook into `check_strict_mode_toc_batch` alongside the
memory check (both guarded by `any_consumes_memory`).
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix CI: Windows disk_usage test + e2e WAL config
- `missing_path_returns_none` panicked on Windows because
`GetDiskFreeSpaceEx` succeeds for non-existent paths (it resolves up to
the containing drive). Relax the assertion to "must not panic; if a
value is returned it must be well-formed". The contract we care about
(None on failure) is platform-defined, not something we can portably
force.
- e2e test failed at batch 0 with "WAL buffer size exceeds available disk
space": Qdrant's existing per-shard `DiskUsageWatcher` enforces
`free >= 2 * wal_capacity_mb` and the default WAL didn't fit in the
50 MB tmpfs. Bump tmpfs to 200 MB and shrink `wal_capacity_mb` to 1 MB
(same pattern as `test_low_disk.py`) so our strict-mode gate is the
one that fires, not the WAL pre-check. Raise the gate threshold to
50% to match the larger headroom.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add Rust edge example: add-named-vector
Port `lib/edge/python/examples/add-named-vector.py` to Rust under
`lib/edge/publish/examples/src/bin/add-named-vector.rs`.
Re-export `VectorNameOperations`, `CreateVectorName`, `DeleteVectorName`,
`VectorNameConfig`, `DenseVectorConfig`, and `SparseVectorConfig` from
`qdrant_edge` so the public Rust API can create/delete named vectors
without reaching into the internal `shard` crate.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fmt
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the qdrant fork of stusmall/murmur3 (pinned at git rev 2c39087) with
the standalone `murmur3_32` crate from crates.io. Output is bit-identical
(verified offline against the previous `murmur3_32_of_slice`, including a
100k-iteration random fuzz), so existing BM25 sparse vectors remain
wire-compatible. The new crate is ~10–18% faster on inputs ≥ 16 bytes,
which speeds up `token_id` during tokenization.
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci(windows): skip IO-heavy tests that aren't OS-specific
On the Windows CI runner, several tests are 3-25x slower than on Ubuntu
purely due to slow filesystem IO. These tests exercise platform-agnostic
logic (optimizer, snapshot, WAL recovery, dedup, deferred points) and
are fully covered by the Linux and macOS jobs.
Mark them with `#[cfg_attr(target_os = "windows", ignore = "...")]` so:
- Windows CI skips them and finishes faster.
- They're still listed and runnable via `cargo test -- --ignored`
on Windows for local debugging.
Based on JUnit timings from CI run 26462785436 (PR #8827), this should
save ~5 minutes wall-clock on the Windows job, taking it closer to the
~13min Ubuntu and ~9min macOS jobs (currently 20m24s).
Tests affected:
- lib/wal: check_wal, check_last_index, check_clear, check_reopen,
check_truncate, check_prefix_truncate, test_prefix_truncate_parametric
- lib/edge/optimize: full tests module
- lib/segment deferred-point tests: read_operations,
dense_segment_combinations, sparse, facets
- lib/collection: snapshot_test, points_dedup, wal_recovery,
collection_test::test_ordered_read_api, snapshot_recovery_test
Co-authored-by: Cursor <cursoragent@cursor.com>
* revert(ci/windows): keep WAL and WAL-recovery tests on Windows
Reviewer correctly pointed out that WAL is mmap-backed and has
substantial Windows-specific code paths:
- Different segment allocation (fs4 vs rustix::ftruncate)
- Windows-specific delete_windows() with mmap-drop + retry loop
- Windows-specific sync_all() because directory fsync is unavailable
- Windows-specific lock proxy file (directories aren't lockable)
So those tests genuinely need Windows coverage. Reverted skips for:
- lib/wal/src/lib.rs: all check_* tests and test_prefix_truncate_parametric
- lib/collection/src/tests/wal_recovery_test.rs: all three tests
Still skipped on Windows (no OS-specific code in their production paths):
- lib/edge/optimize.rs (no cfg(windows) in source)
- lib/segment deferred-point tests (segment/ has no cfg(windows))
- lib/collection snapshot/dedup tests (collection/ has no cfg(windows))
- lib/collection integration snapshot_recovery + ordered_read_api
Co-authored-by: Cursor <cursoragent@cursor.com>
* revert(ci/windows): keep collection integration and snapshot_test
Per reviewer request, keep running these on Windows:
- lib/collection/tests/integration/* (snapshot_recovery_test,
collection_test::test_ordered_read_api)
- lib/collection/src/tests/snapshot_test.rs
These exercise higher-level collection/snapshot behavior that benefits
from cross-platform validation.
Remaining Windows skips (production code has no cfg(windows) branches):
- lib/edge/src/optimize.rs: 14 optimizer tests
- lib/segment/src/segment/tests/mod.rs: 4 deferred-point tests
- lib/collection/src/tests/points_dedup.rs: 2 dedup tests
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci(windows): also skip HNSW/quantization integration tests
Per reviewer, also skip these segment integration test modules on Windows:
- hnsw_quantized_search_test::* (25 tests)
- multivector_filtrable_hnsw_test::* (rstest cases)
- multivector_quantization_test::* (rstest cases)
- byte_storage_quantization_test::* (rstest cases)
- payload_index_test::test_struct_payload_index_nested_fields
These exercise pure HNSW/quantization correctness on top of standard
segment IO that is already covered by tests we keep running on Windows.
Adds ~930s of sequential time to the Windows skip list, bringing the
expected wall-clock saving from ~3 min to ~8-10 min.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: wait for WAL flock release on peer restart in dirty-shard test
The flake addressed by #9124 (bumping `wait_for_peer_online` to 60s) was
misdiagnosed as CPU contention. Logs show the restarted peer panics within
~1s of startup at `consensus_wal.rs:36` with:
Wal error: Can't init WAL: Kind(WouldBlock)
Panic: Can't open consensus WAL: Kind(WouldBlock)
`wal::Wal::open` calls `fs4::FileExt::try_lock` (non-blocking `flock`) on
the WAL directory fd. After `p.kill()` (SIGKILL + waitpid) the kernel
normally releases the killed peer's flock immediately, but under
pytest-xdist load there is a small window where it lags. The fresh peer's
startup then races and panics. After the panic `/readyz` never returns
200, so neither 30s nor 60s rescues the test.
Fix: add a `wait_for_wal_unlocked` helper that polls both the consensus
WAL directory and the local-shard WAL directory with the same exclusive
non-blocking flock that qdrant uses, and call it after every `p.kill()`
that is followed by a `start_peer` on the same `peer_dir`. The two
60s timeouts are restored to the default 30s now that the underlying
race is gone.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: fail fast if sync restart crashes in dirty-shard test
The sync restart in `test_dirty_shard_survives_update_collection` used
plain `wait_for_peer_online(sync_uri)`, which only polls `/readyz`. If
the freshly started peer panics on startup (e.g. WAL `WouldBlock`), the
test waits the full 30s timeout and then reports a `/readyz` timeout
instead of the actual panic message and exit code.
Switch the sync restart to `wait_for_peer_online_or_crash(...)` (same
helper already used for the dirty restart). On crash it dumps the peer
log tail so the next CI failure shows the real reason instead of a
generic timeout.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: drop speculative WAL flock wait, keep crash-detection switch
Reverts the `wait_for_wal_unlocked` helper that polled the WAL
directories with non-blocking flock. The kernel-side flock race it was
guarding against could not be reproduced in isolation (0/300 iters of
SIGKILL+wait+re-flock on bare Linux), so it was speculative.
Kept:
- Sync restart now uses `wait_for_peer_online_or_crash(...)` instead of
plain `wait_for_peer_online`, so any startup panic surfaces fast with
the actual log tail instead of hiding behind a 30s `/readyz` timeout.
- The two 60s timeouts bumped in #9124 are restored to the default 30s.
If the flake recurs in CI, the new failure output will tell us the
actual cause (panic message + exit code), which is more useful than
papering over it.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor Agent <agent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The previous description was outdated: it claimed that enabling this
option "blocks updates at the request level" until segments are
re-optimized. In practice the implementation uses "deferred points":
new points written to large unoptimized segments are persisted but
excluded from read/search results until the segments are optimized.
Updates are not blocked; only `wait=true` clients are made to wait for
the deferred points to become visible. Update this in the REST schema
(via `OptimizersConfig` / `OptimizersConfigDiff`), in the gRPC proto,
in the edge config docstrings, and regenerate the OpenAPI bundle via
`tools/generate_openapi_models.sh`.
Co-authored-by: Cursor Agent <agent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
`commit_read_hashring` returns once the leader has applied the entry,
but followers may not have applied it yet. The CommitRead handler calls
`invalidate_clean_local_shards` for `old.nodes()`, which cancels any
ongoing shard clean task. If the test's subsequent `cleanup?wait=true`
request to a follower arrives just before that follower applies
CommitRead, the cleanup task is started and then cancelled mid-flight,
and the endpoint returns HTTP 500 "Failed to clean shard points due to
cancellation, please try again".
Add `wait_for_same_commit` after `commit_read_hashring` so every peer
has applied the entry (and thus already invalidated any clean tasks
that don't exist yet) before we issue cleanup.
Observed flake:
https://github.com/qdrant/qdrant/actions/runs/26259341982/job/77289143792
Co-authored-by: Cursor Agent <agent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Under parallel pytest-xdist load the rejoining peer needs more than the
default 30s budget to replicate the UpdateCollection entry and report
ready, causing intermittent CI failures:
Exception: Timeout waiting for condition peer_is_online to be satisfied
in 30 seconds
Bump both peer-online waits in the dirty shard crash-loop test to 60s,
matching the precedent set by #8963 for similar CPU-contention flakes.
Co-authored-by: Cursor Agent <agent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* Use cluster default shard transfer method for fallback
When a WAL delta automatic transfer fails, the driver falls back to the
method passed via `fallback_method`. This was hard-coded to
`StreamRecords` (unless `prevent_unoptimized` was enabled), which is
inconsistent with the 1.18.0+ default of `Snapshot` and ignores any
configured `default_shard_transfer_method`.
Use `Collection::default_shard_transfer_method()` instead, so the
fallback matches the cluster default. With `prevent_unoptimized` we
still pin to `Snapshot` to preserve deferred point state exactly (raw
segment copy); stream_records would send deferred points but they
would not be deferred on the target.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Avoid wal_delta fallback, update fallback test
If the cluster default transfer method is wal_delta, the same-method
fallback would be refused by the driver. Use snapshot as a safe fallback
in that case; snapshot is also the 1.18.0+ default.
Update test_shard_wal_delta_transfer_fallback to assert the new
snapshot fallback (was stream_records).
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address clippy wildcard_enum_match_arm
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor Agent <agent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The test_shard_snapshot_transfer_throttled_updates test was flaky because
it checked data consistency immediately after killing background upload
processes, without waiting for in-flight writes to propagate across peers.
All sibling tests (test_shard_snapshot_transfer_fast_burst,
test_shard_stream_transfer_throttled_updates, etc.) already include a
sleep(1) after killing uploaders. This was the only variant missing it.
Co-authored-by: Cursor Agent <agent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Pre-pull the fullstorydev/grpcurl Docker image before running the test
and retry the gRPC health check up to 3 times, so transient Docker
networking hiccups on CI runners no longer cause a hard failure.
Co-authored-by: Cursor Agent <agent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: validate vector dimensions before WAL write for async upserts
When upserting points with wait=false (the default), dimension
mismatches were silently discarded during background processing.
The API returned 200 "acknowledged" but the points were never stored,
causing silent data loss with no error feedback to the user.
This adds an early dimension validation check in do_upsert_points()
that runs before the operation is written to WAL. This ensures that
dimension errors are returned to the client regardless of the wait
parameter, matching the behavior of wait=true.
The validation handles all vector types:
- Dense single vectors
- Multi-dense vectors
- Named vectors (dense, multi-dense, sparse)
- Sparse vectors are skipped (no fixed dimension)
Closes#9039
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor: move vector dimension validation into dedicated module
Extract validate_vector_dimensions and helper functions from update.rs
into src/common/validate_vectors.rs for better code organization.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: update shard update test for early dimension validation
The test expected a shard-level error message, but now dimension
mismatches are caught before reaching the shards. Update the assertion
to accept either the early validation error or the shard-level error.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: assert actual dimension error message in shard update test
Check for the descriptive error ("Vector dimension error: expected dim: 4, got 3")
rather than the generic shard failure wrapper.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor Agent <agent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add integration test for `match: {except: []}` with integer index
Regression test for https://github.com/qdrant/qdrant/issues/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 <cursoragent@cursor.com>
* Fix `match: {except: []}` returning zero results with payload index
Fixes https://github.com/qdrant/qdrant/issues/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 <cursoragent@cursor.com>
* 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 <cursoragent@cursor.com>
---------
Co-authored-by: Cursor Agent <agent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: add failing test for values_per_hash drift on duplicate geo point removal
Reproduces the bug where `remove_point` only calls `decrement_hash_value_counts`
once per unique geohash, while `add_many_geo_points` increments it once per value.
When a point has duplicate geo coordinates (same geohash), the counters drift
upward permanently after removal.
Ref: https://github.com/qdrant/qdrant/pull/9033#discussion_r3241154045
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: decrement values_per_hash once per value on geo point removal
`add_many_geo_points` increments `values_per_hash` once per value, but
`remove_point` deduplicated geohashes with a HashSet + `continue` that
also skipped the per-value decrement. A point with duplicate geo
coordinates (same geohash) therefore left the counters drifted upward
permanently after removal.
Move `decrement_hash_value_counts` above the dedup guard so it runs once
per value, matching the increment side. `points_map` and
`points_per_hash` track points, not values, so they stay deduplicated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Cursor Agent <agent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: generall <andrey@vasnetsov.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The geo_index module was a single 2134-line file. Split it into focused
files for better readability and maintainability:
- mod.rs: GeoMapIndex enum definition and core methods (~290 lines)
- builders.rs: GeoMapIndexMmapBuilder and GeoMapIndexGridstoreBuilder
- payload_index.rs: ValueIndexer, PayloadFieldIndex, PayloadFieldIndexRead impls
- tests.rs: all test code (~1340 lines)
No logic changes; all 40 existing tests pass.
Co-authored-by: Cursor Agent <agent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The ~2150-line map_index/mod.rs was difficult to navigate. Split it into
focused files while preserving all logic and public API:
- key.rs: MapIndexKey trait and impls for str, IntPayloadType, UuidIntType
- builders.rs: MapIndexBuilder, MapIndexMmapBuilder, MapIndexGridstoreBuilder
- payload_index_impl_str.rs: PayloadFieldIndex/Read for MapIndex<str>
- payload_index_impl_int.rs: PayloadFieldIndex/Read for MapIndex<IntPayloadType>
- payload_index_impl_uuid.rs: PayloadFieldIndex/Read for MapIndex<UuidIntType>
- facet_index_impl.rs: FacetIndex impl for MapIndex<N>
- value_indexer_impl.rs: ValueIndexer impls and value_retriever methods
mod.rs retains the MapIndex enum, its core inherent methods, type aliases,
constants, and tests.
Co-authored-by: Cursor Agent <agent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix phrase matching crossing string-array element boundaries
When indexing a string array like ["quick", "brown"], the index
concatenated tokens from all elements into a single flat document.
A phrase query for "quick brown" found the tokens consecutively,
producing false matches.
Insert a sentinel token ("\x00") between array elements during
indexing. It is registered as a normal vocab token that occupies a
position in the document, so phrase windows cannot span it. No
tokenizer ever produces this string, so it never appears in a query.
Closes#8937
Co-authored-by: Cursor <cursoragent@cursor.com>
* Strip null bytes from tokenizer output
Ensure no tokenizer can produce tokens containing '\0', which is
reserved as the array-boundary sentinel for phrase matching.
Covers all tokenizer paths:
- process_token_cow (used by Word, Whitespace, Prefix, Multilingual)
- Japanese tokenizer (has its own inline processing)
Co-authored-by: Cursor <cursoragent@cursor.com>
* Revert "Strip null bytes from tokenizer output"
This reverts commit fe6ea7a955.
* filter sentinel token from query
* Preallocate vector
Co-authored-by: Tim Visée <tim+github@visee.me>
---------
Co-authored-by: Cursor Agent <agent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: generall <andrey@vasnetsov.com>
Co-authored-by: Tim Visée <tim+github@visee.me>