mirror of
https://github.com/qdrant/qdrant.git
synced 2026-08-02 16:10:58 -05:00
* [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>
175 lines
5.5 KiB
Rust
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);
|
|
}
|