Skip to content

Commit fe65e7a

Browse files
committed
Merge branch 'WIP_fuzzyMatchUpdates' into fuzzyMatchUpdates
2 parents 42edb20 + 76727d0 commit fe65e7a

File tree

5 files changed

+208
-179
lines changed

5 files changed

+208
-179
lines changed

Wox.Infrastructure/StringMatcher.cs

Lines changed: 118 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public static class StringMatcher
1212
{
1313
public static MatchOption DefaultMatchOption = new MatchOption();
1414

15-
public static int UserSettingSearchPrecision { get; set; }
15+
public static SearchPrecisionScore UserSettingSearchPrecision { get; set; }
1616

1717
public static bool ShouldUsePinyin { get; set; }
1818

@@ -41,7 +41,15 @@ public static MatchResult FuzzySearch(string query, string stringToCompare)
4141
}
4242

4343
/// <summary>
44-
/// refer to https://github.com/mattyork/fuzzy
44+
/// Current method:
45+
/// Character matching + substring matching;
46+
/// 1. Query search string is split into substrings, separator is whitespace.
47+
/// 2. Check each query substring's characters against full compare string,
48+
/// 3. if a character in the substring is matched, loop back to verify the previous character.
49+
/// 4. If previous character also matches, and is the start of the substring, update list.
50+
/// 5. Once the previous character is verified, move on to the next character in the query substring.
51+
/// 6. Move onto the next substring's characters until all substrings are checked.
52+
/// 7. Consider success and move onto scoring if every char or substring without whitespaces matched
4553
/// </summary>
4654
public static MatchResult FuzzySearch(string query, string stringToCompare, MatchOption opt)
4755
{
@@ -52,107 +60,93 @@ public static MatchResult FuzzySearch(string query, string stringToCompare, Matc
5260
var fullStringToCompareWithoutCase = opt.IgnoreCase ? stringToCompare.ToLower() : stringToCompare;
5361

5462
var queryWithoutCase = opt.IgnoreCase ? query.ToLower() : query;
63+
64+
var querySubstrings = queryWithoutCase.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
65+
int currentQuerySubstringIndex = 0;
66+
var currentQuerySubstring = querySubstrings[currentQuerySubstringIndex];
67+
var currentQuerySubstringCharacterIndex = 0;
5568

56-
int currentQueryToCompareIndex = 0;
57-
var queryToCompareSeparated = queryWithoutCase.Split(' ');
58-
var currentQueryToCompare = queryToCompareSeparated[currentQueryToCompareIndex];
59-
60-
var patternIndex = 0;
6169
var firstMatchIndex = -1;
6270
var firstMatchIndexInWord = -1;
6371
var lastMatchIndex = 0;
64-
bool allMatched = false;
65-
bool isFullWordMatched = false;
66-
bool allWordsFullyMatched = true;
72+
bool allQuerySubstringsMatched = false;
73+
bool matchFoundInPreviousLoop = false;
74+
bool allSubstringsContainedInCompareString = true;
6775

6876
var indexList = new List<int>();
6977

70-
for (var index = 0; index < fullStringToCompareWithoutCase.Length; index++)
78+
for (var compareStringIndex = 0; compareStringIndex < fullStringToCompareWithoutCase.Length; compareStringIndex++)
7179
{
72-
var ch = stringToCompare[index];
73-
if (fullStringToCompareWithoutCase[index] == currentQueryToCompare[patternIndex])
80+
if (fullStringToCompareWithoutCase[compareStringIndex] != currentQuerySubstring[currentQuerySubstringCharacterIndex])
7481
{
75-
if (firstMatchIndex < 0)
76-
{ // first matched char will become the start of the compared string
77-
firstMatchIndex = index;
78-
}
82+
matchFoundInPreviousLoop = false;
83+
continue;
84+
}
7985

80-
if (patternIndex == 0)
81-
{ // first letter of current word
82-
isFullWordMatched = true;
83-
firstMatchIndexInWord = index;
84-
}
85-
else if (!isFullWordMatched)
86-
{ // we want to verify that there is not a better match if this is not a full word
87-
// in order to do so we need to verify all previous chars are part of the pattern
88-
int startIndexToVerify = index - patternIndex;
89-
bool allMatch = true;
90-
for (int indexToCheck = 0; indexToCheck < patternIndex; indexToCheck++)
91-
{
92-
if (fullStringToCompareWithoutCase[startIndexToVerify + indexToCheck] !=
93-
currentQueryToCompare[indexToCheck])
94-
{
95-
allMatch = false;
96-
}
97-
}
98-
99-
if (allMatch)
100-
{ // update to this as a full word
101-
isFullWordMatched = true;
102-
if (currentQueryToCompareIndex == 0)
103-
{ // first word so we need to update start index
104-
firstMatchIndex = startIndexToVerify;
105-
}
106-
107-
indexList.RemoveAll(x => x >= firstMatchIndexInWord);
108-
for (int indexToCheck = 0; indexToCheck < patternIndex; indexToCheck++)
109-
{ // update the index list
110-
indexList.Add(startIndexToVerify + indexToCheck);
111-
}
112-
}
113-
}
86+
if (firstMatchIndex < 0)
87+
{
88+
// first matched char will become the start of the compared string
89+
firstMatchIndex = compareStringIndex;
90+
}
11491

115-
lastMatchIndex = index + 1;
116-
indexList.Add(index);
92+
if (currentQuerySubstringCharacterIndex == 0)
93+
{
94+
// first letter of current word
95+
matchFoundInPreviousLoop = true;
96+
firstMatchIndexInWord = compareStringIndex;
97+
}
98+
else if (!matchFoundInPreviousLoop)
99+
{
100+
// we want to verify that there is not a better match if this is not a full word
101+
// in order to do so we need to verify all previous chars are part of the pattern
102+
var startIndexToVerify = compareStringIndex - currentQuerySubstringCharacterIndex;
117103

118-
// increase the pattern matched index and check if everything was matched
119-
if (++patternIndex == currentQueryToCompare.Length)
104+
if (AllPreviousCharsMatched(startIndexToVerify, currentQuerySubstringCharacterIndex, fullStringToCompareWithoutCase, currentQuerySubstring))
120105
{
121-
if (++currentQueryToCompareIndex >= queryToCompareSeparated.Length)
122-
{ // moved over all the words
123-
allMatched = true;
124-
break;
125-
}
126-
127-
// otherwise move to the next word
128-
currentQueryToCompare = queryToCompareSeparated[currentQueryToCompareIndex];
129-
patternIndex = 0;
130-
if (!isFullWordMatched)
131-
{ // if any of the words was not fully matched all are not fully matched
132-
allWordsFullyMatched = false;
133-
}
106+
matchFoundInPreviousLoop = true;
107+
108+
// if it's the begining character of the first query substring that is matched then we need to update start index
109+
firstMatchIndex = currentQuerySubstringIndex == 0 ? startIndexToVerify : firstMatchIndex;
110+
111+
indexList = GetUpdatedIndexList(startIndexToVerify, currentQuerySubstringCharacterIndex, firstMatchIndexInWord, indexList);
134112
}
135113
}
136-
else
114+
115+
lastMatchIndex = compareStringIndex + 1;
116+
indexList.Add(compareStringIndex);
117+
118+
currentQuerySubstringCharacterIndex++;
119+
120+
// if finished looping through every character in the current substring
121+
if (currentQuerySubstringCharacterIndex == currentQuerySubstring.Length)
137122
{
138-
isFullWordMatched = false;
139-
}
140-
}
123+
// if any of the substrings was not matched then consider as all are not matched
124+
allSubstringsContainedInCompareString = !matchFoundInPreviousLoop ? false : allSubstringsContainedInCompareString;
125+
126+
currentQuerySubstringIndex++;
141127

128+
allQuerySubstringsMatched = AllQuerySubstringsMatched(currentQuerySubstringIndex, querySubstrings.Length);
129+
if (allQuerySubstringsMatched)
130+
break;
142131

143-
// return rendered string if we have a match for every char or all substring without whitespaces matched
144-
if (allMatched)
132+
// otherwise move to the next query substring
133+
currentQuerySubstring = querySubstrings[currentQuerySubstringIndex];
134+
currentQuerySubstringCharacterIndex = 0;
135+
}
136+
}
137+
138+
// proceed to calculate score if every char or substring without whitespaces matched
139+
if (allQuerySubstringsMatched)
145140
{
146-
// check if all query string was contained in string to compare
147-
bool containedFully = lastMatchIndex - firstMatchIndex == queryWithoutCase.Length;
148-
var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex, lastMatchIndex - firstMatchIndex, containedFully, allWordsFullyMatched);
141+
var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex, lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString);
149142
var pinyinScore = ScoreForPinyin(stringToCompare, query);
150143

