1use crate::Result;
8
9pub trait QueryPreprocessor: Send + Sync {
11 fn preprocess(&self, query: &str) -> Result<Vec<String>>;
13
14 fn name(&self) -> &str;
16}
17
18#[derive(Debug, Clone, Default)]
20pub struct PassthroughPreprocessor;
21
22impl QueryPreprocessor for PassthroughPreprocessor {
23 fn preprocess(&self, query: &str) -> Result<Vec<String>> {
24 Ok(vec![query.to_string()])
25 }
26
27 fn name(&self) -> &str {
28 "passthrough"
29 }
30}
31
32#[derive(Debug, Clone)]
39pub struct HydePreprocessor<G: HypotheticalGenerator> {
40 generator: G,
41 include_original: bool,
42}
43
44pub trait HypotheticalGenerator: Send + Sync {
46 fn generate(&self, query: &str) -> Result<String>;
48}
49
50impl<G: HypotheticalGenerator> HydePreprocessor<G> {
51 pub fn new(generator: G) -> Self {
53 Self { generator, include_original: false }
54 }
55
56 #[must_use]
58 pub fn with_original_query(mut self, include: bool) -> Self {
59 self.include_original = include;
60 self
61 }
62}
63
64impl<G: HypotheticalGenerator> QueryPreprocessor for HydePreprocessor<G> {
65 fn preprocess(&self, query: &str) -> Result<Vec<String>> {
66 let hypothetical = self.generator.generate(query)?;
67 if self.include_original {
68 Ok(vec![query.to_string(), hypothetical])
69 } else {
70 Ok(vec![hypothetical])
71 }
72 }
73
74 fn name(&self) -> &str {
75 "hyde"
76 }
77}
78
79#[cfg(feature = "eval")]
85pub struct AnthropicHypotheticalGenerator {
86 client: crate::eval::AnthropicClient,
87 runtime: std::sync::Arc<tokio::runtime::Runtime>,
88 model: String,
89 max_tokens: u32,
90}
91
92#[cfg(feature = "eval")]
93impl std::fmt::Debug for AnthropicHypotheticalGenerator {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 f.debug_struct("AnthropicHypotheticalGenerator")
96 .field("client", &"AnthropicClient{..}")
97 .field("runtime", &"Runtime{..}")
98 .field("model", &self.model)
99 .field("max_tokens", &self.max_tokens)
100 .finish()
101 }
102}
103
104#[cfg(feature = "eval")]
105impl AnthropicHypotheticalGenerator {
106 pub fn from_env() -> std::result::Result<Self, String> {
108 let client = crate::eval::AnthropicClient::from_env()?;
109 let runtime = tokio::runtime::Runtime::new()
110 .map_err(|e| format!("Failed to create tokio runtime: {e}"))?;
111 Ok(Self {
112 client,
113 runtime: std::sync::Arc::new(runtime),
114 model: "claude-haiku-4-5-20251001".to_string(),
115 max_tokens: 256,
116 })
117 }
118
119 #[must_use]
121 pub fn with_model(mut self, model: impl Into<String>) -> Self {
122 self.model = model.into();
123 self
124 }
125
126 #[must_use]
128 pub fn with_max_tokens(mut self, max_tokens: u32) -> Self {
129 self.max_tokens = max_tokens;
130 self
131 }
132}
133
134#[cfg(feature = "eval")]
135impl HypotheticalGenerator for AnthropicHypotheticalGenerator {
136 fn generate(&self, query: &str) -> Result<String> {
137 let system = "You are a technical content generator. Given a user query, write a short \
138 passage (2-4 sentences) that directly answers the query as if it were an excerpt from \
139 a lecture transcript or technical document. Output ONLY the passage text, no preamble \
140 or formatting.";
141
142 let result = self.runtime.block_on(self.client.complete(
143 &self.model,
144 Some(system),
145 query,
146 self.max_tokens,
147 ));
148
149 match result {
150 Ok(completion) => Ok(completion.text),
151 Err(e) => Err(crate::Error::InvalidConfig(format!("HyDE generation failed: {e}"))),
152 }
153 }
154}
155
156#[derive(Debug, Clone, Default)]
158pub struct MockHypotheticalGenerator {
159 prefix: String,
160}
161
162impl MockHypotheticalGenerator {
163 pub fn new() -> Self {
165 Self { prefix: "The answer is:".to_string() }
166 }
167
168 #[must_use]
170 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
171 self.prefix = prefix.into();
172 self
173 }
174}
175
176impl HypotheticalGenerator for MockHypotheticalGenerator {
177 fn generate(&self, query: &str) -> Result<String> {
178 Ok(format!("{} {}", self.prefix, query))
179 }
180}
181
182#[derive(Debug, Clone)]
188pub struct MultiQueryPreprocessor<E: QueryExpander> {
189 expander: E,
190 max_queries: usize,
191 include_original: bool,
192}
193
194pub trait QueryExpander: Send + Sync {
196 fn expand(&self, query: &str) -> Result<Vec<String>>;
198}
199
200impl<E: QueryExpander> MultiQueryPreprocessor<E> {
201 pub fn new(expander: E) -> Self {
203 Self { expander, max_queries: 5, include_original: true }
204 }
205
206 #[must_use]
208 pub fn with_max_queries(mut self, max: usize) -> Self {
209 self.max_queries = max;
210 self
211 }
212
213 #[must_use]
215 pub fn with_original_query(mut self, include: bool) -> Self {
216 self.include_original = include;
217 self
218 }
219}
220
221impl<E: QueryExpander> QueryPreprocessor for MultiQueryPreprocessor<E> {
222 fn preprocess(&self, query: &str) -> Result<Vec<String>> {
223 let mut queries = if self.include_original { vec![query.to_string()] } else { vec![] };
224
225 let expanded = self.expander.expand(query)?;
226 for q in expanded {
227 if queries.len() >= self.max_queries {
228 break;
229 }
230 if queries.contains(&q) || (!self.include_original && q == query) {
233 continue;
234 }
235 queries.push(q);
236 }
237
238 Ok(queries)
239 }
240
241 fn name(&self) -> &str {
242 "multi-query"
243 }
244}
245
246#[derive(Debug, Clone, Default)]
250pub struct KeywordExpander {
251 stopwords: std::collections::HashSet<String>,
252}
253
254impl KeywordExpander {
255 pub fn new() -> Self {
257 let stopwords: std::collections::HashSet<String> = [
258 "a", "an", "the", "is", "are", "was", "were", "be", "been", "being", "have", "has",
259 "had", "do", "does", "did", "will", "would", "could", "should", "may", "might", "must",
260 "shall", "can", "to", "of", "in", "for", "on", "with", "at", "by", "from", "as",
261 "into", "through", "during", "before", "after", "above", "below", "between", "under",
262 "again", "further", "then", "once", "here", "there", "when", "where", "why", "how",
263 "all", "each", "few", "more", "most", "other", "some", "such", "no", "nor", "not",
264 "only", "own", "same", "so", "than", "too", "very", "just", "and", "but", "if", "or",
265 "because", "until", "while", "what", "which", "who", "this", "that", "these", "those",
266 "i", "me", "my", "myself", "we", "our", "you", "your", "he", "him", "she", "her", "it",
267 "its", "they", "them", "their",
268 ]
269 .iter()
270 .map(|s| (*s).to_string())
271 .collect();
272
273 Self { stopwords }
274 }
275
276 fn extract_keywords(&self, text: &str) -> Vec<String> {
278 text.to_lowercase()
279 .split(|c: char| !c.is_alphanumeric())
280 .filter(|w| !w.is_empty() && w.len() > 2 && !self.stopwords.contains(*w))
281 .map(String::from)
282 .collect()
283 }
284}
285
286impl QueryExpander for KeywordExpander {
287 fn expand(&self, query: &str) -> Result<Vec<String>> {
288 let keywords = self.extract_keywords(query);
289 let mut expansions = Vec::new();
290
291 if keywords.len() > 1 {
293 expansions.push(keywords.join(" "));
294 }
295
296 for keyword in keywords.iter().take(3) {
298 expansions.push(format!("{query} {keyword}"));
299 }
300
301 Ok(expansions)
302 }
303}
304
305#[derive(Debug, Clone)]
309pub struct SynonymExpander {
310 synonyms: std::collections::HashMap<String, Vec<String>>,
311}
312
313impl SynonymExpander {
314 pub fn new(synonyms: std::collections::HashMap<String, Vec<String>>) -> Self {
316 Self { synonyms }
317 }
318
319 pub fn with_technical_synonyms() -> Self {
321 let mut synonyms = std::collections::HashMap::new();
322 synonyms.insert(
323 "error".to_string(),
324 vec!["exception".to_string(), "failure".to_string(), "bug".to_string()],
325 );
326 synonyms
327 .insert("function".to_string(), vec!["method".to_string(), "procedure".to_string()]);
328 synonyms.insert(
329 "create".to_string(),
330 vec!["make".to_string(), "build".to_string(), "generate".to_string()],
331 );
332 synonyms.insert("delete".to_string(), vec!["remove".to_string(), "destroy".to_string()]);
333 synonyms.insert(
334 "update".to_string(),
335 vec!["modify".to_string(), "change".to_string(), "edit".to_string()],
336 );
337 synonyms.insert(
338 "find".to_string(),
339 vec!["search".to_string(), "lookup".to_string(), "locate".to_string()],
340 );
341 synonyms.insert(
342 "fast".to_string(),
343 vec!["quick".to_string(), "rapid".to_string(), "speedy".to_string()],
344 );
345 synonyms.insert("slow".to_string(), vec!["sluggish".to_string(), "delayed".to_string()]);
346 Self { synonyms }
347 }
348}
349
350impl Default for SynonymExpander {
351 fn default() -> Self {
352 Self::with_technical_synonyms()
353 }
354}
355
356impl QueryExpander for SynonymExpander {
357 fn expand(&self, query: &str) -> Result<Vec<String>> {
358 let mut expansions = Vec::new();
359 let words: Vec<&str> = query.split_whitespace().collect();
360
361 for (i, word) in words.iter().enumerate() {
362 let lower = word.to_lowercase();
363 if let Some(syns) = self.synonyms.get(&lower) {
364 for syn in syns.iter().take(2) {
365 let mut new_words = words.clone();
366 new_words[i] = syn;
367 expansions.push(new_words.join(" "));
368 }
369 }
370 }
371
372 Ok(expansions)
373 }
374}
375
376#[allow(missing_debug_implementations)]
378pub struct ChainedPreprocessor {
379 preprocessors: Vec<Box<dyn QueryPreprocessor>>,
380 deduplicate: bool,
381 max_total: usize,
382}
383
384impl ChainedPreprocessor {
385 pub fn new() -> Self {
387 Self { preprocessors: Vec::new(), deduplicate: true, max_total: 10 }
388 }
389
390 pub fn add<P: QueryPreprocessor + 'static>(mut self, preprocessor: P) -> Self {
392 self.preprocessors.push(Box::new(preprocessor));
393 self
394 }
395
396 #[must_use]
398 pub fn with_max_total(mut self, max: usize) -> Self {
399 self.max_total = max;
400 self
401 }
402
403 #[must_use]
405 pub fn with_deduplicate(mut self, dedup: bool) -> Self {
406 self.deduplicate = dedup;
407 self
408 }
409}
410
411impl Default for ChainedPreprocessor {
412 fn default() -> Self {
413 Self::new()
414 }
415}
416
417impl QueryPreprocessor for ChainedPreprocessor {
418 fn preprocess(&self, query: &str) -> Result<Vec<String>> {
419 if self.preprocessors.is_empty() {
420 return Ok(vec![query.to_string()]);
421 }
422
423 let mut all_queries = Vec::new();
424
425 for preprocessor in &self.preprocessors {
426 let queries = preprocessor.preprocess(query)?;
427 for q in queries {
428 if all_queries.len() >= self.max_total {
429 break;
430 }
431 if !self.deduplicate || !all_queries.contains(&q) {
432 all_queries.push(q);
433 }
434 }
435 }
436
437 Ok(all_queries)
438 }
439
440 fn name(&self) -> &str {
441 "chained"
442 }
443}
444
445#[derive(Debug, Clone)]
447pub struct QueryAnalyzer {
448 intent_keywords: std::collections::HashMap<QueryIntent, Vec<String>>,
449}
450
451#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
453pub enum QueryIntent {
454 Informational,
456 HowTo,
458 Definition,
460 Troubleshooting,
462 Comparison,
464 Unknown,
466}
467
468#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
470pub struct QueryAnalysis {
471 pub original: String,
473 pub intent: QueryIntent,
475 pub keywords: Vec<String>,
477 pub confidence: f32,
479}
480
481impl QueryAnalyzer {
482 pub fn new() -> Self {
484 let mut intent_keywords = std::collections::HashMap::new();
485
486 intent_keywords.insert(
487 QueryIntent::HowTo,
488 vec![
489 "how".to_string(),
490 "tutorial".to_string(),
491 "guide".to_string(),
492 "steps".to_string(),
493 "way".to_string(),
494 ],
495 );
496
497 intent_keywords.insert(
498 QueryIntent::Definition,
499 vec![
500 "what".to_string(),
501 "define".to_string(),
502 "meaning".to_string(),
503 "definition".to_string(),
504 ],
505 );
506
507 intent_keywords.insert(
508 QueryIntent::Troubleshooting,
509 vec![
510 "error".to_string(),
511 "fix".to_string(),
512 "problem".to_string(),
513 "issue".to_string(),
514 "not working".to_string(),
515 "failed".to_string(),
516 "broken".to_string(),
517 ],
518 );
519
520 intent_keywords.insert(
521 QueryIntent::Comparison,
522 vec![
523 "vs".to_string(),
524 "versus".to_string(),
525 "compare".to_string(),
526 "difference".to_string(),
527 "better".to_string(),
528 ],
529 );
530
531 Self { intent_keywords }
532 }
533
534 pub fn analyze(&self, query: &str) -> QueryAnalysis {
536 let lower = query.to_lowercase();
537 let mut best_intent = QueryIntent::Informational;
538 let mut best_score = 0;
539
540 for (intent, keywords) in &self.intent_keywords {
541 let score = keywords.iter().filter(|kw| lower.contains(kw.as_str())).count();
542 if score > best_score {
543 best_score = score;
544 best_intent = *intent;
545 }
546 }
547
548 let keywords: Vec<String> = lower
550 .split(|c: char| !c.is_alphanumeric())
551 .filter(|w| !w.is_empty() && w.len() > 2)
552 .map(String::from)
553 .collect();
554
555 let confidence =
556 if best_score == 0 { 0.3 } else { (0.5 + 0.1 * best_score as f32).min(1.0) };
557
558 QueryAnalysis {
559 original: query.to_string(),
560 intent: if best_score == 0 { QueryIntent::Unknown } else { best_intent },
561 keywords,
562 confidence,
563 }
564 }
565}
566
567impl Default for QueryAnalyzer {
568 fn default() -> Self {
569 Self::new()
570 }
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576
577 #[test]
580 fn test_passthrough_returns_original() {
581 let preprocessor = PassthroughPreprocessor;
582 let result = preprocessor.preprocess("test query").unwrap();
583 assert_eq!(result, vec!["test query"]);
584 }
585
586 #[test]
587 fn test_passthrough_name() {
588 let preprocessor = PassthroughPreprocessor;
589 assert_eq!(preprocessor.name(), "passthrough");
590 }
591
592 #[test]
595 fn test_hyde_generates_hypothetical() {
596 let generator = MockHypotheticalGenerator::new();
597 let hyde = HydePreprocessor::new(generator);
598 let result = hyde.preprocess("what is rust").unwrap();
599 assert_eq!(result.len(), 1);
600 assert!(result[0].contains("The answer is:"));
601 assert!(result[0].contains("what is rust"));
602 }
603
604 #[test]
605 fn test_hyde_with_original() {
606 let generator = MockHypotheticalGenerator::new();
607 let hyde = HydePreprocessor::new(generator).with_original_query(true);
608 let result = hyde.preprocess("test query").unwrap();
609 assert_eq!(result.len(), 2);
610 assert_eq!(result[0], "test query");
611 assert!(result[1].contains("The answer is:"));
612 }
613
614 #[test]
615 fn test_hyde_custom_prefix() {
616 let generator = MockHypotheticalGenerator::new().with_prefix("Answer:");
617 let hyde = HydePreprocessor::new(generator);
618 let result = hyde.preprocess("query").unwrap();
619 assert!(result[0].starts_with("Answer:"));
620 }
621
622 #[test]
623 fn test_hyde_name() {
624 let generator = MockHypotheticalGenerator::new();
625 let hyde = HydePreprocessor::new(generator);
626 assert_eq!(hyde.name(), "hyde");
627 }
628
629 #[test]
632 fn test_multi_query_with_keyword_expander() {
633 let expander = KeywordExpander::new();
634 let multi = MultiQueryPreprocessor::new(expander);
635 let result = multi.preprocess("rust programming language").unwrap();
636 assert!(!result.is_empty());
637 assert_eq!(result[0], "rust programming language"); }
639
640 #[test]
641 fn test_multi_query_max_queries() {
642 let expander = KeywordExpander::new();
643 let multi = MultiQueryPreprocessor::new(expander).with_max_queries(2);
644 let result = multi.preprocess("rust programming language").unwrap();
645 assert!(result.len() <= 2);
646 }
647
648 #[test]
649 fn test_multi_query_without_original() {
650 let expander = KeywordExpander::new();
651 let multi = MultiQueryPreprocessor::new(expander).with_original_query(false);
652 let result = multi.preprocess("rust programming language").unwrap();
653 assert!(!result.contains(&"rust programming language".to_string()));
654 }
655
656 #[test]
657 fn test_multi_query_name() {
658 let expander = KeywordExpander::new();
659 let multi = MultiQueryPreprocessor::new(expander);
660 assert_eq!(multi.name(), "multi-query");
661 }
662
663 #[test]
666 fn test_keyword_expander_extracts_keywords() {
667 let expander = KeywordExpander::new();
668 let keywords = expander.extract_keywords("the quick brown fox jumps");
669 assert!(keywords.contains(&"quick".to_string()));
670 assert!(keywords.contains(&"brown".to_string()));
671 assert!(keywords.contains(&"jumps".to_string()));
672 assert!(!keywords.contains(&"the".to_string())); }
674
675 #[test]
676 fn test_keyword_expander_filters_short_words() {
677 let expander = KeywordExpander::new();
678 let keywords = expander.extract_keywords("a go at it");
679 assert!(keywords.is_empty() || !keywords.iter().any(|w| w.len() <= 2));
680 }
681
682 #[test]
683 fn test_keyword_expander_expand() {
684 let expander = KeywordExpander::new();
685 let result = expander.expand("rust memory safety").unwrap();
686 assert!(!result.is_empty());
687 }
688
689 #[test]
692 fn test_synonym_expander_basic() {
693 let expander = SynonymExpander::with_technical_synonyms();
694 let result = expander.expand("create a function").unwrap();
695 assert!(!result.is_empty());
696 assert!(result.iter().any(|q| q.contains("make") || q.contains("build")));
698 }
699
700 #[test]
701 fn test_synonym_expander_no_synonyms() {
702 let expander = SynonymExpander::with_technical_synonyms();
703 let result = expander.expand("xyz abc def").unwrap();
704 assert!(result.is_empty()); }
706
707 #[test]
708 fn test_synonym_expander_custom_synonyms() {
709 let mut synonyms = std::collections::HashMap::new();
710 synonyms.insert("test".to_string(), vec!["check".to_string()]);
711 let expander = SynonymExpander::new(synonyms);
712 let result = expander.expand("test code").unwrap();
713 assert!(result.iter().any(|q| q.contains("check")));
714 }
715
716 #[test]
719 fn test_chained_empty() {
720 let chained = ChainedPreprocessor::new();
721 let result = chained.preprocess("query").unwrap();
722 assert_eq!(result, vec!["query"]);
723 }
724
725 #[test]
726 fn test_chained_single() {
727 let chained = ChainedPreprocessor::new().add(PassthroughPreprocessor);
728 let result = chained.preprocess("query").unwrap();
729 assert_eq!(result, vec!["query"]);
730 }
731
732 #[test]
733 fn test_chained_multiple() {
734 let chained = ChainedPreprocessor::new()
735 .add(PassthroughPreprocessor)
736 .add(HydePreprocessor::new(MockHypotheticalGenerator::new()));
737 let result = chained.preprocess("query").unwrap();
738 assert!(result.len() >= 2);
739 assert!(result.contains(&"query".to_string()));
740 }
741
742 #[test]
743 fn test_chained_deduplicates() {
744 let chained = ChainedPreprocessor::new()
745 .add(PassthroughPreprocessor)
746 .add(PassthroughPreprocessor)
747 .with_deduplicate(true);
748 let result = chained.preprocess("query").unwrap();
749 assert_eq!(result.len(), 1); }
751
752 #[test]
753 fn test_chained_max_total() {
754 let chained = ChainedPreprocessor::new()
755 .add(MultiQueryPreprocessor::new(KeywordExpander::new()).with_max_queries(10))
756 .with_max_total(3);
757 let result = chained.preprocess("rust programming language tutorial").unwrap();
758 assert!(result.len() <= 3);
759 }
760
761 #[test]
762 fn test_chained_name() {
763 let chained = ChainedPreprocessor::new();
764 assert_eq!(chained.name(), "chained");
765 }
766
767 #[test]
770 fn test_analyzer_how_to() {
771 let analyzer = QueryAnalyzer::new();
772 let analysis = analyzer.analyze("how to write tests in rust");
773 assert_eq!(analysis.intent, QueryIntent::HowTo);
774 assert!(analysis.confidence > 0.5);
775 }
776
777 #[test]
778 fn test_analyzer_definition() {
779 let analyzer = QueryAnalyzer::new();
780 let analysis = analyzer.analyze("what is a monad");
781 assert_eq!(analysis.intent, QueryIntent::Definition);
782 }
783
784 #[test]
785 fn test_analyzer_troubleshooting() {
786 let analyzer = QueryAnalyzer::new();
787 let analysis = analyzer.analyze("error compiling code fix");
788 assert_eq!(analysis.intent, QueryIntent::Troubleshooting);
789 }
790
791 #[test]
792 fn test_analyzer_comparison() {
793 let analyzer = QueryAnalyzer::new();
794 let analysis = analyzer.analyze("rust vs go comparison");
795 assert_eq!(analysis.intent, QueryIntent::Comparison);
796 }
797
798 #[test]
799 fn test_analyzer_unknown() {
800 let analyzer = QueryAnalyzer::new();
801 let analysis = analyzer.analyze("random words xyz");
802 assert_eq!(analysis.intent, QueryIntent::Unknown);
803 assert!(analysis.confidence < 0.5);
804 }
805
806 #[test]
807 fn test_analyzer_extracts_keywords() {
808 let analyzer = QueryAnalyzer::new();
809 let analysis = analyzer.analyze("rust programming language");
810 assert!(analysis.keywords.contains(&"rust".to_string()));
811 assert!(analysis.keywords.contains(&"programming".to_string()));
812 assert!(analysis.keywords.contains(&"language".to_string()));
813 }
814
815 #[test]
816 fn test_query_analysis_serialization() {
817 let analysis = QueryAnalysis {
818 original: "test".to_string(),
819 intent: QueryIntent::HowTo,
820 keywords: vec!["test".to_string()],
821 confidence: 0.8,
822 };
823 let json = serde_json::to_string(&analysis).unwrap();
824 let deserialized: QueryAnalysis = serde_json::from_str(&json).unwrap();
825 assert_eq!(deserialized.original, "test");
826 assert_eq!(deserialized.intent, QueryIntent::HowTo);
827 }
828
829 use proptest::prelude::*;
831
832 proptest! {
833 #[test]
834 fn prop_passthrough_preserves_input(query in "\\PC{1,100}") {
835 let preprocessor = PassthroughPreprocessor;
836 let result = preprocessor.preprocess(&query).unwrap();
837 prop_assert_eq!(result.len(), 1);
838 prop_assert_eq!(&result[0], &query);
839 }
840
841 #[test]
842 fn prop_hyde_always_returns_something(query in "\\w{1,50}") {
843 let hyde = HydePreprocessor::new(MockHypotheticalGenerator::new());
844 let result = hyde.preprocess(&query).unwrap();
845 prop_assert!(!result.is_empty());
846 }
847
848 #[test]
849 fn prop_chained_respects_max_total(query in "\\w{1,50}", max in 1usize..20) {
850 let chained = ChainedPreprocessor::new()
851 .add(MultiQueryPreprocessor::new(KeywordExpander::new()))
852 .add(HydePreprocessor::new(MockHypotheticalGenerator::new()))
853 .with_max_total(max);
854 let result = chained.preprocess(&query).unwrap();
855 prop_assert!(result.len() <= max);
856 }
857
858 #[test]
859 fn prop_analyzer_always_returns_analysis(query in "\\w{1,100}") {
860 let analyzer = QueryAnalyzer::new();
861 let analysis = analyzer.analyze(&query);
862 prop_assert_eq!(analysis.original, query);
863 prop_assert!(analysis.confidence >= 0.0 && analysis.confidence <= 1.0);
864 }
865
866 #[test]
867 fn prop_keyword_expander_no_empty_results(
868 w1 in "[a-z]{4,10}",
869 w2 in "[a-z]{4,10}",
870 w3 in "[a-z]{4,10}"
871 ) {
872 let expander = KeywordExpander::new();
873 let query = format!("{w1} {w2} {w3}");
874 let result = expander.expand(&query).unwrap();
875 for q in &result {
877 prop_assert!(!q.is_empty());
878 }
879 }
880 }
881}