use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub enum RewriteStrategy {
Expansion,
HyDE,
Decomposition,
Clarification,
Paraphrase,
StepBack,
}
#[derive(Debug, Clone)]
pub struct ExpandedQuery {
pub original: String,
pub rewritten: String,
pub strategy: RewriteStrategy,
pub alternatives: Vec<String>,
pub confidence: f64,
}
pub struct QueryRewriter {
synonyms: HashMap<&'static str, Vec<&'static str>>,
}
impl Default for QueryRewriter {
fn default() -> Self {
Self::new()
}
}
impl QueryRewriter {
pub fn new() -> Self {
let mut synonyms: HashMap<&'static str, Vec<&'static str>> = HashMap::new();
synonyms.insert("fast", vec!["quick", "rapid", "speedy", "swift"]);
synonyms.insert("quick", vec!["fast", "rapid", "speedy"]);
synonyms.insert("big", vec!["large", "huge", "enormous", "massive"]);
synonyms.insert("small", vec!["tiny", "little", "compact", "miniature"]);
synonyms.insert("good", vec!["excellent", "great", "superior", "quality"]);
synonyms.insert("bad", vec!["poor", "inferior", "defective", "faulty"]);
synonyms.insert("use", vec!["utilize", "employ", "apply", "leverage"]);
synonyms.insert("make", vec!["create", "build", "construct", "generate"]);
synonyms.insert("show", vec!["display", "demonstrate", "illustrate", "present"]);
synonyms.insert("find", vec!["locate", "discover", "identify", "search"]);
synonyms.insert("get", vec!["retrieve", "obtain", "fetch", "acquire"]);
synonyms.insert("error", vec!["bug", "fault", "issue", "defect", "problem"]);
synonyms.insert("fix", vec!["repair", "resolve", "correct", "patch"]);
synonyms.insert("code", vec!["source", "program", "implementation", "script"]);
synonyms.insert("best", vec!["optimal", "top", "leading", "premier"]);
synonyms.insert("list", vec!["enumerate", "catalog", "collection", "inventory"]);
synonyms.insert("explain", vec!["describe", "clarify", "elaborate", "detail"]);
synonyms.insert("compare", vec!["contrast", "evaluate", "differentiate", "assess"]);
synonyms.insert("difference", vec!["distinction", "contrast", "variation", "divergence"]);
synonyms.insert("example", vec!["instance", "illustration", "sample", "case"]);
synonyms.insert("method", vec!["approach", "technique", "procedure", "strategy"]);
synonyms.insert("data", vec!["information", "dataset", "records", "values"]);
synonyms.insert("model", vec!["network", "architecture", "system", "framework"]);
synonyms.insert("learn", vec!["train", "acquire", "study", "master"]);
synonyms.insert("improve", vec!["enhance", "optimize", "refine", "upgrade"]);
Self { synonyms }
}
pub fn expand(&self, query: &str) -> ExpandedQuery {
let words: Vec<&str> = query.split_whitespace().collect();
let mut extra_terms: Vec<String> = Vec::new();
for word in &words {
let lower = word.to_lowercase();
let key = lower.trim_matches(|c: char| !c.is_alphabetic());
if let Some(syns) = self.synonyms.get(key) {
for s in syns.iter().take(2) {
extra_terms.push(s.to_string());
}
}
}
let rewritten = if extra_terms.is_empty() {
query.to_string()
} else {
format!("{} {}", query, extra_terms.join(" "))
};
let alternatives = {
let mut alts = Vec::new();
if !extra_terms.is_empty() {
alts.push(format!("{} (also: {})", query, extra_terms.join(", ")));
}
alts
};
ExpandedQuery {
original: query.to_string(),
rewritten,
strategy: RewriteStrategy::Expansion,
alternatives,
confidence: if extra_terms.is_empty() { 0.5 } else { 0.8 },
}
}
pub fn hyde_expand(&self, query: &str, domain: &str) -> String {
let trimmed = query.trim_end_matches('?').trim_end_matches('.').trim();
let statement = if let Some(rest) = trimmed.strip_prefix("How do I ") {
format!("The way to {} is described here", rest)
} else if let Some(rest) = trimmed.strip_prefix("How to ") {
format!("To {}, one should", rest)
} else if let Some(rest) = trimmed.strip_prefix("What is ") {
format!("{} is defined as", rest)
} else if let Some(rest) = trimmed.strip_prefix("Why is ") {
format!("{} because", rest)
} else if let Some(rest) = trimmed.strip_prefix("Why does ") {
format!("{} due to", rest)
} else {
format!("The answer to '{}' is", trimmed)
};
format!("{} context: {}", domain, statement)
}
pub fn decompose(&self, query: &str) -> Vec<String> {
let splitters = [" and ", " also ", " additionally ", " as well as "];
let lower = query.to_lowercase();
for splitter in &splitters {
if let Some(pos) = lower.find(splitter) {
let first = query[..pos].trim().to_string();
let second = query[pos + splitter.len()..].trim().to_string();
if !first.is_empty() && !second.is_empty() {
let second_cap = capitalise_first(&second);
return vec![
ensure_question_mark(&first),
ensure_question_mark(&second_cap),
];
}
}
}
vec![query.to_string()]
}
pub fn generate_clarifications(&self, query: &str) -> Vec<String> {
let mut clarifications = Vec::new();
let lower = query.to_lowercase();
if lower.contains(" it ") || lower.contains(" this ") || lower.contains(" that ") {
clarifications.push(format!("What specific subject does 'it/this/that' refer to in: '{}'?", query));
}
if lower.contains("better") || lower.contains("worse") || lower.contains("faster") {
clarifications.push(format!("Compared to what baseline are you measuring in: '{}'?", query));
}
if lower.contains("recently") || lower.contains("latest") || lower.contains("current") {
clarifications.push(format!("What time period does 'recent/latest/current' refer to in: '{}'?", query));
}
if lower.contains("best") || lower.contains("most") || lower.contains("all") {
clarifications.push(format!("In what context or domain should '{}' be evaluated?", query));
}
clarifications.push(format!("Is this question about a specific field or domain: '{}'?", query));
clarifications
}
pub fn paraphrase(&self, query: &str) -> Vec<String> {
let words: Vec<&str> = query.split_whitespace().collect();
let mut variants: Vec<String> = Vec::new();
let v1: Vec<String> = words
.iter()
.map(|w| {
let lower = w.to_lowercase();
let key = lower.trim_matches(|c: char| !c.is_alphabetic());
if let Some(syns) = self.synonyms.get(key) {
syns[0].to_string()
} else {
w.to_string()
}
})
.collect();
let v1_str = v1.join(" ");
if v1_str != query {
variants.push(v1_str);
}
let lower = query.to_lowercase();
if let Some(rest) = lower.strip_prefix("how do i ") {
variants.push(format!("What is the way to {}?", rest.trim_end_matches('?')));
} else if let Some(rest) = lower.strip_prefix("what is ") {
variants.push(format!("Can you define {}?", rest.trim_end_matches('?')));
} else if let Some(rest) = lower.strip_prefix("why ") {
variants.push(format!("What is the reason that {}?", rest.trim_end_matches('?')));
}
let bare = query.trim_end_matches('?').trim_end_matches('.');
variants.push(format!("Tell me about {}", bare.to_lowercase()));
variants.push(format!("Explain {}", bare.to_lowercase()));
variants.push(format!("{} in detail", bare));
variants.dedup();
variants.truncate(5);
variants
}
pub fn step_back(&self, query: &str) -> String {
let lower = query.to_lowercase();
let generalised = lower
.replace(" specifically", "")
.replace(" exactly", "")
.replace(" precisely", "")
.replace(" in this case", "")
.replace(" in my case", "");
if let Some(rest) = generalised.strip_prefix("how do i ") {
return format!("How does {} work in general?", rest.trim_end_matches('?').trim());
}
if let Some(rest) = generalised.strip_prefix("why is my ") {
return format!("What are common reasons for {}?", rest.trim_end_matches('?').trim());
}
if let Some(rest) = generalised.strip_prefix("why does my ") {
return format!("What are common reasons for {}?", rest.trim_end_matches('?').trim());
}
format!("In general, {}", generalised.trim_end_matches('?').trim())
}
pub fn rewrite(&self, query: &str, strategy: RewriteStrategy) -> ExpandedQuery {
match &strategy {
RewriteStrategy::Expansion => self.expand(query),
RewriteStrategy::HyDE => {
let rewritten = self.hyde_expand(query, "general");
ExpandedQuery {
original: query.to_string(),
rewritten,
strategy,
alternatives: vec![
self.hyde_expand(query, "technical"),
self.hyde_expand(query, "academic"),
],
confidence: 0.7,
}
}
RewriteStrategy::Decomposition => {
let parts = self.decompose(query);
let rewritten = parts.join(" | ");
ExpandedQuery {
original: query.to_string(),
rewritten,
strategy,
alternatives: parts,
confidence: 0.75,
}
}
RewriteStrategy::Clarification => {
let clarifications = self.generate_clarifications(query);
let rewritten = clarifications.first().cloned().unwrap_or_else(|| query.to_string());
ExpandedQuery {
original: query.to_string(),
rewritten,
strategy,
alternatives: clarifications,
confidence: 0.65,
}
}
RewriteStrategy::Paraphrase => {
let alternatives = self.paraphrase(query);
let rewritten = alternatives.first().cloned().unwrap_or_else(|| query.to_string());
ExpandedQuery {
original: query.to_string(),
rewritten,
strategy,
alternatives,
confidence: 0.72,
}
}
RewriteStrategy::StepBack => {
let rewritten = self.step_back(query);
ExpandedQuery {
original: query.to_string(),
rewritten: rewritten.clone(),
strategy,
alternatives: vec![rewritten],
confidence: 0.68,
}
}
}
}
pub fn best_strategy(&self, query: &str) -> RewriteStrategy {
let words: Vec<&str> = query.split_whitespace().collect();
let lower = query.to_lowercase();
let compound_signals = [" and ", " also ", " additionally ", " as well as "];
if compound_signals.iter().any(|s| lower.contains(s)) {
return RewriteStrategy::Decomposition;
}
if words.len() <= 4 {
return RewriteStrategy::Expansion;
}
if words.len() >= 10 {
return RewriteStrategy::StepBack;
}
if lower.contains(" it ") || lower.contains(" this ") || lower.contains(" that ") {
return RewriteStrategy::Clarification;
}
RewriteStrategy::Expansion
}
pub fn batch_rewrite(&self, queries: &[&str]) -> Vec<ExpandedQuery> {
queries
.iter()
.map(|q| {
let strategy = self.best_strategy(q);
self.rewrite(q, strategy)
})
.collect()
}
}
fn capitalise_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
}
fn ensure_question_mark(s: &str) -> String {
let trimmed = s.trim();
if trimmed.ends_with('?') {
trimmed.to_string()
} else {
format!("{}?", trimmed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn expand_adds_synonyms() {
let rw = QueryRewriter::new();
let eq = rw.expand("find the best code");
assert!(eq.rewritten.contains("find"));
assert!(eq.strategy == RewriteStrategy::Expansion);
}
#[test]
fn hyde_expand_basic() {
let rw = QueryRewriter::new();
let result = rw.hyde_expand("What is Rust?", "programming");
assert!(result.contains("programming context:"));
assert!(result.contains("Rust"));
}
#[test]
fn decompose_splits_on_and() {
let rw = QueryRewriter::new();
let parts = rw.decompose("How do I read files and write files?");
assert_eq!(parts.len(), 2);
}
#[test]
fn decompose_no_conjunction() {
let rw = QueryRewriter::new();
let parts = rw.decompose("What is Rust?");
assert_eq!(parts.len(), 1);
}
#[test]
fn paraphrase_returns_multiple() {
let rw = QueryRewriter::new();
let variants = rw.paraphrase("How do I fix a bug?");
assert!(!variants.is_empty());
}
#[test]
fn step_back_generalises() {
let rw = QueryRewriter::new();
let general = rw.step_back("How do I parse JSON in Rust?");
assert!(general.contains("general") || general.contains("work"));
}
#[test]
fn best_strategy_short() {
let rw = QueryRewriter::new();
assert_eq!(rw.best_strategy("fast code"), RewriteStrategy::Expansion);
}
#[test]
fn best_strategy_compound() {
let rw = QueryRewriter::new();
assert_eq!(
rw.best_strategy("What is Rust and how do I use it?"),
RewriteStrategy::Decomposition
);
}
#[test]
fn batch_rewrite_returns_same_count() {
let rw = QueryRewriter::new();
let qs = ["find files", "What is this and that?", "explain"];
let results = rw.batch_rewrite(&qs);
assert_eq!(results.len(), 3);
}
#[test]
fn rewrite_hyde_confidence() {
let rw = QueryRewriter::new();
let eq = rw.rewrite("What is machine learning?", RewriteStrategy::HyDE);
assert!(eq.confidence > 0.0);
assert_eq!(eq.strategy, RewriteStrategy::HyDE);
}
}