Skip to content

Commit 4676a2a

Browse files
authored
Merge pull request #77 from Flow-Launcher/fix_stringmatcher_resultweighting
Fix StringMatcher's score calculation on distance of first match
2 parents 4c20dbc + 2ff4ffd commit 4676a2a

File tree

2 files changed

+104
-19
lines changed

2 files changed

+104
-19
lines changed

Flow.Launcher.Infrastructure/StringMatcher.cs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,18 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption
8383
bool allSubstringsContainedInCompareString = true;
8484

8585
var indexList = new List<int>();
86+
List<int> spaceIndices = new List<int>();
8687

8788
for (var compareStringIndex = 0; compareStringIndex < fullStringToCompareWithoutCase.Length; compareStringIndex++)
8889
{
90+
91+
// To maintain a list of indices which correspond to spaces in the string to compare
92+
// To populate the list only for the first query substring
93+
if (fullStringToCompareWithoutCase[compareStringIndex].Equals(' ') && currentQuerySubstringIndex == 0)
94+
{
95+
spaceIndices.Add(compareStringIndex);
96+
}
97+
8998
if (fullStringToCompareWithoutCase[compareStringIndex] != currentQuerySubstring[currentQuerySubstringCharacterIndex])
9099
{
91100
matchFoundInPreviousLoop = false;
@@ -147,15 +156,31 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption
147156
// proceed to calculate score if every char or substring without whitespaces matched
148157
if (allQuerySubstringsMatched)
149158
{
150-
var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex, lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString);
159+
var nearestSpaceIndex = CalculateClosestSpaceIndex(spaceIndices, firstMatchIndex);
160+
var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1, lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString);
151161

152162
return new MatchResult(true, UserSettingSearchPrecision, indexList, score);
153163
}
154164

155-
return new MatchResult (false, UserSettingSearchPrecision);
165+
return new MatchResult(false, UserSettingSearchPrecision);
166+
}
167+
168+
// To get the index of the closest space which preceeds the first matching index
169+
private int CalculateClosestSpaceIndex(List<int> spaceIndices, int firstMatchIndex)
170+
{
171+
if (spaceIndices.Count == 0)
172+
{
173+
return -1;
174+
}
175+
else
176+
{
177+
int? ind = spaceIndices.OrderBy(item => (firstMatchIndex - item)).Where(item => firstMatchIndex > item).FirstOrDefault();
178+
int closestSpaceIndex = ind ?? -1;
179+
return closestSpaceIndex;
180+
}
156181
}
157182

