Expand the regression suite to the full {optimizer on/off} x {restarts
on/off} matrix via a shared `smoke` helper, and retain storage on panic
(with seed) through a `StorageGuard` for postmortem repro. Op counts
unified to 8000; the optimizer+restarts cell runs at OP_NUM/4 to avoid
being the suite long pole. Module skipped on Windows (too slow).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* model_testing: add QueryFusion op (prefetch + fusion coverage)
The model tester's Query op only issued a top-level Nearest query, leaving
the Query API's prefetch + fusion path untested. QueryFusion issues 1-3
independent Nearest prefetch sources fused with RRF or DBSF, plus an
optional outer num filter.
Fusion ranking is score-based and approximate, so the oracle is upper-bound
only (same convention as Search/Query): every result must be some prefetch's
candidate and pass the outer filter, and the result size can't exceed the
outer-filtered candidate union capped at limit. Scores and order are not
checked. Single prefetch level (no nesting).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* model_testing: address review on QueryFusion
- assert no duplicate ids before collapsing fusion results into a set, so
an engine that fails to dedup overlapping prefetch hits is caught
- log full prefetch descriptors (vector_name, limit, filter_num) in the
trace instead of just vector names
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.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>
* [AI] fix: bound random-sample pre-allocation by available point count
`LocalShard::scroll_randomly` pre-allocated its result set with
`HashSet::with_capacity(limit)`, where `limit` is the client-supplied request
limit and is unbounded by default (`StrictModeConfig::max_query_limit` is `None`
unless an operator sets it). A very large limit made the up-front allocation
fail and abort the process (`handle_alloc_error`), which cannot be caught and
returned as an error.
The sample loop inserts at most `min(limit, total points available across
segments)` points, so bound the pre-allocation to what is actually available.
Capacity is unchanged for normal requests; only the pathological case is bounded.
Same class as the search top-k fix (#4321 / #4328) and the in-flight edge fix
(#9374). Adds a regression test.
* [AI] fix: cap random-sample preallocation by filtered candidate count
Review feedback. `available_point_count_without_deferred()` is filter-unaware,
so `availability.iter().sum()` could still drive `HashSet::with_capacity()` to
reserve a buffer proportional to the full collection on a highly selective
filtered request, re-opening the allocator-abort path on large collections.
The sampling loop only ever inserts points read into `segments_reads`, which is
filter-aware and already capped at `limit` per segment, so bound the
preallocation by that candidate count instead.
* Update lib/collection/src/shards/local_shard/scroll.rs
Co-authored-by: qdrant-cloud-bot <111755117+qdrant-cloud-bot@users.noreply.github.com>
---------
Co-authored-by: Andrey Vasnetsov <vasnetsov93@gmail.com>
Co-authored-by: qdrant-cloud-bot <111755117+qdrant-cloud-bot@users.noreply.github.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>
Turn the single-purpose `edge-s3-scroll` tool into a sub-command-based
`edge-shard-query` that can run different read requests against a
ReadOnlyEdgeShard opened over object storage.
- Add `scroll` and `search` sub-commands; shared connection/cache args
live at the top level.
- Accept arbitrary payload filters as JSON via `--filter` (curl
`--data` style: literal JSON, `@file`, or `@-` for stdin), parsed
straight into the filter DSL. Keep `--filter-key`/`--filter-value`
as a shortcut.
- `search` accepts a query vector (JSON array, comma list, or
`@file`/`@-`), named vector, offset, score threshold, and HNSW
params.
- Rename the package/binary/dir (`s3_scroll` -> `shard_query`) since
the tool is no longer scroll-only nor S3-only.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.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>
Add a `Debug` supertrait bound to the `UserData` marker trait and propagate
it through the universal-io read pipelines and the read APIs built on them.
This lets pipeline user-data (request ids, point ids, internal read metadata)
be formatted for diagnostics. Wrapper types that carry user data through the
pipelines (`RemoteMeta`, gridstore `ReadMeta`, hashmap `Entry`) gain `Debug`,
and the `U: UserData` bound is threaded through `read_vectors`/`read_payloads`/
`read_values`/`iter_vectors` and their implementors in gridstore and segment.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf: read whole objects in a single GET instead of HEAD+GET
* feat: read object tail from offset to EOF in a single GET
Generalize the whole-object single-GET read so a tail read from a known
offset also avoids the separate len()/HEAD round-trip. The backend
primitive becomes `read_from(path, from)`, issuing an open-ended
`GetRange::Offset(from)` GET (or a plain GET for from == 0) and reporting
the object's total size from the response; `read_whole` is now a thin
wrapper over it.
The owned blob pipeline's `schedule_whole` no longer HEADs the remote to
size a tail read. An offset at/past EOF is an unsatisfiable range (HTTP
416) rather than an empty body, so the buffer builder disambiguates with
a single len() only on the error path, yielding an empty read when the
tail is genuinely empty. The disk cache's reopen prefiller is made
tolerant of that empty tail so it no longer truncates the local mirror.
Also split the now-hard-to-follow simple_disk_cache `file.rs` into a
`file/` module (type/state, init state machine, reopen, read surface)
and document the FromScratch vs Prefiller init sources.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: generall <andrey@vasnetsov.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add NumericIndexValue
* MapConditionChecker: get rid of `F: Fn() -> bool` bound
To put this type into the upcoming `ConditionCheckerEnum`, it should be
nameable.
* filter_context: `Box<dyn ConditionChecker>` -> `OptimizedFilter`
Removes one level of dyn indirection, so faster checks.
* NullConditionChecker: merge IsEmpty/IsNull checkers into one
So, less variants in the upcoming `ConditionCheckerEnum`.
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>
* feat: read-only edge shard follower (ReadOnlyEdgeShard)
Add a read-only follower of EdgeShard, mirroring the segment-level
ReadOnlySegment + live_reload design one level up at the shard. A leader
process owns a read-write EdgeShard; one or more followers open the same
on-disk directory as a ReadOnlyEdgeShard, serve reads, and periodically
refresh() to pick up the leader's flushed writes and optimizations.
Shared read logic lives once in a crate-internal EdgeReadView<H>, generic
over a ReadSegmentHandle: the follower is monomorphized over the concrete
ReadOnlySegment<S> (no dynamic dispatch), while the read-write shard's
heterogeneous Segment/ProxySegment holder uses the LockedSegment enum
(the only dyn ReadSegmentEntry left, mirroring LockedSegment::get_read).
EdgeShardRead is the public read API: it exposes the read methods
(search/query/retrieve/count/facet/scroll/info/...) as default methods,
requiring implementors only to provide read_segments() + config_snapshot().
EdgeShard keeps its inherent read methods for backwards compatibility.
Segment discovery is injected via a SegmentEnumerator (temporary seam):
LocalSegmentEnumerator scans the segments/ directory for the local/mmap
case; S3 followers supply their own. This will be replaced by an on-disk
segment manifest in follow-up work.
shard: add LockedSegment::get_read_arc(); split retrieve_blocking into a
handle-collecting wrapper + generic retrieve_over; generalize the private
_read_points over the segment type (public signatures unchanged); remove
the now-unused read_points_locked.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Emj8TFxdtrf3K32eWhGgor
* feat: manifest-based segment loading for ReadOnlyEdgeShard
Replace the read-only follower's directory-scan discovery with the segment
manifest from the leader, the proper mechanism the SegmentEnumerator seam
was a placeholder for.
shard: add SegmentsManifest::load so readers can read manifest.json.
edge (leader): EdgeShard now writes segments/manifest.json (gated by the
write_segment_manifest feature flag), initialized from the live segment
set on new/load and refreshed after optimize() swaps segments.
edge (follower): ManifestSegmentEnumerator reads the manifest's `active`
segments instead of scanning, and open_mmap uses it. It requires a manifest
and errors when none is present (no silent scan fallback); discover by
scanning explicitly via open() with a LocalSegmentEnumerator instead.
Since the manifest only reports ready segments (and future versions will
mark segments retiring/under-construction rather than removing them from
active immediately), the read path no longer races with the leader, so the
defensive `is_transient_open_error` skip handling is removed — a failure to
open a reported segment is now a genuine error and propagates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Emj8TFxdtrf3K32eWhGgor
* feat: edge-s3-scroll experiment binary for ReadOnlyEdgeShard over S3
Adds a standalone `edge-s3-scroll` binary that opens a ReadOnlyEdgeShard
directly over an S3 (or S3-compatible) bucket and runs a single scroll
request with a hard-coded filter, for experimentation.
Takes the bucket endpoint, credentials and key prefix as CLI flags/env
vars, builds an S3-backed BlobFs, and discovers segments via a custom
enumerator that reads the segment manifest over object storage. The
edge_config.json is fetched to a local temp dir because
ReadOnlyEdgeShard::open reads config from the local filesystem.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* filter config in s3 scroll tool
* fix: read segment manifest from new location after dev rebase
dev #9564 moved the segment manifest next to (rather than inside) the
segments/ directory and named it segments_manifest.json. Adapt the
read-only follower enumerators accordingly so the follower reads the
manifest where the leader now writes it:
- ManifestSegmentEnumerator reads via segment_manifest_path() (shard root)
while still resolving segment dirs under segments/<uuid>.
- The S3 scroll tool's enumerator mirrors the same layout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: read-only edge shard works over object storage without edge_config
Make ReadOnlyEdgeShard self-sufficient over arbitrary backends (S3/GCS),
and harden the read-only segment loading it depends on.
- ReadOnlyEdgeShard derives its config from the segments via
EdgeConfig::from_segment_config instead of requiring edge_config.json
(open and refresh both derive); a follower never has that file.
- ReadOnlyEdgeShard::open(fs, path) no longer takes an enumerator: a
read-only follower always discovers segments through the manifest.
open_with_enumerator remains as a pub(crate) seam for tests.
- ManifestSegmentEnumerator is generic over UniversalReadFs (reads the
manifest via read_json_via), so it works over local mmap or a blob/S3
backend; the tool's bespoke enumerator is removed.
- Read-only mutable ID tracker tolerates absent mappings/versions files
(they are not written while empty), matching MutableIdTracker::open:
files are opened lazily and NotFound is treated as empty, avoiding an
extra exists() round-trip on object storage.
edge-s3-scroll experiment tool:
- Supports AWS S3 / S3-compatible and GCS backends (--backend), with a
hard-coded-free --filter-key/--filter-value scroll filter.
- Reads segment data through a DiskCache so remote blocks are fetched
once and served from a local mirror afterwards.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor: remove unused SegmentsManifest::load
Its only caller (edge's ManifestSegmentEnumerator) now reads the manifest
via read_json_via over UniversalReadFs, so the local-only load helper is
dead. Drop it and its now-unused read_json/Path imports.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* hold init_guard during reopen
* add `from` parameter to `OwnedReadPipeline::schedule_whole`
* add `OwnedReadPipeline::into_inner` to extract inner file
* implement proper background populate on `reopen`
* clippy
* coderabbit
* explicit owned uring pipeline destructuring
* refactor to join local and remote into one state
* @ffuugoo's nits
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>
* fix(optimizer): defer source segment destruction until durable
Optimizations copy-on-write move points out of their source segments in
memory, and WAL replay can only re-derive those moves from the sources'
on-disk pre-images. Dropping the sources immediately in finish_optimization
is unsafe: the moved copies may still sit unflushed in appendable segments,
so a restart before they are persisted loses the points ("No point with id"
during replay).
Replace the immediate proxy.drop_data() with a generic post-flush action
mechanism on SegmentHolder:
- register_post_flush_action(ready_at, ack_pin, action) queues a retryable
closure (FnMut returning PostFlushOutcome) to run once a flush proves its
data durable.
- flush_all runs every action whose ready_at is covered by the durable
waterline, and caps the returned version (and thus the WAL acknowledge) at
the minimum ack_pin of the actions still pending, so every operation the
not-yet-cleaned data contradicts (deletions in particular) stays replayable
until the files are gone.
- finish_optimization registers each source's drop via register_segment_drop,
pinned at the source's persisted version.
A crash before an action runs loads the old files next to their replacement;
load-time deduplication resolves the overlap.
Robustness:
- LockedSegment::try_drop_data hands the segment back on a StillInUse failure
(data untouched) with a short timeout, so a failed drop is retried on a
later flush instead of leaking its ack pin or blocking the flush for up to
an hour; drop_data keeps its long timeout for callers without a retry path.
- run_ready_post_flush_actions records the ack-pin floor of the actions it is
running (briefly out of the queue) so a concurrent flush on the background
early-return path cannot advance the WAL acknowledge past them.
Optimizer tests that assert source files are gone now flush_all first to run
the deferred action before counting dirs / asserting. New SegmentHolder unit
tests cover the retry, hard-failure, and in-flight-pin-visibility paths.
The perf caveat (a fresh appendable segment reporting persistent_version()
== 0 drags the waterline down) is documented inline as a follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* capture knowledge
* Update lib/shard/src/segment_holder/mod.rs
Co-authored-by: Tim Visée <tim+github@visee.me>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Tim Visée <tim+github@visee.me>
* override `for_values_map` for `OnDiskMapIndex`
* refactor and apply to filter as id iterator
* add comment
---------
Co-authored-by: generall <andrey@vasnetsov.com>
* feat: feature-flagged segment manifest for LocalShard
Add an on-disk segment manifest (`segments/manifest.json`) that lists a
shard's segments and their state, so out-of-process readers (e.g. a
read-only follower, possibly over object storage) can discover segments
without scanning the filesystem. Gated by the new `write_segment_manifest`
feature flag (off by default).
shard: define the structure + helpers (`SegmentsManifest`,
`SegmentManifestState`, `from_segment_holder`) in a new `segment_manifest`
module, plus the `SEGMENT_MANIFEST_FILE` constant and path helper. The
manifest is a flat `{ "<uuid>": "<state>" }` map; only `active` is written
today, with `under_construction`/`retiring` defined so the format can be
extended without breaking compatibility.
collection: LocalShard owns the writing logic. The manifest is persisted
via `SaveOnDisk<SegmentsManifest>`, initialized from the live segment set
on load/build and refreshed by the optimization worker whenever the
segment set changes (the helper re-derives from the holder and no-ops when
unchanged). No changes to `lib/shard/src/optimize.rs` internals.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Emj8TFxdtrf3K32eWhGgor
* feat: make append_only_mutations a proper feature flag
Replace the debug-only `QDRANT_APPEND_ONLY_MUTATIONS=1` env-var escape
hatch in segment construction with a `FeatureFlags::append_only_mutations`
flag, so it works in release builds and is configurable like the other
flags (config / `QDRANT__FEATURE_FLAGS__APPEND_ONLY_MUTATIONS`).
Deliberately left out of `FeatureFlags::all()`: it changes mutation
semantics and `all` is enabled in dev and e2e configs, so it stays
explicit opt-in.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Emj8TFxdtrf3K32eWhGgor
* upd openapi schema
* feat: register segments in manifest via must-use token, holder builder
Wire segment-manifest maintenance through the segment lifecycle so a
newly created segment is registered as soon as it exists on disk, and
construction can't silently skip it:
- build_segment now returns a #[must_use] NewSegmentToken carrying the
new segment UUID; the lint forces callers to register or drop it.
- SegmentHolder owns the manifest and reconciles it on sync; new
segments are registered ASAP (even before being added to the holder)
via the token, before they can receive writes.
- SegmentHolderBuilder is the only way to obtain a shard's holder; its
build() wires up the manifest, so it can't be forgotten. init/set
manifest helpers are now private / test-only.
- Optimization registers the optimized segment before dropping the
superseded segments' data; deletion is intentionally lenient.
- Document the consistency assumptions on SegmentsManifest: it is a
superset-biased view that may list not-yet-finalized or already-deleted
segments, which readers must tolerate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add two-phase config reload to `ReadOnlySegment`, alongside the existing
live-reload:
- `config_reload_diff(&self, fs)` re-reads the on-disk config, diffs it
against the in-memory config, and eagerly loads every new or changed
component (dense/sparse vector storages, indexes, quantized vectors and
payload field indexes) under a shared `&self` borrow, so the segment
keeps serving reads while loading.
- `apply_config_reload(&mut self, diff)` installs the pre-loaded
components and drops removed ones under `&mut self` — a cheap swap with
no I/O, so the exclusive borrow is held only briefly.
A vector or field whose config changed is reloaded (drop + load). The
payload-index half lives on `ReadOnlyStructPayloadIndex` with the same
diff/apply split, plus register/unregister helpers for its `has_vector`
storage map.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`version()` is the segment's update version — only meaningful for the
mutable/storage path. Every real caller already reaches it through
`StorageSegmentEntry` (flush), `SegmentEntry` (update), or a concrete
`Segment`/`ProxySegment`; none use it through the read-only base trait.
Move the declaration down to `StorageSegmentEntry` and relocate the
`Segment`/`ProxySegment` impls accordingly. `ReadOnlySegment` no longer
needs it, so drop the method and the write-only `version`/`initial_version`
fields it carried (`live_reload` never refreshed `version`, and neither
field was ever read).
This leaves `ReadSegmentEntry` a clean read-only surface and stops a
read-only segment from exposing a meaningless (stale) version.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: immutable field index live_reload
* feat: read-only segment live_reload
* fix: linter
* fix: linter
* fix: review comments
* fix: linter
* feat: ReadOnlyVectorData::live_reload covering all components
Extract the per-vector reload into a dedicated method that destructures
`ReadOnlyVectorData` so storage, index and quantized vectors are all
covered. Adding a field without reloading it won't compile, which guards
against silently skipping a component. The segment orchestrator now just
delegates per named vector.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: keep id-tracker delta pending until all reloads succeed
ReadOnlySegment::live_reload drained the id-tracker delta (which advances
tracker state and cannot be replayed) and only then ran the fallible
payload/vector reloads. On error the delta was lost, so un-updated
components drifted out of sync permanently.
Accumulate the delta into a new `pending_reload` field and clear it only
once every component has reloaded successfully. On a later reload the
tracker's fresh delta is folded in via `LiveReloadResult::merge` and the
union is replayed, so a partial failure self-heals. Offsets are monotonic,
so the only merge conflict is an inserted-but-unapplied offset later
deleted: it is dropped from `inserted` and kept in `deleted` so a
partially-applied component drops it on replay.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: generall <andrey@vasnetsov.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(facet): add sampling strategy for high-cardinality fields
For approximate facet queries the current per-segment implementation
walks every distinct value in the field index — O(unique_values_count)
even when the user asks for a tiny top-K. On UUID-style fields with
millions of unique values this dominates the request latency even
after #9208 capped the cross-shard payload.
This commit adds a parallel sampling strategy that runs in O(limit)
instead of O(unique_values_count):
1. Phase 1 — iterative novelty sampling. Stream point IDs in random
order (filtered if requested), look up each point's value via the
facet index, and collect distinct values into a candidate set until
`limit * 10` (min 1000) candidates have been gathered. Uses a
batch size of 32 to amortise the inner `for_points_values` call,
and bails out early after 128 consecutive empty batches when the
long tail is too thin to keep finding novel values.
2. Phase 2 — exact-count post-pass. For each candidate value, compose
`field == value` with the user filter and count via the payload
index. This guarantees the returned counts are exact (matching the
semantics of the full-scan path); only the *set* of returned values
is approximate.
The two strategies live side by side; `SegmentReadView::approximate_facet`
picks between them per-request based on
`unique_values_count > limit * FACET_FULL_SCAN_FACTOR` (FACTOR = 4).
Below that, the existing scan path runs unchanged — it'd visit most of
the index either way, and the post-pass adds no value.
The Monte-Carlo simulation behind this design (see thread context for
Zipf-distributed fields with cardinality up to 10^5 in ~1000 samples,
and trivially-correct results on UUID-style fields where every value
has count 1.
Adds a new `unique_values_count` method on the `FacetIndex` trait
(implemented for `MapIndex`, `ReadOnlyMapIndex`, `BoolIndex`,
`ReadOnlyBoolIndex`, and the `FacetIndexEnum` dispatcher) so the
strategy switch can run without touching the index.
Co-authored-by: Cursor <cursoragent@cursor.com>
* [AI] simplify, use single file
[AI] better selection of filtering approach
fmt
[AI] simplify, use single file
* manual simplification
* [AI] implement candidate-based lookups
[AI] 🧹
* precollect filter into bitmap
* avoid sampling with restrictive filter
* fix rebase + clippy
* refactor tests
* no duplicate values in map index
* polish comments
---------
Co-authored-by: root <111755117+qdrant-cloud-bot@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
`GridstoreReader`)
- Make the entrypoint of payload storage choose the `Populate` variant
- Mutable payload indexes now populate gridstore blockingly before using
it to load
- Propagate `Populate` into `flags` module
- Add `UniversalRead::populate_auto` to know whether a backend chooses
to populate or not when `Populate::Auto`
* 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>
* feat(model_testing): add --duration-sec to run for a fixed wall-clock time
Bound the soak by wall-clock time instead of op count: when --duration-sec is set, the loop runs until the deadline (or Ctrl-C) and --op-num is ignored.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix clippy
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.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>
* Buffer multi-dense offsets store to prevent reload corruption
The appendable multi-dense storage flushes its `vectors` and `offsets`
chunked stores independently. Each flusher snapshots its `status.len` at
creation time but msyncs chunk bytes at execution time. A re-upsert that
grows a point past its reserved capacity rewrites the point's `offsets`
entry in place to a freshly-appended row region; if that lands in a
flush's creation-to-execution window, the relocated entry becomes durable
while the `vectors` store's recorded length still predates the rows it
references. On reload that point is unreadable and a WAL append reuses the
rows, clobbering another point.
Wrap the offsets store in a write-back buffer (`BufferedOffsets`) so the
durable offsets can never reference rows beyond the durable `vectors`
length. Offset writes stage in a pending overlay and only land in the
durable store while a flush executes; the flusher snapshots the pending
set at creation time, so any write after that stays buffered for the next
flush. Both flushers snapshot at the same instant, yielding a consistent
durable cut. Rows written after the cut are unreferenced garbage the next
append overwrites. This prevents the skew at the source instead of
patching it on reload, and also closes the residual offsets-length smear.
The buffer follows the Gridstore flusher convention: pending writes live
inside the single lock-guarded store, reads consult the overlay then the
durable bytes under one lock, and the durable msync runs after releasing
the write lock so it never stalls concurrent reads.
Enable the "m" multivector in the collection model test now that its
reload divergence is resolved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Narrow offsets flush write-lock scope
Build and sort the pending snapshot before taking the write lock; only the
apply + reconcile need it. Shortens the lock hold so concurrent reads block
less. Addresses review feedback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>