Commit Graph

2178 Commits

Author SHA1 Message Date
Ivan Pleshkov
8d9a946c48 are you happy fmt 2026-07-01 11:02:54 +02:00
Ivan Pleshkov
c88903ce32 write impl 2026-07-01 11:01:22 +02:00
Ivan Pleshkov
1ca8ccf220 tqdt binary vector api 2026-06-30 11:57:46 +02:00
qdrant-cloud-bot
d72bd489d1 ci(windows): skip slow facet sampling filter iter test (#9608)
* 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>
2026-06-29 09:59:47 +02:00
qdrant-cloud-bot
7b25a08799 test: add openapi regression test for values_count on missing fields (#9606)
* 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>
2026-06-29 08:47:17 +02:00
Andrey Vasnetsov
9636944402 feat: require UserData to implement Debug (#9588)
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>
2026-06-27 13:53:30 +02:00
Luis Cossío
eedc32c689 [UIO] Explicit Populate in R/O vector storages 2026-06-26 08:38:45 -04:00
xzfc
db2c68e004 Replace DynConditionChecker with ConditionCheckerEnum (#9560)
* Add UniversalReadExt

Need this trait for the upcoming `ConditionCheckerEnum`.

* Replace DynConditionChecker with ConditionCheckerEnum
2026-06-26 12:29:01 +00:00
xzfc
785815e209 ConditionChecker fixups (#9559)
* 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`.
2026-06-25 20:08:04 +00:00
Andrey Vasnetsov
f70a1462fb feat: read-only edge shard follower (ReadOnlyEdgeShard) (#9529)
* 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>
2026-06-25 13:04:29 +02:00
xzfc
f3c30e5971 OptimizedFilter: Option<Vec<T>> -> Vec<T> (#9555) 2026-06-24 16:31:43 +00:00
Luis Cossío
605ad0f915 Add facet accuracy test (#9549)
* [AI] add facet accuracy test

* [manual] simplify

* [AI] delete and overwrite some points

* [AI] use immutable segments too

* lower zipf skew
2026-06-23 10:37:50 -04:00
Luis Cossío
fe214bf44e [UIO] Batched OnDiskMapIndex::for_values_map (#9505)
* override `for_values_map` for `OnDiskMapIndex`

* refactor and apply to filter as id iterator

* add comment

---------

Co-authored-by: generall <andrey@vasnetsov.com>
2026-06-22 09:50:37 -04:00
Luis Cossío
1fea74f536 [UIO] Batching for OnDiskMapIndex::values_iter (#9491) 2026-06-22 09:48:01 -04:00
Andrey Vasnetsov
852ca200b5 feat: feature-flagged segment manifest for LocalShard (#9530)
* 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>
2026-06-22 12:51:57 +02:00
Andrey Vasnetsov
d1c5a22975 feat: config reload for read-only segments (#9528)
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>
2026-06-22 11:52:19 +02:00
Andrey Vasnetsov
7b52f04dae refactor: move version() from ReadSegmentEntry to StorageSegmentEntry (#9527)
`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>
2026-06-20 15:05:29 +02:00
Daniel Boros
cea6e9f0cc feat: read-only segment live_reload (#9503)
* 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>
2026-06-20 11:48:40 +02:00
Luis Cossío
1bab9c8e9f Perf improvements for facets (#9457)
* 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>
2026-06-19 12:16:30 -04:00
Luis Cossío
8fc7b35d7a Explicit Populate in payload storage (and ` (#9514)
`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`
2026-06-19 08:37:28 -04:00
Tim Visée
b9154713d7 Fix empty min_should with non-zero min_count matching everything (#9401)
* Empty match any with non-zero min count matches nothing

* Update description

* Validate that min_count is greater than 0
2026-06-19 13:28:51 +02:00
qdrant-cloud-bot
8c487a17e8 feat(bm25): explicit Disabled stemmer; deprecate language: "none" hack (#9376)
* 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>
2026-06-19 13:11:13 +02:00
Daniel Boros
562fa1ba0a feat: immutable field index live_reload (#9502) 2026-06-18 16:32:28 +02:00
Arnaud Gourlay
0b642178e2 Buffer multi-dense offsets store to prevent reload corruption (#9501)
* 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>
2026-06-18 11:49:38 +02:00
Arnaud Gourlay
1fda60b38a perf: use Entry API to avoid redundant map double-lookups (#9500)
* perf: use Entry API to avoid redundant map double-lookups

Replace get_mut/contains_key followed by insert with the entry API
across several maps, collapsing two hash lookups into one. Limited to
sites where the key is Copy or already owned and moved, so no extra
key clone is added to any hot path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* More Entry API usage in mutable_geo_index

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: xzfc <xzfcpw@gmail.com>
2026-06-18 09:56:31 +02:00
Daniel Boros
13fb4ade0a feat: ReadOnlySegment::open (S3-verified) (#9466)
* feat: ReadOnlyStructPayloadIndex::open

* use expect instead of allow

* fix: review comments

* fix: read immutable dense vector count through fs

* feat: ReadOnlySegment::open + read-only segment over S3 test

* fix: linter

* rename + TODO

---------

Co-authored-by: Luis Cossío <luis.cossio@outlook.com>
2026-06-17 21:31:47 +02:00
Daniel Boros
acfb979a6e feat: ReadOnlyStructPayloadIndex::open (#9465)
* feat: ReadOnlyStructPayloadIndex::open

* use expect instead of allow

* fix: review comments

---------

Co-authored-by: Luis Cossío <luis.cossio@outlook.com>
2026-06-17 18:23:17 +02:00
xzfc
a1483e87a9 Remove callback-based ConditionChecker (#9487)
* Add BoolConditionChecker

* Add IsEmptyConditionChecker, IsNullConditionChecker

* Add RangeConditionChecker

* Add GeoConditionChecker

* MapIndexRead: move <'a> to the trait level

Reason: avoid repetetive `where N: 'a` in every method

* Add MapConditionChecker

* Add ConstantConditionChecker

* Add FullTextConditionChecker

* Add {Ids,HasVector,Payload}ConditionChecker

* Get rid of fn-based ConditionChecker
2026-06-17 13:10:48 +00:00
Daniel Boros
33b448d66d fix: read-only on-disk field index opens are non-writeable (#9468) 2026-06-16 21:47:04 +02:00
xzfc
f257b97356 Move trait ConditionChecker to common (#9486)
* Add DynConditionChecker type alias

* Add DynConditionChecker type alias (point_scorer.rs)

* Move trait ConditionChecker to `common`

Reason: will be used both in `segment` and `sparse` crates.
2026-06-16 19:36:25 +00:00
xzfc
bf1aa818c6 Sparse InvertedIndex: bring back generic methods (#9485)
* sparse InvertedIndex: bring back generics

* nits

---------

Co-authored-by: Luis Cossío <luis.cossio@outlook.com>
2026-06-16 19:34:42 +00:00
Roman Titov
efc0c7961b Use batched reads in GridstoreView::iter (#9429) 2026-06-16 15:46:26 +02:00
Ivan Pleshkov
d9729925cc [TQDT] Wire TQ multivectors storage (#9439)
* multi tq scorers

* add vector storage enum

* Rebase fixes

* Improve scoring

---------

Co-authored-by: jojii <jojii@gmx.net>
2026-06-16 11:52:22 +02:00
qdrant-cloud-bot
a8d7039852 test(turbo): reduce randomized scenarios on Windows to fix SLOW tests (#9483)
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>
2026-06-16 11:25:03 +02:00
Daniel Boros
179433ae1c refactor: explicit index-type arms for Turbo4 in read-only open (#9479) 2026-06-15 23:02:12 +02:00
Daniel Boros
4d6fb4e0ab feat: add open for read-only sparse vector index (#9435)
* feat: add open for read-only sparse vector index + enum sparse dispatcher

* fix: universal-IO loads for read-only sparse index open

* refactor: rename load_via/open_via to load_universal/open_universal

* refactor: drop StorageVersion::load in favor of load_universal

The regular-IO `load` duplicated `load_universal` over plain `File` IO.
Remove it and route all callers through `load_universal(&MmapFs, ..)`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(sparse): decouple inverted index from concrete storage; split segment_constructor_base (#9461)

Make the read-only index enum generic over storage `S` (no concrete MmapFile),
and remove construction callbacks from the sparse index open paths.

- InvertedIndex is now a pure read/search trait: `open`/`from_ram_index`/
  `type Fs` moved off the trait to inherent methods on each concrete index
  type, so construction no longer requires `S::Fs: Default`.
- SparseVectorIndex open split into a generic `plan` (load-vs-build decision +
  RAM-index build) and generic `finish` (assembly); callers do the concrete
  per-type construction, so no construction callbacks are needed.
- ReadOnlySparseVectorIndex::open takes the already-constructed inverted index
  and caller-loaded config instead of a `load_inverted_index` callback.
- VectorIndexReadEnum is generic over `S: UniversalRead`; sparse mmap variants
  hold `InvertedIndexCompressedMmap<_, S>` rather than a concrete `MmapFile`.
- Split the 1182-line segment_constructor_base.rs into a module (paths,
  vector_storage, payload_storage, id_tracker, vector_index,
  sparse_vector_index, create_segment, segment, legacy_state); the sparse
  dispatcher's match arms collapse into three per-family helpers.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat: LiveReload (no-op) dispatch for read-only vector index enum (#9436)

---------

Co-authored-by: generall <andrey@vasnetsov.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:31:03 +02:00
Daniel Boros
ddb32fed59 feat: live_reload for read-only immutable id tracker + enum dispatch (#9434)
* feat: live_reload for read-only immutable id tracker + enum dispatch

* deleted should be already sorted

---------

Co-authored-by: Luis Cossío <luis.cossio@outlook.com>
2026-06-15 21:19:54 +02:00
xzfc
3d03dd4752 [UIO] condition checker: fallible checks (#9405)
* condition checker: fallible checks

* Update
2026-06-15 18:11:23 +00:00
Daniel Boros
5980de8b3d feat: add VectorIndexReadEnum open dispatcher (#9428)
* feat: add VectorIndexReadEnum open dispatcher

* review nit

---------

Co-authored-by: Luis Cossío <luis.cossio@outlook.com>
2026-06-15 16:23:40 +02:00
Luis Cossío
da8a213ee1 Add facets bench (#9456) 2026-06-15 09:29:12 -04:00
Daniel Boros
d669a08c2b feat: add open to read-only HNSW index (#9427)
* feat: add open to read-only HNSW index

* fix: dedicated universal-IO load for read-only graph

* fix: report actual graph residency in read-only is_on_disk

* refactor: rename load_via to load_universal

* refactor: unify graph links loading on universal IO

Replace the dual GraphLinks load paths (regular mmap + universal IO) with a
single universal-IO loader, and the `Mmap` GraphLinksEnum variant with a
`Universal` variant that keeps a type-erased `UniversalRead` handle alive
behind `Box<dyn GraphLinksStorage>` (UniversalRead is not object-safe).

- GraphLinksEnum::from_storage picks the variant from UniversalKind:
  borrowable (mmap-backed) kinds stay `Universal`, others are materialized
  into `Ram`, so the borrowability invariant in GraphLinksStorage::bytes
  holds by construction.
- Loading takes a `Populate` parameter, derived from `hnsw_config.on_disk`:
  on_disk -> Populate::No (lazy), otherwise Populate::Blocking.
- is_on_disk is reported from config again instead of the enum variant.
- Split graph_links.rs into a graph_links/ module (format, vectors, storage,
  links, tests) with a relationship diagram in mod.rs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: drop LoadOption in favor of a Populate parameter

After unifying the link load paths on universal IO, LoadOption only encoded a
populate choice with a fs that was always MmapFs. Replace it with a `Populate`
argument to GraphLayers::load, removing the enum, its constructors, the
load_links helper, and the unused generic backend.

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>
2026-06-15 09:19:58 +02:00
Ivan Pleshkov
47767d19fd unpadded rotations (#9450) 2026-06-12 22:24:23 +02:00
Jojii
eb0ef2a9d8 [TQDT] TurboMultiVectorStorage (#9414)
* TurboQuant Datatype for Multivectors

* Fix update_from and add non-appendable open()

* Fix unnecessary allocations + wrap chunk boundaries

* Congruence tests TurboVectorStorage <-> TurboMultiVectorStorage(vecs=1)

* Only use appendable backend for turbo4-multivectors

* Simplify update_from

* Never place multivectors across chunk boundaries (skip tail, reject oversized)

* Review remarks
2026-06-12 12:03:17 +02:00
Daniel Boros
7f76e12b77 feat: read-only immutable id tracker open + enum open dispatcher (#9433) 2026-06-12 12:01:15 +02:00
Daniel Boros
96954f5d73 feat: add open to read-only plain vector index (#9426) 2026-06-12 11:35:59 +02:00
Ivan Pleshkov
097e049b9c [TQDT] Dense TQ inline scoring (#9440)
* tqdt inline scoring

* remove QueryScorer::score
2026-06-12 10:31:03 +02:00
Marcelo Machuca
3b6f535a6e test(segment): cover quantized RAM storage file size validation (#9348) 2026-06-11 12:35:06 +02:00
Arnaud Gourlay
40f5309f74 Don't abort segment flush when a captured component storage was dropped (#9424)
A DropIndex (or vector name deletion) landing between flusher capture
and execution drops the component's storage, making its captured
flusher return Cancelled. Aborting the rest of the flush sequence at
that point leaves the components flushed so far durably ahead of
payload storage and point versions. WAL replay then re-derives
filter-based operations through the too-new field index and silently
skips points whose payload still needs the operation re-applied,
losing the update.

Skip the dropped storage and keep flushing instead. The drop itself is
a versioned operation that WAL replay re-applies, so skipping it is
safe.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 09:35:16 +02:00
xzfc
dccfc9a19a [UIO] condition checker: fallible constructor (#9404)
* Drop StructFilterContext, use OptimizedFilter directly

Reason: it's a no-op wrapper around OptimizedFilter.

* GeoMapIndexRead::check_values_any: return OperationResult

* condition checker: fallible constructor
2026-06-10 22:47:06 +00:00
Daniel Boros
997bf32687 feat: read-only sparse vector index for VectorIndexReadEnum (#9406)
* feat: read-only sparse vector index for VectorIndexReadEnum

* fix: review comments
2026-06-10 13:38:46 -04:00