From f89431d8bfe731dde3b55f9526e22383c4bb2ca3 Mon Sep 17 00:00:00 2001 From: fibreditoniocartonio Date: Tue, 7 Oct 2025 22:02:41 +0200 Subject: [PATCH] feat: Add option for fuzzy app search --- .../neamar/kiss/dataprovider/AppProvider.java | 125 +++++++++++++++--- .../fr/neamar/kiss/utils/fuzzy/MatchInfo.java | 2 +- app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/preferences.xml | 6 + 4 files changed, 117 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/fr/neamar/kiss/dataprovider/AppProvider.java b/app/src/main/java/fr/neamar/kiss/dataprovider/AppProvider.java index f64b5141f2..6e851aaaf2 100644 --- a/app/src/main/java/fr/neamar/kiss/dataprovider/AppProvider.java +++ b/app/src/main/java/fr/neamar/kiss/dataprovider/AppProvider.java @@ -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 { @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); + 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 getAllApps() { diff --git a/app/src/main/java/fr/neamar/kiss/utils/fuzzy/MatchInfo.java b/app/src/main/java/fr/neamar/kiss/utils/fuzzy/MatchInfo.java index 82557ffaa0..b09b10b7b7 100644 --- a/app/src/main/java/fr/neamar/kiss/utils/fuzzy/MatchInfo.java +++ b/app/src/main/java/fr/neamar/kiss/utils/fuzzy/MatchInfo.java @@ -16,7 +16,7 @@ public class MatchInfo { public boolean match; final List matchedIndices; - MatchInfo(boolean match, int score) { + public MatchInfo(boolean match, int score) { this(); this.match = match; this.score = score; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 733e7ff49f..dade514b96 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,6 +24,8 @@ Contacts Click to call contacts Search results + Enable fuzzy search + Allow for typos and approximate matching Web search providers Links to web search providers Device settings diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 75787a405e..fdb186a669 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -477,6 +477,12 @@ android:key="enable-excluded-apps" android:order="7" android:title="@string/excluded_apps_toggle" /> +