151144
var result = new MatchResult
152145
{
153146
Success = true,
154147
MatchData = indexList,
155-
RawScore = Math.Max(score, pinyinScore)
148+
RawScore = Math.Max(score, pinyinScore),
149+
AllSubstringsContainedInCompareString = allSubstringsContainedInCompareString
156150
};
157151

158152
return result;
@@ -161,8 +155,44 @@ public static MatchResult FuzzySearch(string query, string stringToCompare, Matc
161155
return new MatchResult { Success = false };
162156
}
163157

164-
private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, int matchLen,
165-
bool isFullyContained, bool allWordsFullyMatched)
158+
private static bool AllPreviousCharsMatched(int startIndexToVerify, int currentQuerySubstringCharacterIndex,
159+
string fullStringToCompareWithoutCase, string currentQuerySubstring)
160+
{
161+
var allMatch = true;
162+
for (int indexToCheck = 0; indexToCheck < currentQuerySubstringCharacterIndex; indexToCheck++)
163+
{
164+
if (fullStringToCompareWithoutCase[startIndexToVerify + indexToCheck] !=
165+
currentQuerySubstring[indexToCheck])
166+
{
167+
allMatch = false;
168+
}
169+
}
170+
171+
return allMatch;
172+
}
173+
174+
private static List<int> GetUpdatedIndexList(int startIndexToVerify, int currentQuerySubstringCharacterIndex, int firstMatchIndexInWord, List<int> indexList)
175+
{
176+
var updatedList = new List<int>();
177+
178+
indexList.RemoveAll(x => x >= firstMatchIndexInWord);
179+
180+
updatedList.AddRange(indexList);
181+
182+
for (int indexToCheck = 0; indexToCheck < currentQuerySubstringCharacterIndex; indexToCheck++)
183+
{
184+
updatedList.Add(startIndexToVerify + indexToCheck);
185+
}
186+
187+
return updatedList;
188+
}
189+
190+
private static bool AllQuerySubstringsMatched(int currentQuerySubstringIndex, int querySubstringsLength)
191+
{
192+
return currentQuerySubstringIndex >= querySubstringsLength;
193+
}
194+
195+
private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, int matchLen, bool allSubstringsContainedInCompareString)
166196
{
167197
// A match found near the beginning of a string is scored more than a match found near the end
168198
// A match is scored more if the characters in the patterns are closer to each other,
@@ -179,15 +209,8 @@ private static int CalculateSearchScore(string query, string stringToCompare, in
179209
score += 10;
180210
}
181211

182-
if (isFullyContained)
183-
{
184-
score += 20; // honestly I'm not sure what would be a good number here or should it factor the size of the pattern
185-
}
186-
187-
if (allWordsFullyMatched)
188-
{
189-
score += 20;
190-
}
212+
if (allSubstringsContainedInCompareString)
213+
score += 10 * string.Concat(query.Where(c => !char.IsWhiteSpace(c))).Count();
191214

192215
return score;
193216
}
@@ -256,6 +279,11 @@ public int RawScore
256279
}
257280
}
258281

