fuse_rust/lib.rs
1#![warn(missing_docs)]
2
3//! Fuse-RS
4//!
5//! A super lightweight fuzzy-search library.
6//! A port of [Fuse-Swift](https://github.com/krisk/fuse-swift) written purely in rust!
7
8#[cfg(test)]
9mod tests;
10mod utils;
11
12#[cfg(feature = "async")]
13use crossbeam_utils::thread;
14
15#[cfg(any(feature = "async", feature = "rayon"))]
16use std::sync::{Arc, Mutex};
17
18/// Required for scoped threads
19use std::collections::HashMap;
20use std::ops::Range;
21
22/// Defines the fuseproperty object to be returned as part of the list
23/// returned by properties() implemented by the Fuseable trait.
24/// # Examples:
25/// Basic Usage:
26/// ```no_run
27/// use fuse_rust::{ Fuse, Fuseable, FuseProperty };
28/// struct Book<'a> {
29/// title: &'a str,
30/// author: &'a str,
31/// }
32///
33/// impl Fuseable for Book<'_>{
34/// fn properties(&self) -> Vec<FuseProperty> {
35/// return vec!(
36/// FuseProperty{value: String::from("title"), weight: 0.3},
37/// FuseProperty{value: String::from("author"), weight: 0.7},
38/// )
39/// }
40/// fn lookup(&self, key: &str) -> Option<&str> {
41/// return match key {
42/// "title" => Some(self.title),
43/// "author" => Some(self.author),
44/// _ => None
45/// }
46/// }
47/// }
48/// ```
49pub struct FuseProperty {
50 /// The name of the field with an associated weight in the search.
51 pub value: String,
52 /// The weight associated with the specified field.
53 pub weight: f64,
54}
55
56impl FuseProperty {
57 /// create a fuse property with weight 1.0 and a string reference.
58 pub fn init(value: &str) -> Self {
59 Self {
60 value: String::from(value),
61 weight: 1.0,
62 }
63 }
64 /// create a fuse property with a specified weight and string reference.
65 pub fn init_with_weight(value: &str, weight: f64) -> Self {
66 Self {
67 value: String::from(value),
68 weight,
69 }
70 }
71}
72
73/// A datatype to store the pattern's text, its length, a mask
74/// and a hashmap against each alphabet in the text.
75/// Always use fuse.create_pattern("search string") to create a pattern
76/// # Examples:
77/// Basic usage:
78/// ```no_run
79/// use fuse_rust::{ Fuse };
80/// let fuse = Fuse::default();
81/// let pattern = fuse.create_pattern("Hello");
82/// ```
83pub struct Pattern {
84 text: String,
85 len: usize,
86 mask: u64,
87 alphabet: HashMap<u8, u64>,
88}
89
90/// Return type for performing a search on a list of strings
91#[derive(Debug, PartialEq)]
92pub struct SearchResult {
93 /// corresponding index of the search result in the original list
94 pub index: usize,
95 /// Search rating of the search result, 0.0 is a perfect match 1.0 is a perfect mismatch
96 pub score: f64,
97 /// Ranges of matches in the search query, useful if you want to hightlight matches.
98 pub ranges: Vec<Range<usize>>,
99}
100
101/// Return type for performing a search on a single string.
102#[derive(Debug, PartialEq)]
103pub struct ScoreResult {
104 /// Search rating of the search result, 0.0 is a perfect match 1.0 is a perfect mismatch
105 pub score: f64,
106 /// Ranges of matches in the search query, useful if you want to hightlight matches.
107 pub ranges: Vec<Range<usize>>,
108}
109
110/// Return type for performing a search with a single fuseable property of struct
111#[derive(Debug, PartialEq)]
112pub struct FResult {
113 /// The corresponding field name for this search result
114 pub value: String,
115 /// Search rating of the search result, 0.0 is a perfect match 1.0 is a perfect mismatch
116 pub score: f64,
117 /// Ranges of matches in the search query, useful if you want to hightlight matches.
118 pub ranges: Vec<Range<usize>>,
119}
120
121/// Return type for performing a search over a list of Fuseable structs
122#[derive(Debug, PartialEq)]
123pub struct FuseableSearchResult {
124 /// corresponding index of the search result in the original list
125 pub index: usize,
126 /// Search rating of the search result, 0.0 is a perfect match 1.0 is a perfect mismatch
127 pub score: f64,
128 /// Ranges of matches in the search query, useful if you want to hightlight matches.
129 pub results: Vec<FResult>,
130}
131
132/// Creates a new fuse object with given config settings
133/// Use to create patterns and access the search methods.
134/// Also implements a default method to quickly get a fuse
135/// object ready with the default config.
136/// # Examples:
137/// Basic Usage:
138/// ```no_run
139/// # use fuse_rust::{ Fuse };
140/// let fuse = Fuse{
141/// location: 0,
142/// distance: 100,
143/// threshold: 0.6,
144/// max_pattern_length: 32,
145/// is_case_sensitive: false,
146/// tokenize: false,
147/// };
148/// ```
149pub struct Fuse {
150 /// location to starting looking for patterns
151 pub location: i32,
152 /// maximum distance to look away from the location
153 pub distance: i32,
154 /// threshold for the search algorithm to give up at, 0.0 is perfect match 1.0 is imperfect match
155 pub threshold: f64,
156 /// maximum allowed pattern length
157 pub max_pattern_length: i32,
158 /// check for lowercase and uppercase seperately
159 pub is_case_sensitive: bool,
160 /// tokenize search patterns
161 pub tokenize: bool,
162}
163
164impl std::default::Default for Fuse {
165 fn default() -> Self {
166 Self {
167 location: 0,
168 distance: 100,
169 threshold: 0.6,
170 max_pattern_length: 32,
171 is_case_sensitive: false,
172 tokenize: false,
173 }
174 }
175}
176
177impl Fuse {
178 /// Creates a pattern object from input string.
179 ///
180 /// - Parameter string: A string from which to create the pattern object
181 /// - Returns: A tuple containing pattern metadata
182 pub fn create_pattern(&self, string: &str) -> Option<Pattern> {
183 let lowercase = string.to_lowercase();
184 let pattern = if self.is_case_sensitive {
185 string
186 } else {
187 &lowercase
188 };
189 let pattern_chars = pattern.as_bytes();
190 let len = pattern_chars.len();
191
192 if len == 0 {
193 None
194 } else {
195 let alphabet = utils::calculate_pattern_alphabet(pattern_chars);
196 let new_pattern = Pattern {
197 text: String::from(pattern),
198 len,
199 mask: 1 << (len - 1),
200 alphabet,
201 };
202 Some(new_pattern)
203 }
204 }
205
206 #[allow(clippy::single_range_in_vec_init)]
207 fn search_util(&self, pattern: &Pattern, string: &str) -> ScoreResult {
208 let string = if self.is_case_sensitive {
209 String::from(string)
210 } else {
211 string.to_ascii_lowercase()
212 };
213
214 let string_chars = string.as_bytes();
215 let text_length = string.len();
216
217 // Exact match
218 if pattern.text == string {
219 return ScoreResult {
220 score: 0.,
221 ranges: vec![0..text_length],
222 };
223 }
224
225 let location = self.location;
226 let distance = self.distance;
227 let mut threshold = self.threshold;
228
229 let mut best_location = string.find(&pattern.text).unwrap_or(0_usize);
230
231 let mut match_mask_arr = vec![0; text_length];
232
233 let mut index = string[best_location..].find(&pattern.text);
234
235 let mut score;
236
237 while index.is_some() {
238 let i = best_location + index.unwrap();
239 score = utils::calculate_score(pattern.len, 0, i as i32, location, distance);
240
241 threshold = threshold.min(score);
242
243 best_location = i + pattern.len;
244
245 index = string[best_location..].find(&pattern.text);
246
247 for idx in 0..pattern.len {
248 match_mask_arr[i + idx] = 1;
249 }
250 }
251
252 score = 1.;
253 let mut bin_max = pattern.len + text_length;
254 let mut last_bit_arr = vec![];
255
256 let text_count = string_chars.len();
257
258 for i in 0..pattern.len {
259 let mut bin_min = 0;
260 let mut bin_mid = bin_max;
261 while bin_min < bin_mid {
262 if utils::calculate_score(
263 pattern.len,
264 i as i32,
265 location,
266 location + bin_mid as i32,
267 distance,
268 ) <= threshold
269 {
270 bin_min = bin_mid;
271 } else {
272 bin_max = bin_mid;
273 }
274 bin_mid = ((bin_max - bin_min) / 2) + bin_min;
275 }
276 bin_max = bin_mid;
277
278 let start = 1.max(location - bin_mid as i32 + 1) as usize;
279 let finish = text_length.min(location as usize + bin_mid) + pattern.len;
280
281 let mut bit_arr = vec![0; finish + 2];
282
283 bit_arr[finish + 1] = (1 << i) - 1;
284
285 if start > finish {
286 continue;
287 };
288
289 let mut current_location_index: usize = 0;
290 for j in (start as u64..=finish as u64).rev() {
291 let current_location: usize = (j - 1) as usize;
292 let char_match: u64 = *(if current_location < text_count {
293 current_location_index = current_location_index
294 .checked_sub(1)
295 .unwrap_or(current_location);
296 pattern
297 .alphabet
298 .get(string.as_bytes().get(current_location_index).unwrap())
299 } else {
300 None
301 })
302 .unwrap_or(&0);
303
304 if char_match != 0 {
305 match_mask_arr[current_location] = 1;
306 }
307
308 let j2 = j as usize;
309 bit_arr[j2] = ((bit_arr[j2 + 1] << 1) | 1) & char_match;
310 if i > 0 {
311 bit_arr[j2] |= (((last_bit_arr[j2 + 1] | last_bit_arr[j2]) << 1_u64) | 1)
312 | last_bit_arr[j2 + 1];
313 };
314
315 if (bit_arr[j2] & pattern.mask) != 0 {
316 score = utils::calculate_score(
317 pattern.len,
318 i as i32,
319 location,
320 current_location as i32,
321 distance,
322 );
323
324 if score <= threshold {
325 threshold = score;
326 best_location = current_location;
327
328 if best_location as i32 <= location {
329 break;
330 };
331 }
332 }
333 }
334 if utils::calculate_score(pattern.len, i as i32 + 1, location, location, distance)
335 > threshold
336 {
337 break;
338 }
339
340 last_bit_arr = bit_arr.clone();
341 }
342
343 ScoreResult {
344 score,
345 ranges: utils::find_ranges(&match_mask_arr).unwrap(),
346 }
347 }
348
349 /// Searches for a pattern in a given string.
350 /// - Parameters:
351 /// - pattern: The pattern to search for. This is created by calling `createPattern`
352 /// - string: The string in which to search for the pattern
353 /// - Returns: Some(ScoreResult) if a match is found containing a `score` between `0.0` (exact match) and `1` (not a match), and `ranges` of the matched characters. If no match is found or if search pattern was empty will return None.
354 /// # Example:
355 /// ```no_run
356 /// use fuse_rust::{ Fuse };
357 /// let fuse = Fuse::default();
358 /// let pattern = fuse.create_pattern("some text");
359 /// fuse.search(pattern.as_ref(), "some string");
360 /// ```
361 pub fn search(&self, pattern: Option<&Pattern>, string: &str) -> Option<ScoreResult> {
362 let pattern = pattern?;
363
364 if self.tokenize {
365 let word_patterns = pattern
366 .text
367 .split_whitespace()
368 .filter_map(|x| self.create_pattern(x));
369
370 let full_pattern_result = self.search_util(pattern, string);
371
372 let (length, results) = word_patterns.fold(
373 (0, full_pattern_result),
374 |(n, mut total_result), pattern| {
375 let mut result = self.search_util(&pattern, string);
376 total_result.score += result.score;
377 total_result.ranges.append(&mut result.ranges);
378 (n + 1, total_result)
379 },
380 );
381
382 let averaged_result = ScoreResult {
383 score: results.score / (length + 1) as f64,
384 ranges: results.ranges,
385 };
386
387 if (averaged_result.score - 1.0).abs() < 0.00001 {
388 None
389 } else {
390 Some(averaged_result)
391 }
392 } else {
393 let result = self.search_util(pattern, string);
394 if (result.score - 1.0).abs() < 0.00001 {
395 None
396 } else {
397 Some(result)
398 }
399 }
400 }
401}
402
403/// Implementable trait for user defined structs, requires two methods to me implemented.
404/// A properties method that should return a list of FuseProperties.
405/// and a lookup method which should return the value of field, provided the field name.
406/// # Examples:
407/// Usage:
408/// ```no_run
409/// use fuse_rust::{ Fuse, Fuseable, FuseProperty };
410/// struct Book<'a> {
411/// title: &'a str,
412/// author: &'a str,
413/// }
414///
415/// impl Fuseable for Book<'_>{
416/// fn properties(&self) -> Vec<FuseProperty> {
417/// return vec!(
418/// FuseProperty{value: String::from("title"), weight: 0.3},
419/// FuseProperty{value: String::from("author"), weight: 0.7},
420/// )
421/// }
422/// fn lookup(&self, key: &str) -> Option<&str> {
423/// return match key {
424/// "title" => Some(self.title),
425/// "author" => Some(self.author),
426/// _ => None
427/// }
428/// }
429/// }
430/// ```
431pub trait Fuseable {
432 /// Returns a list of FuseProperty that contains the field name and its corresponding weight
433 fn properties(&self) -> Vec<FuseProperty>;
434 /// Provided a field name as argument, returns the value of the field. eg book.loopkup("author") === book.author
435 fn lookup(&self, key: &str) -> Option<&str>;
436}
437
438impl Fuse {
439 /// Searches for a text pattern in a given string.
440 /// - Parameters:
441 /// - text: the text string to search for.
442 /// - string: The string in which to search for the pattern
443 /// - Returns: Some(ScoreResult) if a match is found, containing a `score` between `0.0` (exact match) and `1` (not a match), and `ranges` of the matched characters. Otherwise if a match is not found, returns None.
444 /// # Examples:
445 /// ```no_run
446 /// use fuse_rust::{ Fuse };
447 /// let fuse = Fuse::default();
448 /// fuse.search_text_in_string("some text", "some string");
449 /// ```
450 /// **Note**: if the same text needs to be searched across many strings, consider creating the pattern once via `createPattern`, and then use the other `search` function. This will improve performance, as the pattern object would only be created once, and re-used across every search call:
451 /// ```no_run
452 /// use fuse_rust::{ Fuse };
453 /// let fuse = Fuse::default();
454 /// let pattern = fuse.create_pattern("some text");
455 /// fuse.search(pattern.as_ref(), "some string");
456 /// fuse.search(pattern.as_ref(), "another string");
457 /// fuse.search(pattern.as_ref(), "yet another string");
458 /// ```
459 pub fn search_text_in_string(&self, text: &str, string: &str) -> Option<ScoreResult> {
460 self.search(self.create_pattern(text).as_ref(), string)
461 }
462
463 /// Searches for a text pattern in an iterable containing string references.
464 ///
465 /// - Parameters:
466 /// - text: The pattern string to search for
467 /// - list: Iterable over string references
468 /// - Returns: Vec<SearchResult> containing Search results corresponding to matches found, with its `index`, its `score`, and the `ranges` of the matched characters.
469 ///
470 /// # Example:
471 /// ```no_run
472 /// use fuse_rust::{ Fuse };
473 /// let fuse = Fuse::default();
474 /// let books = [
475 /// "The Silmarillion",
476 /// "The Lock Artist",
477 /// "The Lost Symbol"
478 /// ];
479 ///
480 /// let results = fuse.search_text_in_iterable("Te silm", books.iter());
481 /// ```
482 pub fn search_text_in_iterable<It>(&self, text: &str, list: It) -> Vec<SearchResult>
483 where
484 It: IntoIterator,
485 It::Item: AsRef<str>,
486 {
487 let pattern = self.create_pattern(text);
488 let mut items = vec![];
489
490 for (index, item) in list.into_iter().enumerate() {
491 if let Some(result) = self.search(pattern.as_ref(), item.as_ref()) {
492 items.push(SearchResult {
493 index,
494 score: result.score,
495 ranges: result.ranges,
496 })
497 }
498 }
499 items.sort_unstable_by(|a, b| a.score.partial_cmp(&b.score).unwrap());
500 items
501 }
502
503 /// Searches for a text pattern in an array of `Fuseable` objects.
504 /// - Parameters:
505 /// - text: The pattern string to search for
506 /// - list: A list of `Fuseable` objects, i.e. structs implementing the Fuseable trait in which to search
507 /// - Returns: A list of `FuseableSearchResult` objects
508 /// Each `Fuseable` object contains a `properties` method which returns `FuseProperty` array. Each `FuseProperty` is a struct containing a `value` (the name of the field which should be included in the search), and a `weight` (how much "weight" to assign to the score)
509 ///
510 /// # Example
511 /// ```no_run
512 /// # use fuse_rust::{ Fuse, Fuseable, FuseProperty };
513 ///
514 /// struct Book<'a> {
515 /// title: &'a str,
516 /// author: &'a str,
517 /// }
518 ///
519 /// impl Fuseable for Book<'_>{
520 /// fn properties(&self) -> Vec<FuseProperty> {
521 /// return vec!(
522 /// FuseProperty{value: String::from("title"), weight: 0.3},
523 /// FuseProperty{value: String::from("author"), weight: 0.7},
524 /// )
525 /// }
526 ///
527 /// fn lookup(&self, key: &str) -> Option<&str> {
528 /// return match key {
529 /// "title" => Some(self.title),
530 /// "author" => Some(self.author),
531 /// _ => None
532 /// }
533 /// }
534 /// }
535 /// let books = [
536 /// Book{author: "John X", title: "Old Man's War fiction"},
537 /// Book{author: "P.D. Mans", title: "Right Ho Jeeves"},
538 /// ];
539 ///
540 /// let fuse = Fuse::default();
541 /// let results = fuse.search_text_in_fuse_list("man", &books);
542 ///
543 /// ```
544 pub fn search_text_in_fuse_list(
545 &self,
546 text: &str,
547 list: &[impl Fuseable],
548 ) -> Vec<FuseableSearchResult> {
549 let pattern = self.create_pattern(text);
550 let mut result = vec![];
551 for (index, item) in list.iter().enumerate() {
552 let mut scores = vec![];
553 let mut total_score = 0.0;
554
555 let mut property_results = vec![];
556 item.properties().iter().for_each(|property| {
557 let value = item.lookup(&property.value).unwrap_or_else(|| {
558 panic!(
559 "Lookup Failed: Lookup doesnt contain requested value => {}.",
560 &property.value
561 );
562 });
563 if let Some(result) = self.search(pattern.as_ref(), value) {
564 let weight = if (property.weight - 1.0).abs() < 0.00001 {
565 1.0
566 } else {
567 1.0 - property.weight
568 };
569 let score = if result.score == 0.0 && (weight - 1.0).abs() < f64::EPSILON {
570 0.001
571 } else {
572 result.score
573 } * weight;
574 total_score += score;
575
576 scores.push(score);
577
578 property_results.push(FResult {
579 value: String::from(&property.value),
580 score,
581 ranges: result.ranges,
582 });
583 }
584 });
585 if scores.is_empty() {
586 continue;
587 }
588
589 let count = scores.len() as f64;
590 result.push(FuseableSearchResult {
591 index,
592 score: total_score / count,
593 results: property_results,
594 })
595 }
596
597 result.sort_unstable_by(|a, b| a.score.partial_cmp(&b.score).unwrap());
598 result
599 }
600}
601
602#[cfg(feature = "rayon")]
603impl Fuse {
604 /// Asynchronously searches for a text pattern in a slice of string references.
605 ///
606 /// - Parameters:
607 /// - text: The pattern string to search for
608 /// - list: &[&str] A reference to a slice of string references.
609 /// - chunkSize: The size of a single chunk of the array. For example, if the slice has `1000` items, it may be useful to split the work into 10 chunks of 100. This should ideally speed up the search logic.
610 /// - completion: The handler which is executed upon completion
611 ///
612 /// # Example:
613 /// ```no_run
614 /// use fuse_rust::{ Fuse, SearchResult };
615 /// let fuse = Fuse::default();
616 /// let books = [
617 /// "The Silmarillion",
618 /// "The Lock Artist",
619 /// "The Lost Symbol"
620 /// ];
621 ///
622 /// fuse.search_text_in_string_list_rayon("Te silm", &books, 100 as usize, &|x: Vec<SearchResult>| {
623 /// dbg!(x);
624 /// });
625 /// ```
626 pub fn search_text_in_string_list_rayon(
627 &self,
628 text: &str,
629 list: &[&str],
630 chunk_size: usize,
631 completion: &dyn Fn(Vec<SearchResult>),
632 ) {
633 let pattern = Arc::new(self.create_pattern(text));
634
635 let item_queue = Arc::new(Mutex::new(Some(vec![])));
636 let count = list.len();
637
638 rayon::scope(|scope| {
639 (0..=count).step_by(chunk_size).for_each(|offset| {
640 let chunk = &list[offset..count.min(offset + chunk_size)];
641 let queue_ref = Arc::clone(&item_queue);
642 let pattern_ref = Arc::clone(&pattern);
643 scope.spawn(move |_| {
644 let mut chunk_items = vec![];
645
646 for (index, item) in chunk.iter().enumerate() {
647 if let Some(result) = self.search((*pattern_ref).as_ref(), item) {
648 chunk_items.push(SearchResult {
649 index: offset + index,
650 score: result.score,
651 ranges: result.ranges,
652 });
653 }
654 }
655
656 let mut inner_ref = queue_ref.lock().unwrap();
657 if let Some(item_queue) = inner_ref.as_mut() {
658 item_queue.append(&mut chunk_items);
659 }
660 });
661 });
662 });
663
664 let mut items = Arc::try_unwrap(item_queue)
665 .ok()
666 .unwrap()
667 .into_inner()
668 .unwrap()
669 .unwrap();
670 items.sort_unstable_by(|a, b| a.score.partial_cmp(&b.score).unwrap());
671 completion(items);
672 }
673 /// Asynchronously searches for a text pattern in an array of `Fuseable` objects.
674 /// - Parameters:
675 /// - text: The pattern string to search for
676 /// - list: A list of `Fuseable` objects, i.e. structs implementing the Fuseable trait in which to search
677 /// - chunkSize: The size of a single chunk of the array. For example, if the array has `1000` items, it may be useful to split the work into 10 chunks of 100. This should ideally speed up the search logic. Defaults to `100`.
678 /// - completion: The handler which is executed upon completion
679 /// Each `Fuseable` object contains a `properties` method which returns `FuseProperty` array. Each `FuseProperty` is a struct containing a `value` (the name of the field which should be included in the search), and a `weight` (how much "weight" to assign to the score)
680 ///
681 /// # Example
682 /// ```no_run
683 /// # use fuse_rust::{ Fuse, Fuseable, FuseProperty, FuseableSearchResult };
684 ///
685 /// struct Book<'a> {
686 /// title: &'a str,
687 /// author: &'a str,
688 /// }
689 ///
690 /// impl Fuseable for Book<'_>{
691 /// fn properties(&self) -> Vec<FuseProperty> {
692 /// return vec!(
693 /// FuseProperty{value: String::from("title"), weight: 0.3},
694 /// FuseProperty{value: String::from("author"), weight: 0.7},
695 /// )
696 /// }
697 ///
698 /// fn lookup(&self, key: &str) -> Option<&str> {
699 /// return match key {
700 /// "title" => Some(self.title),
701 /// "author" => Some(self.author),
702 /// _ => None
703 /// }
704 /// }
705 /// }
706 /// let books = [
707 /// Book{author: "John X", title: "Old Man's War fiction"},
708 /// Book{author: "P.D. Mans", title: "Right Ho Jeeves"},
709 /// ];
710 ///
711 /// let fuse = Fuse::default();
712 /// let results = fuse.search_text_in_fuse_list_with_chunk_size_rayon("man", &books, 1, &|x: Vec<FuseableSearchResult>| {
713 /// dbg!(x);
714 /// });
715 /// ```
716 pub fn search_text_in_fuse_list_with_chunk_size_rayon<T>(
717 &self,
718 text: &str,
719 list: &[T],
720 chunk_size: usize,
721 completion: &dyn Fn(Vec<FuseableSearchResult>),
722 ) where
723 T: Fuseable + std::marker::Sync,
724 {
725 let pattern = Arc::new(self.create_pattern(text));
726
727 let item_queue = Arc::new(Mutex::new(Some(vec![])));
728 let count = list.len();
729
730 rayon::scope(|scope| {
731 (0..=count).step_by(chunk_size).for_each(|offset| {
732 let chunk = &list[offset..count.min(offset + chunk_size)];
733 let queue_ref = Arc::clone(&item_queue);
734 let pattern_ref = Arc::clone(&pattern);
735 scope.spawn(move |_| {
736 let mut chunk_items = vec![];
737
738 for (index, item) in chunk.iter().enumerate() {
739 let mut scores = vec![];
740 let mut total_score = 0.0;
741
742 let mut property_results = vec![];
743 item.properties().iter().for_each(|property| {
744 let value = item.lookup(&property.value).unwrap_or_else(|| {
745 panic!(
746 "Lookup doesnt contain requested value => {}.",
747 &property.value
748 )
749 });
750 if let Some(result) = self.search((*pattern_ref).as_ref(), value) {
751 let weight = if (property.weight - 1.0).abs() < 0.00001 {
752 1.0
753 } else {
754 1.0 - property.weight
755 };
756 // let score = if result.score == 0.0 && weight == 1.0 { 0.001 } else { result.score } * weight;
757 let score = result.score * weight;
758 total_score += score;
759
760 scores.push(score);
761
762 property_results.push(FResult {
763 value: String::from(&property.value),
764 score,
765 ranges: result.ranges,
766 });
767 }
768 });
769
770 if scores.is_empty() {
771 continue;
772 }
773
774 let count = scores.len() as f64;
775 chunk_items.push(FuseableSearchResult {
776 index,
777 score: total_score / count,
778 results: property_results,
779 })
780 }
781
782 let mut inner_ref = queue_ref.lock().unwrap();
783 if let Some(item_queue) = inner_ref.as_mut() {
784 item_queue.append(&mut chunk_items);
785 }
786 });
787 });
788 });
789
790 let mut items = Arc::try_unwrap(item_queue)
791 .ok()
792 .unwrap()
793 .into_inner()
794 .unwrap()
795 .unwrap();
796 items.sort_unstable_by(|a, b| a.score.partial_cmp(&b.score).unwrap());
797 completion(items);
798 }
799}
800
801#[cfg(feature = "async")]
802impl Fuse {
803 /// Asynchronously searches for a text pattern in a slice of string references.
804 ///
805 /// - Parameters:
806 /// - text: The pattern string to search for
807 /// - list: &[&str] A reference to a slice of string references.
808 /// - chunkSize: The size of a single chunk of the array. For example, if the slice has `1000` items, it may be useful to split the work into 10 chunks of 100. This should ideally speed up the search logic.
809 /// - completion: The handler which is executed upon completion
810 ///
811 /// # Example:
812 /// ```no_run
813 /// use fuse_rust::{ Fuse, SearchResult };
814 /// let fuse = Fuse::default();
815 /// let books = [
816 /// "The Silmarillion",
817 /// "The Lock Artist",
818 /// "The Lost Symbol"
819 /// ];
820 ///
821 /// fuse.search_text_in_string_list("Te silm", &books, 100 as usize, &|x: Vec<SearchResult>| {
822 /// dbg!(x);
823 /// });
824 /// ```
825 pub fn search_text_in_string_list(
826 &self,
827 text: &str,
828 list: &[&str],
829 chunk_size: usize,
830 completion: &dyn Fn(Vec<SearchResult>),
831 ) {
832 let pattern = Arc::new(self.create_pattern(text));
833
834 let item_queue = Arc::new(Mutex::new(Some(vec![])));
835 let count = list.len();
836
837 thread::scope(|scope| {
838 (0..=count).step_by(chunk_size).for_each(|offset| {
839 let chunk = &list[offset..count.min(offset + chunk_size)];
840 let queue_ref = Arc::clone(&item_queue);
841 let pattern_ref = Arc::clone(&pattern);
842 scope.spawn(move |_| {
843 let mut chunk_items = vec![];
844
845 for (index, item) in chunk.iter().enumerate() {
846 if let Some(result) = self.search((*pattern_ref).as_ref(), item) {
847 chunk_items.push(SearchResult {
848 index: offset + index,
849 score: result.score,
850 ranges: result.ranges,
851 });
852 }
853 }
854
855 let mut inner_ref = queue_ref.lock().unwrap();
856 if let Some(item_queue) = inner_ref.as_mut() {
857 item_queue.append(&mut chunk_items);
858 }
859 });
860 });
861 })
862 .unwrap();
863
864 let mut items = Arc::try_unwrap(item_queue)
865 .ok()
866 .unwrap()
867 .into_inner()
868 .unwrap()
869 .unwrap();
870 items.sort_unstable_by(|a, b| a.score.partial_cmp(&b.score).unwrap());
871 completion(items);
872 }
873 /// Asynchronously searches for a text pattern in an array of `Fuseable` objects.
874 /// - Parameters:
875 /// - text: The pattern string to search for
876 /// - list: A list of `Fuseable` objects, i.e. structs implementing the Fuseable trait in which to search
877 /// - chunkSize: The size of a single chunk of the array. For example, if the array has `1000` items, it may be useful to split the work into 10 chunks of 100. This should ideally speed up the search logic. Defaults to `100`.
878 /// - completion: The handler which is executed upon completion
879 /// Each `Fuseable` object contains a `properties` method which returns `FuseProperty` array. Each `FuseProperty` is a struct containing a `value` (the name of the field which should be included in the search), and a `weight` (how much "weight" to assign to the score)
880 ///
881 /// # Example
882 /// ```no_run
883 /// # use fuse_rust::{ Fuse, Fuseable, FuseProperty, FuseableSearchResult };
884 ///
885 /// struct Book<'a> {
886 /// title: &'a str,
887 /// author: &'a str,
888 /// }
889 ///
890 /// impl Fuseable for Book<'_>{
891 /// fn properties(&self) -> Vec<FuseProperty> {
892 /// return vec!(
893 /// FuseProperty{value: String::from("title"), weight: 0.3},
894 /// FuseProperty{value: String::from("author"), weight: 0.7},
895 /// )
896 /// }
897 ///
898 /// fn lookup(&self, key: &str) -> Option<&str> {
899 /// return match key {
900 /// "title" => Some(self.title),
901 /// "author" => Some(self.author),
902 /// _ => None
903 /// }
904 /// }
905 /// }
906 /// let books = [
907 /// Book{author: "John X", title: "Old Man's War fiction"},
908 /// Book{author: "P.D. Mans", title: "Right Ho Jeeves"},
909 /// ];
910 ///
911 /// let fuse = Fuse::default();
912 /// let results = fuse.search_text_in_fuse_list_with_chunk_size("man", &books, 1, &|x: Vec<FuseableSearchResult>| {
913 /// dbg!(x);
914 /// });
915 /// ```
916 pub fn search_text_in_fuse_list_with_chunk_size<T>(
917 &self,
918 text: &str,
919 list: &[T],
920 chunk_size: usize,
921 completion: &dyn Fn(Vec<FuseableSearchResult>),
922 ) where
923 T: Fuseable + std::marker::Sync,
924 {
925 let pattern = Arc::new(self.create_pattern(text));
926
927 let item_queue = Arc::new(Mutex::new(Some(vec![])));
928 let count = list.len();
929
930 thread::scope(|scope| {
931 (0..=count).step_by(chunk_size).for_each(|offset| {
932 let chunk = &list[offset..count.min(offset + chunk_size)];
933 let queue_ref = Arc::clone(&item_queue);
934 let pattern_ref = Arc::clone(&pattern);
935 scope.spawn(move |_| {
936 let mut chunk_items = vec![];
937
938 for (index, item) in chunk.iter().enumerate() {
939 let mut scores = vec![];
940 let mut total_score = 0.0;
941
942 let mut property_results = vec![];
943 item.properties().iter().for_each(|property| {
944 let value = item.lookup(&property.value).unwrap_or_else(|| {
945 panic!(
946 "Lookup doesnt contain requested value => {}.",
947 &property.value
948 )
949 });
950 if let Some(result) = self.search((*pattern_ref).as_ref(), &value) {
951 let weight = if (property.weight - 1.0).abs() < 0.00001 {
952 1.0
953 } else {
954 1.0 - property.weight
955 };
956 // let score = if result.score == 0.0 && weight == 1.0 { 0.001 } else { result.score } * weight;
957 let score = result.score * weight;
958 total_score += score;
959
960 scores.push(score);
961
962 property_results.push(FResult {
963 value: String::from(&property.value),
964 score,
965 ranges: result.ranges,
966 });
967 }
968 });
969
970 if scores.is_empty() {
971 continue;
972 }
973
974 let count = scores.len() as f64;
975 chunk_items.push(FuseableSearchResult {
976 index,
977 score: total_score / count,
978 results: property_results,
979 })
980 }
981
982 let mut inner_ref = queue_ref.lock().unwrap();
983 if let Some(item_queue) = inner_ref.as_mut() {
984 item_queue.append(&mut chunk_items);
985 }
986 });
987 });
988 })
989 .unwrap();
990
991 let mut items = Arc::try_unwrap(item_queue)
992 .ok()
993 .unwrap()
994 .into_inner()
995 .unwrap()
996 .unwrap();
997 items.sort_unstable_by(|a, b| a.score.partial_cmp(&b.score).unwrap());
998 completion(items);
999 }
1000}