use crate::helpers::constants::Vertical;
use serde_json::Value;
use std::collections::{HashMap, HashSet};
use std::path::Path;
#[allow(dead_code)]
pub struct EvalQuery {
pub id: String,
pub query: String,
pub query_type: String,
pub strategy: String,
pub expected_doc_ids: Vec<String>,
pub min_score: f64,
pub description: String,
}
#[allow(dead_code)]
#[derive(Clone)]
pub struct Document {
pub id: String,
pub title: String,
pub content: String,
pub category: String,
pub tags: Vec<String>,
}
pub struct DocIndex {
pub docs: HashMap<String, Document>,
pub key_terms: HashMap<String, Vec<String>>,
}
const STOP_WORDS: &[&str] = &[
"the", "and", "for", "are", "but", "not", "you", "all", "can", "had", "her", "was", "one",
"our", "out", "has", "have", "each", "make", "like", "from", "this", "that", "with", "they",
"been", "will", "more", "when", "than", "into", "some", "very", "what", "about", "which",
"their", "other", "also", "its", "over", "such", "most", "only", "then", "them", "same",
"much", "would", "where", "does", "using", "based", "uses", "used",
];
impl DocIndex {
pub fn build(docs: &[Document]) -> Self {
let stop_set: HashSet<&str> = STOP_WORDS.iter().copied().collect();
let mut doc_freq: HashMap<String, usize> = HashMap::new();
let mut per_doc_words: HashMap<String, HashSet<String>> = HashMap::new();
for doc in docs {
let mut words = HashSet::new();
for word in tokenize(&doc.content) {
if word.len() > 3 && !stop_set.contains(word.as_str()) {
words.insert(word);
}
}
for word in tokenize(&doc.title) {
if word.len() > 3 && !stop_set.contains(word.as_str()) {
words.insert(word);
}
}
per_doc_words.insert(doc.id.clone(), words);
}
for words in per_doc_words.values() {
for word in words {
*doc_freq.entry(word.clone()).or_insert(0) += 1;
}
}
let mut key_terms: HashMap<String, Vec<String>> = HashMap::new();
for (doc_id, words) in &per_doc_words {
let terms: Vec<String> = words
.iter()
.filter(|w| doc_freq.get(*w).copied().unwrap_or(0) <= 2)
.cloned()
.collect();
key_terms.insert(doc_id.clone(), terms);
}
let docs_map: HashMap<String, Document> = HashMap::new();
Self {
docs: docs_map,
key_terms,
}
}
pub fn from_documents(docs: &[Document]) -> Self {
let index = Self::build(docs);
let docs_map: HashMap<String, Document> =
docs.iter().cloned().map(|d| (d.id.clone(), d)).collect();
Self {
docs: docs_map,
key_terms: index.key_terms,
}
}
}
fn tokenize(text: &str) -> Vec<String> {
text.split(|c: char| !c.is_alphanumeric() && c != '-' && c != '_')
.filter(|s| !s.is_empty())
.map(|s| s.to_lowercase())
.collect()
}
pub fn load_eval_queries(data_dir: &Path) -> Vec<EvalQuery> {
let path = data_dir.join("eval_queries.json");
let content = std::fs::read_to_string(&path).unwrap_or_else(|e| {
panic!("Failed to read {}: {e}", path.display());
});
let json: Value = serde_json::from_str(&content).unwrap_or_else(|e| {
panic!("Failed to parse {}: {e}", path.display());
});
let queries = json["queries"]
.as_array()
.expect("eval_queries.json must have a 'queries' array");
queries
.iter()
.map(|q| EvalQuery {
id: q["id"].as_str().unwrap().to_string(),
query: q["query"].as_str().unwrap().to_string(),
query_type: q["query_type"].as_str().unwrap().to_string(),
strategy: q["strategy"].as_str().unwrap_or("unknown").to_string(),
expected_doc_ids: q["expected_doc_ids"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect(),
min_score: q["min_score"].as_f64().unwrap_or(0.3),
description: q["description"].as_str().unwrap_or("").to_string(),
})
.collect()
}
pub fn load_eval_documents(data_dir: &Path) -> Vec<Document> {
let path = data_dir.join("eval_docs.json");
let content = std::fs::read_to_string(&path).unwrap_or_else(|e| {
panic!("Failed to read {}: {e}", path.display());
});
let json: Value = serde_json::from_str(&content).unwrap_or_else(|e| {
panic!("Failed to parse {}: {e}", path.display());
});
let documents = json["documents"]
.as_array()
.expect("eval_docs.json must have a 'documents' array");
documents
.iter()
.map(|d| Document {
id: d["id"].as_str().unwrap().to_string(),
title: d["title"].as_str().unwrap().to_string(),
content: d["content"].as_str().unwrap().to_string(),
category: d["category"].as_str().unwrap_or("").to_string(),
tags: d["tags"]
.as_array()
.map(|arr| {
arr.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect()
})
.unwrap_or_default(),
})
.collect()
}
pub fn partition_queries_by_vertical(
queries: &[EvalQuery],
verticals: &[Vertical],
) -> Vec<Vec<EvalQuery>> {
let mut by_strategy: HashMap<&str, Vec<&EvalQuery>> = HashMap::new();
for q in queries {
by_strategy.entry(q.strategy.as_str()).or_default().push(q);
}
for strategy in &["fts", "vector", "cascade"] {
let group = by_strategy.get(strategy).map(|g| g.len()).unwrap_or(0);
assert!(
group >= verticals.len(),
"Strategy '{}' has {} queries but need {} (one per vertical)",
strategy,
group,
verticals.len(),
);
}
let mut result: Vec<Vec<EvalQuery>> = (0..verticals.len())
.map(|_| Vec::with_capacity(3))
.collect();
for strategy in &["fts", "vector", "cascade"] {
if let Some(group) = by_strategy.get(strategy) {
for (vi, q) in group.iter().enumerate().take(verticals.len()) {
result[vi].push(EvalQuery {
id: q.id.clone(),
query: q.query.clone(),
query_type: q.query_type.clone(),
strategy: q.strategy.clone(),
expected_doc_ids: q.expected_doc_ids.clone(),
min_score: q.min_score,
description: q.description.clone(),
});
}
}
}
result
}