use anyhow::{Context, Result};
use bgr::BugguDB; use clap::Parser;
use std::fs::File;
use std::io::{self, BufReader};
use std::path::PathBuf;
use std::time::Instant;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(name = "FILE")]
files: Vec<PathBuf>,
#[arg(short, long)]
query: Option<String>,
#[arg(short, long, default_value = "20")]
limit: usize,
#[arg(short, long)]
timing: bool,
}
fn main() -> Result<()> {
let args = Args::parse();
let mut db = BugguDB::new();
let start = Instant::now();
let mut total_lines = 0;
if args.files.is_empty() {
println!("Loading from stdin...");
let stdin = io::stdin();
let reader = stdin.lock();
total_lines += db.ingest_from_reader(reader)
.context("Failed to read from stdin")?;
} else {
for path in &args.files {
println!("Loading from {}...", path.display());
let file = File::open(path)
.with_context(|| format!("Failed to open file: {}", path.display()))?;
let reader = BufReader::new(file);
total_lines += db.ingest_from_reader(reader)
.with_context(|| format!("Failed to read from file: {}", path.display()))?;
}
}
let load_duration = start.elapsed();
println!(
"Loaded {} lines in {:.2?} ({:.2} lines/sec)",
total_lines,
load_duration,
total_lines as f64 / load_duration.as_secs_f64()
);
if let Some(query) = args.query {
run_query(&db, &query, args.limit, args.timing);
} else {
run_interactive_session(db, args.limit, args.timing)?;
}
Ok(())
}
fn run_query(db: &BugguDB, query: &str, limit: usize, show_timing: bool) {
let start = Instant::now();
if query.trim() == "help" || query.trim() == "?" {
print_help();
return;
}
let is_json_query = query.contains("json:");
if is_json_query {
let results = db.query_with_meta(query);
let duration = start.elapsed();
println!("Query: {}", query);
println!("Found {} results in {:.2?}", results.len(), duration);
if show_timing && !results.is_empty() {
println!("Average: {:.2} µs per document",
duration.as_micros() as f64 / db.docs.len() as f64);
}
for (i, (id, content, level, service)) in results.iter().take(limit).enumerate() {
print!("{}. [ID:{}]", i + 1, id);
if let Some(lvl) = level {
print!(" [{}]", lvl);
}
if let Some(svc) = service {
print!(" [{}]", svc);
}
println!(" {}", content);
}
if results.len() > limit {
println!("... and {} more matches", results.len() - limit);
}
} else {
let results = db.query_content(query);
let duration = start.elapsed();
println!("Query: {}", query);
println!("Found {} results in {:.2?}", results.len(), duration);
if show_timing && !results.is_empty() {
println!("Average: {:.2} µs per document",
duration.as_micros() as f64 / db.docs.len() as f64);
}
for (i, result) in results.iter().take(limit).enumerate() {
println!("{}. {}", i + 1, result);
}
if results.len() > limit {
println!("... and {} more matches", results.len() - limit);
}
}
}
fn print_help() {
println!("\nBugguDB Search Commands:");
println!("------------------------");
println!(" term Simple text search");
println!(" \"exact phrase\" Phrase search (quoted)");
println!(" level:ERROR Field search");
println!(" term1 AND term2 Logical AND (default between terms)");
println!(" term1 OR term2 Logical OR");
println!(" term NOT other Exclude matches containing 'other'");
println!(" regex:pattern\\d+ Regular expression search");
println!(" json:user.name JSON field existence");
println!(" json:user.active=true JSON field value matching");
println!(" json:metrics.cpu>=90 JSON numeric comparison");
println!("\nInteractive Commands:");
println!("--------------------");
println!(" help, ? Show this help message");
println!(" limit N Set result limit to N");
println!(" timing on|off Toggle detailed timing info");
println!(" quit, exit, q Exit the program");
println!("");
}
fn run_interactive_session(db: BugguDB, mut limit: usize, mut show_timing: bool) -> Result<()> {
let mut rl = rustyline::Editor::<()>::new()?;
println!("BugguDB CLI v{} - Ultra-fast search engine", env!("CARGO_PKG_VERSION"));
println!("Features: term search, field filters, regex, JSON queries, boolean operators");
println!("Type 'help' to see available commands or 'quit' to exit.");
println!("");
loop {
let readline = rl.readline("> ");
match readline {
Ok(line) => {
let line = line.trim();
if line.is_empty() {
continue;
}
rl.add_history_entry(line);
match line.to_lowercase().as_str() {
"quit" | "exit" | "q" => break,
"help" | "?" => {
print_help();
continue;
}
_ if line.starts_with("limit ") => {
if let Some(num_str) = line.strip_prefix("limit ") {
if let Ok(num) = num_str.trim().parse::<usize>() {
limit = num;
println!("Result limit set to {}", limit);
} else {
println!("Invalid limit. Usage: limit <number>");
}
}
continue;
}
"timing on" => {
show_timing = true;
println!("Detailed timing enabled");
continue;
}
"timing off" => {
show_timing = false;
println!("Detailed timing disabled");
continue;
}
_ => {} }
run_query(&db, line, limit, show_timing);
println!(); }
Err(_) => break, }
}
println!("Goodbye!");
Ok(())
}