Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 108 additions & 17 deletions app/src/main/java/fr/neamar/kiss/dataprovider/AppProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Copy link
Collaborator

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 of FuzzyScore including levenshtein.


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);
Copy link
Owner

Choose a reason for hiding this comment

The 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 :)

Copy link
Collaborator

Choose a reason for hiding this comment

The 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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class MatchInfo {
public boolean match;
final List<Integer> matchedIndices;

MatchInfo(boolean match, int score) {
public MatchInfo(boolean match, int score) {
this();
this.match = match;
this.score = score;
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.
Current search algorithm is fuzzy and the other one too.
Difference is that one is just not checking if words are similar.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Typo tolerant search"?

Copy link
Collaborator

@marunjar marunjar Oct 8, 2025

Choose a reason for hiding this comment

The 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>
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/res/xml/preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,12 @@
android:key="enable-excluded-apps"
android:order="7"
android:title="@string/excluded_apps_toggle" />
<fr.neamar.kiss.preference.SwitchPreference
android:defaultValue="false"
android:key="enable-fuzzy-search"
android:order="4"
android:summary="@string/fuzzy_search_desc"
android:title="@string/fuzzy_search_name" />
</PreferenceCategory>
<PreferenceCategory
android:key="web-providers"
Expand Down