mirror of
https://github.com/qdrant/qdrant.git
synced 2026-08-06 18:10:58 -05:00
* Deferred threshold integration * update deferred id * apply update_deferred_internal_id * fix segment inspector * use avaliable bytes count * renamings * review remarks * review remarks * move has_deferred_points * remove todo * update comments
1407 lines
45 KiB
Rust
1407 lines
45 KiB
Rust
use std::collections::HashMap;
|
|
use std::path::Path;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::AtomicBool;
|
|
|
|
use anyhow::{Context, Result};
|
|
use atomic_refcell::AtomicRefCell;
|
|
use common::counter::hardware_counter::HardwareCounterCell;
|
|
use common::types::PointOffsetType;
|
|
use fnv::FnvBuildHasher;
|
|
use fs_err as fs;
|
|
use indexmap::IndexSet;
|
|
use itertools::Itertools;
|
|
use ordered_float::OrderedFloat;
|
|
use rand::prelude::StdRng;
|
|
use rand::{RngExt, SeedableRng};
|
|
use segment::data_types::facets::{FacetParams, FacetValue};
|
|
use segment::data_types::index::{
|
|
FloatIndexParams, FloatIndexType, IntegerIndexParams, IntegerIndexType, KeywordIndexParams,
|
|
KeywordIndexType, TextIndexParams, TextIndexType,
|
|
};
|
|
use segment::data_types::vectors::{DEFAULT_VECTOR_NAME, only_default_vector};
|
|
use segment::entry::entry_point::{NonAppendableSegmentEntry, SegmentEntry};
|
|
use segment::fixtures::payload_context_fixture::create_id_tracker_fixture;
|
|
use segment::fixtures::payload_fixtures::{
|
|
FLICKING_KEY, FLT_KEY, GEO_KEY, INT_KEY, INT_KEY_2, INT_KEY_3, LAT_RANGE, LON_RANGE, STR_KEY,
|
|
STR_PROJ_KEY, STR_ROOT_PROJ_KEY, TEXT_KEY, generate_diverse_nested_payload,
|
|
generate_diverse_payload, random_filter, random_nested_filter, random_vector,
|
|
};
|
|
use segment::id_tracker::IdTracker;
|
|
use segment::index::PayloadIndex;
|
|
use segment::index::field_index::{FieldIndex, PrimaryCondition};
|
|
use segment::index::struct_payload_index::StructPayloadIndex;
|
|
use segment::json_path::JsonPath;
|
|
use segment::payload_json;
|
|
use segment::payload_storage::PayloadStorage;
|
|
use segment::payload_storage::in_memory_payload_storage::InMemoryPayloadStorage;
|
|
use segment::segment::Segment;
|
|
use segment::segment_constructor::build_segment;
|
|
use segment::segment_constructor::segment_builder::SegmentBuilder;
|
|
use segment::segment_constructor::simple_segment_constructor::build_simple_segment;
|
|
use segment::types::PayloadFieldSchema::{FieldParams, FieldType};
|
|
use segment::types::PayloadSchemaType::{Integer, Keyword};
|
|
use segment::types::{
|
|
AnyVariants, Condition, Distance, FieldCondition, Filter, GeoBoundingBox, GeoLineString,
|
|
GeoPoint, GeoPolygon, GeoRadius, HnswConfig, HnswGlobalConfig, Indexes, IsEmptyCondition,
|
|
Match, Payload, PayloadField, PayloadFieldSchema, PayloadSchemaParams, PayloadSchemaType,
|
|
Range, SegmentConfig, ValueVariants, VectorDataConfig, VectorStorageType, WithPayload,
|
|
};
|
|
use segment::utils::scored_point_ties::ScoredPointTies;
|
|
use tempfile::{Builder, TempDir};
|
|
|
|
macro_rules! here {
|
|
() => {
|
|
format!("at {}:{}", file!(), line!())
|
|
};
|
|
}
|
|
|
|
/// `anyhow::ensure!` but with location, as what `assert!` would do
|
|
macro_rules! ensure {
|
|
($($arg:tt)*) => {
|
|
(|| Ok(anyhow::ensure!($($arg)*)))().map_err(|e| {
|
|
e.context(here!())
|
|
})?
|
|
};
|
|
}
|
|
|
|
const DIM: usize = 5;
|
|
const ATTEMPTS: usize = 20;
|
|
|
|
struct TestSegments {
|
|
_base_dir: TempDir,
|
|
struct_segment: Segment,
|
|
plain_segment: Segment,
|
|
mmap_segment: Segment,
|
|
}
|
|
|
|
impl TestSegments {
|
|
fn new() -> Self {
|
|
let base_dir = Builder::new().prefix("test_segments").tempdir().unwrap();
|
|
|
|
let hw_counter = HardwareCounterCell::new();
|
|
|
|
let mut rng = StdRng::seed_from_u64(42);
|
|
|
|
let config = Self::make_simple_config(true);
|
|
|
|
let mut plain_segment =
|
|
build_segment(&base_dir.path().join("plain"), &config, None, true).unwrap();
|
|
let mut struct_segment =
|
|
build_segment(&base_dir.path().join("struct"), &config, None, true).unwrap();
|
|
|
|
let num_points = 3000;
|
|
let points_to_delete = 500;
|
|
let points_to_clear = 500;
|
|
|
|
let mut opnum = 0;
|
|
struct_segment
|
|
.create_field_index(
|
|
opnum,
|
|
&JsonPath::new(INT_KEY_2),
|
|
Some(&Integer.into()),
|
|
&hw_counter,
|
|
)
|
|
.unwrap();
|
|
|
|
opnum += 1;
|
|
for n in 0..num_points {
|
|
let idx = n.into();
|
|
let vector = random_vector(&mut rng, DIM);
|
|
let payload: Payload = generate_diverse_payload(&mut rng);
|
|
|
|
plain_segment
|
|
.upsert_point(opnum, idx, only_default_vector(&vector), &hw_counter)
|
|
.unwrap();
|
|
struct_segment
|
|
.upsert_point(opnum, idx, only_default_vector(&vector), &hw_counter)
|
|
.unwrap();
|
|
plain_segment
|
|
.set_full_payload(opnum, idx, &payload, &hw_counter)
|
|
.unwrap();
|
|
struct_segment
|
|
.set_full_payload(opnum, idx, &payload, &hw_counter)
|
|
.unwrap();
|
|
|
|
opnum += 1;
|
|
}
|
|
|
|
struct_segment
|
|
.create_field_index(
|
|
opnum,
|
|
&JsonPath::new(STR_KEY),
|
|
Some(&Keyword.into()),
|
|
&hw_counter,
|
|
)
|
|
.unwrap();
|
|
let int_payload_schema = PayloadFieldSchema::FieldType(PayloadSchemaType::Integer);
|
|
struct_segment
|
|
.create_field_index(
|
|
opnum,
|
|
&JsonPath::new(INT_KEY),
|
|
Some(&int_payload_schema),
|
|
&hw_counter,
|
|
)
|
|
.unwrap();
|
|
struct_segment
|
|
.create_field_index(
|
|
opnum,
|
|
&JsonPath::new(INT_KEY_2),
|
|
Some(&FieldParams(PayloadSchemaParams::Integer(
|
|
IntegerIndexParams {
|
|
r#type: IntegerIndexType::Integer,
|
|
lookup: Some(true),
|
|
range: Some(false),
|
|
is_principal: None,
|
|
on_disk: None,
|
|
enable_hnsw: None,
|
|
},
|
|
))),
|
|
&hw_counter,
|
|
)
|
|
.unwrap();
|
|
struct_segment
|
|
.create_field_index(
|
|
opnum,
|
|
&JsonPath::new(INT_KEY_3),
|
|
Some(&FieldParams(PayloadSchemaParams::Integer(
|
|
IntegerIndexParams {
|
|
r#type: IntegerIndexType::Integer,
|
|
lookup: Some(false),
|
|
range: Some(true),
|
|
is_principal: None,
|
|
on_disk: None,
|
|
enable_hnsw: None,
|
|
},
|
|
))),
|
|
&hw_counter,
|
|
)
|
|
.unwrap();
|
|
struct_segment
|
|
.create_field_index(
|
|
opnum,
|
|
&JsonPath::new(GEO_KEY),
|
|
Some(&PayloadSchemaType::Geo.into()),
|
|
&hw_counter,
|
|
)
|
|
.unwrap();
|
|
struct_segment
|
|
.create_field_index(
|
|
opnum,
|
|
&JsonPath::new(TEXT_KEY),
|
|
Some(&PayloadSchemaType::Text.into()),
|
|
&hw_counter,
|
|
)
|
|
.unwrap();
|
|
struct_segment
|
|
.create_field_index(
|
|
opnum,
|
|
&JsonPath::new(FLICKING_KEY),
|
|
Some(&Integer.into()),
|
|
&hw_counter,
|
|
)
|
|
.unwrap();
|
|
|
|
// Make mmap segment after inserting the points, but before deleting some of them
|
|
let mut mmap_segment =
|
|
Self::make_mmap_segment(&base_dir.path().join("mmap"), &plain_segment);
|
|
|
|
for _ in 0..points_to_clear {
|
|
opnum += 1;
|
|
let idx_to_remove = rng.random_range(0..num_points);
|
|
plain_segment
|
|
.clear_payload(opnum, idx_to_remove.into(), &hw_counter)
|
|
.unwrap();
|
|
struct_segment
|
|
.clear_payload(opnum, idx_to_remove.into(), &hw_counter)
|
|
.unwrap();
|
|
mmap_segment
|
|
.clear_payload(opnum, idx_to_remove.into(), &hw_counter)
|
|
.unwrap();
|
|
}
|
|
|
|
for _ in 0..points_to_delete {
|
|
opnum += 1;
|
|
let idx_to_remove = rng.random_range(0..num_points);
|
|
plain_segment
|
|
.delete_point(opnum, idx_to_remove.into(), &hw_counter)
|
|
.unwrap();
|
|
struct_segment
|
|
.delete_point(opnum, idx_to_remove.into(), &hw_counter)
|
|
.unwrap();
|
|
mmap_segment
|
|
.delete_point(opnum, idx_to_remove.into(), &hw_counter)
|
|
.unwrap();
|
|
}
|
|
|
|
for (field, indexes) in struct_segment.payload_index.borrow().field_indexes.iter() {
|
|
for index in indexes {
|
|
assert!(index.count_indexed_points() <= num_points as usize);
|
|
if field.to_string() != FLICKING_KEY {
|
|
assert!(
|
|
index.count_indexed_points()
|
|
>= (num_points as usize - points_to_delete - points_to_clear)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Self {
|
|
_base_dir: base_dir,
|
|
struct_segment,
|
|
plain_segment,
|
|
mmap_segment,
|
|
}
|
|
}
|
|
|
|
fn make_simple_config(appendable: bool) -> SegmentConfig {
|
|
let conf = SegmentConfig {
|
|
vector_data: HashMap::from([(
|
|
DEFAULT_VECTOR_NAME.to_owned(),
|
|
VectorDataConfig {
|
|
size: DIM,
|
|
distance: Distance::Dot,
|
|
storage_type: VectorStorageType::default(),
|
|
index: if appendable {
|
|
Indexes::Plain {}
|
|
} else {
|
|
Indexes::Hnsw(HnswConfig::default())
|
|
},
|
|
quantization_config: None,
|
|
multivector_config: None,
|
|
datatype: None,
|
|
},
|
|
)]),
|
|
sparse_vector_data: Default::default(),
|
|
payload_storage_type: Default::default(),
|
|
};
|
|
assert_eq!(conf.is_appendable(), appendable);
|
|
conf
|
|
}
|
|
|
|
fn make_mmap_segment(path: &Path, plain_segment: &Segment) -> Segment {
|
|
let stopped = AtomicBool::new(false);
|
|
fs::create_dir(path).unwrap();
|
|
|
|
let mut builder = SegmentBuilder::new(
|
|
&path.with_extension("tmp"),
|
|
&Self::make_simple_config(false),
|
|
&HnswGlobalConfig::default(),
|
|
)
|
|
.unwrap();
|
|
|
|
builder.update(&[plain_segment], &stopped).unwrap();
|
|
let hw_counter = HardwareCounterCell::new();
|
|
|
|
let mut segment = builder.build_for_test(path);
|
|
let opnum = segment.version() + 1;
|
|
|
|
segment
|
|
.create_field_index(
|
|
opnum,
|
|
&JsonPath::new(STR_KEY),
|
|
Some(&FieldParams(PayloadSchemaParams::Keyword(
|
|
KeywordIndexParams {
|
|
r#type: KeywordIndexType::Keyword,
|
|
is_tenant: None,
|
|
on_disk: Some(true),
|
|
enable_hnsw: None,
|
|
},
|
|
))),
|
|
&hw_counter,
|
|
)
|
|
.unwrap();
|
|
segment
|
|
.create_field_index(
|
|
opnum,
|
|
&JsonPath::new(INT_KEY),
|
|
Some(&FieldParams(PayloadSchemaParams::Integer(
|
|
IntegerIndexParams {
|
|
r#type: IntegerIndexType::Integer,
|
|
lookup: Some(true),
|
|
range: Some(true),
|
|
is_principal: None,
|
|
on_disk: Some(true),
|
|
enable_hnsw: None,
|
|
},
|
|
))),
|
|
&hw_counter,
|
|
)
|
|
.unwrap();
|
|
segment
|
|
.create_field_index(
|
|
opnum,
|
|
&JsonPath::new(INT_KEY_2),
|
|
Some(&FieldParams(PayloadSchemaParams::Integer(
|
|
IntegerIndexParams {
|
|
r#type: IntegerIndexType::Integer,
|
|
lookup: Some(true),
|
|
range: Some(false),
|
|
is_principal: None,
|
|
on_disk: Some(true),
|
|
enable_hnsw: None,
|
|
},
|
|
))),
|
|
&hw_counter,
|
|
)
|
|
.unwrap();
|
|
segment
|
|
.create_field_index(
|
|
opnum,
|
|
&JsonPath::new(INT_KEY_3),
|
|
Some(&FieldParams(PayloadSchemaParams::Integer(
|
|
IntegerIndexParams {
|
|
r#type: IntegerIndexType::Integer,
|
|
lookup: Some(false),
|
|
range: Some(true),
|
|
is_principal: None,
|
|
on_disk: Some(true),
|
|
enable_hnsw: None,
|
|
},
|
|
))),
|
|
&hw_counter,
|
|
)
|
|
.unwrap();
|
|
segment
|
|
.create_field_index(
|
|
opnum,
|
|
&JsonPath::new(FLT_KEY),
|
|
Some(&FieldParams(PayloadSchemaParams::Float(FloatIndexParams {
|
|
r#type: FloatIndexType::Float,
|
|
is_principal: None,
|
|
on_disk: Some(true),
|
|
enable_hnsw: None,
|
|
}))),
|
|
&hw_counter,
|
|
)
|
|
.unwrap();
|
|
segment
|
|
.create_field_index(
|
|
opnum,
|
|
&JsonPath::new(TEXT_KEY),
|
|
Some(&FieldParams(PayloadSchemaParams::Text(TextIndexParams {
|
|
r#type: TextIndexType::Text,
|
|
on_disk: Some(true),
|
|
..Default::default()
|
|
}))),
|
|
&hw_counter,
|
|
)
|
|
.unwrap();
|
|
|
|
segment
|
|
}
|
|
}
|
|
|
|
fn build_test_segments_nested_payload(path_struct: &Path, path_plain: &Path) -> (Segment, Segment) {
|
|
let mut rng = StdRng::seed_from_u64(42);
|
|
|
|
let mut plain_segment = build_simple_segment(path_plain, DIM, Distance::Dot).unwrap();
|
|
let mut struct_segment = build_simple_segment(path_struct, DIM, Distance::Dot).unwrap();
|
|
|
|
let num_points = 3000;
|
|
let points_to_delete = 500;
|
|
let points_to_clear = 500;
|
|
|
|
// Nested payload keys
|
|
let nested_str_key = JsonPath::new(&format!("{}.{}.{}", STR_KEY, "nested_1", "nested_2"));
|
|
let nested_str_proj_key =
|
|
JsonPath::new(&format!("{}.{}[].{}", STR_PROJ_KEY, "nested_1", "nested_2"));
|
|
let deep_nested_str_proj_key = JsonPath::new(&format!(
|
|
"{}[].{}[].{}",
|
|
STR_ROOT_PROJ_KEY, "nested_1", "nested_2"
|
|
));
|
|
|
|
let hw_counter = HardwareCounterCell::new();
|
|
|
|
let mut opnum = 0;
|
|
struct_segment
|
|
.create_field_index(opnum, &nested_str_key, Some(&Keyword.into()), &hw_counter)
|
|
.unwrap();
|
|
|
|
struct_segment
|
|
.create_field_index(
|
|
opnum,
|
|
&nested_str_proj_key,
|
|
Some(&Keyword.into()),
|
|
&hw_counter,
|
|
)
|
|
.unwrap();
|
|
|
|
struct_segment
|
|
.create_field_index(
|
|
opnum,
|
|
&deep_nested_str_proj_key,
|
|
Some(&Keyword.into()),
|
|
&hw_counter,
|
|
)
|
|
.unwrap();
|
|
|
|
eprintln!("{deep_nested_str_proj_key}");
|
|
|
|
opnum += 1;
|
|
for n in 0..num_points {
|
|
let idx = n.into();
|
|
let vector = random_vector(&mut rng, DIM);
|
|
let payload: Payload = generate_diverse_nested_payload(&mut rng);
|
|
|
|
plain_segment
|
|
.upsert_point(opnum, idx, only_default_vector(&vector), &hw_counter)
|
|
.unwrap();
|
|
struct_segment
|
|
.upsert_point(opnum, idx, only_default_vector(&vector), &hw_counter)
|
|
.unwrap();
|
|
plain_segment
|
|
.set_full_payload(opnum, idx, &payload, &hw_counter)
|
|
.unwrap();
|
|
struct_segment
|
|
.set_full_payload(opnum, idx, &payload, &hw_counter)
|
|
.unwrap();
|
|
|
|
opnum += 1;
|
|
}
|
|
|
|
for _ in 0..points_to_clear {
|
|
opnum += 1;
|
|
let idx_to_remove = rng.random_range(0..num_points);
|
|
plain_segment
|
|
.clear_payload(opnum, idx_to_remove.into(), &hw_counter)
|
|
.unwrap();
|
|
struct_segment
|
|
.clear_payload(opnum, idx_to_remove.into(), &hw_counter)
|
|
.unwrap();
|
|
}
|
|
|
|
for _ in 0..points_to_delete {
|
|
opnum += 1;
|
|
let idx_to_remove = rng.random_range(0..num_points);
|
|
plain_segment
|
|
.delete_point(opnum, idx_to_remove.into(), &hw_counter)
|
|
.unwrap();
|
|
struct_segment
|
|
.delete_point(opnum, idx_to_remove.into(), &hw_counter)
|
|
.unwrap();
|
|
}
|
|
|
|
for (_field, indexes) in struct_segment.payload_index.borrow().field_indexes.iter() {
|
|
for index in indexes {
|
|
assert!(index.count_indexed_points() <= num_points as usize);
|
|
assert!(
|
|
index.count_indexed_points()
|
|
> (num_points as usize - points_to_delete - points_to_clear)
|
|
);
|
|
}
|
|
}
|
|
|
|
(struct_segment, plain_segment)
|
|
}
|
|
|
|
fn validate_geo_filter(test_segments: &TestSegments, query_filter: Filter) -> Result<()> {
|
|
let mut rng = rand::rng();
|
|
|
|
for _i in 0..ATTEMPTS {
|
|
let query = random_vector(&mut rng, DIM).into();
|
|
let plain_result = test_segments
|
|
.plain_segment
|
|
.search(
|
|
DEFAULT_VECTOR_NAME,
|
|
&query,
|
|
&WithPayload::default(),
|
|
&false.into(),
|
|
Some(&query_filter),
|
|
5,
|
|
None,
|
|
)
|
|
.unwrap();
|
|
|
|
let hw_counter = HardwareCounterCell::new();
|
|
let estimation = test_segments
|
|
.plain_segment
|
|
.payload_index
|
|
.borrow()
|
|
.estimate_cardinality(&query_filter, &hw_counter);
|
|
|
|
ensure!(estimation.min <= estimation.exp, "{estimation:#?}");
|
|
ensure!(estimation.exp <= estimation.max, "{estimation:#?}");
|
|
ensure!(
|
|
estimation.max
|
|
<= test_segments
|
|
.struct_segment
|
|
.id_tracker
|
|
.borrow()
|
|
.available_point_count(),
|
|
"{estimation:#?}",
|
|
);
|
|
|
|
let struct_result = test_segments
|
|
.struct_segment
|
|
.search(
|
|
DEFAULT_VECTOR_NAME,
|
|
&query,
|
|
&WithPayload::default(),
|
|
&false.into(),
|
|
Some(&query_filter),
|
|
5,
|
|
None,
|
|
)
|
|
.unwrap();
|
|
|
|
let estimation = test_segments
|
|
.struct_segment
|
|
.payload_index
|
|
.borrow()
|
|
.estimate_cardinality(&query_filter, &hw_counter);
|
|
|
|
ensure!(estimation.min <= estimation.exp, "{estimation:#?}");
|
|
ensure!(estimation.exp <= estimation.max, "{estimation:#?}");
|
|
ensure!(
|
|
estimation.max
|
|
<= test_segments
|
|
.struct_segment
|
|
.id_tracker
|
|
.borrow()
|
|
.available_point_count(),
|
|
"{estimation:#?}",
|
|
);
|
|
|
|
for (r1, r2) in plain_result.iter().zip(struct_result.iter()) {
|
|
ensure!(r1.id == r2.id);
|
|
ensure!((r1.score - r2.score) < 0.0001)
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test read operations on segments.
|
|
/// The segments fixtures are created only once to improve test speed.
|
|
#[test]
|
|
fn test_read_operations() -> Result<()> {
|
|
let test_segments = Arc::new(TestSegments::new());
|
|
let mut handles = vec![];
|
|
|
|
for test_fn in [
|
|
test_is_empty_conditions,
|
|
test_integer_index_types,
|
|
test_cardinality_estimation,
|
|
test_struct_payload_index,
|
|
test_struct_payload_geo_boundingbox_index,
|
|
test_struct_payload_geo_radius_index,
|
|
test_struct_payload_geo_polygon_index,
|
|
test_any_matcher_cardinality_estimation,
|
|
test_struct_keyword_facet,
|
|
test_mmap_keyword_facet,
|
|
test_struct_keyword_facet_filtered,
|
|
test_mmap_keyword_facet_filtered,
|
|
] {
|
|
let segments = Arc::clone(&test_segments);
|
|
handles.push(std::thread::spawn(move || test_fn(&segments)));
|
|
}
|
|
|
|
for handle in handles {
|
|
handle.join().unwrap()?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn test_is_empty_conditions(test_segments: &TestSegments) -> Result<()> {
|
|
let filter = Filter::new_must(Condition::IsEmpty(IsEmptyCondition {
|
|
is_empty: PayloadField {
|
|
key: JsonPath::new(FLICKING_KEY),
|
|
},
|
|
}));
|
|
|
|
let hw_counter = HardwareCounterCell::new();
|
|
let is_stopped = AtomicBool::new(false);
|
|
|
|
let estimation_struct = test_segments
|
|
.struct_segment
|
|
.payload_index
|
|
.borrow()
|
|
.estimate_cardinality(&filter, &hw_counter);
|
|
|
|
let estimation_plain = test_segments
|
|
.plain_segment
|
|
.payload_index
|
|
.borrow()
|
|
.estimate_cardinality(&filter, &hw_counter);
|
|
|
|
let plain_result = test_segments
|
|
.plain_segment
|
|
.payload_index
|
|
.borrow()
|
|
.query_points(&filter, &hw_counter, &is_stopped);
|
|
|
|
let real_number = plain_result.len();
|
|
|
|
let id_tracker = test_segments.struct_segment.id_tracker.borrow();
|
|
let struct_result = test_segments
|
|
.struct_segment
|
|
.payload_index
|
|
.borrow()
|
|
.query_points(&filter, &hw_counter, &is_stopped)
|
|
.into_iter()
|
|
// null index does not track deleted points, so we need to filter them out here. In callsites,
|
|
// the deleted check is done externally anyway
|
|
.filter(|id| !id_tracker.is_deleted_point(*id))
|
|
.collect::<Vec<_>>();
|
|
|
|
ensure!(plain_result == struct_result);
|
|
|
|
eprintln!("estimation_plain = {estimation_plain:#?}");
|
|
eprintln!("estimation_struct = {estimation_struct:#?}");
|
|
eprintln!("real_number = {real_number:#?}");
|
|
|
|
ensure!(estimation_plain.max >= real_number);
|
|
ensure!(estimation_plain.min <= real_number);
|
|
|
|
ensure!(estimation_struct.max >= real_number);
|
|
ensure!(estimation_struct.min <= real_number);
|
|
|
|
ensure!(
|
|
(estimation_struct.exp as f64 - real_number as f64).abs()
|
|
<= (estimation_plain.exp as f64 - real_number as f64).abs()
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn test_integer_index_types(test_segments: &TestSegments) -> Result<()> {
|
|
for (kind, indexes) in [
|
|
(
|
|
"struct",
|
|
&test_segments.struct_segment.payload_index.borrow(),
|
|
),
|
|
("mmap", &test_segments.mmap_segment.payload_index.borrow()),
|
|
] {
|
|
eprintln!("Checking {kind}_segment");
|
|
let field_indexes = indexes.field_indexes.get(&JsonPath::new(INT_KEY)).unwrap();
|
|
|
|
let has_map_index = field_indexes
|
|
.iter()
|
|
.any(|index| matches!(index, FieldIndex::IntMapIndex(_)));
|
|
let has_int_index = field_indexes
|
|
.iter()
|
|
.any(|index| matches!(index, FieldIndex::IntIndex(_)));
|
|
|
|
ensure!(has_map_index);
|
|
ensure!(has_int_index);
|
|
|
|
let field_indexes = indexes
|
|
.field_indexes
|
|
.get(&JsonPath::new(INT_KEY_2))
|
|
.unwrap();
|
|
|
|
let has_map_index = field_indexes
|
|
.iter()
|
|
.any(|index| matches!(index, FieldIndex::IntMapIndex(_)));
|
|
let has_int_index = field_indexes
|
|
.iter()
|
|
.any(|index| matches!(index, FieldIndex::IntIndex(_)));
|
|
|
|
ensure!(has_map_index);
|
|
ensure!(!has_int_index);
|
|
|
|
let field_indexes = indexes
|
|
.field_indexes
|
|
.get(&JsonPath::new(INT_KEY_3))
|
|
.unwrap();
|
|
|
|
let has_map_index = field_indexes
|
|
.iter()
|
|
.any(|index| matches!(index, FieldIndex::IntMapIndex(_)));
|
|
let has_int_index = field_indexes
|
|
.iter()
|
|
.any(|index| matches!(index, FieldIndex::IntIndex(_)));
|
|
|
|
ensure!(!has_map_index);
|
|
ensure!(has_int_index);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn test_cardinality_estimation(test_segments: &TestSegments) -> Result<()> {
|
|
let filter = Filter::new_must(Condition::Field(FieldCondition::new_range(
|
|
JsonPath::new(INT_KEY),
|
|
Range {
|
|
lt: None,
|
|
gt: None,
|
|
gte: Some(OrderedFloat(50.)),
|
|
lte: Some(OrderedFloat(100.)),
|
|
},
|
|
)));
|
|
|
|
let hw_counter = HardwareCounterCell::new();
|
|
|
|
let estimation = test_segments
|
|
.struct_segment
|
|
.payload_index
|
|
.borrow()
|
|
.estimate_cardinality(&filter, &hw_counter);
|
|
|
|
let hw_counter = HardwareCounterCell::new();
|
|
|
|
let payload_index = test_segments.struct_segment.payload_index.borrow();
|
|
let filter_context = payload_index.filter_context(&filter, &hw_counter);
|
|
let exact = test_segments
|
|
.struct_segment
|
|
.id_tracker
|
|
.borrow()
|
|
.iter_internal()
|
|
.filter(|x| filter_context.check(*x))
|
|
.collect_vec()
|
|
.len();
|
|
|
|
eprintln!("exact = {exact:#?}");
|
|
eprintln!("estimation = {estimation:#?}");
|
|
|
|
ensure!(exact <= estimation.max);
|
|
ensure!(exact >= estimation.min);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_root_nested_array_filter_cardinality_estimation() {
|
|
let dir1 = Builder::new().prefix("segment1_dir").tempdir().unwrap();
|
|
let dir2 = Builder::new().prefix("segment2_dir").tempdir().unwrap();
|
|
|
|
let (struct_segment, _) = build_test_segments_nested_payload(dir1.path(), dir2.path());
|
|
|
|
// rely on test data from `build_test_segments_nested_payload`
|
|
let nested_key = "nested_1[].nested_2";
|
|
let nested_match =
|
|
FieldCondition::new_match(JsonPath::new(nested_key), "some value".to_owned().into());
|
|
let filter = Filter::new_must(Condition::new_nested(
|
|
JsonPath::new(STR_ROOT_PROJ_KEY),
|
|
Filter::new_must(Condition::Field(nested_match)),
|
|
));
|
|
|
|
let hw_counter = HardwareCounterCell::new();
|
|
|
|
let estimation = struct_segment
|
|
.payload_index
|
|
.borrow()
|
|
.estimate_cardinality(&filter, &hw_counter);
|
|
|
|
// not empty primary clauses
|
|
assert_eq!(estimation.primary_clauses.len(), 1);
|
|
eprintln!("primary_clauses = {:#?}", estimation.primary_clauses);
|
|
let primary_clause = estimation.primary_clauses.first().unwrap();
|
|
|
|
let expected_primary_clause = FieldCondition::new_match(
|
|
JsonPath::new(&format!("{STR_ROOT_PROJ_KEY}[].{nested_key}")), // full key expected
|
|
"some value".to_owned().into(),
|
|
);
|
|
|
|
match primary_clause {
|
|
PrimaryCondition::Condition(field_condition) => {
|
|
assert_eq!(*field_condition, Box::new(expected_primary_clause));
|
|
}
|
|
o => panic!("unexpected primary clause: {o:?}"),
|
|
}
|
|
|
|
let hw_counter = HardwareCounterCell::new();
|
|
|
|
let payload_index = struct_segment.payload_index.borrow();
|
|
let filter_context = payload_index.filter_context(&filter, &hw_counter);
|
|
let exact = struct_segment
|
|
.id_tracker
|
|
.borrow()
|
|
.iter_internal()
|
|
.filter(|x| filter_context.check(*x))
|
|
.collect_vec()
|
|
.len();
|
|
|
|
eprintln!("exact = {exact:#?}");
|
|
eprintln!("estimation = {estimation:#?}");
|
|
|
|
assert!(exact <= estimation.max);
|
|
assert!(exact >= estimation.min);
|
|
}
|
|
|
|
#[test]
|
|
fn test_nesting_nested_array_filter_cardinality_estimation() {
|
|
let dir1 = Builder::new().prefix("segment1_dir").tempdir().unwrap();
|
|
let dir2 = Builder::new().prefix("segment2_dir").tempdir().unwrap();
|
|
|
|
let (struct_segment, _) = build_test_segments_nested_payload(dir1.path(), dir2.path());
|
|
|
|
// rely on test data from `build_test_segments_nested_payload`
|
|
let nested_match_key = "nested_2";
|
|
let nested_match = FieldCondition::new_match(
|
|
JsonPath::new(nested_match_key),
|
|
"some value".to_owned().into(),
|
|
);
|
|
let filter = Filter::new_must(Condition::new_nested(
|
|
JsonPath::new(STR_ROOT_PROJ_KEY),
|
|
Filter::new_must(Condition::new_nested(
|
|
JsonPath::new("nested_1"),
|
|
Filter::new_must(Condition::Field(nested_match)),
|
|
)),
|
|
));
|
|
|
|
let hw_counter = HardwareCounterCell::new();
|
|
|
|
let estimation = struct_segment
|
|
.payload_index
|
|
.borrow()
|
|
.estimate_cardinality(&filter, &hw_counter);
|
|
|
|
// not empty primary clauses
|
|
assert_eq!(estimation.primary_clauses.len(), 1);
|
|
eprintln!("primary_clauses = {:#?}", estimation.primary_clauses);
|
|
let primary_clause = estimation.primary_clauses.first().unwrap();
|
|
|
|
let expected_primary_clause = FieldCondition::new_match(
|
|
// full key expected
|
|
JsonPath::new(&format!(
|
|
"{STR_ROOT_PROJ_KEY}[].nested_1[].{nested_match_key}"
|
|
)),
|
|
"some value".to_owned().into(),
|
|
);
|
|
|
|
match primary_clause {
|
|
PrimaryCondition::Condition(field_condition) => {
|
|
assert_eq!(*field_condition, Box::new(expected_primary_clause));
|
|
}
|
|
o => panic!("unexpected primary clause: {o:?}"),
|
|
}
|
|
|
|
let hw_counter = HardwareCounterCell::new();
|
|
|
|
let payload_index = struct_segment.payload_index.borrow();
|
|
let filter_context = payload_index.filter_context(&filter, &hw_counter);
|
|
let exact = struct_segment
|
|
.id_tracker
|
|
.borrow()
|
|
.iter_internal()
|
|
.filter(|x| filter_context.check(*x))
|
|
.collect_vec()
|
|
.len();
|
|
|
|
eprintln!("exact = {exact:#?}");
|
|
eprintln!("estimation = {estimation:#?}");
|
|
|
|
assert!(exact <= estimation.max);
|
|
assert!(exact >= estimation.min);
|
|
}
|
|
|
|
/// Compare search with plain, struct, and mmap indices.
|
|
fn test_struct_payload_index(test_segments: &TestSegments) -> Result<()> {
|
|
let mut rng = rand::rng();
|
|
|
|
for _i in 0..ATTEMPTS {
|
|
let query_vector = random_vector(&mut rng, DIM).into();
|
|
let query_filter = random_filter(&mut rng, 3);
|
|
|
|
let plain_result = test_segments
|
|
.plain_segment
|
|
.search(
|
|
DEFAULT_VECTOR_NAME,
|
|
&query_vector,
|
|
&WithPayload::default(),
|
|
&false.into(),
|
|
Some(&query_filter),
|
|
5,
|
|
None,
|
|
)
|
|
.unwrap();
|
|
let struct_result = test_segments
|
|
.struct_segment
|
|
.search(
|
|
DEFAULT_VECTOR_NAME,
|
|
&query_vector,
|
|
&WithPayload::default(),
|
|
&false.into(),
|
|
Some(&query_filter),
|
|
5,
|
|
None,
|
|
)
|
|
.unwrap();
|
|
let mmap_result = test_segments
|
|
.mmap_segment
|
|
.search(
|
|
DEFAULT_VECTOR_NAME,
|
|
&query_vector,
|
|
&WithPayload::default(),
|
|
&false.into(),
|
|
Some(&query_filter),
|
|
5,
|
|
None,
|
|
)
|
|
.unwrap();
|
|
|
|
let hw_counter = HardwareCounterCell::new();
|
|
|
|
let estimation = test_segments
|
|
.struct_segment
|
|
.payload_index
|
|
.borrow()
|
|
.estimate_cardinality(&query_filter, &hw_counter);
|
|
|
|
ensure!(estimation.min <= estimation.exp, "{estimation:#?}");
|
|
ensure!(estimation.exp <= estimation.max, "{estimation:#?}");
|
|
ensure!(
|
|
estimation.max
|
|
<= test_segments
|
|
.struct_segment
|
|
.id_tracker
|
|
.borrow()
|
|
.available_point_count(),
|
|
"{estimation:#?}",
|
|
);
|
|
|
|
// Perform additional sort to break ties by score
|
|
let mut plain_result_sorted_ties: Vec<ScoredPointTies> =
|
|
plain_result.iter().map(|x| x.into()).collect_vec();
|
|
plain_result_sorted_ties.sort();
|
|
|
|
let mut struct_result_sorted_ties: Vec<ScoredPointTies> =
|
|
struct_result.iter().map(|x| x.into()).collect_vec();
|
|
struct_result_sorted_ties.sort();
|
|
|
|
let mut mmap_result_sorted_ties: Vec<ScoredPointTies> =
|
|
mmap_result.iter().map(|x| x.into()).collect_vec();
|
|
mmap_result_sorted_ties.sort();
|
|
|
|
ensure!(
|
|
plain_result_sorted_ties.len() == struct_result_sorted_ties.len(),
|
|
"query vector {query_vector:?}\n\
|
|
query filter {query_filter:?}\n\
|
|
plain result {plain_result:?}\n\
|
|
struct result{struct_result:?}",
|
|
);
|
|
ensure!(
|
|
plain_result_sorted_ties.len() == mmap_result_sorted_ties.len(),
|
|
"query vector {query_vector:?}\n\
|
|
query filter {query_filter:?}\n\
|
|
plain result {plain_result:?}\n\
|
|
mmap result {mmap_result:?}",
|
|
);
|
|
|
|
for (r1, r2, r3) in itertools::izip!(
|
|
plain_result_sorted_ties,
|
|
struct_result_sorted_ties,
|
|
mmap_result_sorted_ties,
|
|
)
|
|
.map(|(r1, r2, r3)| (r1.0, r2.0, r3.0))
|
|
{
|
|
ensure!(
|
|
r1.id == r2.id,
|
|
"got different ScoredPoint {r1:?} and {r2:?} for\n\
|
|
query vector {query_vector:?}\n\
|
|
query filter {query_filter:?}\n\
|
|
plain result {plain_result:?}\n\
|
|
struct result{struct_result:?}"
|
|
);
|
|
ensure!((r1.score - r2.score) < 0.0001);
|
|
ensure!(
|
|
r1.id == r3.id,
|
|
"got different ScoredPoint {r1:?} and {r3:?} for\n\
|
|
query vector {query_vector:?}\n\
|
|
query filter {query_filter:?}\n\
|
|
plain result {plain_result:?}\n\
|
|
mmap result {mmap_result:?}",
|
|
);
|
|
ensure!((r1.score - r3.score) < 0.0001);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn test_struct_payload_geo_boundingbox_index(test_segments: &TestSegments) -> Result<()> {
|
|
let mut rng = rand::rng();
|
|
|
|
let geo_bbox = GeoBoundingBox {
|
|
top_left: GeoPoint::new_unchecked(rng.random_range(LON_RANGE), rng.random_range(LAT_RANGE)),
|
|
bottom_right: GeoPoint::new_unchecked(
|
|
rng.random_range(LON_RANGE),
|
|
rng.random_range(LAT_RANGE),
|
|
),
|
|
};
|
|
|
|
let condition = Condition::Field(FieldCondition::new_geo_bounding_box(
|
|
JsonPath::new("geo_key"),
|
|
geo_bbox,
|
|
));
|
|
|
|
let query_filter = Filter::new_must(condition);
|
|
|
|
validate_geo_filter(test_segments, query_filter).context(here!())
|
|
}
|
|
|
|
fn test_struct_payload_geo_radius_index(test_segments: &TestSegments) -> Result<()> {
|
|
let mut rng = rand::rng();
|
|
|
|
let r_meters = rng.random_range(1.0..10000.0);
|
|
let geo_radius = GeoRadius {
|
|
center: GeoPoint::new_unchecked(rng.random_range(LON_RANGE), rng.random_range(LAT_RANGE)),
|
|
radius: OrderedFloat(r_meters),
|
|
};
|
|
|
|
let condition = Condition::Field(FieldCondition::new_geo_radius(
|
|
JsonPath::new("geo_key"),
|
|
geo_radius,
|
|
));
|
|
|
|
let query_filter = Filter::new_must(condition);
|
|
|
|
validate_geo_filter(test_segments, query_filter).context(here!())
|
|
}
|
|
|
|
fn test_struct_payload_geo_polygon_index(test_segments: &TestSegments) -> Result<()> {
|
|
let polygon_edge = 5;
|
|
let interiors_num = 3;
|
|
|
|
fn generate_ring(polygon_edge: i32) -> GeoLineString {
|
|
let mut rng = rand::rng();
|
|
let mut line = GeoLineString {
|
|
points: (0..polygon_edge)
|
|
.map(|_| {
|
|
GeoPoint::new_unchecked(
|
|
rng.random_range(LON_RANGE),
|
|
rng.random_range(LAT_RANGE),
|
|
)
|
|
})
|
|
.collect(),
|
|
};
|
|
line.points.push(line.points[0]); // add last point that is identical to the first
|
|
line
|
|
}
|
|
|
|
let exterior = generate_ring(polygon_edge);
|
|
let interiors = Some(
|
|
std::iter::repeat_with(|| generate_ring(polygon_edge))
|
|
.take(interiors_num)
|
|
.collect(),
|
|
);
|
|
|
|
let geo_polygon = GeoPolygon {
|
|
exterior,
|
|
interiors,
|
|
};
|
|
|
|
let condition = Condition::Field(FieldCondition::new_geo_polygon(
|
|
JsonPath::new("geo_key"),
|
|
geo_polygon,
|
|
));
|
|
|
|
let query_filter = Filter::new_must(condition);
|
|
|
|
validate_geo_filter(test_segments, query_filter).context(here!())
|
|
}
|
|
|
|
#[test]
|
|
fn test_struct_payload_index_nested_fields() {
|
|
// Compare search with plain and struct indexes
|
|
let dir1 = Builder::new().prefix("segment1_dir").tempdir().unwrap();
|
|
let dir2 = Builder::new().prefix("segment2_dir").tempdir().unwrap();
|
|
|
|
let mut rng = rand::rng();
|
|
|
|
let (struct_segment, plain_segment) =
|
|
build_test_segments_nested_payload(dir1.path(), dir2.path());
|
|
|
|
let attempts = 100;
|
|
for _i in 0..attempts {
|
|
let query_vector = random_vector(&mut rng, DIM).into();
|
|
let query_filter = random_nested_filter(&mut rng);
|
|
let plain_result = plain_segment
|
|
.search(
|
|
DEFAULT_VECTOR_NAME,
|
|
&query_vector,
|
|
&WithPayload {
|
|
enable: true,
|
|
payload_selector: None,
|
|
},
|
|
&false.into(),
|
|
Some(&query_filter),
|
|
5,
|
|
None,
|
|
)
|
|
.unwrap();
|
|
let struct_result = struct_segment
|
|
.search(
|
|
DEFAULT_VECTOR_NAME,
|
|
&query_vector,
|
|
&WithPayload {
|
|
enable: true,
|
|
payload_selector: None,
|
|
},
|
|
&false.into(),
|
|
Some(&query_filter),
|
|
5,
|
|
None,
|
|
)
|
|
.unwrap();
|
|
|
|
let hw_counter = HardwareCounterCell::new();
|
|
|
|
let estimation = struct_segment
|
|
.payload_index
|
|
.borrow()
|
|
.estimate_cardinality(&query_filter, &hw_counter);
|
|
|
|
assert!(estimation.min <= estimation.exp, "{estimation:#?}");
|
|
assert!(estimation.exp <= estimation.max, "{estimation:#?}");
|
|
assert!(
|
|
estimation.max <= struct_segment.id_tracker.borrow().available_point_count(),
|
|
"{estimation:#?}",
|
|
);
|
|
|
|
// warning: report flakiness at https://github.com/qdrant/qdrant/issues/534
|
|
plain_result
|
|
.iter()
|
|
.zip(struct_result.iter())
|
|
.for_each(|(r1, r2)| {
|
|
assert_eq!(
|
|
r1.id, r2.id,
|
|
"got different ScoredPoint {r1:?} and {r2:?} for\n\
|
|
query vector {query_vector:?}\n\
|
|
query filter {query_filter:?}\n\
|
|
plain result {plain_result:?}\n\
|
|
struct result{struct_result:?}"
|
|
);
|
|
assert!((r1.score - r2.score) < 0.0001)
|
|
});
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_payload_index_type() {
|
|
let dir = Builder::new().prefix("storage_dir").tempdir().unwrap();
|
|
let mut payload_storage = InMemoryPayloadStorage::default();
|
|
|
|
let point_num = 10;
|
|
let mut points = HashMap::new();
|
|
|
|
let mut payloads: Vec<Payload> = vec![];
|
|
for i in 0..point_num {
|
|
payloads.push(payload_json! {"field": i});
|
|
}
|
|
|
|
let hw_counter = HardwareCounterCell::new();
|
|
|
|
for (idx, payload) in payloads.into_iter().enumerate() {
|
|
points.insert(idx, payload.clone());
|
|
payload_storage
|
|
.set(idx as PointOffsetType, &payload, &hw_counter)
|
|
.unwrap();
|
|
}
|
|
|
|
let wrapped_payload_storage = Arc::new(AtomicRefCell::new(payload_storage.into()));
|
|
let id_tracker = Arc::new(AtomicRefCell::new(create_id_tracker_fixture(point_num)));
|
|
|
|
let mut index = StructPayloadIndex::open(
|
|
wrapped_payload_storage,
|
|
id_tracker,
|
|
HashMap::new(),
|
|
dir.path(),
|
|
true,
|
|
true,
|
|
)
|
|
.unwrap();
|
|
|
|
let field = JsonPath::new("field");
|
|
|
|
// set field to Integer type
|
|
index.set_indexed(&field, Integer, &hw_counter).unwrap();
|
|
assert_eq!(
|
|
*index.indexed_fields().get(&field).unwrap(),
|
|
FieldType(Integer)
|
|
);
|
|
let field_index = index.field_indexes.get(&field).unwrap();
|
|
assert_eq!(field_index[0].count_indexed_points(), point_num);
|
|
assert_eq!(field_index[1].count_indexed_points(), point_num);
|
|
|
|
// update field to Keyword type
|
|
index.set_indexed(&field, Keyword, &hw_counter).unwrap();
|
|
assert_eq!(
|
|
*index.indexed_fields().get(&field).unwrap(),
|
|
FieldType(Keyword)
|
|
);
|
|
let field_index = index.field_indexes.get(&field).unwrap();
|
|
assert_eq!(field_index[0].count_indexed_points(), 0); // only one field index for Keyword
|
|
|
|
// set field to Integer type (again)
|
|
index.set_indexed(&field, Integer, &hw_counter).unwrap();
|
|
assert_eq!(
|
|
*index.indexed_fields().get(&field).unwrap(),
|
|
FieldType(Integer)
|
|
);
|
|
let field_index = index.field_indexes.get(&field).unwrap();
|
|
assert_eq!(field_index[0].count_indexed_points(), point_num);
|
|
assert_eq!(field_index[1].count_indexed_points(), point_num);
|
|
}
|
|
|
|
fn test_any_matcher_cardinality_estimation(test_segments: &TestSegments) -> Result<()> {
|
|
let keywords: IndexSet<String, FnvBuildHasher> = ["value1", "value2"]
|
|
.iter()
|
|
.map(|&i| i.to_string())
|
|
.collect();
|
|
let any_match = FieldCondition::new_match(
|
|
JsonPath::new(STR_KEY),
|
|
Match::new_any(AnyVariants::Strings(keywords)),
|
|
);
|
|
|
|
let filter = Filter::new_must(Condition::Field(any_match.clone()));
|
|
|
|
let hw_counter = HardwareCounterCell::new();
|
|
|
|
let estimation = test_segments
|
|
.struct_segment
|
|
.payload_index
|
|
.borrow()
|
|
.estimate_cardinality(&filter, &hw_counter);
|
|
|
|
ensure!(estimation.primary_clauses.len() == 1);
|
|
for clause in estimation.primary_clauses.iter() {
|
|
let expected_primary_clause = any_match.clone();
|
|
|
|
match clause {
|
|
PrimaryCondition::Condition(field_condition) => {
|
|
ensure!(*field_condition == Box::new(expected_primary_clause));
|
|
}
|
|
o => panic!("unexpected primary clause: {o:?}"),
|
|
}
|
|
}
|
|
|
|
let hw_counter = HardwareCounterCell::new();
|
|
|
|
let payload_index = test_segments.struct_segment.payload_index.borrow();
|
|
let filter_context = payload_index.filter_context(&filter, &hw_counter);
|
|
let exact = test_segments
|
|
.struct_segment
|
|
.id_tracker
|
|
.borrow()
|
|
.iter_internal()
|
|
.filter(|x| filter_context.check(*x))
|
|
.collect_vec()
|
|
.len();
|
|
|
|
eprintln!("exact = {exact:#?}");
|
|
eprintln!("estimation = {estimation:#?}");
|
|
|
|
ensure!(exact <= estimation.max);
|
|
ensure!(exact >= estimation.min);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// FacetParams fixture without a filter
|
|
fn keyword_facet_request() -> FacetParams {
|
|
let limit = 1000;
|
|
let key: JsonPath = STR_KEY.try_into().unwrap();
|
|
let exact = false; // This is only used at local shard level
|
|
|
|
// *** Without filter ***
|
|
FacetParams {
|
|
key: key.clone(),
|
|
limit,
|
|
filter: None,
|
|
exact,
|
|
}
|
|
}
|
|
|
|
/// Checks that the counts are the same as counting each value exactly.
|
|
fn validate_facet_result(
|
|
segment: &Segment,
|
|
facet_hits: HashMap<FacetValue, usize>,
|
|
filter: Option<Filter>,
|
|
) -> Result<()> {
|
|
let hw_counter = HardwareCounterCell::new();
|
|
|
|
for (value, count) in facet_hits.iter() {
|
|
// Compare against exact count
|
|
let value = ValueVariants::from(value.clone());
|
|
|
|
let count_filter = Filter::new_must(Condition::Field(FieldCondition::new_match(
|
|
JsonPath::new(STR_KEY),
|
|
Match::from(value.clone()),
|
|
)));
|
|
let count_filter = Filter::merge_opts(Some(count_filter), filter.clone());
|
|
|
|
let exact = segment
|
|
.read_filtered(
|
|
None,
|
|
None,
|
|
count_filter.as_ref(),
|
|
&Default::default(),
|
|
&hw_counter,
|
|
)
|
|
.len();
|
|
|
|
ensure!(*count == exact, "Facet value: {value:?}");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn test_struct_keyword_facet(test_segments: &TestSegments) -> Result<()> {
|
|
let request = keyword_facet_request();
|
|
|
|
// Plain segment should fail, as it does not have a keyword index
|
|
assert!(
|
|
test_segments
|
|
.plain_segment
|
|
.facet(&request, &Default::default(), &Default::default())
|
|
.is_err(),
|
|
);
|
|
|
|
// Struct segment
|
|
let facet_hits = test_segments
|
|
.struct_segment
|
|
.facet(&request, &Default::default(), &Default::default())
|
|
.unwrap();
|
|
|
|
validate_facet_result(&test_segments.struct_segment, facet_hits, None).context(here!())
|
|
}
|
|
|
|
fn test_mmap_keyword_facet(test_segments: &TestSegments) -> Result<()> {
|
|
let request = keyword_facet_request();
|
|
|
|
let facet_hits = test_segments
|
|
.mmap_segment
|
|
.facet(&request, &Default::default(), &Default::default())
|
|
.unwrap();
|
|
|
|
validate_facet_result(&test_segments.mmap_segment, facet_hits, None).context(here!())
|
|
}
|
|
|
|
fn test_struct_keyword_facet_filtered(test_segments: &TestSegments) -> Result<()> {
|
|
let mut request = keyword_facet_request();
|
|
|
|
for _ in 0..ATTEMPTS {
|
|
let filter = random_filter(&mut rand::rng(), 3);
|
|
request.filter = Some(filter.clone());
|
|
|
|
let facet_hits = test_segments
|
|
.struct_segment
|
|
.facet(&request, &Default::default(), &Default::default())
|
|
.unwrap();
|
|
|
|
validate_facet_result(&test_segments.struct_segment, facet_hits, Some(filter))
|
|
.context(here!())?
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn test_mmap_keyword_facet_filtered(test_segments: &TestSegments) -> Result<()> {
|
|
let mut request = keyword_facet_request();
|
|
|
|
for _ in 0..ATTEMPTS {
|
|
let filter = random_filter(&mut rand::rng(), 3);
|
|
request.filter = Some(filter.clone());
|
|
|
|
let facet_hits = test_segments
|
|
.mmap_segment
|
|
.facet(&request, &Default::default(), &Default::default())
|
|
.unwrap();
|
|
|
|
validate_facet_result(&test_segments.mmap_segment, facet_hits, Some(filter))
|
|
.context(here!())?
|
|
}
|
|
Ok(())
|
|
}
|