Skip to content

Commit 80ed545

Browse files
committed
better scan heuristic
1 parent 1f8a740 commit 80ed545

2 files changed

Lines changed: 243 additions & 90 deletions

File tree

benches/bool_queries_with_range.rs

Lines changed: 66 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ const CLUSTERED_SIGNAL_FRAC: f64 = 0.3;
8383
enum TermLayout {
8484
/// Each doc is independently "a" with probability `p_a`: the "a" docs are scattered uniformly.
8585
Uniform,
86-
/// The "a" docs arrive in bursts (see [`TitleField::next_token`]). Real attributes rarely sprinkle
87-
/// evenly — a log level spikes, a topic is ingested together — so their matching docs cluster.
88-
/// Bursts are statistically denser regions, not solid runs, and a background floor keeps some
89-
/// "a" docs scattered in between.
86+
/// The "a" docs arrive in bursts (see [`TitleField::next_token`]). Real attributes rarely
87+
/// sprinkle evenly — a log level spikes, a topic is ingested together — so their matching
88+
/// docs cluster. Bursts are statistically denser regions, not solid runs, and a background
89+
/// floor keeps some "a" docs scattered in between.
9090
Clustered,
9191
}
9292

@@ -100,9 +100,10 @@ impl TermLayout {
100100
}
101101
}
102102

