-
-
Notifications
You must be signed in to change notification settings - Fork 624
Feature: Add Option for Fuzzy/Flexible App Search (e.g. handles typos) (issue #2461) #2465
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,10 +19,13 @@ | |
| import fr.neamar.kiss.pojo.AppPojo; | ||
| import fr.neamar.kiss.searcher.Searcher; | ||
| import fr.neamar.kiss.utils.UserHandle; | ||
| import fr.neamar.kiss.utils.fuzzy.FuzzyFactory; | ||
| import fr.neamar.kiss.utils.fuzzy.FuzzyScore; | ||
| import fr.neamar.kiss.utils.fuzzy.FuzzyFactory; | ||
| import fr.neamar.kiss.utils.fuzzy.FuzzyScoreV1; | ||
| import fr.neamar.kiss.utils.fuzzy.FuzzyScoreV2; | ||
| import fr.neamar.kiss.utils.fuzzy.MatchInfo; | ||
|
|
||
|
|
||
| public class AppProvider extends Provider<AppPojo> { | ||
|
|
||
| @Override | ||
|
|
@@ -125,31 +128,119 @@ public void requestResults(String query, Searcher searcher) { | |
| return; | ||
| } | ||
|
|
||
| FuzzyScore fuzzyScore = FuzzyFactory.createFuzzyScore(this, queryNormalized.codePoints); | ||
| boolean flexibleFuzzy = prefs.getBoolean("enable-fuzzy-search", false); | ||
|
|
||
| if (flexibleFuzzy) { | ||
| // New flexible fuzzy search logic for partial, misspelled queries | ||
| for (AppPojo pojo : getPojos()) { | ||
| // exclude apps from results | ||
| if (pojo.isExcluded() && !prefs.getBoolean("enable-excluded-apps", false)) { | ||
| continue; | ||
| } | ||
| // exclude favorites from results | ||
| if (excludedFavoriteIds.contains(pojo.getFavoriteId())) { | ||
| continue; | ||
| } | ||
|
|
||
| // Match against app name | ||
| int distance = substringLevenshteinDistance(queryNormalized.codePoints, pojo.normalizedName.codePoints); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder how this behaves in terms of performance with 5k contacts. I remember Levenshtein isn't computationally free, so I'll be curious to test this with live data :)
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think jaro/jaro-winkler is faster |
||
| double similarity = 1.0 - (double) distance / queryNormalized.codePoints.length; | ||
| boolean nameMatch = similarity > 0.6; | ||
| int relevance = nameMatch ? (int) (similarity * 100) : 0; | ||
| MatchInfo nameMatchInfo = new MatchInfo(nameMatch, relevance); | ||
| boolean match = pojo.updateMatchingRelevance(nameMatchInfo, false); | ||
|
|
||
| // Match against tags | ||
| if (pojo.getNormalizedTags() != null) { | ||
| int distanceTags = substringLevenshteinDistance(queryNormalized.codePoints, pojo.getNormalizedTags().codePoints); | ||
| double similarityTags = 1.0 - (double) distanceTags / queryNormalized.codePoints.length; | ||
| boolean tagsMatch = similarityTags > 0.6; | ||
| int relevanceTags = tagsMatch ? (int) (similarityTags * 100) : 0; | ||
| MatchInfo tagsMatchInfo = new MatchInfo(tagsMatch, relevanceTags); | ||
| match = pojo.updateMatchingRelevance(tagsMatchInfo, match); | ||
| } | ||
|
|
||
| for (AppPojo pojo : getPojos()) { | ||
| // exclude apps from results | ||
| if (pojo.isExcluded() && !prefs.getBoolean("enable-excluded-apps", false)) { | ||
| continue; | ||
| if (match && !searcher.addResult(pojo)) { | ||
| return; | ||
| } | ||
| } | ||
| // exclude favorites from results | ||
| if (excludedFavoriteIds.contains(pojo.getFavoriteId())) { | ||
| continue; | ||
| } else { | ||
| // Original fuzzy search logic | ||
| FuzzyScore fuzzyScore = FuzzyFactory.createFuzzyScore(this, queryNormalized.codePoints); | ||
|
|
||
| for (AppPojo pojo : getPojos()) { | ||
| // exclude apps from results | ||
| if (pojo.isExcluded() && !prefs.getBoolean("enable-excluded-apps", false)) { | ||
| continue; | ||
| } | ||
| // exclude favorites from results | ||
| if (excludedFavoriteIds.contains(pojo.getFavoriteId())) { | ||
| continue; | ||
| } | ||
|
|
||
| MatchInfo matchInfo = fuzzyScore.match(pojo.normalizedName.codePoints); | ||
| boolean match = pojo.updateMatchingRelevance(matchInfo, false); | ||
|
|
||
| // check relevance for tags | ||
| if (pojo.getNormalizedTags() != null) { | ||
| matchInfo = fuzzyScore.match(pojo.getNormalizedTags().codePoints); | ||
| match = pojo.updateMatchingRelevance(matchInfo, match); | ||
| } | ||
|
|
||
| if (match && !searcher.addResult(pojo)) { | ||
| return; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private int substringLevenshteinDistance(int[] s1, int[] s2) { | ||
| // s1 is the query, s2 is the text to search within | ||
| int s1Len = s1.length; | ||
| int s2Len = s2.length; | ||
|
|
||
| if (s1Len == 0) { | ||
| return 0; | ||
| } | ||
|
|
||
| MatchInfo matchInfo = fuzzyScore.match(pojo.normalizedName.codePoints); | ||
| boolean match = pojo.updateMatchingRelevance(matchInfo, false); | ||
| int[][] dp = new int[s1Len + 1][s2Len + 1]; | ||
|
|
||
| // check relevance for tags | ||
| if (pojo.getNormalizedTags() != null) { | ||
| matchInfo = fuzzyScore.match(pojo.getNormalizedTags().codePoints); | ||
| match = pojo.updateMatchingRelevance(matchInfo, match); | ||
| // Initialize first column (cost of deleting query characters) | ||
| for (int i = 0; i <= s1Len; i++) { | ||
| dp[i][0] = i; | ||
| } | ||
|
|
||
| // Initialize first row to 0 (no cost for starting match anywhere in s2) | ||
| // This is the key difference for substring matching. | ||
| for (int j = 0; j <= s2Len; j++) { | ||
| dp[0][j] = 0; | ||
| } | ||
|
|
||
| // Fill the rest of the matrix | ||
| for (int i = 1; i <= s1Len; i++) { | ||
| for (int j = 1; j <= s2Len; j++) { | ||
| int cost = (s1[i - 1] == s2[j - 1]) ? 0 : 1; | ||
| dp[i][j] = min( | ||
| dp[i - 1][j - 1] + cost, // Substitution/Match | ||
| dp[i - 1][j] + 1, // Deletion from s1 | ||
| dp[i][j - 1] + 1 // Insertion into s1 | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| if (match && !searcher.addResult(pojo)) { | ||
| return; | ||
| // The result is the minimum value in the last row | ||
| int minDistance = s1Len; // Max possible distance | ||
| for (int j = 0; j <= s2Len; j++) { | ||
| if (dp[s1Len][j] < minDistance) { | ||
| minDistance = dp[s1Len][j]; | ||
| } | ||
| } | ||
|
|
||
| return minDistance; | ||
| } | ||
|
|
||
| private int min(int a, int b, int c) { | ||
| return Math.min(a, Math.min(b, c)); | ||
| } | ||
|
|
||
| public List<AppPojo> getAllApps() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,8 @@ | |
| <string name="contacts_name">Contacts</string> | ||
| <string name="contacts_call_on_click">Click to call contacts</string> | ||
| <string name="search_results_options">Search results</string> | ||
| <string name="fuzzy_search_name">Enable fuzzy search</string> | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I personally would change "Fuzzy" in text to something else, as this is misleading.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Typo tolerant search"?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. or something containing "similar"? |
||
| <string name="fuzzy_search_desc">Allow for typos and approximate matching</string> | ||
| <string name="search_name">Web search providers</string> | ||
| <string name="search_provider_toggle">Links to web search providers</string> | ||
| <string name="settings_name">Device settings</string> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please encapsulate new search same way as other search algorithm?
This way it's just working for apps, but there are lots of other places using fuzzy search like shortcuts, contacts (surname, last name, nickname, ...) and some others. Also refresh of results needs it at some point.
See
FuzzyFactory, there you can return new implementation ofFuzzyScoreincluding levenshtein.