use std::collections::HashSet;
use std::time::{Duration, Instant};
use color_eyre::Result;
use rust_apt::cache::PackageSort;
use rusqlite::{Connection, params};
use crate::apt::AptCache;
pub struct SearchIndex {
conn: Connection,
}
impl SearchIndex {
pub fn new() -> Result<Self> {
let conn = Connection::open_in_memory()?;
conn.execute(
"CREATE VIRTUAL TABLE IF NOT EXISTS packages USING fts5(name, description)",
[],
)?;
Ok(Self { conn })
}
pub fn build(&mut self, apt: &AptCache) -> Result<(usize, Duration)> {
let start = Instant::now();
let mut count = 0;
self.conn.execute("BEGIN TRANSACTION", [])?;
self.conn.execute("DELETE FROM packages", [])?;
let mut stmt = self.conn.prepare("INSERT INTO packages (name, description) VALUES (?, ?)")?;
for pkg in apt.packages(&PackageSort::default()) {
let name = pkg.name();
let desc = pkg.candidate()
.and_then(|c| c.summary())
.unwrap_or_default();
stmt.execute(params![name, desc])?;
count += 1;
}
self.conn.execute("COMMIT", [])?;
Ok((count, start.elapsed()))
}
pub fn search(&self, query: &str) -> Result<HashSet<String>> {
let mut results = HashSet::new();
if query.is_empty() {
return Ok(results);
}
let fts_query = query
.split_whitespace()
.map(|word| {
let cleaned: String = word.chars()
.filter(|c| !matches!(c, '"' | '*' | '^' | '(' | ')' | '{' | '}' | ':' | '+'))
.collect();
if cleaned.is_empty() {
String::new()
} else {
format!("{cleaned}*")
}
})
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ");
let mut stmt = self.conn.prepare(
"SELECT DISTINCT name FROM packages WHERE packages MATCH ?"
)?;
let rows = stmt.query_map([&fts_query], |row| row.get::<_, String>(0))?;
for name in rows.flatten() {
results.insert(name);
}
Ok(results)
}
}