diff --git a/src/lib.rs b/src/lib.rs index cf8aa92..7e50a29 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -244,6 +244,7 @@ #![forbid(unsafe_code)] #![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] +#![allow(clippy::module_name_repetitions)] #![doc(html_favicon_url = "https://www.arkiteq.ca/crates/indicium/icon.png")] #![doc(html_logo_url = "https://www.arkiteq.ca/crates/indicium/logo.png")] diff --git a/src/simple/autocomplete/context.rs b/src/simple/autocomplete/context.rs index 1328eb4..1027d26 100644 --- a/src/simple/autocomplete/context.rs +++ b/src/simple/autocomplete/context.rs @@ -100,7 +100,7 @@ impl SearchIndex { ) -> Vec { // Split search `String` into keywords according to the `SearchIndex` // settings. Force "use entire string as a keyword" option off: - let mut keywords: Vec = self.string_keywords(string, SplitContext::Searching); + let mut keywords: Vec = self.string_keywords(string, &SplitContext::Searching); // For debug builds: #[cfg(debug_assertions)] @@ -108,7 +108,7 @@ impl SearchIndex { // Pop the last keyword off the list - the keyword that we'll be // autocompleting: - if let Some(last_keyword) = keywords.pop() { + keywords.pop().map_or_else(Vec::new, |last_keyword| { // Perform `And` search for entire string without the last keyword: let search_results: BTreeSet<&K> = self.internal_search_and(keywords.as_slice()); @@ -218,10 +218,6 @@ impl SearchIndex { }) // Collect all string autocompletions into a `Vec`: .collect() - } else { - // The search string did not have a last keyword to autocomplete. - // Return an empty `Vec`: - Vec::new() - } // if + }) // map_or_else } // fn } // impl diff --git a/src/simple/autocomplete/global.rs b/src/simple/autocomplete/global.rs index 40a38af..cfc74ae 100644 --- a/src/simple/autocomplete/global.rs +++ b/src/simple/autocomplete/global.rs @@ -103,7 +103,7 @@ impl SearchIndex { ) -> Vec { // Split search `String` into keywords according to the `SearchIndex` // settings. Force "use entire string as a keyword" option off: - let mut keywords: Vec = self.string_keywords(string, SplitContext::Searching); + let mut keywords: Vec = self.string_keywords(string, &SplitContext::Searching); // For debug builds: #[cfg(debug_assertions)] @@ -111,7 +111,7 @@ impl SearchIndex { // Pop the last keyword off the list. It's the keyword that we'll be // autocompleting: - if let Some(last_keyword) = keywords.pop() { + keywords.pop().map_or_else(Vec::new, |last_keyword| { // Autocomplete the last keyword: let mut autocompletions: Vec<&KString> = self .b_tree_map @@ -216,10 +216,6 @@ impl SearchIndex { }) // Collect all string autocompletions into a `Vec`: .collect() - } else { - // The search string did not have a last keyword to autocomplete. - // Return an empty `Vec`: - Vec::new() - } // if + }) // map_or_else } // fn } // impl diff --git a/src/simple/default.rs b/src/simple/default.rs index 04fb82b..764269e 100644 --- a/src/simple/default.rs +++ b/src/simple/default.rs @@ -7,6 +7,7 @@ use std::cmp::Ord; /// `SearchIndex::new()` or `SearchIndexBuilder`. impl Default for SearchIndex { + #![allow(clippy::too_many_lines)] fn default() -> Self { Self::new( SearchType::Live, // Search type. diff --git a/src/simple/insert.rs b/src/simple/insert.rs index 3f051bf..db1e5ff 100644 --- a/src/simple/insert.rs +++ b/src/simple/insert.rs @@ -159,40 +159,33 @@ impl SearchIndex { .for_each(|keyword| // Attempt to get mutuable reference to the _keyword entry_ in // the search index: - match self.b_tree_map.get_mut(&keyword) { - // If keyword was found in search index, add _key reference_ - // for this record to _keyword entry_: - Some(keys) => { - // Check if the maximum number of keys per keyword - // (records per keyword) limit has been reached. Note - // that the `dump_keyword` does not observe this - // limit. - if keys.len() < self.maximum_keys_per_keyword - || self.dump_keyword == Some(keyword.as_ref().into()) { + if let Some(keys) = self.b_tree_map.get_mut(&keyword) { + // Check if the maximum number of keys per keyword + // (records per keyword) limit has been reached. Note + // that the `dump_keyword` does not observe this + // limit. + if keys.len() < self.maximum_keys_per_keyword + || self.dump_keyword == Some(keyword.as_ref().into()) { // If it hasn't, insert the key (record) into the // list: keys.insert(key.clone()); - } else { - // If the limit has been reached, do not insert. - // Display warning for debug builds. - #[cfg(debug_assertions)] - tracing::warn!( - "Internal table limit of {} keys per keyword has been reached on insert. \ - Record was not attached to `{}` keyword. \ - This will impact accuracy of results. \ - For this data set, consider using a more comprehensive search solution like MeiliSearch.", - self.maximum_keys_per_keyword, - keyword, - ); // warn! - } // if - }, // Some - // If keyword was not found in search index, initialize - // _keyword entry_ with the _key reference_ for this record: - None => { - let mut b_tree_set = BTreeSet::new(); - b_tree_set.insert(key.clone()); - self.b_tree_map.insert(keyword.as_ref().into(), b_tree_set); - }, // None + } else { + // If the limit has been reached, do not insert. + // Display warning for debug builds. + #[cfg(debug_assertions)] + tracing::warn!( + "Internal table limit of {} keys per keyword has been reached on insert. \ + Record was not attached to `{}` keyword. \ + This will impact accuracy of results. \ + For this data set, consider using a more comprehensive search solution like MeiliSearch.", + self.maximum_keys_per_keyword, + keyword, + ); // warn! + } // if + } else { + let mut b_tree_set = BTreeSet::new(); + b_tree_set.insert(key.clone()); + self.b_tree_map.insert(keyword.as_ref().into(), b_tree_set); } // match ); // for_each } // fn diff --git a/src/simple/internal/eddie/eddie_autocomplete.rs b/src/simple/internal/eddie/eddie_autocomplete.rs index 6668d6e..ac6f73d 100644 --- a/src/simple/internal/eddie/eddie_autocomplete.rs +++ b/src/simple/internal/eddie/eddie_autocomplete.rs @@ -91,10 +91,11 @@ impl SearchIndex { pub fn eddie_autocomplete(&self, keyword: &str) -> Vec<&str> { // If case sensitivity set, leave case intact. Otherwise, normalize // keyword to lower case: - let keyword = match self.case_sensitive { - true => keyword.to_string(), - false => keyword.to_lowercase(), - }; // match + let keyword = if self.case_sensitive { + keyword.to_string() + } else { + keyword.to_lowercase() + }; // if // Call global autocompletion provider: self.eddie_global_autocomplete(&keyword) diff --git a/src/simple/internal/eddie/eddie_context_autocomplete.rs b/src/simple/internal/eddie/eddie_context_autocomplete.rs index 2cd3460..149da79 100644 --- a/src/simple/internal/eddie/eddie_context_autocomplete.rs +++ b/src/simple/internal/eddie/eddie_context_autocomplete.rs @@ -63,8 +63,9 @@ impl SearchIndex { // Attempt to find the top matches for the user's (partial) keyword // using the selected string similarity metric defined in the // `SearchIndex`: - if let Some(eddie_metric) = &self.eddie_metric { - match eddie_metric { + self.eddie_metric + .as_ref() + .map_or_else(Vec::new, |eddie_metric| match eddie_metric { EddieMetric::DamerauLevenshtein => self .eddie_autocomplete_context_damerau_levenshtein( index_range, @@ -84,12 +85,6 @@ impl SearchIndex { EddieMetric::Levenshtein => self .eddie_autocomplete_context_levenshtein(index_range, key_set, user_keyword) .collect(), - } // match - } else { - // No string similarity metric was defined in the `SearchIndex` - // settings. Fuzzy string matching effectively turned off. - // Return an empty `Vec` to the caller: - vec![] - } // if + }) // map_or_else } // fn } // impl diff --git a/src/simple/internal/eddie/eddie_global_autocomplete.rs b/src/simple/internal/eddie/eddie_global_autocomplete.rs index f01832c..0b7e9d2 100644 --- a/src/simple/internal/eddie/eddie_global_autocomplete.rs +++ b/src/simple/internal/eddie/eddie_global_autocomplete.rs @@ -62,11 +62,11 @@ impl SearchIndex { // Attempt to find the top matches for the user's (partial) keyword // using the selected string similarity metric defined in the // `SearchIndex`: - self.eddie_metric.as_ref().map_or_else( + self.eddie_metric.as_ref().map_or_else( // No string similarity metric was defined in the `SearchIndex` // settings. Fuzzy string matching effectively turned off. // Return an empty `Vec` to the caller: - || vec![], + std::vec::Vec::new, |eddie_metric| match eddie_metric { EddieMetric::DamerauLevenshtein => self .eddie_autocomplete_global_damerau_levenshtein(index_range, user_keyword) @@ -83,7 +83,7 @@ impl SearchIndex { EddieMetric::Levenshtein => self .eddie_autocomplete_global_levenshtein(index_range, user_keyword) .collect(), - } // match + }, // match ) // map_or_else } // fn } // impl diff --git a/src/simple/internal/eddie/eddie_global_keyword.rs b/src/simple/internal/eddie/eddie_global_keyword.rs index ef240be..d1a002d 100644 --- a/src/simple/internal/eddie/eddie_global_keyword.rs +++ b/src/simple/internal/eddie/eddie_global_keyword.rs @@ -57,8 +57,9 @@ impl SearchIndex { // Attempt to find the closest match for the user's keyword using the // selected string similarity metric defined in the `SearchIndex`: - if let Some(eddie_metric) = &self.eddie_metric { - match eddie_metric { + self.eddie_metric + .as_ref() + .and_then(|eddie_metric| match eddie_metric { EddieMetric::DamerauLevenshtein => { self.eddie_keyword_global_damerau_levenshtein(index_range, user_keyword) } @@ -72,12 +73,6 @@ impl SearchIndex { EddieMetric::Levenshtein => { self.eddie_keyword_global_levenshtein(index_range, user_keyword) } - } // match - } else { - // No string similarity metric was defined in the `SearchIndex` - // settings. Fuzzy string matching effectively turned off. - // Return a `None` to the caller: - None - } // if + }) // map_or } // fn } // impl diff --git a/src/simple/internal/eddie/eddie_keyword.rs b/src/simple/internal/eddie/eddie_keyword.rs index 0ee4259..06e64d3 100644 --- a/src/simple/internal/eddie/eddie_keyword.rs +++ b/src/simple/internal/eddie/eddie_keyword.rs @@ -86,10 +86,11 @@ impl SearchIndex { pub fn eddie_keyword(&self, keyword: &str) -> Option<&str> { // If case sensitivity set, leave case intact. Otherwise, normalize // keyword to lower case: - let keyword = match self.case_sensitive { - true => keyword.to_string(), - false => keyword.to_lowercase(), - }; // match + let keyword = if self.case_sensitive { + keyword.to_string() + } else { + keyword.to_lowercase() + }; // if // Call global keyword subtitution provider: self.eddie_global_keyword(&keyword) diff --git a/src/simple/internal/fuzzy_top_scores/with_capacity.rs b/src/simple/internal/fuzzy_top_scores/with_capacity.rs index dee6bbe..a46d5ee 100644 --- a/src/simple/internal/fuzzy_top_scores/with_capacity.rs +++ b/src/simple/internal/fuzzy_top_scores/with_capacity.rs @@ -14,14 +14,14 @@ use std::{cmp::Ord, cmp::PartialOrd, hash::Hash}; impl<'a, K: Hash + Ord, S: PartialOrd> FuzzyTopScores<'a, K, S> { // ------------------------------------------------------------------------- - // + #![allow(clippy::default_trait_access)] /// Instantiates a new "top scores" struct with the caller provided /// capacity. If the caller wants to track the "top 10 matches" for a user /// provided keyword, the caller would call `FuzzyTopScores::with_capacity(10)`. pub(crate) fn with_capacity(capacity: usize) -> FuzzyTopScores<'a, K, S> { FuzzyTopScores { - top: HashMap::with_capacity_and_hasher(capacity, std::default::Default::default()), // HashMap + top: HashMap::with_capacity_and_hasher(capacity, std::default::Default::default()), bottom: None, capacity, } // FuzzyTopScores diff --git a/src/simple/internal/indexable_keywords.rs b/src/simple/internal/indexable_keywords.rs index 4eb79ac..0b63cb8 100644 --- a/src/simple/internal/indexable_keywords.rs +++ b/src/simple/internal/indexable_keywords.rs @@ -34,7 +34,7 @@ impl SearchIndex { // settings. Note that `string_keywords` will allow "use entire // string as a keyword" if enabled in user settings. Flatten the // string's keywords into the `HashSet`: - .flat_map(|string| self.string_keywords(&string, SplitContext::Indexing)) + .flat_map(|string| self.string_keywords(&string, &SplitContext::Indexing)) // Collect all keywords into a `HashSet`: .collect() } // fn diff --git a/src/simple/internal/search.rs b/src/simple/internal/search.rs index 1465c5e..c077bab 100644 --- a/src/simple/internal/search.rs +++ b/src/simple/internal/search.rs @@ -39,21 +39,19 @@ impl SearchIndex { }; // if */ // Attempt to get matching keys for the search keyword from BTreeMap: - let search_results: BTreeSet<&K> = if let Some(keys) = self.b_tree_map.get(keyword) { - // Attempt to get matching keys for search keyword: - keys - // Iterate over all matching keys and only return - // `maximum_search_results` number of keys: - .iter() - // Only return `maximum_search_results` number of keys: - .take(self.maximum_keys_per_keyword) - // Insert a reference to each resulting key into the hash set: - .collect() - } else { - // The search keyword did not result in any matches. Return an - // empty `BTreeSet`: - BTreeSet::new() - }; // if + let search_results: BTreeSet<&K> = + self.b_tree_map + .get(keyword) + .map_or_else(BTreeSet::new, |keys| { + keys + // Iterate over all matching keys and only return + // `maximum_search_results` number of keys: + .iter() + // Only return `maximum_search_results` number of keys: + .take(self.maximum_keys_per_keyword) + // Insert a reference to each resulting key into hash set: + .collect() + }); // map_or_else // For debug builds: #[cfg(debug_assertions)] diff --git a/src/simple/internal/search_and.rs b/src/simple/internal/search_and.rs index f1965c0..6760e56 100644 --- a/src/simple/internal/search_and.rs +++ b/src/simple/internal/search_and.rs @@ -32,31 +32,27 @@ impl SearchIndex { Some(keyword_results) => { search_results = Some( // Check if `search_results` is already populated: - match &search_results { - // If `search_results` is is not empty, intersect - // the current keyword's results with the master - // search results: - Some(search_results) => search_results - // Iterate over each search result record: - .iter() - // Intersect the search result record with the - // keyword results. If the search result record - // doesn't exist in this keyword's results, - // filter it out: - .filter(|key| keyword_results.contains(key)) - // Copy each key from the `Intersection` - // iterator or we'll get a doubly-referenced - // `&&K` key: - .copied() - // And collect each key into a `BTreeSet` that - // will become the new `search_results`: - .collect(), - - // If `search_results` is empty, initialize it with - // the first keyword's full search results: - None => self.internal_keyword_search(keyword), - }, // match - ); + search_results.as_ref().map_or_else( + || self.internal_keyword_search(keyword), + |search_results| { + search_results + // Iterate over each search result record: + .iter() + // Intersect the search result record with the + // keyword results. If the search result record + // doesn't exist in this keyword's results, + // filter it out: + .filter(|key| keyword_results.contains(key)) + // Copy each key from the `Intersection` + // iterator or we'll get a doubly-referenced + // `&&K` key: + .copied() + // And collect each key into a `BTreeSet` that + // will become the new `search_results`: + .collect() + }, + ), // map_or_else + ); // Some } // Some // Any keyword that returns no results will short-circuit @@ -80,11 +76,6 @@ impl SearchIndex { } // if // Return search results: - match search_results { - // If master `search_results` is not empty, return it: - Some(search_results) => search_results, - // If master `search_results` is empty, return an empty `BTreeSet`: - None => BTreeSet::new(), - } // match + search_results.map_or_else(BTreeSet::new, |search_results| search_results) } // fn } // impl diff --git a/src/simple/internal/search_top_scores/with_capacity.rs b/src/simple/internal/search_top_scores/with_capacity.rs index 0352d12..294194a 100644 --- a/src/simple/internal/search_top_scores/with_capacity.rs +++ b/src/simple/internal/search_top_scores/with_capacity.rs @@ -14,14 +14,14 @@ use std::{cmp::Ord, hash::Hash}; impl<'a, K: Hash + Ord> SearchTopScores<'a, K> { // ------------------------------------------------------------------------- - // + #![allow(clippy::default_trait_access)] /// Instantiates a new "top scores" struct with the caller provided /// capacity. If the caller wants to track the "top 10 matches" for a user /// provided keyword, the caller would call `SearchTopScores::with_capacity(10)`. pub(crate) fn with_capacity(capacity: usize) -> SearchTopScores<'a, K> { SearchTopScores { - top: HashMap::with_capacity_and_hasher(capacity, std::default::Default::default()), // HashMap + top: HashMap::with_capacity_and_hasher(capacity, std::default::Default::default()), bottom: None, capacity, } // SearchTopScores diff --git a/src/simple/internal/string_keywords.rs b/src/simple/internal/string_keywords.rs index 3b81006..e9db5af 100644 --- a/src/simple/internal/string_keywords.rs +++ b/src/simple/internal/string_keywords.rs @@ -26,17 +26,11 @@ pub enum SplitContext { pub fn exclude_keyword(keyword: &str, exclude_keywords: &Option>) -> bool { // Check to see if there's any keywords in the exclusion list: - if let Some(exclude_keywords) = exclude_keywords { - // If there are keywords to be excluded, scan the list to see if this - // keyword is in it. If so, filter it out (true = filter, false = keep): + exclude_keywords.as_ref().map_or(false, |exclude_keywords| { exclude_keywords .iter() .any(|excluded| excluded.as_str() == keyword) - } else { - // If there are no keywords to be excluded, always allow the keyword - // (true = filter, false = keep): - false - } // if + }) // map_or } // fn // ----------------------------------------------------------------------------- @@ -72,39 +66,38 @@ impl SearchIndex { /// keywords that don't meet the defined length restrictions, and remove /// excluded keywords. - pub(crate) fn string_keywords(&self, string: &str, context: SplitContext) -> Vec { + pub(crate) fn string_keywords(&self, string: &str, context: &SplitContext) -> Vec { // If case sensitivity set, leave case intact. Otherwise, normalize the // entire string to lower case: - let string: KString = match self.case_sensitive { - true => KString::from_ref(string), - false => KString::from(string.to_lowercase()), - }; // match + let string: KString = if self.case_sensitive { + KString::from_ref(string) + } else { + KString::from(string.to_lowercase()) + }; // if // Split the the string into keywords: - let mut keywords: Vec = if let Some(split_pattern) = &self.split_pattern { - // Use the split pattern (a `Vec`) to split the `KString` into - // keywords and filter the results: - string - // Split the `KString` into smaller strings / keywords on - // specified characters: - .split(split_pattern.as_slice()) - // Only keep the keyword if it's longer than the minimum length - // and shorter than the maximum length: - .filter(|keyword| { - let chars = keyword.chars().count(); - chars >= self.minimum_keyword_length && chars <= self.maximum_keyword_length - }) // filter - // Only keep the keyword if it's not in the exclusion list: - .filter(|keyword| !exclude_keyword(keyword, &self.exclude_keywords)) // filter - // Copy string from reference: - .map(KString::from_ref) - // Collect all keywords into a `Vec`: - .collect() - } else { - // Split pattern was set to `None`, so do not split the `KString` - // into keywords. Return an empty `Vec` instead: - Vec::new() - }; + let mut keywords: Vec = + self.split_pattern + .as_ref() + .map_or_else(Vec::new, |split_pattern| { + string + // Split the `KString` into smaller strings / keywords on + // specified characters: + .split(split_pattern.as_slice()) + // Only keep the keyword if it's longer than the minimum length + // and shorter than the maximum length: + .filter(|keyword| { + let chars = keyword.chars().count(); + chars >= self.minimum_keyword_length + && chars <= self.maximum_keyword_length + }) // filter + // Only keep the keyword if it's not in the exclusion list: + .filter(|keyword| !exclude_keyword(keyword, &self.exclude_keywords)) // filter + // Copy string from reference: + .map(KString::from_ref) + // Collect all keywords into a `Vec`: + .collect() + }); // map_or_else // Using the whole string as a keyword: // @@ -120,7 +113,7 @@ impl SearchIndex { // If we're searching, keep the whole string if there is no split // pattern defined. We'll search by the whole search string without // any keyword splitting: - if context == SplitContext::Searching + if context == &SplitContext::Searching && self.split_pattern.is_none() && chars >= self.minimum_keyword_length { @@ -131,7 +124,7 @@ impl SearchIndex { // criteria: 1) we're using whole strings as keywords, 2) it's shorter // than the maximum, and 3) the keyword is not in the exclusion list. } else if let Some(maximum_string_length) = self.maximum_string_length { - if context == SplitContext::Indexing + if context == &SplitContext::Indexing && chars >= self.minimum_keyword_length && chars <= maximum_string_length && !exclude_keyword(&string, &self.exclude_keywords) diff --git a/src/simple/remove.rs b/src/simple/remove.rs index a11cc63..3170859 100644 --- a/src/simple/remove.rs +++ b/src/simple/remove.rs @@ -113,21 +113,13 @@ impl SearchIndex { for keyword in keywords { // Attempt to get mutuable reference to the _keyword entry_ in // the search index: - let is_empty = if let Some(keys) = self.b_tree_map.get_mut(&keyword) { + let is_empty = self.b_tree_map.get_mut(&keyword).map_or(false, |keys| { // If keyword found in search index, remove the _key // reference_ for this record from _keyword entry_: keys.remove(key); // Return whether the _keyword entry_ is now empty or not: keys.is_empty() - } else { - // If keyword not found in search index, signal that we - // should **not** remove the _keyword entry_ because that - // would result in an error: - false - }; // if - // If the _keyword entry_ no longer contains any _key - // references_, it is empty and we should remove the keyword - // from the search index: + }); // map_or if is_empty { self.b_tree_map.remove(&keyword); } diff --git a/src/simple/search/and.rs b/src/simple/search/and.rs index 5a2fef9..04ba2fc 100644 --- a/src/simple/search/and.rs +++ b/src/simple/search/and.rs @@ -97,7 +97,7 @@ impl SearchIndex { // Split search `String` into keywords (according to the `SearchIndex` // settings). `string_keywords` will **not** allow "use entire string as // a keyword," even if enabled in user settings: - let keywords: Vec = self.string_keywords(string, SplitContext::Searching); + let keywords: Vec = self.string_keywords(string, &SplitContext::Searching); // For debug builds: #[cfg(debug_assertions)] @@ -118,32 +118,27 @@ impl SearchIndex { Some(keyword_results) => { search_results = Some( // Check if `search_results` is already populated: - match &search_results { - // If `search_results` is is not empty, intersect - // the current keyword's results with the master - // search results: - Some(search_results) => search_results - // Iterate over each search result record: - .iter() - // Intersect the search result record with the - // keyword results. If the search result record - // doesn't exist in this keyword's results, - // filter it out: - .filter(|key| keyword_results.contains(key)) - // Clone each key from the `Intersection` - // iterator or we'll get a doubly-referenced - // `&&K` key: - .copied() - // And collect each key into a `BTreeSet` that - // will become the new `search_results`: - .collect(), - - // If `search_results` is currently empty, - // initialize it with the first keyword's full - // search results: - None => self.internal_keyword_search(&keyword), - }, // match - ); + search_results.as_ref().map_or_else( + || self.internal_keyword_search(&keyword), + |search_results| { + search_results + // Iterate over each search result record: + .iter() + // Intersect the search result record with the + // keyword results. If the search result record + // doesn't exist in this keyword's results, + // filter it out: + .filter(|key| keyword_results.contains(key)) + // Clone each key from the `Intersection` + // iterator or we'll get a doubly-referenced + // `&&K` key: + .copied() + // And collect each key into a `BTreeSet` that + // will become the new `search_results`: + .collect() + }, + ), // map_or_else + ); // Some } // Some // Any keyword that returns no results will short-circuit @@ -153,15 +148,11 @@ impl SearchIndex { } // for_each // Return search results: - match search_results { - // If `search_results` is is not empty, convert the `BTreeMap` to a - // `Vec` for caller while observing `maximum_search_results`: - Some(search_results) => search_results + search_results.map_or_else(Vec::new, |search_results| { + search_results .into_iter() .take(*maximum_search_results) - .collect(), - // If `search_results` is empty, return an empty `Vec`: - None => Vec::new(), - } // match + .collect() + }) // map_or_else } // fn } // impl diff --git a/src/simple/search/keyword.rs b/src/simple/search/keyword.rs index 9191337..f0e25a6 100644 --- a/src/simple/search/keyword.rs +++ b/src/simple/search/keyword.rs @@ -101,33 +101,28 @@ impl SearchIndex { pub(crate) fn search_keyword(&self, maximum_search_results: &usize, keyword: &str) -> Vec<&K> { // If case sensitivity set, leave case intact. Otherwise, normalize // keyword to lower case: - let keyword = match self.case_sensitive { - true => keyword.to_string(), - false => keyword.to_lowercase(), - }; // match + let keyword = if self.case_sensitive { + keyword.to_string() + } else { + keyword.to_lowercase() + }; // if // For debug builds: #[cfg(debug_assertions)] tracing::debug!("searching: {}", keyword); // Attempt to get matching keys for the search keyword from BTreeMap: - if let Some(keys) = self.b_tree_map.get(&KString::from_ref(&keyword)) { - // Attempt to get matching keys for search keyword: - keys - // Iterate over all matching keys and only return - // `maximum_search_results` number of keys: - .iter() - // Only return `maximum_search_results` number of keys: - .take(*maximum_search_results) - // Insert a reference to each resulting key into the hash set: - .collect() - - // -> If fuzzy matching were to be implemented for - // `indicium::simple` it would probably be put here. <- - } else { - // The search keyword did not result in any matches. Return an - // empty `Vec`: - Vec::new() - } // if + self.b_tree_map + .get(&KString::from_ref(&keyword)) + .map_or_else(Vec::new, |keys| { + keys + // Iterate over all matching keys and only return + // `maximum_search_results` number of keys: + .iter() + // Only return `maximum_search_results` number of keys: + .take(*maximum_search_results) + // Insert a reference to each resulting key into the hash set: + .collect() + }) // map_or_else } // fn } // impl diff --git a/src/simple/search/live.rs b/src/simple/search/live.rs index c5690ba..0bd8931 100644 --- a/src/simple/search/live.rs +++ b/src/simple/search/live.rs @@ -105,7 +105,7 @@ impl SearchIndex { pub(crate) fn search_live(&self, maximum_search_results: &usize, string: &str) -> BTreeSet<&K> { // Split search `String` into keywords according to the `SearchIndex` // settings. Force "use entire string as a keyword" option off: - let mut keywords: Vec = self.string_keywords(string, SplitContext::Searching); + let mut keywords: Vec = self.string_keywords(string, &SplitContext::Searching); // For debug builds: #[cfg(debug_assertions)] @@ -113,230 +113,195 @@ impl SearchIndex { // Pop the last keyword off the list - the keyword that we'll be // autocompleting: - if let Some(last_keyword) = keywords.pop() { - // How we combine `search_results` and `autocomplete_options` - // together depends on how many keywords there are in the search - // string. Strings that have only a single keyword, and strings - // that have multiple keywords must be handled differently: + keywords.pop().map_or_else(BTreeSet::new, |last_keyword| { + if keywords.is_empty() { + let mut search_results: BTreeSet<&K> = self + .b_tree_map + // Get matching keywords starting with (partial) keyword + // string: + .range(last_keyword.clone()..) + // We did not specify an end bound for our `range` + // function (see above.) `range` will return _every_ + // keyword greater than the supplied keyword. The below + // `take_while` will effectively break iteration when we + // reach a keyword that does not start with our supplied + // (partial) keyword. + .take_while(|(keyword, _keys)| keyword.starts_with(&*last_keyword)) + // Only return `maximum_search_results` number of keys: + .take(*maximum_search_results) + // We're not interested in the `keyword` since we're + // returning `&K` keys. Return only `&K` from the tuple. + // Flatten the `BTreeSet` from each autocomplete + // keyword option into our collection: + .flat_map(|(_keyword, keys)| keys) + // Collect all keyword search results into a `BTreeSet`: + .collect(); - match keywords.len() { - // Consider this example search string: `t`. - // - // Depending on the data-set, autocomplete options `trouble` and - // `tribble` may be given. - // - // There are no previous keywords to intersect with, just the - // autocomplete options for the letter `t`. If we attempt to - // intersect this with an empty `search_results`, no keys will - // ever be returned. So we must handle this scenario - // differently. We will return the keys for these autocomplete - // options without further processing: - 0 => { - let mut search_results: BTreeSet<&K> = self - .b_tree_map - // Get matching keywords starting with (partial) keyword - // string: - .range(last_keyword.clone()..) - // We did not specify an end bound for our `range` - // function (see above.) `range` will return _every_ - // keyword greater than the supplied keyword. The below - // `take_while` will effectively break iteration when we - // reach a keyword that does not start with our supplied - // (partial) keyword. - .take_while(|(keyword, _keys)| keyword.starts_with(&*last_keyword)) // take_while - // Only return `maximum_search_results` number of keys: - .take(*maximum_search_results) - // We're not interested in the `keyword` since we're - // returning `&K` keys. Return only `&K` from the tuple. - // Flatten the `BTreeSet` from each autocomplete - // keyword option into our collection: + // If `eddie` fuzzy matching enabled, examine the search + // results before returning them: + #[cfg(feature = "eddie")] + if search_results.is_empty() { + // No search results were found for the user's last + // (partial) keyword. Attempt to use fuzzy string + // search to find other options: + search_results = self + .eddie_context_autocomplete(&search_results, &last_keyword) + .into_iter() + // `strsim_autocomplete` returns both the keyword + // and keys. We're searching for the last (partial) + // keyword, so discard the keywords. Flatten the + // `BTreeSet` from each search result into our + // collection: .flat_map(|(_keyword, keys)| keys) - // Collect all keyword search results into a `BTreeSet`: + // Only return `maximum_search_results` number of + // keys: + .take(*maximum_search_results) + // Collect all keyword autocompletions into a + // `BTreeSet`: .collect(); + } // if - // If `eddie` fuzzy matching enabled, examine the search - // results before returning them: - #[cfg(feature = "eddie")] - if search_results.is_empty() { - // No search results were found for the user's last - // (partial) keyword. Attempt to use fuzzy string - // search to find other options: - search_results = self - .eddie_context_autocomplete(&search_results, &last_keyword) // eddie_context_autocomplete - .into_iter() - // `strsim_autocomplete` returns both the keyword - // and keys. We're searching for the last (partial) - // keyword, so discard the keywords. Flatten the - // `BTreeSet` from each search result into our - // collection: - .flat_map(|(_keyword, keys)| keys) - // Only return `maximum_search_results` number of - // keys: - .take(*maximum_search_results) - // Collect all keyword autocompletions into a - // `BTreeSet`: - .collect(); - } // if - - // If `strsim` fuzzy matching enabled, examine the search - // results before returning them: - #[cfg(all(feature = "strsim", not(feature = "eddie")))] - if search_results.is_empty() { - // No search results were found for the user's last - // (partial) keyword. Attempt to use fuzzy string - // search to find other options: - search_results = self - .strsim_context_autocomplete(&search_results, &last_keyword) // strsim_context_autocomplete - .into_iter() - // `strsim_autocomplete` returns both the keyword - // and keys. We're searching for the last (partial) - // keyword, so discard the keywords. Flatten the - // `BTreeSet` from each search result into our - // collection: - .flat_map(|(_keyword, keys)| keys) - // Only return `maximum_search_results` number of - // keys: - .take(*maximum_search_results) - // Collect all keyword autocompletions into a - // `BTreeSet`: - .collect() - } // if + // If `strsim` fuzzy matching enabled, examine the search + // results before returning them: + #[cfg(all(feature = "strsim", not(feature = "eddie")))] + if search_results.is_empty() { + // No search results were found for the user's last + // (partial) keyword. Attempt to use fuzzy string + // search to find other options: + search_results = self + .strsim_context_autocomplete(&search_results, &last_keyword) + .into_iter() + // `strsim_autocomplete` returns both the keyword + // and keys. We're searching for the last (partial) + // keyword, so discard the keywords. Flatten the + // `BTreeSet` from each search result into our + // collection: + .flat_map(|(_keyword, keys)| keys) + // Only return `maximum_search_results` number of + // keys: + .take(*maximum_search_results) + // Collect all keyword autocompletions into a + // `BTreeSet`: + .collect() + } // if - // Return search results to caller: - search_results - } // 0 + // Return search results to caller: + search_results + } else { + // Perform `And` search for entire string, excluding the + // last (partial) keyword: + let search_results: BTreeSet<&K> = self.internal_search_and(keywords.as_slice()); - // Consider this example search string: `Shatner t`. - // - // Depending on the data-set, autocomplete options for `t` might - // be `trouble` and `tribble`. However, in this example there is - // a previous keyword: `Shatner`. - // - // This match arm will intersect the results from each - // autocomplete option with `Shatner`. For both `trouble` and - // `tribble` autocomplete options, only keys that also exist for - // `Shatner` will be returned: - _ => { - // Perform `And` search for entire string, excluding the - // last (partial) keyword: - let search_results: BTreeSet<&K> = - self.internal_search_and(keywords.as_slice()); + // Get keys for the last (partial) keyword: + let mut last_results: BTreeSet<&K> = self + .b_tree_map + // Get matching keywords starting with (partial) keyword + // string: + .range(last_keyword.clone()..) + // We did not specify an end bound for our `range` + // function (see above.) `range` will return _every_ + // keyword greater than the supplied keyword. The below + // `take_while` will effectively break iteration when we + // reach a keyword that does not start with our supplied + // (partial) keyword. + .take_while(|(keyword, _keys)| keyword.starts_with(&*last_keyword)) + // Only keep this autocompletion if hasn't already been + // used as a keyword: + .filter(|(keyword, _keys)| !keywords.contains(keyword)) + // We're not interested in the `keyword` since we're + // returning `&K` keys. Return only `&K` from the tuple. + // Flatten the `BTreeSet` from each autocomplete + // keyword option into individual `K` keys: + .flat_map(|(_key, value)| value) + // Intersect the key results from the autocomplete + // options (produced from this iterator) with the search + // results produced above: + .filter(|key| search_results.contains(key)) + // Only return `maximum_search_results` number of keys: + .take(*maximum_search_results) + // Collect all keyword autocompletions into a + // `BTreetSet`: + .collect(); - // Get keys for the last (partial) keyword: - let mut last_results: BTreeSet<&K> = self - .b_tree_map - // Get matching keywords starting with (partial) keyword - // string: - .range(last_keyword.clone()..) - // We did not specify an end bound for our `range` - // function (see above.) `range` will return _every_ - // keyword greater than the supplied keyword. The below - // `take_while` will effectively break iteration when we - // reach a keyword that does not start with our supplied - // (partial) keyword. - .take_while(|(keyword, _keys)| keyword.starts_with(&*last_keyword)) // take_while - // Only keep this autocompletion if hasn't already been - // used as a keyword: + // If fuzzy string searching enabled, examine the search + // results before returning them: + #[cfg(feature = "eddie")] + if last_results.is_empty() { + // No search results were found for the user's last + // (partial) keyword. Attempt to use fuzzy string + // search to find other options: + last_results = self + .eddie_context_autocomplete(&search_results, &last_keyword) + .into_iter() + // Only keep this result if hasn't already been used + // as a keyword: .filter(|(keyword, _keys)| !keywords.contains(keyword)) - // We're not interested in the `keyword` since we're - // returning `&K` keys. Return only `&K` from the tuple. - // Flatten the `BTreeSet` from each autocomplete - // keyword option into individual `K` keys: - .flat_map(|(_key, value)| value) // Intersect the key results from the autocomplete - // options (produced from this iterator) with the search - // results produced above: - .filter(|key| search_results.contains(key)) - // Only return `maximum_search_results` number of keys: + // options (produced from this iterator) with the + // search results produced at the top: + .map(|(keyword, keys)| { + ( + keyword, + keys.iter() + .filter(|key| search_results.contains(key)) + .collect::>(), + ) + }) // map + // Autocomplete returns both the keyword and keys. + // We're searching for the last (partial) keyword, + // so discard the keywords. Flatten the + // `BTreeSet` from each search result into our + // collection: + .flat_map(|(_keyword, keys)| keys) + // Only return `maximum_search_results` number of + // keys: .take(*maximum_search_results) // Collect all keyword autocompletions into a - // `BTreetSet`: + // `BTreeSet`: .collect(); + } // if - // If fuzzy string searching enabled, examine the search - // results before returning them: - #[cfg(feature = "eddie")] - if last_results.is_empty() { - // No search results were found for the user's last - // (partial) keyword. Attempt to use fuzzy string - // search to find other options: - last_results = self - .eddie_context_autocomplete(&search_results, &last_keyword) // eddie_context_autocomplete - .into_iter() - // Only keep this result if hasn't already been used - // as a keyword: - .filter(|(keyword, _keys)| !keywords.contains(keyword)) - // Intersect the key results from the autocomplete - // options (produced from this iterator) with the - // search results produced at the top: - .map(|(keyword, keys)| { - ( - keyword, - keys.iter() - .filter(|key| search_results.contains(key)) - .collect::>(), - ) - }) // map - // Autocomplete returns both the keyword and keys. - // We're searching for the last (partial) keyword, - // so discard the keywords. Flatten the - // `BTreeSet` from each search result into our - // collection: - .flat_map(|(_keyword, keys)| keys) - // Only return `maximum_search_results` number of - // keys: - .take(*maximum_search_results) - // Collect all keyword autocompletions into a - // `BTreeSet`: - .collect(); - } // if - - // If fuzzy string searching enabled, examine the search - // results before returning them: - #[cfg(all(feature = "strsim", not(feature = "eddie")))] - if last_results.is_empty() { - // No search results were found for the user's last - // (partial) keyword. Attempt to use fuzzy string - // search to find other options: - last_results = self - .strsim_context_autocomplete(&search_results, &last_keyword) // strsim_context_autocomplete - .into_iter() - // Only keep this result if hasn't already been used - // as a keyword: - .filter(|(keyword, _keys)| !keywords.contains(keyword)) - // Intersect the key results from the autocomplete - // options (produced from this iterator) with the - // search results produced at the top: - .map(|(keyword, keys)| { - ( - keyword, - keys.iter() - .filter(|key| search_results.contains(key)) - .collect::>(), - ) - }) // map - // Autocomplete returns both the keyword and keys. - // We're searching for the last (partial) keyword, - // so discard the keywords. Flatten the - // `BTreeSet` from each search result into our - // collection: - .flat_map(|(_keyword, keys)| keys) - // Only return `maximum_search_results` number of - // keys: - .take(*maximum_search_results) - // Collect all keyword autocompletions into a - // `BTreeSet`: - .collect() - } // if + // If fuzzy string searching enabled, examine the search + // results before returning them: + #[cfg(all(feature = "strsim", not(feature = "eddie")))] + if last_results.is_empty() { + // No search results were found for the user's last + // (partial) keyword. Attempt to use fuzzy string + // search to find other options: + last_results = self + .strsim_context_autocomplete(&search_results, &last_keyword) + .into_iter() + // Only keep this result if hasn't already been used + // as a keyword: + .filter(|(keyword, _keys)| !keywords.contains(keyword)) + // Intersect the key results from the autocomplete + // options (produced from this iterator) with the + // search results produced at the top: + .map(|(keyword, keys)| { + ( + keyword, + keys.iter() + .filter(|key| search_results.contains(key)) + .collect::>(), + ) + }) // map + // Autocomplete returns both the keyword and keys. + // We're searching for the last (partial) keyword, + // so discard the keywords. Flatten the + // `BTreeSet` from each search result into our + // collection: + .flat_map(|(_keyword, keys)| keys) + // Only return `maximum_search_results` number of + // keys: + .take(*maximum_search_results) + // Collect all keyword autocompletions into a + // `BTreeSet`: + .collect() + } // if - // Return search results to caller: - last_results - } // _ - } // match - } else { - // The search string did not have a last keyword to autocomplete (or - // any keywords to search for.) Return an empty `BTreeSet`: - BTreeSet::new() - } // if + // Return search results to caller: + last_results + } + }) // if } // fn } // impl diff --git a/src/simple/search/or.rs b/src/simple/search/or.rs index 0e50a3d..52e6306 100644 --- a/src/simple/search/or.rs +++ b/src/simple/search/or.rs @@ -107,7 +107,7 @@ impl<'a, K: 'a + Hash + Ord> SearchIndex { // Split search `String` into keywords (according to the `SearchIndex` // settings). `string_keywords` will allow "use entire string as a // keyword" if enabled in user settings: - let keywords: Vec = self.string_keywords(string, SplitContext::Searching); + let keywords: Vec = self.string_keywords(string, &SplitContext::Searching); // For debug builds: #[cfg(debug_assertions)] diff --git a/src/simple/tests.rs b/src/simple/tests.rs index e54b448..a8be770 100644 --- a/src/simple/tests.rs +++ b/src/simple/tests.rs @@ -1,3 +1,5 @@ +#![allow(clippy::cognitive_complexity)] +#![allow(clippy::too_many_lines)] #[test] fn simple() { use crate::simple::internal::string_keywords::SplitContext; @@ -51,7 +53,7 @@ fn simple() { let string_keywords: Vec = search_index.string_keywords( "All is not lost, the unconquerable will, and study of revenge, \ immortal hate, and the courage never to submit or yield.", - SplitContext::Indexing, + &SplitContext::Indexing, ); assert_eq!( @@ -77,7 +79,7 @@ fn simple() { let string_keywords: Vec = search_index.string_keywords( "He prayeth best, who loveth best All things both great and small; For \ the dear God who loveth us, He made and loveth all.", - SplitContext::Searching, + &SplitContext::Searching, ); assert_eq!( @@ -92,7 +94,7 @@ fn simple() { "Digby was a floccinaucinihilipilificator at heart—which is an \ eight-dollar word meaning a joker who does not believe in anything he \ can't bite.", - SplitContext::Indexing, + &SplitContext::Indexing, ); assert_eq!(