Files
qdrant/lib/segment/benches
Andrey Vasnetsov 5436eb3bde refactor: read-only numeric index (#9038)
* refactor: split numeric index variants into dedicated modules

Move MutableNumericIndex, ImmutableNumericIndex, and MmapNumericIndex
into their own directories, each split into mod.rs (struct definitions),
lifecycle.rs (open/build/wipe/mutations), and read_ops.rs (accessors),
mirroring the map_index layout.

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

* refactor: drop single-variant Storage enums in mutable/immutable numeric index

Replace `Storage<T>` wrappers around the only backing store with the
store types directly: `Gridstore<Vec<T>>` for `MutableNumericIndex` and
`Box<MmapNumericIndex<T>>` for `ImmutableNumericIndex`. Collapses the
trivial single-arm matches into direct method calls.

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

* refactor: introduce NumericIndexRead trait

Mirror `MapIndexRead` from the map_index refactor: define a
`NumericIndexRead<T>` trait in `numeric_index/read_ops.rs` and implement
it on each of the three storage variants
(`MutableNumericIndex`, `ImmutableNumericIndex`, `MmapNumericIndex`).

Trait signatures are unified across variants — in-memory variants accept
and ignore the `hw_counter` argument that the mmap-backed variant uses
for IO tracking, and `total_unique_values_count`, `values_range`, and
`orderable_values_range` return `OperationResult` everywhere so the
dispatcher in `NumericIndexInner` can call them generically.

Variant-specific helpers that don't fit the shared shape stay as
inherent methods: `MutableNumericIndex::map()`,
`ImmutableNumericIndex::values_range_size()`, and
`MmapNumericIndex::{values_range_size, is_on_disk}`.

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

* refactor: add ReadOnlyAppendableNumericIndex

Counterpart to `MutableNumericIndex`, mirroring `ReadOnlyAppendableMapIndex`
from the map_index refactor. It reuses the shared `InMemoryNumericIndex`
in-memory state but is backed by a `GridstoreReader` over generic
`UniversalRead` instead of a writable `Gridstore`, and implements
`NumericIndexRead` by forwarding to the in-memory index — no mutation
surface.

Loading / lifecycle (constructor, files, populate, clear_cache) will
follow in a separate change; the storage field is held only to pin the
on-disk layout for now.

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

* refactor: rename MmapNumericIndex to UniversalNumericIndex, expose UniversalRead param

Mirror `UniversalMapIndex`: the type is now generic over `S: UniversalRead`
with a `MmapFile` default, so the index can be served from any
`UniversalRead` backend (io_uring, disk-cache wrappers, …) rather than
the hard-coded `MmapFile`.

The `NumericIndexRead` impl and read-side helpers are generic over `S`;
`build` / `open` and the other lifecycle methods stay `MmapFile`-only
since they construct mmap-backed storage from a path. The
`NumericIndexInner::Mmap` enum variant keeps its name and uses the
default `S = MmapFile`.

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

* refactor: split numeric_index/storage/mod.rs into lifecycle and read_ops

`storage/mod.rs` now holds only the `NumericIndexInner` enum and module
wiring. The variant-dispatch impls are split into sibling modules
matching the layout of the individual storage variants:

- `lifecycle.rs`: construction, persistence, file listing, cache
  control, and `remove_point`.
- `read_ops.rs`: read-path forwarding — value lookups, telemetry, RAM
  accounting, `is_on_disk`.

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

* refactor: implement NumericIndexRead for NumericIndexInner

The enum-level read dispatch was a set of inherent methods scattered
across storage/read_ops.rs and storage/statistics.rs with signatures
that drifted from the variant trait (`values_count` returned `usize`,
`max_values_per_point` vs `get_max_values_per_point`, a hand-rolled
`get_telemetry_data`). Make `NumericIndexInner` implement
`NumericIndexRead` directly so it shares one interface with the three
storage variants.

- All 12 trait methods are forwarded via match dispatch in
  storage/read_ops.rs; `values_range` / `orderable_values_range` box
  the per-variant iterators.
- `get_histogram`, `get_points_count`, `total_unique_values_count` move
  out of statistics.rs into the trait impl; `values_is_empty` and
  `get_telemetry_data` now come from the trait defaults.
- `point_ids_by_value` and `is_on_disk` stay as enum-only inherent
  helpers (not part of the shared trait).
- Callers updated: `NumericIndex::values_count` unwraps the now
  `Option`-returning trait method; `filter` boxes `point_ids_by_value`;
  `field_index.rs` and `numeric_field_index.rs` import the trait.

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

* refactor: add ReadOnlyNumericIndexInner

Read-only counterpart to `NumericIndexInner`, mirroring `ReadOnlyMapIndex`
from the map_index refactor. Lives under
`numeric_index/storage/read_only` and selects across the two read-only
storage backends:

- `Appendable(ReadOnlyAppendableNumericIndex<T, S>)` — loaded into RAM
  from the appendable Gridstore format.
- `Immutable(UniversalNumericIndex<T, S>)` — served directly from the
  immutable stored format.

Implements `NumericIndexRead` by forwarding each method to the active
variant; `values_is_empty` / `get_telemetry_data` come from the trait
defaults.

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

* refactor: rename numeric_index read_ops to numeric_index_read, split mod.rs

Two changes:

- Rename `numeric_index/read_ops.rs` (the `NumericIndexRead` trait
  definition) to `numeric_index_read.rs`, freeing the `read_ops` name.
- Split the leftover content of `numeric_index/mod.rs` into sibling
  modules, matching the per-variant layout:
  - `lifecycle.rs`: the `Encodable` key-format trait + impls and the
    `HISTOGRAM_*` construction constants.
  - `read_ops.rs`: the `StreamRange` trait and the `Range` →
    index-key-bounds conversion.

`mod.rs` now only wires modules and re-exports. `Encodable` and
`StreamRange` keep their public paths via re-export; `tests.rs` gains
explicit imports for the symbols it previously picked up through the
`mod.rs` glob.

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

* refactor: dissolve numeric_index/index.rs into mod, lifecycle, read_ops

`index.rs` only held the `NumericIndex` wrapper and its two impl blocks;
spread them to match the per-module layout used elsewhere:

- `NumericIndex` struct + `NumericIndexIntoInnerValue` trait → `mod.rs`
  (type definitions live with the module wiring).
- The inherent `impl NumericIndex` (open / build / cache control /
  storage introspection) → `lifecycle.rs`, alongside the `HISTOGRAM_*`
  seed constants.
- The `PayloadFieldIndexRead` impl → `read_ops.rs`.

Also move the `Encodable` key-format trait out of `lifecycle.rs` into
its own `encodable.rs`. `mod.rs` keeps re-exporting `Encodable` so its
public path is unchanged.

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

* feat: add ReadOnlyNumericIndex with NumericIndexRead + PayloadFieldIndexRead

Read-only counterpart to `NumericIndex`, wrapping `ReadOnlyNumericIndexInner`
plus the payload value type parameter `P`. Implements both `NumericIndexRead`
and `PayloadFieldIndexRead` by forwarding to the inner storage-variant enum.

To support `PayloadFieldIndexRead` without duplicating the query logic, the
cardinality/filter/payload-block/condition-checker code is extracted into a
new `query` module of generic free functions over `NumericIndexRead<T>`.
`ReadOnlyNumericIndexInner` implements `PayloadFieldIndexRead` by plugging
into those helpers; `ReadOnlyNumericIndex` delegates to its inner.

The writable `NumericIndexInner` path is left untouched — its existing
variant-specialized `estimate_points` heuristic stays in `storage`.

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

* refactor: make query.rs the single source of truth for numeric index queries

The generic `query` helpers and the per-`NumericIndexInner` impls in
`storage/{trait_impls,statistics}.rs` had duplicated cardinality /
filter / payload-block / condition-checker logic. Collapse them onto
the shared `query` helpers:

- `storage/trait_impls.rs`: `PayloadFieldIndexRead for NumericIndexInner`
  now forwards each method to `query::*` instead of carrying its own copy.
- `storage/statistics.rs`: deleted — `range_cardinality` and
  `estimate_points` were duplicates of the `query` versions.
- `estimate_points` needs a range size; add `values_range_size` to the
  `NumericIndexRead` trait with a default that counts `values_range`,
  overridden by the `Immutable` / `Mmap` variants with their `O(log n)`
  boundary search. The `MutableNumericIndex::map()` accessor (its only
  caller was the old `estimate_points`) is removed.
- `values_range_size` takes `hw_counter` and threads it into
  `values_range` rather than fabricating a disposable counter.

`tests.rs` calls `query::range_cardinality` directly now that the
inherent method is gone.

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

* feat: integrate read-only numeric index into ReadOnlyFieldIndex

Wire the four numeric variants (`IntIndex`, `DatetimeIndex`,
`FloatIndex`, `UuidIndex`) into `ReadOnlyFieldIndex`, mirroring
`FieldIndex`:

- `PayloadFieldIndexRead` / `FieldIndexRead` dispatch covers the new
  variants — telemetry, value counts, value retrievers, `as_numeric`.
- `ReadOnlyNumericFieldIndex` is the read-only counterpart of
  `NumericFieldIndex` (Int/Float order-by erasure over
  `ReadOnlyNumericIndexInner`); `as_numeric` returns it for the
  Int/Datetime/Float variants (UUIDs aren't numerically order-by-able,
  matching `FieldIndex`).
- `ReadOnlyNumericIndex` gains per-`(T, P)` `value_retriever` methods
  (in `read_only/value_retriever.rs`) and an `inner()` accessor.

`StreamRange` is now backed by a shared generic `query::stream_range`
helper over `NumericIndexRead`, implemented for both `NumericIndexInner`
and `ReadOnlyNumericIndexInner` — replacing the bespoke `EitherVariant`
dispatch in `storage/trait_impls.rs`.

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

* refactor: collapse ReadOnlyNumericIndex value retrievers onto one generic method

The four per-`(T, P)` `value_retriever` methods were identical except
for the per-value `T -> Value` conversion. Extract that conversion into
a `NumericValueToJson` trait (one tiny impl per `(T, P)`) and keep a
single generic `value_retriever` that builds the retriever closure once.

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

* fmt

* refactor: dedup NumericFieldIndex / ReadOnlyNumericFieldIndex

The two enums were structurally identical — same `StreamRange`,
`get_ordering_values`, and `NumericFieldIndexRead` bodies — differing
only in the backing storage type. Collapse them onto one generic
`NumericFieldIndexView<'a, I, F>` with a single set of impls (over
`I: NumericIndexRead<i64> + StreamRange<i64>` and the `f64` counterpart).

`NumericFieldIndex` and `ReadOnlyNumericFieldIndex` are now type aliases
of the generic view, so every existing call site is unchanged.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:06:32 +02:00
..
2025-09-29 12:47:10 +00:00
2025-09-29 12:47:10 +00:00