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}