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 #[must_use]
392 #[allow(clippy::should_implement_trait)]
393 pub fn add<P: QueryPreprocessor + 'static>(mut self, preprocessor: P) -> Self {
394 self.preprocessors.push(Box::new(preprocessor));
395 self
396 }
397
398 #[must_use]
400 pub fn with_max_total(mut self, max: usize) -> Self {
401 self.max_total = max;
402 self
403 }
404
405 #[must_use]
407 pub fn with_deduplicate(mut self, dedup: bool) -> Self {
408 self.deduplicate = dedup;
409 self
410 }
411}
412
413impl Default for ChainedPreprocessor {
414 fn default() -> Self {
415 Self::new()
416 }
417}
418
419impl QueryPreprocessor for ChainedPreprocessor {
420 fn preprocess(&self, query: &str) -> Result<Vec<String>> {
421 if self.preprocessors.is_empty() {
422 return Ok(vec![query.to_string()]);
423 }
424
425 let mut all_queries = Vec::new();
426
427 for preprocessor in &self.preprocessors {
428 let queries = preprocessor.preprocess(query)?;
429 for q in queries {
430 if all_queries.len() >= self.max_total {
431 break;
432 }
433 if !self.deduplicate || !all_queries.contains(&q) {
434 all_queries.push(q);
435 }
436 }
437 }
438
439 Ok(all_queries)
440 }
441
442 fn name(&self) -> &str {
443 "chained"
444 }
445}
446
447#[derive(Debug, Clone)]
449pub struct QueryAnalyzer {
450 intent_keywords: std::collections::HashMap<QueryIntent, Vec<String>>,
451}
452
453#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
455pub enum QueryIntent {
456 Informational,
458 HowTo,
460 Definition,
462 Troubleshooting,
464 Comparison,
466 Unknown,
468}
469
470#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
472pub struct QueryAnalysis {
473 pub original: String,
475 pub intent: QueryIntent,
477 pub keywords: Vec<String>,
479 pub confidence: f32,
481}
482
483impl QueryAnalyzer {
484 pub fn new() -> Self {
486 let mut intent_keywords = std::collections::HashMap::new();
487
488 intent_keywords.insert(
489 QueryIntent::HowTo,
490 vec![
491 "how".to_string(),
492 "tutorial".to_string(),
493 "guide".to_string(),
494 "steps".to_string(),
495 "way".to_string(),
496 ],
497 );
498
499 intent_keywords.insert(
500 QueryIntent::Definition,
501 vec![
502 "what".to_string(),
503 "define".to_string(),
504 "meaning".to_string(),
505 "definition".to_string(),
506 ],
507 );
508
509 intent_keywords.insert(
510 QueryIntent::Troubleshooting,
511 vec![
512 "error".to_string(),
513 "fix".to_string(),
514 "problem".to_string(),
515 "issue".to_string(),
516 "not working".to_string(),
517 "failed".to_string(),
518 "broken".to_string(),
519 ],
520 );
521
522 intent_keywords.insert(
523 QueryIntent::Comparison,
524 vec![
525 "vs".to_string(),
526 "versus".to_string(),
527 "compare".to_string(),
528 "difference".to_string(),
529 "better".to_string(),
530 ],
531 );
532
533 Self { intent_keywords }
534 }
535
536 pub fn analyze(&self, query: &str) -> QueryAnalysis {
538 let lower = query.to_lowercase();
539 let mut best_intent = QueryIntent::Informational;
540 let mut best_score = 0;
541
542 for (intent, keywords) in &self.intent_keywords {
543 let score = keywords.iter().filter(|kw| lower.contains(kw.as_str())).count();
544 if score > best_score {
545 best_score = score;
546 best_intent = *intent;
547 }
548 }
549
550 let keywords: Vec<String> = lower
552 .split(|c: char| !c.is_alphanumeric())
553 .filter(|w| !w.is_empty() && w.len() > 2)
554 .map(String::from)
555 .collect();
556
557 let confidence =
558 if best_score == 0 { 0.3 } else { (0.5 + 0.1 * best_score as f32).min(1.0) };
559
560 QueryAnalysis {
561 original: query.to_string(),
562 intent: if best_score == 0 { QueryIntent::Unknown } else { best_intent },
563 keywords,
564 confidence,
565 }
566 }
567}
568
569impl Default for QueryAnalyzer {
570 fn default() -> Self {
571 Self::new()
572 }
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578
579 #[test]
582 fn test_passthrough_returns_original() {
583 let preprocessor = PassthroughPreprocessor;
584 let result = preprocessor.preprocess("test query").unwrap();
585 assert_eq!(result, vec!["test query"]);
586 }
587
588 #[test]
589 fn test_passthrough_name() {
590 let preprocessor = PassthroughPreprocessor;
591 assert_eq!(preprocessor.name(), "passthrough");
592 }
593
594 #[test]
597 fn test_hyde_generates_hypothetical() {
598 let generator = MockHypotheticalGenerator::new();
599 let hyde = HydePreprocessor::new(generator);
600 let result = hyde.preprocess("what is rust").unwrap();
601 assert_eq!(result.len(), 1);
602 assert!(result[0].contains("The answer is:"));
603 assert!(result[0].contains("what is rust"));
604 }
605
606 #[test]
607 fn test_hyde_with_original() {
608 let generator = MockHypotheticalGenerator::new();
609 let hyde = HydePreprocessor::new(generator).with_original_query(true);
610 let result = hyde.preprocess("test query").unwrap();
611 assert_eq!(result.len(), 2);
612 assert_eq!(result[0], "test query");
613 assert!(result[1].contains("The answer is:"));
614 }
615
616 #[test]
617 fn test_hyde_custom_prefix() {
618 let generator = MockHypotheticalGenerator::new().with_prefix("Answer:");
619 let hyde = HydePreprocessor::new(generator);
620 let result = hyde.preprocess("query").unwrap();
621 assert!(result[0].starts_with("Answer:"));
622 }
623
624 #[test]
625 fn test_hyde_name() {
626 let generator = MockHypotheticalGenerator::new();
627 let hyde = HydePreprocessor::new(generator);
628 assert_eq!(hyde.name(), "hyde");
629 }
630
631 #[test]
634 fn test_multi_query_with_keyword_expander() {
635 let expander = KeywordExpander::new();
636 let multi = MultiQueryPreprocessor::new(expander);
637 let result = multi.preprocess("rust programming language").unwrap();
638 assert!(!result.is_empty());
639 assert_eq!(result[0], "rust programming language"); }
641
642 #[test]
643 fn test_multi_query_max_queries() {
644 let expander = KeywordExpander::new();
645 let multi = MultiQueryPreprocessor::new(expander).with_max_queries(2);
646 let result = multi.preprocess("rust programming language").unwrap();
647 assert!(result.len() <= 2);
648 }
649
650 #[test]
651 fn test_multi_query_without_original() {
652 let expander = KeywordExpander::new();
653 let multi = MultiQueryPreprocessor::new(expander).with_original_query(false);
654 let result = multi.preprocess("rust programming language").unwrap();
655 assert!(!result.contains(&"rust programming language".to_string()));
656 }
657
658 #[test]
659 fn test_multi_query_name() {
660 let expander = KeywordExpander::new();
661 let multi = MultiQueryPreprocessor::new(expander);
662 assert_eq!(multi.name(), "multi-query");
663 }
664
665 #[test]
668 fn test_keyword_expander_extracts_keywords() {
669 let expander = KeywordExpander::new();
670 let keywords = expander.extract_keywords("the quick brown fox jumps");
671 assert!(keywords.contains(&"quick".to_string()));
672 assert!(keywords.contains(&"brown".to_string()));
673 assert!(keywords.contains(&"jumps".to_string()));
674 assert!(!keywords.contains(&"the".to_string())); }
676
677 #[test]
678 fn test_keyword_expander_filters_short_words() {
679 let expander = KeywordExpander::new();
680 let keywords = expander.extract_keywords("a go at it");
681 assert!(keywords.is_empty() || !keywords.iter().any(|w| w.len() <= 2));
682 }
683
684 #[test]
685 fn test_keyword_expander_expand() {
686 let expander = KeywordExpander::new();
687 let result = expander.expand("rust memory safety").unwrap();
688 assert!(!result.is_empty());
689 }
690
691 #[test]
694 fn test_synonym_expander_basic() {
695 let expander = SynonymExpander::with_technical_synonyms();
696 let result = expander.expand("create a function").unwrap();
697 assert!(!result.is_empty());
698 assert!(result.iter().any(|q| q.contains("make") || q.contains("build")));
700 }
701
702 #[test]
703 fn test_synonym_expander_no_synonyms() {
704 let expander = SynonymExpander::with_technical_synonyms();
705 let result = expander.expand("xyz abc def").unwrap();
706 assert!(result.is_empty()); }
708
709 #[test]
710 fn test_synonym_expander_custom_synonyms() {
711 let mut synonyms = std::collections::HashMap::new();
712 synonyms.insert("test".to_string(), vec!["check".to_string()]);
713 let expander = SynonymExpander::new(synonyms);
714 let result = expander.expand("test code").unwrap();
715 assert!(result.iter().any(|q| q.contains("check")));
716 }
717
718 #[test]
721 fn test_chained_empty() {
722 let chained = ChainedPreprocessor::new();
723 let result = chained.preprocess("query").unwrap();
724 assert_eq!(result, vec!["query"]);
725 }
726
727 #[test]
728 fn test_chained_single() {
729 let chained = ChainedPreprocessor::new().add(PassthroughPreprocessor);
730 let result = chained.preprocess("query").unwrap();
731 assert_eq!(result, vec!["query"]);
732 }
733
734 #[test]
735 fn test_chained_multiple() {
736 let chained = ChainedPreprocessor::new()
737 .add(PassthroughPreprocessor)
738 .add(HydePreprocessor::new(MockHypotheticalGenerator::new()));
739 let result = chained.preprocess("query").unwrap();
740 assert!(result.len() >= 2);
741 assert!(result.contains(&"query".to_string()));
742 }
743
744 #[test]
745 fn test_chained_deduplicates() {
746 let chained = ChainedPreprocessor::new()
747 .add(PassthroughPreprocessor)
748 .add(PassthroughPreprocessor)
749 .with_deduplicate(true);
750 let result = chained.preprocess("query").unwrap();
751 assert_eq!(result.len(), 1); }
753
754 #[test]
755 fn test_chained_max_total() {
756 let chained = ChainedPreprocessor::new()
757 .add(MultiQueryPreprocessor::new(KeywordExpander::new()).with_max_queries(10))
758 .with_max_total(3);
759 let result = chained.preprocess("rust programming language tutorial").unwrap();
760 assert!(result.len() <= 3);
761 }
762
763 #[test]
764 fn test_chained_name() {
765 let chained = ChainedPreprocessor::new();
766 assert_eq!(chained.name(), "chained");
767 }
768
769 #[test]
772 fn test_analyzer_how_to() {
773 let analyzer = QueryAnalyzer::new();
774 let analysis = analyzer.analyze("how to write tests in rust");
775 assert_eq!(analysis.intent, QueryIntent::HowTo);
776 assert!(analysis.confidence > 0.5);
777 }
778
779 #[test]
780 fn test_analyzer_definition() {
781 let analyzer = QueryAnalyzer::new();
782 let analysis = analyzer.analyze("what is a monad");
783 assert_eq!(analysis.intent, QueryIntent::Definition);
784 }
785
786 #[test]
787 fn test_analyzer_troubleshooting() {
788 let analyzer = QueryAnalyzer::new();
789 let analysis = analyzer.analyze("error compiling code fix");
790 assert_eq!(analysis.intent, QueryIntent::Troubleshooting);
791 }
792
793 #[test]
794 fn test_analyzer_comparison() {
795 let analyzer = QueryAnalyzer::new();
796 let analysis = analyzer.analyze("rust vs go comparison");
797 assert_eq!(analysis.intent, QueryIntent::Comparison);
798 }
799
800 #[test]
801 fn test_analyzer_unknown() {
802 let analyzer = QueryAnalyzer::new();
803 let analysis = analyzer.analyze("random words xyz");
804 assert_eq!(analysis.intent, QueryIntent::Unknown);
805 assert!(analysis.confidence < 0.5);
806 }
807
808 #[test]
809 fn test_analyzer_extracts_keywords() {
810 let analyzer = QueryAnalyzer::new();
811 let analysis = analyzer.analyze("rust programming language");
812 assert!(analysis.keywords.contains(&"rust".to_string()));
813 assert!(analysis.keywords.contains(&"programming".to_string()));
814 assert!(analysis.keywords.contains(&"language".to_string()));
815 }
816
817 #[test]
818 fn test_query_analysis_serialization() {
819 let analysis = QueryAnalysis {
820 original: "test".to_string(),
821 intent: QueryIntent::HowTo,
822 keywords: vec!["test".to_string()],
823 confidence: 0.8,
824 };
825 let json = serde_json::to_string(&analysis).unwrap();
826 let deserialized: QueryAnalysis = serde_json::from_str(&json).unwrap();
827 assert_eq!(deserialized.original, "test");
828 assert_eq!(deserialized.intent, QueryIntent::HowTo);
829 }
830
831 use proptest::prelude::*;
833
834 proptest! {
835 #[test]
836 fn prop_passthrough_preserves_input(query in "\\PC{1,100}") {
837 let preprocessor = PassthroughPreprocessor;
838 let result = preprocessor.preprocess(&query).unwrap();
839 prop_assert_eq!(result.len(), 1);
840 prop_assert_eq!(&result[0], &query);
841 }
842
843 #[test]
844 fn prop_hyde_always_returns_something(query in "\\w{1,50}") {
845 let hyde = HydePreprocessor::new(MockHypotheticalGenerator::new());
846 let result = hyde.preprocess(&query).unwrap();
847 prop_assert!(!result.is_empty());
848 }
849
850 #[test]
851 fn prop_chained_respects_max_total(query in "\\w{1,50}", max in 1usize..20) {
852 let chained = ChainedPreprocessor::new()
853 .add(MultiQueryPreprocessor::new(KeywordExpander::new()))
854 .add(HydePreprocessor::new(MockHypotheticalGenerator::new()))
855 .with_max_total(max);
856 let result = chained.preprocess(&query).unwrap();
857 prop_assert!(result.len() <= max);
858 }
859
860 #[test]
861 fn prop_analyzer_always_returns_analysis(query in "\\w{1,100}") {
862 let analyzer = QueryAnalyzer::new();
863 let analysis = analyzer.analyze(&query);
864 prop_assert_eq!(analysis.original, query);
865 prop_assert!(analysis.confidence >= 0.0 && analysis.confidence <= 1.0);
866 }
867
868 #[test]
869 fn prop_keyword_expander_no_empty_results(
870 w1 in "[a-z]{4,10}",
871 w2 in "[a-z]{4,10}",
872 w3 in "[a-z]{4,10}"
873 ) {
874 let expander = KeywordExpander::new();
875 let query = format!("{w1} {w2} {w3}");
876 let result = expander.expand(&query).unwrap();
877 for q in &result {
879 prop_assert!(!q.is_empty());
880 }
881 }
882 }
883}