mod common;
use std::any::Any;
use std::collections::{HashMap, HashSet};
use std::hint::black_box;
use std::sync::Arc;
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use laurus::lexical::index::structures::bkd_tree::BKDTree;
use laurus::lexical::reader::{FieldStats, LexicalIndexReader, PostingIterator, ReaderTermInfo};
use laurus::{DataValue, Document, Result as LaurusResult};
const N_DOCS: usize = 100;
const TOP_K: u64 = 10;
#[derive(Debug)]
struct MockStoreReader {
documents: Vec<Document>,
}
impl MockStoreReader {
fn new(documents: Vec<Document>) -> Self {
Self { documents }
}
}
impl LexicalIndexReader for MockStoreReader {
fn doc_count(&self) -> u64 {
self.documents.len() as u64
}
fn max_doc(&self) -> u64 {
self.documents.len() as u64
}
fn is_deleted(&self, _doc_id: u64) -> bool {
false
}
fn document(&self, doc_id: u64) -> LaurusResult<Option<Document>> {
Ok(self.documents.get(doc_id as usize).cloned())
}
fn document_fields(
&self,
doc_id: u64,
field_names: &[&str],
) -> LaurusResult<Option<HashMap<String, DataValue>>> {
if let Some(doc) = self.documents.get(doc_id as usize) {
let mut out = HashMap::with_capacity(field_names.len());
for &name in field_names {
if let Some(value) = doc.fields.get(name) {
out.insert(name.to_string(), value.clone());
}
}
return Ok(Some(out));
}
Ok(None)
}
fn term_info(&self, _field: &str, _term: &str) -> LaurusResult<Option<ReaderTermInfo>> {
Ok(None)
}
fn postings(
&self,
_field: &str,
_term: &str,
) -> LaurusResult<Option<Box<dyn PostingIterator>>> {
Ok(None)
}
fn field_stats(&self, _field: &str) -> LaurusResult<Option<FieldStats>> {
Ok(None)
}
fn close(&mut self) -> LaurusResult<()> {
Ok(())
}
fn is_closed(&self) -> bool {
false
}
fn get_bkd_tree(&self, _field: &str) -> LaurusResult<Option<Arc<dyn BKDTree>>> {
Ok(None)
}
fn as_any(&self) -> &dyn Any {
self
}
}
fn fetch_and_filter(
reader: &dyn LexicalIndexReader,
doc_id: u64,
selected: &HashSet<&str>,
) -> HashMap<String, String> {
let mut out = HashMap::new();
if let Ok(Some(doc)) = reader.document(doc_id) {
for (name, value) in &doc.fields {
if selected.contains(name.as_str()) {
out.insert(name.clone(), data_value_to_string(value));
}
}
}
out
}
fn fetch_subset(
reader: &dyn LexicalIndexReader,
doc_id: u64,
selected: &[&str],
) -> HashMap<String, String> {
let mut out = HashMap::new();
if let Ok(Some(fields)) = reader.document_fields(doc_id, selected) {
for (name, value) in fields {
out.insert(name, data_value_to_string(&value));
}
}
out
}
fn data_value_to_string(value: &DataValue) -> String {
match value {
DataValue::Text(s) => s.clone(),
other => format!("{other:?}"),
}
}
fn build_documents(n_docs: usize, n_fields: usize, value_bytes: usize) -> Vec<Document> {
let payload: String = (0..value_bytes)
.map(|i| (b'a' + (i % 26) as u8) as char)
.collect();
(0..n_docs)
.map(|_| {
let mut builder = Document::builder();
for f in 0..n_fields {
builder = builder.add_text(format!("field_{f}"), payload.clone());
}
builder.build()
})
.collect()
}
fn assert_selection_probe(
reader: &dyn LexicalIndexReader,
n_fields: usize,
selected: &HashSet<&str>,
label: &str,
) {
let result = fetch_and_filter(reader, 0, selected);
for &name in selected.iter() {
assert!(
result.contains_key(name),
"{label}: selected field '{name}' must be present in fetch result"
);
}
for f in 0..n_fields {
let name = format!("field_{f}");
if selected.contains(name.as_str()) {
continue;
}
assert!(
!result.contains_key(&name),
"{label}: non-selected field '{name}' must be absent from fetch result"
);
}
}
fn bench_wide_narrow(c: &mut Criterion) {
let mut group = c.benchmark_group("store_fetch/wide_narrow");
const N_FIELDS: usize = 50;
const VALUE_BYTES: usize = 50;
let reader = MockStoreReader::new(build_documents(N_DOCS, N_FIELDS, VALUE_BYTES));
let all_field_names: Vec<String> = (0..N_FIELDS).map(|f| format!("field_{f}")).collect();
for &n_selected in &[1usize, 5, 50] {
let selected_owned: Vec<&str> = all_field_names
.iter()
.take(n_selected)
.map(String::as_str)
.collect();
let selected: HashSet<&str> = selected_owned.iter().copied().collect();
assert_selection_probe(&reader, N_FIELDS, &selected, "wide_narrow");
group.throughput(Throughput::Elements(TOP_K));
group.bench_with_input(
BenchmarkId::from_parameter(format!("selected_{n_selected}")),
&(),
|b, _| {
b.iter(|| {
for doc_id in 0..TOP_K {
let result = fetch_and_filter(
black_box(&reader),
black_box(doc_id),
black_box(&selected),
);
black_box(result);
}
});
},
);
}
group.finish();
}
fn bench_narrow_baseline(c: &mut Criterion) {
let mut group = c.benchmark_group("store_fetch/narrow_baseline");
const N_FIELDS: usize = 5;
const VALUE_BYTES: usize = 50;
let reader = MockStoreReader::new(build_documents(N_DOCS, N_FIELDS, VALUE_BYTES));
let all_field_names: Vec<String> = (0..N_FIELDS).map(|f| format!("field_{f}")).collect();
let selected_owned: Vec<&str> = all_field_names.iter().map(String::as_str).collect();
let selected: HashSet<&str> = selected_owned.iter().copied().collect();
assert_selection_probe(&reader, N_FIELDS, &selected, "narrow_baseline");
group.throughput(Throughput::Elements(TOP_K));
group.bench_function("all_5", |b| {
b.iter(|| {
for doc_id in 0..TOP_K {
let result =
fetch_and_filter(black_box(&reader), black_box(doc_id), black_box(&selected));
black_box(result);
}
});
});
group.finish();
}
fn bench_field_size_variance(c: &mut Criterion) {
let mut group = c.benchmark_group("store_fetch/field_size");
const N_FIELDS: usize = 5;
for &(label, value_bytes) in &[("small_100B", 100usize), ("large_100KB", 100 * 1024)] {
let reader = MockStoreReader::new(build_documents(N_DOCS, N_FIELDS, value_bytes));
let all_field_names: Vec<String> = (0..N_FIELDS).map(|f| format!("field_{f}")).collect();
let selected_owned: Vec<&str> =
all_field_names.iter().take(1).map(String::as_str).collect();
let selected: HashSet<&str> = selected_owned.iter().copied().collect();
assert_selection_probe(&reader, N_FIELDS, &selected, label);
group.throughput(Throughput::Elements(TOP_K));
group.bench_with_input(BenchmarkId::from_parameter(label), &(), |b, _| {
b.iter(|| {
for doc_id in 0..TOP_K {
let result = fetch_and_filter(
black_box(&reader),
black_box(doc_id),
black_box(&selected),
);
black_box(result);
}
});
});
}
group.finish();
}
fn bench_wide_narrow_subset(c: &mut Criterion) {
let mut group = c.benchmark_group("store_fetch/wide_narrow_subset");
const N_FIELDS: usize = 50;
const VALUE_BYTES: usize = 50;
let reader = MockStoreReader::new(build_documents(N_DOCS, N_FIELDS, VALUE_BYTES));
let all_field_names: Vec<String> = (0..N_FIELDS).map(|f| format!("field_{f}")).collect();
for &n_selected in &[1usize, 5, 50] {
let selected_owned: Vec<&str> = all_field_names
.iter()
.take(n_selected)
.map(String::as_str)
.collect();
let selected_set: HashSet<&str> = selected_owned.iter().copied().collect();
group.throughput(Throughput::Elements(TOP_K));
group.bench_with_input(
BenchmarkId::from_parameter(format!("full_clone/selected_{n_selected}")),
&(),
|b, _| {
b.iter(|| {
for doc_id in 0..TOP_K {
let result = fetch_and_filter(
black_box(&reader),
black_box(doc_id),
black_box(&selected_set),
);
black_box(result);
}
});
},
);
group.bench_with_input(
BenchmarkId::from_parameter(format!("subset/selected_{n_selected}")),
&(),
|b, _| {
b.iter(|| {
for doc_id in 0..TOP_K {
let result = fetch_subset(
black_box(&reader),
black_box(doc_id),
black_box(&selected_owned),
);
black_box(result);
}
});
},
);
}
group.finish();
}
criterion_group!(
benches,
bench_wide_narrow,
bench_wide_narrow_subset,
bench_narrow_baseline,
bench_field_size_variance,
);
criterion_main!(benches);