Files
qdrant/lib/edge/tests/wal_options.rs
Andrey Vasnetsov fb681b7d9d Lazy roaring flags bitmap and bool index counts (#9749)
* [AI] make ReadOnlyRoaringFlags bitmap and bool index counts lazy

Opening a read-only segment scanned every flags file end to end:
`ReadOnlyRoaringFlags::open` materialized the whole RoaringBitmap via
`iter_ones()`. Every payload field carries a null index, so this was paid
per field per segment, for bitmaps most queries never touch.

Make the bitmap a `OnceLock`, filled by a scan on first access. Open now
reads only the tiny status file. `ReadOnlyBoolIndex`'s three eager count
fields collapse into one lazily-derived, cached `BoolCounts`; its
`live_reload` refreshes them in place when present and leaves them unset
otherwise, so reloading an index nothing queries stays scan-free.

Propagate the resulting `OperationResult` through `RoaringFlagsRead`,
`PayloadFieldIndexRead::count_indexed_points`, `FieldIndexRead`,
`PayloadIndexRead::{indexed_points, get_telemetry_data}`, `build_info` /
`build_telemetry` and `SegmentEntry::{info, get_telemetry_data}`, out
into shard, edge and collection.

`ram_usage_bytes` stays infallible: an unmaterialized bitmap holds no
RAM, so it reports 0 via the new `bitmap_if_materialized`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [AI] correct `preopen` comment: `open` no longer scans the flags file

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [AI] fix edge examples for fallible `info()`

`EdgeShardRead::info` now returns `OperationResult<ShardInfo>`. The
examples live in their own workspace (lib/edge/publish), so the main
`cargo check --workspace` never saw them.

Every call site sits in `fn main() -> Result<(), Box<dyn Error>>`, so
propagate with `?`. `bm25-search` compiled either way but would have
printed the `Result` rather than the `ShardInfo`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 17:48:40 +02:00

175 lines
5.5 KiB
Rust

use std::collections::HashMap;
use std::num::NonZero;
use edge::{Distance, EdgeConfig, EdgeShard, EdgeVectorParams, WalOptions};
use segment::data_types::vectors::{VectorInternal, VectorStructInternal};
use segment::types::ExtendedPointId;
use shard::operations::CollectionUpdateOperations::PointOperation;
use shard::operations::point_ops::PointInsertOperationsInternal::PointsList;
use shard::operations::point_ops::PointOperations::UpsertPoints;
use shard::operations::point_ops::{PointStructPersisted, VectorStructPersisted};
const VECTOR_NAME: &str = "edge-wal-options-test-vector";
fn base_builder() -> edge::EdgeConfigBuilder {
EdgeConfig::builder().on_disk_payload(false).vector(
VECTOR_NAME,
EdgeVectorParams {
size: 1,
distance: Distance::Dot,
quantization_config: None,
multivector_config: None,
datatype: None,
on_disk: None,
hnsw_config: None,
},
)
}
fn default_config() -> EdgeConfig {
base_builder().build()
}
fn small_wal_options() -> WalOptions {
WalOptions {
segment_capacity: 4 * 1024 * 1024,
segment_queue_len: 0,
retain_closed: NonZero::new(1).unwrap(),
}
}
fn config_with_small_wal() -> EdgeConfig {
base_builder().wal_options(small_wal_options()).build()
}
fn point(id: u64) -> PointStructPersisted {
PointStructPersisted {
id: ExtendedPointId::NumId(id),
vector: VectorStructPersisted::from(VectorStructInternal::Named(HashMap::from([(
VECTOR_NAME.to_string(),
VectorInternal::from(vec![id as f32]),
)]))),
payload: None,
}
}
#[test]
fn create_with_custom_wal_capacity_persists() {
let dir = tempfile::Builder::new()
.prefix("edge-wal-options-create")
.tempdir()
.unwrap();
let shard = EdgeShard::new(dir.path(), config_with_small_wal()).unwrap();
drop(shard);
// No config passed: persisted wal_options (small) must be picked up.
let shard = EdgeShard::load(dir.path(), None).unwrap();
assert_eq!(
shard
.config()
.wal_options
.as_ref()
.unwrap()
.segment_capacity,
4 * 1024 * 1024,
);
drop(shard);
}
#[test]
fn load_still_works_with_default_wal_options() {
let dir = tempfile::Builder::new()
.prefix("edge-wal-options-default")
.tempdir()
.unwrap();
let shard = EdgeShard::new(dir.path(), default_config()).unwrap();
drop(shard);
let shard = EdgeShard::load(dir.path(), None).unwrap();
drop(shard);
}
/// Mismatching WAL options on reload: create with default 32 MiB, reload
/// with custom 4 MiB after upserting a point. Verifies the WAL segments
/// on disk can be opened with a smaller segment_capacity and the point
/// is still readable. This is the central concern raised by upstream
/// review (qdrant/qdrant#9067).
#[test]
fn reload_with_smaller_wal_capacity_after_upsert() {
let dir = tempfile::Builder::new()
.prefix("edge-wal-options-shrink")
.tempdir()
.unwrap();
// Phase 1: create with default 32 MiB WAL, upsert one point.
{
let shard = EdgeShard::new(dir.path(), default_config()).unwrap();
shard
.update(PointOperation(UpsertPoints(PointsList(vec![point(42)]))))
.unwrap();
assert_eq!(shard.info().unwrap().points_count, 1);
// drop -> flushes WAL + segments.
}
// Phase 2: reload with custom small 4 MiB WAL (passed via EdgeConfig).
let shard = EdgeShard::load(dir.path(), Some(config_with_small_wal())).unwrap();
assert_eq!(
shard.info().unwrap().points_count,
1,
"point must survive reload with mismatched WAL options"
);
// Phase 3: write another point under the smaller WAL.
shard
.update(PointOperation(UpsertPoints(PointsList(vec![point(43)]))))
.unwrap();
assert_eq!(shard.info().unwrap().points_count, 2);
}
/// Symmetric case: create with custom small 4 MiB, reload with default
/// 32 MiB. Verifies WAL segments aren't truncated or rejected when the
/// new segment_capacity is larger than what they were created with.
#[test]
fn reload_with_larger_wal_capacity_after_upsert() {
let dir = tempfile::Builder::new()
.prefix("edge-wal-options-grow")
.tempdir()
.unwrap();
// Phase 1: create with small 4 MiB WAL, upsert one point.
{
let shard = EdgeShard::new(dir.path(), config_with_small_wal()).unwrap();
shard
.update(PointOperation(UpsertPoints(PointsList(vec![point(100)]))))
.unwrap();
assert_eq!(shard.info().unwrap().points_count, 1);
}
// Phase 2: reload with explicit default 32 MiB WAL options, overwriting the
// persisted small ones. (Leaving wal_options unspecified would keep them.)
let config = base_builder().wal_options(WalOptions::default()).build();
let shard = EdgeShard::load(dir.path(), Some(config)).unwrap();
assert_eq!(
shard
.config()
.wal_options
.as_ref()
.unwrap()
.segment_capacity,
WalOptions::default().segment_capacity,
);
assert_eq!(
shard.info().unwrap().points_count,
1,
"point must survive reload with default WAL options"
);
// Phase 3: write another point under the default WAL.
shard
.update(PointOperation(UpsertPoints(PointsList(vec![point(101)]))))
.unwrap();
assert_eq!(shard.info().unwrap().points_count, 2);
}