2026-09-25 14:58:51 -04:00
|
|
|
//! Query latency on an existing index, the counterweight to `indexprobe`:
|
|
|
|
|
//! every FTS write-side knob buys indexing time by leaving more segments
|
|
|
|
|
//! behind, and each segment is a b-tree a query visits.
|
2026-08-21 02:05:23 -04:00
|
|
|
//!
|
|
|
|
|
//! ```text
|
|
|
|
|
//! cargo build -p quicksearch-core --example searchtime --release
|
|
|
|
|
//! ./target/release/examples/searchtime /path/to/index.db
|
2026-09-26 16:29:36 -04:00
|
|
|
//! ./target/release/examples/searchtime /path/to/index.db serialize "fn main" ~recieve
|
2026-08-21 02:05:23 -04:00
|
|
|
//! ```
|
|
|
|
|
//!
|
2026-09-26 16:29:36 -04:00
|
|
|
//! Queries after the path replace the built-in set, for a corpus whose
|
|
|
|
|
//! vocabulary the defaults do not hit; a leading `~` runs one fuzzy.
|
|
|
|
|
//!
|
2026-08-23 00:33:53 -04:00
|
|
|
//! Queries run against one held connection, as the worker holds one; each
|
|
|
|
|
//! is timed best-of-N so a scheduling hiccup is not the headline.
|
|
|
|
|
|
|
|
|
|
mod common;
|
2026-08-21 02:05:23 -04:00
|
|
|
|
|
|
|
|
use std::path::PathBuf;
|
2026-09-25 14:58:51 -04:00
|
|
|
use std::time::Duration;
|
2026-08-21 02:05:23 -04:00
|
|
|
|
2026-09-25 14:58:51 -04:00
|
|
|
use quicksearch_core::search::SearchOptions;
|
|
|
|
|
use quicksearch_core::testutil;
|
2026-08-21 02:05:23 -04:00
|
|
|
|
|
|
|
|
const RUNS: u32 = 5;
|
|
|
|
|
|
2026-08-23 00:33:53 -04:00
|
|
|
/// The content queries go through however many segments the write side
|
2026-09-26 16:29:36 -04:00
|
|
|
/// left behind; the filename query is the control and must not move. The
|
|
|
|
|
/// filter rows price `modified:` and `mime:` against that control: neither
|
|
|
|
|
/// column has an index, so they must cost no more than the scan it already is.
|
2026-08-21 02:05:23 -04:00
|
|
|
const QUERIES: &[(&str, bool, &str)] = &[
|
|
|
|
|
("filename (control)", false, "doc42"),
|
2026-09-26 16:29:36 -04:00
|
|
|
("filename + modified", false, "doc42 modified:>=2025-06-01"),
|
|
|
|
|
("filename + one day", false, "doc42 modified:2025-06-01"),
|
|
|
|
|
("filename + mime", false, "doc42 mime:application/pdf"),
|
2026-08-21 02:05:23 -04:00
|
|
|
("content, common", false, "mountain"),
|
|
|
|
|
("content, rare", false, "quartzite"),
|
|
|
|
|
("content, two words", false, "ocean forest"),
|
|
|
|
|
("fuzzy content", true, "mountian"),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
|
let db = PathBuf::from(
|
|
|
|
|
std::env::args()
|
|
|
|
|
.nth(1)
|
|
|
|
|
.expect("usage: searchtime <index.db>"),
|
|
|
|
|
);
|
|
|
|
|
let conn = quicksearch_core::db::open::open_search_reader(&db.to_string_lossy())
|
|
|
|
|
.expect("open the index");
|
2026-09-25 14:58:51 -04:00
|
|
|
// `QSB_CACHE_KIB` overrides the cache ceiling, to tell a working set too
|
|
|
|
|
// big for the cache from an index too big.
|
2026-08-21 02:05:23 -04:00
|
|
|
if let Ok(kib) = std::env::var("QSB_CACHE_KIB") {
|
|
|
|
|
conn.execute_batch(&format!("PRAGMA cache_size = -{};", kib.trim()))
|
|
|
|
|
.expect("set cache_size");
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-25 14:58:51 -04:00
|
|
|
let segments = common::count(&conn, "SELECT COUNT(*) FROM searchabletext_idx");
|
|
|
|
|
let rows = common::count(&conn, "SELECT COUNT(*) FROM files");
|
2026-08-21 02:05:23 -04:00
|
|
|
println!(
|
|
|
|
|
"{} ({} rows, {} segment-index entries)",
|
|
|
|
|
db.display(),
|
|
|
|
|
rows,
|
|
|
|
|
segments
|
|
|
|
|
);
|
|
|
|
|
println!("{:<22} {:>10} {:>8}", "query", "best", "hits");
|
|
|
|
|
|
2026-09-26 16:29:36 -04:00
|
|
|
let custom: Vec<(String, bool, String)> = std::env::args()
|
|
|
|
|
.skip(2)
|
|
|
|
|
.map(|q| match q.strip_prefix('~') {
|
|
|
|
|
Some(term) => (q.clone(), true, term.to_string()),
|
|
|
|
|
None => (q.clone(), false, q),
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
let queries: Vec<(&str, bool, &str)> = if custom.is_empty() {
|
|
|
|
|
QUERIES.to_vec()
|
|
|
|
|
} else {
|
|
|
|
|
custom
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(label, fuzzy, query)| (label.as_str(), *fuzzy, query.as_str()))
|
|
|
|
|
.collect()
|
|
|
|
|
};
|
|
|
|
|
|
2026-08-21 02:05:23 -04:00
|
|
|
let mut total = Duration::ZERO;
|
2026-09-26 16:29:36 -04:00
|
|
|
for &(label, fuzzy, query) in &queries {
|
2026-08-23 00:33:53 -04:00
|
|
|
// The display limit makes this unfair: `scan_pass` stops when the
|
|
|
|
|
// limit fills, and candidates stream in insertion order, so which
|
2026-09-25 14:58:51 -04:00
|
|
|
// documents drew low ids changes bytes read 13x. A `QSB_LIMIT` past
|
|
|
|
|
// the corpus makes every index do equal work.
|
2026-08-21 02:05:23 -04:00
|
|
|
let limit = std::env::var("QSB_LIMIT")
|
|
|
|
|
.ok()
|
|
|
|
|
.and_then(|v| v.parse().ok())
|
|
|
|
|
.unwrap_or(1000);
|
|
|
|
|
let options = SearchOptions {
|
2026-09-26 16:29:36 -04:00
|
|
|
fuzzy,
|
2026-08-21 02:05:23 -04:00
|
|
|
limit,
|
|
|
|
|
..SearchOptions::default()
|
|
|
|
|
};
|
|
|
|
|
let mut best = Duration::MAX;
|
|
|
|
|
let mut hits = 0usize;
|
2026-08-23 00:33:53 -04:00
|
|
|
// Time varying while `rchar` does not means waiting on the disk.
|
|
|
|
|
let io_before = common::Io::read();
|
2026-08-21 02:05:23 -04:00
|
|
|
for _ in 0..RUNS {
|
2026-09-25 14:58:51 -04:00
|
|
|
let (took, count) = testutil::run_cascade(&conn, query, &options);
|
|
|
|
|
best = best.min(took);
|
2026-08-21 02:05:23 -04:00
|
|
|
hits = count;
|
|
|
|
|
}
|
|
|
|
|
total += best;
|
2026-08-23 00:33:53 -04:00
|
|
|
let io = common::Io::read().since(&io_before);
|
2026-08-21 02:05:23 -04:00
|
|
|
println!(
|
|
|
|
|
"{:<22} {:>10.1?} {:>8} disk-read {:>8.1} MiB rchar {:>8.1} MiB (over {} runs)",
|
|
|
|
|
label,
|
|
|
|
|
best,
|
|
|
|
|
hits,
|
2026-08-23 00:33:53 -04:00
|
|
|
io.read_bytes as f64 / 1048576.0,
|
|
|
|
|
io.rchar as f64 / 1048576.0,
|
2026-08-21 02:05:23 -04:00
|
|
|
RUNS,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
println!("{:<22} {:>10.1?}", "TOTAL", total);
|
|
|
|
|
}
|