#![allow(dead_code)]
use std::collections::{HashMap, HashSet};
use rusqlite::Connection;
pub fn compile_match_expression(raw: &str) -> String {
compile_with_op(raw, " AND ")
}
pub fn compile_match_expression_or(raw: &str) -> String {
compile_with_op(raw, " OR ")
}
pub fn compile_with_op(raw: &str, op: &str) -> String {
raw.split_whitespace()
.filter(|t| !t.is_empty())
.map(|t| format!("\"{}\"", t.replace('"', "\"\"")))
.collect::<Vec<_>>()
.join(op)
}
pub fn compile_content_or(raw: &str) -> String {
let toks = content_tokens(raw);
if toks.is_empty() {
return compile_match_expression_or(raw);
}
toks.iter().map(|t| format!("\"{t}\"")).collect::<Vec<_>>().join(" OR ")
}
pub const STOPWORDS: &[&str] = &[
"the", "and", "for", "are", "was", "were", "what", "when", "where", "who", "whom", "which",
"how", "why", "did", "does", "do", "is", "of", "to", "in", "on", "at", "by", "an", "a", "it",
"its", "this", "that", "these", "those", "with", "from", "as", "be", "or", "if", "about",
"into", "over", "than", "then", "they", "them", "their", "you", "your", "we", "our", "i",
];
pub fn tokenize_set(text: &str) -> HashSet<String> {
text.to_lowercase()
.split(|c: char| !c.is_alphanumeric())
.filter(|t| t.len() >= 3)
.map(|t| t.to_string())
.collect()
}
pub fn content_tokens(query: &str) -> HashSet<String> {
let stop: HashSet<&str> = STOPWORDS.iter().copied().collect();
tokenize_set(query).into_iter().filter(|t| !stop.contains(t.as_str())).collect()
}
pub fn word_spans(body: &str) -> Vec<(usize, usize)> {
let mut spans = Vec::new();
let mut start: Option<usize> = None;
for (i, c) in body.char_indices() {
if c.is_whitespace() {
if let Some(s) = start.take() {
spans.push((s, i));
}
} else if start.is_none() {
start = Some(i);
}
}
if let Some(s) = start {
spans.push((s, body.len()));
}
spans
}
pub fn chunk_words_offsets(
body: &str,
size: usize,
stride: usize,
max_chunks: usize,
) -> Vec<(String, usize, usize)> {
let spans = word_spans(body);
if spans.len() <= size {
return vec![(body.to_string(), 0, body.len())];
}
let mut chunks = Vec::new();
let mut start = 0;
while start < spans.len() && chunks.len() < max_chunks {
let end = (start + size).min(spans.len());
let text =
spans[start..end].iter().map(|&(s, e)| &body[s..e]).collect::<Vec<_>>().join(" ");
chunks.push((text, spans[start].0, spans[end - 1].1));
if end == spans.len() {
break;
}
start += stride;
}
chunks
}
pub fn chunk_words(body: &str, size: usize, stride: usize, max_chunks: usize) -> Vec<String> {
chunk_words_offsets(body, size, stride, max_chunks).into_iter().map(|(t, _, _)| t).collect()
}
#[derive(Clone, Copy)]
pub enum Pool {
Max, Mean, Top2, }
pub fn knn_docs_pool(
qv: &[f32],
passages: &[(String, Vec<f32>)],
k: usize,
pool: Pool,
) -> Vec<String> {
struct Acc {
sum: f32,
n: u32,
b1: f32,
b2: f32,
}
let mut by_doc: HashMap<&str, Acc> = HashMap::new();
for (doc_id, pv) in passages {
let dot: f32 = qv.iter().zip(pv).map(|(a, b)| a * b).sum();
let e = by_doc.entry(doc_id.as_str()).or_insert(Acc {
sum: 0.0,
n: 0,
b1: f32::MIN,
b2: f32::MIN,
});
e.sum += dot;
e.n += 1;
if dot > e.b1 {
e.b2 = e.b1;
e.b1 = dot;
} else if dot > e.b2 {
e.b2 = dot;
}
}
let mut v: Vec<(&str, f32)> = by_doc
.into_iter()
.map(|(d, a)| {
let s = match pool {
Pool::Max => a.b1,
Pool::Mean => a.sum / a.n as f32,
Pool::Top2 => {
if a.n >= 2 {
(a.b1 + a.b2) / 2.0
} else {
a.b1
}
}
};
(d, s)
})
.collect();
v.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
v.into_iter().take(k).map(|(d, _)| d.to_string()).collect()
}
pub fn fts_bodies(conn: &Connection, match_expr: &str, order_sql: &str, cap: usize) -> Vec<String> {
if match_expr.is_empty() {
return Vec::new();
}
let sql = format!(
"SELECT body FROM search_index WHERE search_index MATCH ?1 ORDER BY {order_sql} LIMIT {cap}"
);
let Ok(mut stmt) = conn.prepare(&sql) else { return Vec::new() };
let rows = stmt.query_map([match_expr], |row| row.get::<_, String>(0));
match rows {
Ok(it) => it.flatten().collect(),
Err(_) => Vec::new(),
}
}
pub fn map_bodies(bodies: &[String], m: &HashMap<String, String>) -> Vec<String> {
bodies.iter().filter_map(|b| m.get(b).cloned()).collect()
}