Skip to content

Commit df940e3

Browse files
authored
Merge pull request #16 from luistrivelatto/master
Fixes #15, #17, #18, and does other improvements
2 parents 4e58f1c + f17fbbd commit df940e3

File tree

8 files changed

+394
-29
lines changed

8 files changed

+394
-29
lines changed

.github/actions/dart-test/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM cirrusci/flutter
1+
FROM cirrusci/flutter:beta
22

33
USER root
44

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# CHANGELOG
22

3+
## 0.3.0
4+
- Improve search results on weighted search to combine scores from all keys
5+
- Improve search results on single-keyed search, making it consistent with non-weighted search
6+
- Add parameter to ignore tokens smaller than a certain length when searching
7+
- Add normalization of WeightedKey weights
8+
- Fix bug where results returned from search all had arrayIndex = -1
9+
- Fix bug where the token scores didn't count towards the result score
10+
11+
## 0.2.5
12+
- Fix bug for search that started or ended with whitespace when tokenize option is true
13+
314
## 0.2.4
415
- Bump dependencies, fix CI
516

lib/data/fuzzy_options.dart

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@ class WeightedKey<T> {
99
@required this.name,
1010
@required this.getter,
1111
@required this.weight,
12-
}) : assert(weight >= 0 && weight <= 1);
12+
}) : assert(weight > 0, 'Weight should be positive and non-zero');
1313

1414
/// Name of this getter
1515
final String name;
1616

1717
/// Getter to a specifc string inside item
1818
final String Function(T obj) getter;
1919

20-
/// Weight of this getter
20+
/// Weight of this getter. When passing a list of WeightedKey to FuzzyOptions,
21+
/// the weight can be any positive number; FuzzyOptions normalizes it on
22+
/// construction.
2123
final double weight;
2224
}
2325

@@ -28,17 +30,20 @@ int _defaultSortFn<T>(Result<T> a, Result<T> b) => a.score.compareTo(b.score);
2830

