mod common;
use std::any::Any;
use std::hint::black_box;
use std::sync::Arc;
use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use laurus::lexical::core::field::FieldValue;
use laurus::lexical::index::structures::bkd_tree::BKDTree;
use laurus::lexical::reader::{FieldStats, LexicalIndexReader, PostingIterator, ReaderTermInfo};
use laurus::lexical::search::features::facet::{FacetCollector, FacetConfig};
use laurus::{Document, Result as LaurusResult};
const FACET_VALUES_PER_FIELD: usize = 50;
const MAX_DOCS: usize = 100_000;
#[derive(Debug)]
struct MockFacetReader {
documents: Vec<Document>,
}
impl MockFacetReader {
fn new(documents: Vec<Document>) -> Self {
Self { documents }
}
}
impl LexicalIndexReader for MockFacetReader {
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 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 has_doc_values(&self, _field: &str) -> bool {
true
}
fn get_doc_value(&self, field: &str, doc_id: u64) -> LaurusResult<Option<FieldValue>> {
Ok(self
.documents
.get(doc_id as usize)
.and_then(|d| d.get(field).cloned()))
}
}
fn payload(i: usize) -> String {
format!("doc {i}: {}", "lorem ipsum dolor sit amet ".repeat(10))
}
fn build_flat_documents(n: usize) -> Vec<Document> {
(0..n)
.map(|i| {
let value = format!("value_{}", i % FACET_VALUES_PER_FIELD);
Document::builder()
.add_text("field_a", value)
.add_text("title", format!("Title {i}"))
.add_text("body", payload(i))
.build()
})
.collect()
}
fn build_multi_field_documents(n: usize) -> Vec<Document> {
(0..n)
.map(|i| {
let v_a = format!("a_{}", i % FACET_VALUES_PER_FIELD);
let v_b = format!("b_{}", (i / 2) % FACET_VALUES_PER_FIELD);
let v_c = format!("c_{}", (i / 3) % FACET_VALUES_PER_FIELD);
Document::builder()
.add_text("field_a", v_a)
.add_text("field_b", v_b)
.add_text("field_c", v_c)
.add_text("title", format!("Title {i}"))
.add_text("body", payload(i))
.build()
})
.collect()
}
fn build_hierarchical_documents(n: usize) -> Vec<Document> {
(0..n)
.map(|i| {
let leaf_idx = i % FACET_VALUES_PER_FIELD;
let region = leaf_idx % 5;
let country = (leaf_idx / 5) % 5;
let state = (leaf_idx / 25) % 5;
let city = leaf_idx % 5;
let path = format!("r{region}/c{country}/s{state}/v{city}");
Document::builder()
.add_text("hier_field", path)
.add_text("title", format!("Title {i}"))
.add_text("body", payload(i))
.build()
})
.collect()
}
fn make_reader(documents: Vec<Document>) -> Arc<MockFacetReader> {
Arc::new(MockFacetReader::new(documents))
}
fn assert_collector_probe(fields: &[String], reader: &MockFacetReader, n: usize, label: &str) {
let mut probe = FacetCollector::new(FacetConfig::default(), fields.to_vec());
for doc_id in 0..n as u64 {
probe
.collect_doc(doc_id, reader)
.expect("facet probe must not error");
}
let results = probe.finalize().expect("facet probe finalize must succeed");
let total_count: u64 = fields
.iter()
.filter_map(|f| results.get_field_facets(f))
.flat_map(|counts| counts.iter().map(|c| c.count))
.sum();
let lower_bound = (n as u64) * (fields.len() as u64);
assert!(
total_count >= lower_bound,
"{label}: expected sum(facet_count) >= {lower_bound}, got {total_count} (each doc must contribute at least one increment per facet field)"
);
}
fn bench_flat_single_field(c: &mut Criterion) {
let mut group = c.benchmark_group("facet/flat_single");
let reader = make_reader(build_flat_documents(MAX_DOCS));
let fields = vec!["field_a".to_string()];
for &n in &[1000usize, 10_000, 100_000] {
assert_collector_probe(&fields, &reader, n, "flat_single");
group.throughput(Throughput::Elements(n as u64));
group.bench_with_input(BenchmarkId::from_parameter(n), &n, |b, &n| {
b.iter_batched(
|| FacetCollector::new(FacetConfig::default(), fields.clone()),
|mut collector| {
for doc_id in 0..n as u64 {
collector
.collect_doc(black_box(doc_id), reader.as_ref())
.unwrap();
}
black_box(collector);
},
BatchSize::SmallInput,
);
});
}
group.finish();
}
fn bench_multi_field(c: &mut Criterion) {
let mut group = c.benchmark_group("facet/multi_field");
let reader = make_reader(build_multi_field_documents(MAX_DOCS));
let fields = vec![
"field_a".to_string(),
"field_b".to_string(),
"field_c".to_string(),
];
for &n in &[1000usize, 10_000, 100_000] {
assert_collector_probe(&fields, &reader, n, "multi_field");
group.throughput(Throughput::Elements(n as u64));
group.bench_with_input(BenchmarkId::from_parameter(n), &n, |b, &n| {
b.iter_batched(
|| FacetCollector::new(FacetConfig::default(), fields.clone()),
|mut collector| {
for doc_id in 0..n as u64 {
collector
.collect_doc(black_box(doc_id), reader.as_ref())
.unwrap();
}
black_box(collector);
},
BatchSize::SmallInput,
);
});
}
group.finish();
}
fn bench_hierarchical(c: &mut Criterion) {
let mut group = c.benchmark_group("facet/hierarchical");
let reader = make_reader(build_hierarchical_documents(MAX_DOCS));
let fields = vec!["hier_field".to_string()];
for &n in &[1000usize, 10_000, 100_000] {
assert_collector_probe(&fields, &reader, n, "hierarchical");
group.throughput(Throughput::Elements(n as u64));
group.bench_with_input(BenchmarkId::from_parameter(n), &n, |b, &n| {
b.iter_batched(
|| FacetCollector::new(FacetConfig::default(), fields.clone()),
|mut collector| {
for doc_id in 0..n as u64 {
collector
.collect_doc(black_box(doc_id), reader.as_ref())
.unwrap();
}
black_box(collector);
},
BatchSize::SmallInput,
);
});
}
group.finish();
}
criterion_group!(
benches,
bench_flat_single_field,
bench_multi_field,
bench_hierarchical,
);
criterion_main!(benches);