282+
/// <summary>
283+
/// Indicates if all query's substrings are contained in the string to compare
284+
/// </summary>
285+
public bool AllSubstringsContainedInCompareString { get; set; }
286+
259287
/// <summary>
260288
/// Matched data to highlight.
261289
/// </summary>
@@ -268,7 +296,7 @@ public bool IsSearchPrecisionScoreMet()
268296

269297
private bool IsSearchPrecisionScoreMet(int score)
270298
{
271-
return score >= UserSettingSearchPrecision;
299+
return score >= (int)UserSettingSearchPrecision;
272300
}
273301

274302
private int ApplySearchPrecisionFilter(int score)

Wox.Infrastructure/UserSettings/Settings.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,19 @@ public string QuerySearchPrecisionString
4545
{
4646
try
4747
{
48-
var precisionScore = (StringMatcher.SearchPrecisionScore)Enum.Parse(
49-
typeof(StringMatcher.SearchPrecisionScore),
50-
value);
48+
var precisionScore = (StringMatcher.SearchPrecisionScore)Enum
49+
.Parse(typeof(StringMatcher.SearchPrecisionScore), value);
50+
5151
QuerySearchPrecision = precisionScore;
52-
StringMatcher.UserSettingSearchPrecision = (int)precisionScore;
52+
StringMatcher.UserSettingSearchPrecision = precisionScore;
5353
}
54-
catch (System.Exception e)
54+
catch (ArgumentException e)
5555
{
56-
// what do we do here?!
57-
Logger.Log.Exception(nameof(Settings), "Fail to set QuerySearchPrecision", e);
56+
Logger.Log.Exception(nameof(Settings), "Failed to load QuerySearchPrecisionString value from Settings file", e);
57+
58+
QuerySearchPrecision = StringMatcher.SearchPrecisionScore.Regular;
59+
StringMatcher.UserSettingSearchPrecision = StringMatcher.SearchPrecisionScore.Regular;
60+
5861
throw;
5962
}
6063
}

0 commit comments

Comments
 (0)