158-
private static bool AllPreviousCharsMatched(int startIndexToVerify, int currentQuerySubstringCharacterIndex,
183+
private static bool AllPreviousCharsMatched(int startIndexToVerify, int currentQuerySubstringCharacterIndex,
159184
string fullStringToCompareWithoutCase, string currentQuerySubstring)
160185
{
161186
var allMatch = true;
@@ -299,13 +324,13 @@ private int ScoreAfterSearchPrecisionFilter(int rawScore)
299324
public class MatchOption
300325
{
301326
/// <summary>
302-
/// prefix of match char, use for hightlight
327+
/// prefix of match char, use for highlight
303328
/// </summary>
304329
[Obsolete("this is never used")]
305330
public string Prefix { get; set; } = "";
306331

307332
/// <summary>
308-
/// suffix of match char, use for hightlight
333+
/// suffix of match char, use for highlight
309334
/// </summary>
310335
[Obsolete("this is never used")]
311336
public string Suffix { get; set; } = "";

Flow.Launcher.Test/FuzzyMatcherTest.cs

Lines changed: 74 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public void MatchTest()
7777
}
7878

7979
[TestCase("Chrome")]
80-
public void WhenGivenNotAllCharactersFoundInSearchStringThenShouldReturnZeroScore(string searchString)
80+
public void WhenNotAllCharactersFoundInSearchString_ThenShouldReturnZeroScore(string searchString)
8181
{
8282
var compareString = "Can have rum only in my glass";
8383
var matcher = new StringMatcher();
@@ -92,7 +92,7 @@ public void WhenGivenNotAllCharactersFoundInSearchStringThenShouldReturnZeroScor
9292
[TestCase("cand")]
9393
[TestCase("cpywa")]
9494
[TestCase("ccs")]
95-
public void WhenGivenStringsAndAppliedPrecisionFilteringThenShouldReturnGreaterThanPrecisionScoreResults(string searchTerm)
95+
public void GivenQueryString_WhenAppliedPrecisionFiltering_ThenShouldReturnGreaterThanPrecisionScoreResults(string searchTerm)
9696
{
9797
var results = new List<Result>();
9898
var matcher = new StringMatcher();
@@ -107,7 +107,10 @@ public void WhenGivenStringsAndAppliedPrecisionFilteringThenShouldReturnGreaterT
107107

108108
foreach (var precisionScore in GetPrecisionScores())
109109
{
110-
var filteredResult = results.Where(result => result.Score >= precisionScore).Select(result => result).OrderByDescending(x => x.Score).ToList();
110+
var filteredResult = results.Where(result => result.Score >= precisionScore)
111+
.Select(result => result)
112+
.OrderByDescending(x => x.Score)
113+
.ToList();
111114

112115
Debug.WriteLine("");
113116
Debug.WriteLine("###############################################");
@@ -124,20 +127,22 @@ public void WhenGivenStringsAndAppliedPrecisionFilteringThenShouldReturnGreaterT
124127
}
125128

126129
[TestCase(Chrome, Chrome, 157)]
127-
[TestCase(Chrome, LastIsChrome, 103)]
128-
[TestCase(Chrome, HelpCureHopeRaiseOnMindEntityChrome, 21)]
129-
[TestCase(Chrome, UninstallOrChangeProgramsOnYourComputer, 15)]
130+
[TestCase(Chrome, LastIsChrome, 147)]
131+
[TestCase(Chrome, HelpCureHopeRaiseOnMindEntityChrome, 25)]
132+
[TestCase(Chrome, UninstallOrChangeProgramsOnYourComputer, 21)]
130133
[TestCase(Chrome, CandyCrushSagaFromKing, 0)]
131-
[TestCase("sql", MicrosoftSqlServerManagementStudio, 56)]
132-
[TestCase("sql manag", MicrosoftSqlServerManagementStudio, 99)]//double spacing intended
133-
public void WhenGivenQueryStringThenShouldReturnCurrentScoring(string queryString, string compareString, int expectedScore)
134+
[TestCase("sql", MicrosoftSqlServerManagementStudio, 110)]
135+
[TestCase("sql manag", MicrosoftSqlServerManagementStudio, 121)]//double spacing intended
136+
public void WhenGivenQueryString_ThenShouldReturn_TheDesiredScoring(
137+
string queryString, string compareString, int expectedScore)
134138
{
135139
// When, Given
136140
var matcher = new StringMatcher();
137141
var rawScore = matcher.FuzzyMatch(queryString, compareString).RawScore;
138142

139143
// Should
140-
Assert.AreEqual(expectedScore, rawScore, $"Expected score for compare string '{compareString}': {expectedScore}, Actual: {rawScore}");
144+
Assert.AreEqual(expectedScore, rawScore,
145+
$"Expected score for compare string '{compareString}': {expectedScore}, Actual: {rawScore}");
141146
}
142147

143148
[TestCase("goo", "Google Chrome", StringMatcher.SearchPrecisionScore.Regular, true)]
@@ -150,7 +155,7 @@ public void WhenGivenQueryStringThenShouldReturnCurrentScoring(string queryStrin
150155
[TestCase("ccs", "Candy Crush Saga from King", StringMatcher.SearchPrecisionScore.Low, true)]
151156
[TestCase("cand", "Candy Crush Saga from King",StringMatcher.SearchPrecisionScore.Regular, true)]
152157
[TestCase("cand", "Help cure hope raise on mind entity Chrome", StringMatcher.SearchPrecisionScore.Regular, false)]
153-
public void WhenGivenDesiredPrecisionThenShouldReturnAllResultsGreaterOrEqual(
158+
public void WhenGivenDesiredPrecision_ThenShouldReturn_AllResultsGreaterOrEqual(
154159
string queryString,
155160
string compareString,
156161
StringMatcher.SearchPrecisionScore expectedPrecisionScore,
@@ -185,8 +190,8 @@ public void WhenGivenDesiredPrecisionThenShouldReturnAllResultsGreaterOrEqual(
185190
[TestCase("sql manag", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)]
186191
[TestCase("sql", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)]
187192
[TestCase("sql serv", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)]
188-
[TestCase("sqlserv", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)]
189-
[TestCase("sql servman", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)]
193+
[TestCase("servez", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)]
194+
[TestCase("sql servz", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)]
190195
[TestCase("sql serv man", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)]
191196
[TestCase("sql studio", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)]
192197
[TestCase("mic", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)]
@@ -199,7 +204,7 @@ public void WhenGivenDesiredPrecisionThenShouldReturnAllResultsGreaterOrEqual(
199204
[TestCase("cod", VisualStudioCode, StringMatcher.SearchPrecisionScore.Regular, true)]
200205
[TestCase("code", VisualStudioCode, StringMatcher.SearchPrecisionScore.Regular, true)]
201206
[TestCase("codes", "Visual Studio Codes", StringMatcher.SearchPrecisionScore.Regular, true)]
202-
public void WhenGivenQueryShouldReturnResultsContainingAllQuerySubstrings(
207+
public void WhenGivenQuery_ShouldReturnResults_ContainingAllQuerySubstrings(
203208
string queryString,
204209
string compareString,
205210
StringMatcher.SearchPrecisionScore expectedPrecisionScore,
@@ -225,5 +230,60 @@ public void WhenGivenQueryShouldReturnResultsContainingAllQuerySubstrings(
225230
$"Raw Score: {matchResult.RawScore}{Environment.NewLine}" +
226231
$"Precision Score: {(int)expectedPrecisionScore}");
227232
}
233+
234+
[TestCase("man", "Task Manager", "eManual")]
235+
[TestCase("term", "Windows Terminal", "Character Map")]
236+
[TestCase("winterm", "Windows Terminal", "Cygwin64 Terminal")]
237+
public void WhenGivenAQuery_Scoring_ShouldGiveMoreWeightToStartOfNewWord(
238+
string queryString, string compareString1, string compareString2)
239+
{
240+
// When
241+
var matcher = new StringMatcher { UserSettingSearchPrecision = StringMatcher.SearchPrecisionScore.Regular };
242+
243+
// Given
244+
var compareString1Result = matcher.FuzzyMatch(queryString, compareString1);
245+
var compareString2Result = matcher.FuzzyMatch(queryString, compareString2);
246+
247+
Debug.WriteLine("");
248+
Debug.WriteLine("###############################################");
249+
Debug.WriteLine($"QueryString: \"{queryString}\"{Environment.NewLine}");
250+
Debug.WriteLine($"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}");
251+
Debug.WriteLine($"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}");
252+
Debug.WriteLine("###############################################");
253+
Debug.WriteLine("");
254+
255+
// Should
256+
Assert.True(compareString1Result.Score > compareString2Result.Score,
257+
$"Query: \"{queryString}\"{Environment.NewLine} " +
258+
$"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" +
259+
$"Should be greater than{ Environment.NewLine}" +
260+
$"CompareString2: \"{compareString2}\", Score: {compareString1Result.Score}{Environment.NewLine}");
261+
}
262+
263+
[TestCase("vim", "Vim", "ignoreDescription", "ignore.exe", "Vim Diff", "ignoreDescription", "ignore.exe")]
264+
public void WhenMultipleResults_ExactMatchingResult_ShouldHaveGreatestScore(
265+
string queryString, string firstName, string firstDescription, string firstExecutableName,
266+
string secondName, string secondDescription, string secondExecutableName)
267+
{
268+
// Act
269+
var matcher = new StringMatcher();
270+
var firstNameMatch = matcher.FuzzyMatch(queryString, firstName).RawScore;
271+
var firstDescriptionMatch = matcher.FuzzyMatch(queryString, firstDescription).RawScore;
272+
var firstExecutableNameMatch = matcher.FuzzyMatch(queryString, firstExecutableName).RawScore;
273+
274+
var secondNameMatch = matcher.FuzzyMatch(queryString, secondName).RawScore;
275+
var secondDescriptionMatch = matcher.FuzzyMatch(queryString, secondDescription).RawScore;
276+
var secondExecutableNameMatch = matcher.FuzzyMatch(queryString, secondExecutableName).RawScore;
277+
278+
var firstScore = new[] { firstNameMatch, firstDescriptionMatch, firstExecutableNameMatch }.Max();
279+
var secondScore = new[] { secondNameMatch, secondDescriptionMatch, secondExecutableNameMatch }.Max();
280+
281+
// Assert
282+
Assert.IsTrue(firstScore > secondScore,
283+
$"Query: \"{queryString}\"{Environment.NewLine} " +
284+
$"Name of first: \"{firstName}\", Final Score: {firstScore}{Environment.NewLine}" +
285+
$"Should be greater than{ Environment.NewLine}" +
286+
$"Name of second: \"{secondName}\", Final Score: {secondScore}{Environment.NewLine}");
287+
}
228288
}
229289
}

0 commit comments

Comments
 (0)