Skip to main content

alf/search/
mod.rs

1//! Fuzzy search implementation using nucleo-matcher.
2
3use nucleo_matcher::{Config, Matcher};
4
5use crate::config::CaseMatching;
6use crate::models::{AliasEntry, SearchResult};
7
8/// Search options for configuring fuzzy matching behavior
9#[derive(Debug, Clone)]
10pub struct SearchOptions {
11   /// Case matching strategy
12   pub case_matching: CaseMatching,
13   /// Enable Unicode normalization
14   pub normalize: bool,
15}
16
17impl Default for SearchOptions {
18   fn default() -> Self {
19      Self {
20         case_matching: CaseMatching::Smart,
21         normalize: true,
22      }
23   }
24}
25
26/// Perform fuzzy search on entries using nucleo-matcher
27///
28/// # Arguments
29/// * `entries` - The list of aliases/functions to search through
30/// * `query` - The search query string
31/// * `opts` - Search configuration options
32///
33/// # Returns
34/// A vector of search results sorted by match score (highest first)
35pub fn fuzzy_search(
36   entries: &[AliasEntry],
37   query: &str,
38   opts: &SearchOptions,
39) -> Vec<SearchResult> {
40   if query.is_empty() {
41      // Empty query matches all entries with equal score
42      return entries
43         .iter()
44         .map(|entry| SearchResult {
45            entry: entry.clone(),
46            score: 0,
47         })
48         .collect();
49   }
50
51   let mut matcher = create_matcher(opts);
52   let mut results = Vec::new();
53
54   // Convert query to nucleo format
55   let query_haystack = nucleo_matcher::Utf32Str::Ascii(query.as_bytes());
56
57   for entry in entries {
58      // Try matching against entry name first (prioritized)
59      let name_haystack = nucleo_matcher::Utf32Str::Ascii(entry.name.as_bytes());
60      let name_score = matcher.fuzzy_match(name_haystack, query_haystack);
61
62      let comment_score = entry.comments.as_ref().and_then(|comments| {
63         // Join comments with spaces and try to match
64         let comment_text = comments.join(" ");
65         let comment_haystack = nucleo_matcher::Utf32Str::Ascii(comment_text.as_bytes());
66         matcher.fuzzy_match(comment_haystack, query_haystack)
67      });
68
69      // Use name score if available, otherwise use comment score
70      if let Some(score) = name_score.or(comment_score) {
71         results.push(SearchResult {
72            entry: entry.clone(),
73            score: score as u32,
74         });
75      }
76   }
77
78   results.sort_by_key(|r| std::cmp::Reverse(r.score));
79
80   results
81}
82
83/// Create and configure a nucleo Matcher based on search options
84fn create_matcher(_opts: &SearchOptions) -> Matcher {
85   // Note: nucleo-matcher has limited API for case matching configuration
86   // The default Smart case matching should work well for most users
87   // Future versions may expose more configurability
88   Matcher::new(Config::DEFAULT)
89}
90
91#[cfg(test)]
92mod search_tests;