103-
/// Derives a schema-safe title field name from a term probability and layout, e.g. `(0.001, Uniform)`
104-
/// -> `title_0_1pct` and `(0.001, Clustered)` -> `title_clustered_0_1pct`. A title field is populated
105-
/// with token "a" for a `p_a` fraction of documents, so `<name>:a` matches ~`p_a` of them.
103+
/// Derives a schema-safe title field name from a term probability and layout, e.g. `(0.001,
104+
/// Uniform)` -> `title_0_1pct` and `(0.001, Clustered)` -> `title_clustered_0_1pct`. A title field
105+
/// is populated with token "a" for a `p_a` fraction of documents, so `<name>:a` matches ~`p_a` of
106+
/// them.
106107
fn title_field_name(p_a: f64, layout: TermLayout) -> String {
107108
let pct = format_pct(p_a).replace('.', "_").replace('%', "pct");
108109
match layout {
@@ -131,19 +132,22 @@ struct TitleField {
131132
/// Latent-field value a doc must exceed to be "a". Set to `Φ⁻¹(1 - p_a)` for the unit-variance
132133
/// field below, so `P(exceed) = p_a` and the marginal is `p_a` however bursty the drift is.
133134
threshold: f64,
134-
/// Amplitude per octave, so the summed drift carries `CLUSTERED_SIGNAL_FRAC` of the unit variance.
135+
/// Amplitude per octave, so the summed drift carries `CLUSTERED_SIGNAL_FRAC` of the unit
136+
/// variance.
135137
octave_weight: f64,
136-
/// s.d. of the per-doc noise carrying the remaining `1 - CLUSTERED_SIGNAL_FRAC` of the variance.
138+
/// s.d. of the per-doc noise carrying the remaining `1 - CLUSTERED_SIGNAL_FRAC` of the
139+
/// variance.
137140
noise_sd: f64,
138141
octaves: Vec<Ar1Octave>,
139142
}
140143

141144
impl TitleField {
142145
fn new(p_a: f64, layout: TermLayout, field: Field, num_docs: usize) -> Self {
143-
// Octave j has correlation length CLUSTERED_DRIFT_SCALE * num_docs / 4^j; rho = e^(-1/len) is
144-
// the per-doc retention producing that length. Splitting the signal variance equally across
145-
// octaves (weight = sqrt(signal/octaves)) plus noise variance (1 - signal) makes the latent
146-
// field unit-variance, so the threshold is a plain standard-normal quantile.
146+
// Octave j has correlation length CLUSTERED_DRIFT_SCALE * num_docs / 4^j; rho = e^(-1/len)
147+
// is the per-doc retention producing that length. Splitting the signal variance
148+
// equally across octaves (weight = sqrt(signal/octaves)) plus noise variance (1 -
149+
// signal) makes the latent field unit-variance, so the threshold is a plain
150+
// standard-normal quantile.
147151
let octaves = (0..CLUSTERED_OCTAVES)
148152
.map(|j| {
149153
let len = (CLUSTERED_DRIFT_SCALE * num_docs as f64 / 4f64.powi(j as i32)).max(1.0);
@@ -165,10 +169,11 @@ impl TitleField {
165169
}
166170

167171
/// Draws this document's token. Uniform: independently "a" with probability `p_a`. Clustered:
168-
/// advance the drift, add per-doc noise to get a unit-variance latent value, and emit "a" when it
169-
/// clears `threshold`. Because the drift moves slowly, neighbouring docs land on the same side of
170-
/// the threshold together — that is the clustering; the noise softens the boundary and scatters a
171-
/// background of "a" docs through the cold stretches (and "b" docs through the hot ones).
172+
/// advance the drift, add per-doc noise to get a unit-variance latent value, and emit "a" when
173+
/// it clears `threshold`. Because the drift moves slowly, neighbouring docs land on the
174+
/// same side of the threshold together — that is the clustering; the noise softens the
175+
/// boundary and scatters a background of "a" docs through the cold stretches (and "b" docs
176+
/// through the hot ones).
172177
fn next_token(&mut self, rng: &mut StdRng) -> &'static str {
173178
match self.layout {
174179
TermLayout::Uniform => {
@@ -202,25 +207,38 @@ fn standard_normal(rng: &mut StdRng) -> f64 {
202207
(-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
203208
}
204209

205-
/// Inverse standard-normal CDF (the probit / quantile function) via Acklam's rational approximation,
206-
/// accurate to ~1e-9 over `p ∈ (0, 1)` — enough to place the clustered-layout threshold. Only called
207-
/// once per field, so speed is irrelevant.
210+
/// Inverse standard-normal CDF (the probit / quantile function) via Acklam's rational
211+
/// approximation, accurate to ~1e-9 over `p ∈ (0, 1)` — enough to place the clustered-layout
212+
/// threshold. Only called once per field, so speed is irrelevant.
208213
fn inverse_normal_cdf(p: f64) -> f64 {
209214
// Coefficients for the central and tail regions of the approximation.
210215
const A: [f64; 6] = [
211-
-3.969683028665376e+01, 2.209460984245205e+02, -2.759285104469687e+02,
212-
1.383577518672690e+02, -3.066479806614716e+01, 2.506628277459239e+00,
216+
-3.969683028665376e+01,
217+
2.209460984245205e+02,
218+
-2.759285104469687e+02,
219+
1.383577518672690e+02,
220+
-3.066479806614716e+01,
221+
2.506628277459239e+00,
213222
];
214223
const B: [f64; 5] = [
215-
-5.447609879822406e+01, 1.615858368580409e+02, -1.556989798598866e+02,
216-
6.680131188771972e+01, -1.328068155288572e+01,
224+
-5.447609879822406e+01,
225+
1.615858368580409e+02,
226+
-1.556989798598866e+02,
227+
6.680131188771972e+01,
228+
-1.328068155288572e+01,
217229
];
218230
const C: [f64; 6] = [
219-
-7.784894002430293e-03, -3.223964580411365e-01, -2.400758277161838e+00,
220-
-2.549732539343734e+00, 4.374664141464968e+00, 2.938163982698783e+00,
231+
-7.784894002430293e-03,
232+
-3.223964580411365e-01,
233+
-2.400758277161838e+00,
234+
-2.549732539343734e+00,
235+
4.374664141464968e+00,
236+
2.938163982698783e+00,
221237
];
222238
const D: [f64; 4] = [
223-
7.784695709041462e-03, 3.224671290700398e-01, 2.445134137142996e+00,
239+
7.784695709041462e-03,
240+
3.224671290700398e-01,
241+
2.445134137142996e+00,
224242
3.754408661907416e+00,
225243
];
226244
const P_LOW: f64 = 0.02425;
@@ -283,7 +301,8 @@ impl NumericFields {
283301
/// rebuilding a near-identical index.
284302
fn build_shared_index(num_docs: usize, term_specs: &[(f64, TermLayout)]) -> BenchIndex {
285303
let mut schema_builder = Schema::builder();
286-
// One title field per distinct (probability, layout); querying `<name>:a` matches ~p of documents.
304+
// One title field per distinct (probability, layout); querying `<name>:a` matches ~p of
305+
// documents.
287306
let mut title_fields: Vec<TitleField> = Vec::new();
288307
for &(p_a, layout) in term_specs {
289308
if title_fields
@@ -362,9 +381,10 @@ fn main() {
362381
(0.7, 0.1),
363382
];
364383

365-
// Each scenario is run under both term layouts: `uniform` ("a" scattered evenly) and `clustered`
366-
// ("a" arriving in organic bursts, same marginal `p(a)`). Real corpora look like the latter, and
367-
// where the matching docs land is exactly what an intersection's block-skipping sees.
384+
// Each scenario is run under both term layouts: `uniform` ("a" scattered evenly) and
385+
// `clustered` ("a" arriving in organic bursts, same marginal `p(a)`). Real corpora look
386+
// like the latter, and where the matching docs land is exactly what an intersection's
387+
// block-skipping sees.
368388
let layouts = [TermLayout::Uniform, TermLayout::Clustered];
369389

370390
// Build a single shared corpus with a title field for each (probability, layout).
@@ -382,9 +402,9 @@ fn main() {
382402
term_specs.push((regression_term_p, TermLayout::Uniform));
383403
let bench_index = build_shared_index(5_000_000, &term_specs);
384404

385-
// Precompute each group's name and queries once; every collector runner reuses them. Layouts are
386-
// interleaved per scenario so a scenario's `uniform` and `clustered` groups sit next to each
387-
// other in the output for easy comparison.
405+
// Precompute each group's name and queries once; every collector runner reuses them. Layouts
406+
// are interleaved per scenario so a scenario's `uniform` and `clustered` groups sit next to
407+
// each other in the output for easy comparison.
388408
let mut prepared: Vec<(String, Vec<Query3>)> = Vec::new();
389409
for &(p_a, range_sel) in &scenarios {
390410
for &layout in &layouts {
@@ -394,7 +414,10 @@ fn main() {
394414
layout.tag(),
395415
format_pct(range_sel)
396416
);
397-
prepared.push((group_name, build_term_and_range_queries(p_a, range_sel, layout)));
417+
prepared.push((
418+
group_name,
419+
build_term_and_range_queries(p_a, range_sel, layout),
420+
));
398421
}
399422
}
400423

@@ -434,9 +457,13 @@ fn main() {
434457

435458
// A separate runner per collector type: the collector heads the output section (via the runner
436459
// name) instead of being repeated in every task name.
437-
run_collector(BenchRunner::with_name("count"), &bench_index, &prepared, false, |_| {
438-
Count
439-
});
460+
run_collector(
461+
BenchRunner::with_name("count"),
462+
&bench_index,
463+
&prepared,
464+
false,
465+
|_| Count,
466+
);
440467
run_collector(
441468
BenchRunner::with_name("cnt+top_score"),
442469
&bench_index,
@@ -473,8 +500,7 @@ fn build_term_and_range_queries(p_a: f64, range_sel: f64, layout: TermLayout) ->
473500
.iter()
474501
.map(|&(field_name, dist)| {
475502
let (low, high) = dist.bounds_for(range_sel);
476-
let query_str =
477-
format!("{}:a AND {}:[{} TO {}]", title_field, field_name, low, high);
503+
let query_str = format!("{}:a AND {}:[{} TO {}]", title_field, field_name, low, high);
478504
let label = format!("a_AND_{}", field_name);
479505
(query_str, label, field_name.to_string())
480506
})

0 commit comments

Comments
 (0)