11use std:: collections:: HashMap ;
22
3+ use nucleo_matcher:: {
4+ pattern:: { CaseMatching , Normalization , Pattern } ,
5+ Config , Matcher , Utf32String ,
6+ } ;
37use rayon:: iter:: { IntoParallelIterator , ParallelIterator } ;
48use soar_config:: config:: get_config;
59use soar_core:: { database:: models:: Package , package:: query:: PackageQuery , SoarResult } ;
6- use soar_db:: repository:: {
7- core:: { CoreRepository , SortDirection } ,
8- metadata:: MetadataRepository ,
10+ use soar_db:: {
11+ models:: metadata:: FuzzyCandidate ,
12+ repository:: {
13+ core:: { CoreRepository , SortDirection } ,
14+ metadata:: MetadataRepository ,
15+ } ,
916} ;
1017use tracing:: { debug, trace} ;
1118
1219use crate :: { SearchEntry , SearchResult , SoarContext } ;
1320
1421/// Search for packages across all repositories.
22+ ///
23+ /// Uses fuzzy matching by default. Falls back to SQL LIKE for case-sensitive searches.
1524pub async fn search_packages (
1625 ctx : & SoarContext ,
1726 query : & str ,
@@ -24,27 +33,27 @@ pub async fn search_packages(
2433 limit = ?limit,
2534 "searching packages"
2635 ) ;
36+
2737 let metadata_mgr = ctx. metadata_manager ( ) . await ?;
2838 let diesel_db = ctx. diesel_core_db ( ) ?;
39+ let search_limit = limit. or ( get_config ( ) . search_limit ) . unwrap_or ( 20 ) ;
2940
30- let search_limit = limit. or ( get_config ( ) . search_limit ) . unwrap_or ( 20 ) as i64 ;
31- trace ! ( search_limit = search_limit, "using search limit" ) ;
32-
33- let packages: Vec < Package > = metadata_mgr. query_all_flat ( |repo_name, conn| {
34- let pkgs = if case_sensitive {
35- MetadataRepository :: search_case_sensitive ( conn, query, Some ( search_limit) ) ?
36- } else {
37- MetadataRepository :: search ( conn, query, Some ( search_limit) ) ?
38- } ;
39- Ok ( pkgs
40- . into_iter ( )
41- . map ( |p| {
42- let mut pkg: Package = p. into ( ) ;
43- pkg. repo_name = repo_name. to_string ( ) ;
44- pkg
45- } )
46- . collect ( ) )
47- } ) ?;
41+ let packages: Vec < Package > = if case_sensitive {
42+ let sql_limit = search_limit as i64 ;
43+ metadata_mgr. query_all_flat ( |repo_name, conn| {
44+ let pkgs = MetadataRepository :: search_case_sensitive ( conn, query, Some ( sql_limit) ) ?;
45+ Ok ( pkgs
46+ . into_iter ( )
47+ . map ( |p| {
48+ let mut pkg: Package = p. into ( ) ;
49+ pkg. repo_name = repo_name. to_string ( ) ;
50+ pkg
51+ } )
52+ . collect ( ) )
53+ } ) ?
54+ } else {
55+ fuzzy_search ( ctx, query, search_limit) . await ?
56+ } ;
4857
4958 let installed_pkgs: HashMap < ( String , String , String ) , bool > = diesel_db
5059 . with_conn ( |conn| {
@@ -58,15 +67,14 @@ pub async fn search_packages(
5867
5968 let entries: Vec < SearchEntry > = packages
6069 . into_iter ( )
61- . take ( search_limit as usize )
70+ . take ( search_limit)
6271 . map ( |package| {
6372 let key = (
6473 package. repo_name . clone ( ) ,
6574 package. pkg_id . clone ( ) ,
6675 package. pkg_name . clone ( ) ,
6776 ) ;
6877 let installed = installed_pkgs. get ( & key) . copied ( ) . unwrap_or ( false ) ;
69-
7078 SearchEntry {
7179 package,
7280 installed,
@@ -80,6 +88,111 @@ pub async fn search_packages(
8088 } )
8189}
8290
91+ /// Returns top fuzzy-matched packages across all repositories.
92+ async fn fuzzy_search ( ctx : & SoarContext , query : & str , limit : usize ) -> SoarResult < Vec < Package > > {
93+ let metadata_mgr = ctx. metadata_manager ( ) . await ?;
94+
95+ let candidates: Vec < ( String , FuzzyCandidate ) > =
96+ metadata_mgr. query_all_flat ( |repo_name, conn| {
97+ let items = MetadataRepository :: load_fuzzy_candidates ( conn) ?;
98+ Ok ( items
99+ . into_iter ( )
100+ . map ( |c| ( repo_name. to_string ( ) , c) )
101+ . collect ( ) )
102+ } ) ?;
103+
104+ let scored = score_candidates ( query, & candidates) ;
105+ let top: Vec < _ > = scored. into_iter ( ) . take ( limit) . collect ( ) ;
106+
107+ let mut repo_ids: HashMap < & str , Vec < i32 > > = HashMap :: new ( ) ;
108+ for & ( _, idx) in & top {
109+ let ( repo_name, candidate) = & candidates[ idx] ;
110+ repo_ids
111+ . entry ( repo_name. as_str ( ) )
112+ . or_default ( )
113+ . push ( candidate. id ) ;
114+ }
115+
116+ let mut full_packages: HashMap < ( String , i32 ) , Package > = HashMap :: new ( ) ;
117+ for ( repo_name, ids) in & repo_ids {
118+ if let Some ( pkgs) =
119+ metadata_mgr. query_repo ( repo_name, |conn| MetadataRepository :: find_by_ids ( conn, ids) ) ?
120+ {
121+ for p in pkgs {
122+ let db_id = p. id ;
123+ let mut pkg: Package = p. into ( ) ;
124+ pkg. repo_name = repo_name. to_string ( ) ;
125+ full_packages. insert ( ( repo_name. to_string ( ) , db_id) , pkg) ;
126+ }
127+ }
128+ }
129+
130+ let packages: Vec < Package > = top
131+ . into_iter ( )
132+ . filter_map ( |( _, idx) | {
133+ let ( repo_name, candidate) = & candidates[ idx] ;
134+ full_packages. remove ( & ( repo_name. clone ( ) , candidate. id ) )
135+ } )
136+ . collect ( ) ;
137+
138+ Ok ( packages)
139+ }
140+
141+ /// Suggest similar package names for "did you mean?" messages.
142+ pub async fn suggest_similar (
143+ ctx : & SoarContext ,
144+ query : & str ,
145+ max : usize ,
146+ ) -> SoarResult < Vec < String > > {
147+ let metadata_mgr = ctx. metadata_manager ( ) . await ?;
148+
149+ let candidates: Vec < ( String , FuzzyCandidate ) > =
150+ metadata_mgr. query_all_flat ( |repo_name, conn| {
151+ let items = MetadataRepository :: load_fuzzy_candidates ( conn) ?;
152+ Ok ( items
153+ . into_iter ( )
154+ . map ( |c| ( repo_name. to_string ( ) , c) )
155+ . collect ( ) )
156+ } ) ?;
157+
158+ let scored = score_candidates ( query, & candidates) ;
159+
160+ let suggestions: Vec < String > = scored
161+ . into_iter ( )
162+ . take ( max)
163+ . map ( |( _, idx) | {
164+ let ( _, candidate) = & candidates[ idx] ;
165+ candidate. pkg_name . clone ( )
166+ } )
167+ . collect ( ) ;
168+
169+ Ok ( suggestions)
170+ }
171+
172+ fn score_candidates ( query : & str , candidates : & [ ( String , FuzzyCandidate ) ] ) -> Vec < ( u32 , usize ) > {
173+ let mut matcher = Matcher :: new ( Config :: DEFAULT ) ;
174+ let pattern = Pattern :: parse ( query, CaseMatching :: Ignore , Normalization :: Smart ) ;
175+
176+ let mut scored: Vec < ( u32 , usize ) > = Vec :: new ( ) ;
177+
178+ for ( idx, ( _repo_name, candidate) ) in candidates. iter ( ) . enumerate ( ) {
179+ let name_buf = Utf32String :: from ( candidate. pkg_name . as_str ( ) ) ;
180+ let name_score = pattern. score ( name_buf. slice ( ..) , & mut matcher) ;
181+
182+ let id_buf = Utf32String :: from ( candidate. pkg_id . as_str ( ) ) ;
183+ let id_score = pattern. score ( id_buf. slice ( ..) , & mut matcher) ;
184+
185+ let best_score = [ name_score, id_score] . into_iter ( ) . flatten ( ) . max ( ) ;
186+
187+ if let Some ( score) = best_score {
188+ scored. push ( ( score, idx) ) ;
189+ }
190+ }
191+
192+ scored. sort_by ( |a, b| b. 0 . cmp ( & a. 0 ) ) ;
193+ scored
194+ }
195+
83196/// Query detailed package information.
84197///
85198/// Accepts query strings in the format `name#pkg_id@version:repo`.
0 commit comments