2931
/// Options for performing a fuzzy search
3032
class FuzzyOptions<T> {
31-
/// Instantiate an options object
33+
/// Instantiate an options object.
34+
/// The `keys` list requires a positive number (they'll be normalized upon
35+
/// instantiation). If any weight is not positive, throws an ArgumentError.
3236
FuzzyOptions({
3337
this.location = 0,
3438
this.distance = 100,
3539
this.threshold = 0.6,
3640
this.maxPatternLength = 32,
3741
this.isCaseSensitive = false,
3842
Pattern tokenSeparator,
43+
this.minTokenCharLength = 1,
3944
this.findAllMatches = false,
4045
this.minMatchCharLength = 1,
41-
this.keys = const [],
46+
List<WeightedKey<T>> keys = const [],
4247
this.shouldSort = true,
4348
SorterFn<T> sortFn,
4449
this.tokenize = false,
@@ -47,6 +52,7 @@ class FuzzyOptions<T> {
4752
this.shouldNormalize = false,
4853
}) : tokenSeparator =
4954
tokenSeparator ?? RegExp(r' +', caseSensitive: isCaseSensitive),
55+
keys = _normalizeWeights(keys),
5056
sortFn = sortFn ?? _defaultSortFn;
5157

5258
/// Approximately where in the text is the pattern expected to be found?
@@ -72,6 +78,9 @@ class FuzzyOptions<T> {
7278
/// Regex used to separate words when searching. Only applicable when `tokenize` is `true`.
7379
final Pattern tokenSeparator;
7480

81+
/// Ignore tokens with length smaller than this. Only applicable when `tokenize` is `true`.
82+
final int minTokenCharLength;
83+
7584
/// When true, the algorithm continues searching to the end of the input even if a perfect
7685
/// match is found before the end of the same input.
7786
final bool findAllMatches;
@@ -112,6 +121,7 @@ class FuzzyOptions<T> {
112121
maxPatternLength: options?.maxPatternLength ?? maxPatternLength,
113122
isCaseSensitive: options?.isCaseSensitive ?? isCaseSensitive,
114123
tokenSeparator: options?.tokenSeparator ?? tokenSeparator,
124+
minTokenCharLength: options?.minTokenCharLength ?? minTokenCharLength,
115125
findAllMatches: options?.findAllMatches ?? findAllMatches,
116126
minMatchCharLength: options?.minMatchCharLength ?? minMatchCharLength,
117127
keys: options?.keys ?? keys,
@@ -122,4 +132,22 @@ class FuzzyOptions<T> {
122132
verbose: options?.verbose ?? verbose,
123133
shouldNormalize: options?.shouldNormalize ?? shouldNormalize,
124134
);
135+
136+
static List<WeightedKey<T>> _normalizeWeights<T>(List<WeightedKey<T>> keys) {
137+
if (keys.isEmpty) {
138+
return [];
139+
}
140+
141+
var weightSum = keys
142+
.map((key) => key.weight)
143+
.fold<double>(0, (previousValue, element) => previousValue + element);
144+
145+
return keys
146+
.map((key) => WeightedKey<T>(
147+
name: key.name,
148+
getter: key.getter,
149+
weight: key.weight / weightSum,
150+
))
151+
.toList();
152+
}
125153
}

lib/data/result.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ class ResultDetails<T> {
8181
/// Score of this result
8282
final double score;
8383

84-
/// nScore of this result (?)
84+
/// nScore of this result. It's the weighted score of the match, when it's
85+
/// a weighted search (i.e. uses WeightedKeys).
8586
double nScore;
8687

8788
/// Indexes of matched patterns on the value

lib/fuzzy.dart

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ class Fuzzy<T> {
6767

6868
if (options.tokenize) {
6969
// Tokenize on the separator
70-
final tokens = pattern.split(options.tokenSeparator);
70+
final tokens = pattern.split(options.tokenSeparator)
71+
..removeWhere((token) => token.isEmpty)
72+
..removeWhere((token) => token.length < options.minTokenCharLength);
7173
for (var i = 0, len = tokens.length; i < len; i += 1) {
7274
tokenSearchers.add(Bitap(tokens[i], options: options));
7375
}
@@ -136,7 +138,6 @@ class Fuzzy<T> {
136138

137139
List<Result<T>> _analyze({
138140
String key = '',
139-
int arrayIndex = -1,
140141
String value,
141142
T record,
142143
int index,
@@ -153,7 +154,7 @@ class Fuzzy<T> {
153154
}
154155

155156
var exists = false;
156-
var averageScore = -1;
157+
var averageScore = -1.0;
157158
var numTextMatches = 0;
158159

159160
final mainSearchResult = fullSearcher.search(value.toString());
@@ -190,8 +191,8 @@ class Fuzzy<T> {
190191
}
191192
}
192193

193-
final averageScore =
194-
scores.fold(0, (memo, score) => memo + score) / scores.length;
194+
averageScore =
195+
scores.fold<double>(0, (memo, score) => memo + score) / scores.length;
195196

196197
_log('Token score average: $averageScore');
197198
}
@@ -218,7 +219,7 @@ class Fuzzy<T> {
218219
// existingResult.score, bitapResult.score
219220
existingResult.matches.add(ResultDetails<T>(
220221
key: key,
221-
arrayIndex: arrayIndex,
222+
arrayIndex: index,
222223
value: value,
223224
score: finalScore,
224225
matchedIndices: mainSearchResult.matchedIndices,
@@ -230,7 +231,7 @@ class Fuzzy<T> {
230231
matches: [
231232
ResultDetails<T>(
232233
key: key,
233-
arrayIndex: arrayIndex,
234+
arrayIndex: index,
234235
value: value,
235236
score: finalScore,
236237
matchedIndices: mainSearchResult.matchedIndices,
@@ -254,29 +255,40 @@ class Fuzzy<T> {
254255
void _computeScore(Map<String, double> weights, List<Result<T>> results) {
255256
_log('\n\nComputing score:\n');
256257

258+
if (weights.length <= 1) {
259+
_computeScoreNoWeights(results);
260+
} else {
261+
_computeScoreWithWeights(weights, results);
262+
}
263+
}
264+
265+
void _computeScoreNoWeights(List<Result<T>> results) {
257266
for (var i = 0, len = results.length; i < len; i += 1) {
258267
final matches = results[i].matches;
259-
final scoreLen = matches.length;
268+
var bestScore = matches.map((m) => m.score).fold<double>(
269+
1.0, (previousValue, element) => min(previousValue, element));
270+
results[i].score = bestScore;
271+
}
272+
}
260273

274+
void _computeScoreWithWeights(
275+
Map<String, double> weights, List<Result<T>> results) {
276+
for (var i = 0, len = results.length; i < len; i += 1) {
261277
var currScore = 1.0;
262-
var bestScore = 1.0;
263278

264-
for (var j = 0; j < scoreLen; j += 1) {
265-
final weight = weights[matches[j].key] ?? 1.0;
266-
final score = weight == 1.0
267-
? matches[j].score
268-
: (matches[j].score == 0.0 ? 0.001 : matches[j].score);
279+
for (var match in results[i].matches) {
280+
var weight = weights[match.key];
281+
assert(weight != null);
282+
283+
// We don't use 0 so that the weight differences don't get zeroed out
284+
final score = match.score == 0.0 ? 0.001 : match.score;
269285
final nScore = score * weight;
270286

271-
if (weight != 1) {
272-
bestScore = min(bestScore, nScore);
273-
} else {
274-
matches[j].nScore = nScore;
275-
currScore *= nScore;
276-
}
287+
match.nScore = nScore;
288+
currScore *= nScore;
277289
}
278290

279-
results[i].score = bestScore == 1.0 ? currScore : bestScore;
291+
results[i].score = currScore;
280292
}
281293
}
282294

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: fuzzy
2-
version: 0.2.4
2+
version: 0.3.0
33

44
description: >
55
Fuzzy search in Dart. initially translated from Fuse.js.

test/fixtures/games.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
class Game {
2+
Game({this.tournament, this.stage});
3+
4+
final String tournament;
5+
final String stage;
6+
7+
@override
8+
String toString() => '$tournament $stage';
9+
}
10+
11+
final customGameList = [
12+
Game(
13+
tournament: 'WorldCup',
14+
stage: 'Groups',
15+
),
16+
Game(
17+
tournament: 'WorldCup',
18+
stage: 'Semi-finals',
19+
),
20+
Game(
21+
tournament: 'WorldCup',
22+
stage: 'Final',
23+
),
24+
Game(
25+
tournament: 'ChampionsLeague',
26+
stage: 'Groups',
27+
),
28+
Game(
29+
tournament: 'ChampionsLeague',
30+
stage: 'Semi-finals',
31+
),
32+
Game(
33+
tournament: 'ChampionsLeague',
34+
stage: 'Final',
35+
),
36+
];

0 commit comments

Comments
 (0)