mirror of
https://github.com/quickwit-oss/tantivy.git
synced 2026-05-18 01:00:40 +00:00
* enhance and_or_queries bench * optimize unions for count/non-score, bitset fix for ARM Benchmarks run on M4 Max ``` single_field_only_union_5%_OR_1% count Avg: 0.1100ms (-17.46%) Median: 0.1079ms (-14.08%) [0.1045ms .. 0.1410ms] Output: 54_110 top10_inv_idx Avg: 0.1663ms (+0.79%) Median: 0.1660ms (+0.75%) [0.1634ms .. 0.1702ms] Output: 10 count+top10 Avg: 0.2639ms (-1.24%) Median: 0.2634ms (-0.31%) [0.2512ms .. 0.2813ms] Output: 54_110 top10_by_ff Avg: 0.2875ms (-8.67%) Median: 0.2852ms (-8.80%) [0.2737ms .. 0.3083ms] Output: 10 top10_by_2ff Avg: 0.3137ms (-5.79%) Median: 0.3128ms (-0.35%) [0.3044ms .. 0.3313ms] Output: 10 single_field_only_union_5%_OR_1%_OR_15% count Avg: 0.4122ms (-33.05%) Median: 0.4140ms (-32.20%) [0.3940ms .. 0.4341ms] Output: 181_663 top10_inv_idx Avg: 0.3999ms (+2.39%) Median: 0.3987ms (+2.02%) [0.3939ms .. 0.4160ms] Output: 10 count+top10 Avg: 0.8520ms (-8.63%) Median: 0.8516ms (-8.65%) [0.8413ms .. 0.8676ms] Output: 181_663 top10_by_ff Avg: 0.9694ms (-13.06%) Median: 0.9645ms (-13.77%) [0.9403ms .. 1.0122ms] Output: 10 top10_by_2ff Avg: 0.9880ms (-13.01%) Median: 0.9838ms (-13.59%) [0.9781ms .. 1.0306ms] Output: 10 single_field_only_union_5%_OR_30% count Avg: 0.7364ms (-33.11%) Median: 0.7347ms (-33.19%) [0.7233ms .. 0.7547ms] Output: 303_337 top10_inv_idx Avg: 0.8932ms (-0.89%) Median: 0.8919ms (-0.75%) [0.8861ms .. 0.9249ms] Output: 10 count+top10 Avg: 1.3611ms (-9.23%) Median: 1.3598ms (-9.39%) [1.3426ms .. 1.3891ms] Output: 303_337 top10_by_ff Avg: 1.6575ms (-18.64%) Median: 1.6224ms (-20.81%) [1.6051ms .. 1.7560ms] Output: 10 top10_by_2ff Avg: 1.6800ms (-16.24%) Median: 1.6769ms (-15.72%) [1.6661ms .. 1.7229ms] Output: 10 single_field_only_union_30%_OR_0.01% count Avg: 0.6471ms (-33.73%) Median: 0.6464ms (-33.46%) [0.6375ms .. 0.6604ms] Output: 270_268 top10_inv_idx Avg: 0.0338ms (-0.27%) Median: 0.0338ms (+0.11%) [0.0331ms .. 0.0351ms] Output: 10 count+top10 Avg: 1.2209ms (-9.27%) Median: 1.2207ms (-9.25%) [1.2158ms .. 1.2351ms] Output: 270_268 top10_by_ff Avg: 1.4808ms (-17.20%) Median: 1.4690ms (-17.91%) [1.4384ms .. 1.5553ms] Output: 10 top10_by_2ff Avg: 1.5011ms (-14.30%) Median: 1.4992ms (-13.88%) [1.4891ms .. 1.5320ms] Output: 10 multi_field_only_union_5%_OR_1% count Avg: 0.1196ms (-17.67%) Median: 0.1166ms (-14.83%) [0.1123ms .. 0.1462ms] Output: 60_183 top10_inv_idx Avg: 0.2356ms (-0.21%) Median: 0.2355ms (+0.23%) [0.2330ms .. 0.2406ms] Output: 10 count+top10 Avg: 0.2985ms (-5.06%) Median: 0.2957ms (-5.79%) [0.2875ms .. 0.3186ms] Output: 60_183 top10_by_ff Avg: 0.3102ms (-9.44%) Median: 0.3031ms (-11.09%) [0.2994ms .. 0.3324ms] Output: 10 top10_by_2ff Avg: 0.3435ms (-0.91%) Median: 0.3447ms (-0.62%) [0.3342ms .. 0.3530ms] Output: 10 multi_field_only_union_5%_OR_1%_OR_15% count Avg: 0.4465ms (-35.41%) Median: 0.4456ms (-36.25%) [0.4250ms .. 0.4936ms] Output: 201_114 top10_inv_idx Avg: 1.1542ms (+2.38%) Median: 1.1560ms (+2.96%) [1.1193ms .. 1.1912ms] Output: 10 count+top10 Avg: 0.9334ms (-8.89%) Median: 0.9330ms (-8.95%) [0.9191ms .. 0.9542ms] Output: 201_114 top10_by_ff Avg: 1.0590ms (-14.10%) Median: 1.0424ms (-15.08%) [1.0304ms .. 1.1174ms] Output: 10 top10_by_2ff Avg: 1.0779ms (-17.06%) Median: 1.0754ms (-17.40%) [1.0650ms .. 1.1155ms] Output: 10 multi_field_only_union_5%_OR_30% count Avg: 0.8137ms (-33.48%) Median: 0.7976ms (-34.84%) [0.7734ms .. 1.0855ms] Output: 335_682 top10_inv_idx Avg: 1.5108ms (+0.36%) Median: 1.4943ms (-0.72%) [1.4805ms .. 1.5865ms] Output: 10 count+top10 Avg: 1.4985ms (-9.75%) Median: 1.4936ms (-9.63%) [1.4784ms .. 1.5472ms] Output: 335_682 top10_by_ff Avg: 1.8531ms (-15.70%) Median: 1.8583ms (-16.30%) [1.7467ms .. 2.2297ms] Output: 10 top10_by_2ff Avg: 1.8735ms (-16.67%) Median: 1.8421ms (-18.05%) [1.8146ms .. 2.3650ms] Output: 10 multi_field_only_union_30%_OR_0.01% count Avg: 0.7020ms (-34.40%) Median: 0.7004ms (-34.05%) [0.6943ms .. 0.7156ms] Output: 300_315 top10_inv_idx Avg: 0.1445ms (-1.57%) Median: 0.1442ms (-1.35%) [0.1426ms .. 0.1478ms] Output: 10 count+top10 Avg: 1.3309ms (-9.84%) Median: 1.3284ms (-9.71%) [1.3234ms .. 1.3549ms] Output: 300_315 top10_by_ff Avg: 1.6152ms (-17.39%) Median: 1.6037ms (-18.72%) [1.5778ms .. 1.7227ms] Output: 10 top10_by_2ff Avg: 1.6479ms (-17.10%) Median: 1.6444ms (-15.46%) [1.6307ms .. 1.6901ms] Output: 10 ``` * add comment * fix comment * remove inline(never), bounds check
250 lines
8.0 KiB
Rust
250 lines
8.0 KiB
Rust
// Benchmarks boolean conjunction queries using binggan.
|
||
//
|
||
// What’s measured:
|
||
// - Or and And queries with varying selectivity (only `Term` queries for now on leafs)
|
||
// - Nested AND/OR combinations (on multiple fields)
|
||
// - No-scoring path using the Count collector (focus on iterator/skip performance)
|
||
// - Top-K retrieval (k=10) using the TopDocs collector
|
||
//
|
||
// Corpus model:
|
||
// - Synthetic docs; each token a/b/c is independently included per doc
|
||
// - If none of a/b/c are included, emit a neutral filler token to keep doc length similar
|
||
//
|
||
// Notes:
|
||
// - After optimization, when scoring is disabled Tantivy reads doc-only postings
|
||
// (IndexRecordOption::Basic), avoiding frequency decoding overhead.
|
||
// - This bench isolates boolean iteration speed and intersection/union cost.
|
||
// - Use `cargo bench --bench boolean_conjunction` to run.
|
||
|
||
use binggan::{black_box, BenchGroup, BenchRunner};
|
||
use rand::prelude::*;
|
||
use rand::rngs::StdRng;
|
||
use rand::SeedableRng;
|
||
use tantivy::collector::sort_key::SortByStaticFastValue;
|
||
use tantivy::collector::{Collector, Count, TopDocs};
|
||
use tantivy::query::QueryParser;
|
||
use tantivy::schema::{Schema, FAST, TEXT};
|
||
use tantivy::{doc, Index, Order, ReloadPolicy, Searcher};
|
||
|
||
#[derive(Clone)]
|
||
struct BenchIndex {
|
||
#[allow(dead_code)]
|
||
index: Index,
|
||
searcher: Searcher,
|
||
query_parser: QueryParser,
|
||
}
|
||
|
||
/// Build a single index containing both fields (title, body) and
|
||
/// return two BenchIndex views:
|
||
/// - single_field: QueryParser defaults to only "body"
|
||
/// - multi_field: QueryParser defaults to ["title", "body"]
|
||
fn build_index(num_docs: usize, terms: &[(&str, f32)]) -> (BenchIndex, BenchIndex) {
|
||
// Unified schema (two text fields)
|
||
let mut schema_builder = Schema::builder();
|
||
let f_title = schema_builder.add_text_field("title", TEXT);
|
||
let f_body = schema_builder.add_text_field("body", TEXT);
|
||
let f_score = schema_builder.add_u64_field("score", FAST);
|
||
let f_score2 = schema_builder.add_u64_field("score2", FAST);
|
||
let schema = schema_builder.build();
|
||
let index = Index::create_in_ram(schema.clone());
|
||
|
||
// Populate index with stable RNG for reproducibility.
|
||
let mut rng = StdRng::from_seed([7u8; 32]);
|
||
|
||
// Populate: spread each present token 90/10 to body/title
|
||
{
|
||
let mut writer = index.writer_with_num_threads(1, 500_000_000).unwrap();
|
||
for _ in 0..num_docs {
|
||
let score = rng.random_range(0u64..100u64);
|
||
let score2 = rng.random_range(0u64..100_000u64);
|
||
let mut title_tokens: Vec<&str> = Vec::new();
|
||
let mut body_tokens: Vec<&str> = Vec::new();
|
||
for &(tok, prob) in terms {
|
||
if rng.random_bool(prob as f64) {
|
||
if rng.random_bool(0.1) {
|
||
title_tokens.push(tok);
|
||
} else {
|
||
body_tokens.push(tok);
|
||
}
|
||
}
|
||
}
|
||
if title_tokens.is_empty() && body_tokens.is_empty() {
|
||
body_tokens.push("z");
|
||
}
|
||
writer
|
||
.add_document(doc!(
|
||
f_title=>title_tokens.join(" "),
|
||
f_body=>body_tokens.join(" "),
|
||
f_score=>score,
|
||
f_score2=>score2,
|
||
))
|
||
.unwrap();
|
||
}
|
||
writer.commit().unwrap();
|
||
}
|
||
|
||
// Prepare reader/searcher once.
|
||
let reader = index
|
||
.reader_builder()
|
||
.reload_policy(ReloadPolicy::Manual)
|
||
.try_into()
|
||
.unwrap();
|
||
let searcher = reader.searcher();
|
||
|
||
// Build two query parsers with different default fields.
|
||
let qp_single = QueryParser::for_index(&index, vec![f_body]);
|
||
let qp_multi = QueryParser::for_index(&index, vec![f_title, f_body]);
|
||
|
||
let only_title = BenchIndex {
|
||
index: index.clone(),
|
||
searcher: searcher.clone(),
|
||
query_parser: qp_single,
|
||
};
|
||
let title_and_body = BenchIndex {
|
||
index,
|
||
searcher,
|
||
query_parser: qp_multi,
|
||
};
|
||
(only_title, title_and_body)
|
||
}
|
||
|
||
fn format_pct(p: f32) -> String {
|
||
let pct = (p as f64) * 100.0;
|
||
let rounded = (pct * 1_000_000.0).round() / 1_000_000.0;
|
||
if rounded.fract() <= 0.001 {
|
||
format!("{}%", rounded as u64)
|
||
} else {
|
||
format!("{}%", rounded)
|
||
}
|
||
}
|
||
|
||
fn query_label(query_str: &str, term_pcts: &[(&str, String)]) -> String {
|
||
let mut label = query_str.to_string();
|
||
for (term, pct) in term_pcts {
|
||
label = label.replace(term, pct);
|
||
}
|
||
label.replace(' ', "_")
|
||
}
|
||
|
||
fn main() {
|
||
// terms with varying selectivity, ordered from rarest to most common.
|
||
// With 1M docs, we expect:
|
||
// a: 0.01% (100), b: 1% (10k), c: 5% (50k), d: 15% (150k), e: 30% (300k)
|
||
let num_docs = 1_000_000;
|
||
let terms: &[(&str, f32)] = &[
|
||
("a", 0.0001),
|
||
("b", 0.01),
|
||
("c", 0.05),
|
||
("d", 0.15),
|
||
("e", 0.30),
|
||
];
|
||
|
||
let queries: &[(&str, &[&str])] = &[
|
||
(
|
||
"only_union",
|
||
&["c OR b", "c OR b OR d", "c OR e", "e OR a"] as &[&str],
|
||
),
|
||
(
|
||
"only_intersection",
|
||
&["+c +b", "+c +b +d", "+c +e", "+e +a"] as &[&str],
|
||
),
|
||
(
|
||
"union_intersection",
|
||
&["+c +(b OR d)", "+e +(c OR a)", "+(c OR b) +(d OR e)"] as &[&str],
|
||
),
|
||
];
|
||
|
||
let mut runner = BenchRunner::new();
|
||
let (only_title, title_and_body) = build_index(num_docs, terms);
|
||
let term_pcts: Vec<(&str, String)> = terms
|
||
.iter()
|
||
.map(|&(term, p)| (term, format_pct(p)))
|
||
.collect();
|
||
|
||
for (view_name, bench_index) in [
|
||
("single_field", only_title),
|
||
("multi_field", title_and_body),
|
||
] {
|
||
for (category_name, category_queries) in queries {
|
||
for query_str in *category_queries {
|
||
let mut group = runner.new_group();
|
||
let query_label = query_label(query_str, &term_pcts);
|
||
group.set_name(format!("{}_{}_{}", view_name, category_name, query_label));
|
||
add_bench_task(&mut group, &bench_index, query_str, Count, "count");
|
||
add_bench_task(
|
||
&mut group,
|
||
&bench_index,
|
||
query_str,
|
||
TopDocs::with_limit(10).order_by_score(),
|
||
"top10_inv_idx",
|
||
);
|
||
add_bench_task(
|
||
&mut group,
|
||
&bench_index,
|
||
query_str,
|
||
(Count, TopDocs::with_limit(10).order_by_score()),
|
||
"count+top10",
|
||
);
|
||
|
||
add_bench_task(
|
||
&mut group,
|
||
&bench_index,
|
||
query_str,
|
||
TopDocs::with_limit(10).order_by_fast_field::<u64>("score", Order::Asc),
|
||
"top10_by_ff",
|
||
);
|
||
add_bench_task(
|
||
&mut group,
|
||
&bench_index,
|
||
query_str,
|
||
TopDocs::with_limit(10).order_by((
|
||
SortByStaticFastValue::<u64>::for_field("score"),
|
||
SortByStaticFastValue::<u64>::for_field("score2"),
|
||
)),
|
||
"top10_by_2ff",
|
||
);
|
||
|
||
group.run();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
trait FruitCount {
|
||
fn count(&self) -> usize;
|
||
}
|
||
|
||
impl FruitCount for usize {
|
||
fn count(&self) -> usize {
|
||
*self
|
||
}
|
||
}
|
||
|
||
impl<T> FruitCount for Vec<T> {
|
||
fn count(&self) -> usize {
|
||
self.len()
|
||
}
|
||
}
|
||
|
||
impl<A: FruitCount, B> FruitCount for (A, B) {
|
||
fn count(&self) -> usize {
|
||
self.0.count()
|
||
}
|
||
}
|
||
|
||
fn add_bench_task<C: Collector + 'static>(
|
||
bench_group: &mut BenchGroup,
|
||
bench_index: &BenchIndex,
|
||
query_str: &str,
|
||
collector: C,
|
||
collector_name: &str,
|
||
) where
|
||
C::Fruit: FruitCount,
|
||
{
|
||
let query = bench_index.query_parser.parse_query(query_str).unwrap();
|
||
let searcher = bench_index.searcher.clone();
|
||
bench_group.register(collector_name.to_string(), move |_| {
|
||
black_box(searcher.search(&query, &collector).unwrap().count())
|
||
});
|
||
}
|