diff --git a/lib/data/fuzzy_options.dart b/lib/data/fuzzy_options.dart index 684f9d2..2223046 100644 --- a/lib/data/fuzzy_options.dart +++ b/lib/data/fuzzy_options.dart @@ -1,8 +1,25 @@ import 'result.dart'; -typedef SorterFn = int Function(Result a, Result b); +/// Represents a weighted getter of an item +class WeightedKey { + /// Instantiates it + WeightedKey({ + this.name, + this.getter, + this.weight, + }) : assert(weight > 0 && weight <= 1); + + /// Name of this getter + final String name; + + /// Getter to a specifc string inside item + final String Function(T obj) getter; + + /// Weight of this getter + final double weight; +} -typedef GetterFn = String Function(T obj); +typedef SorterFn = int Function(Result a, Result b); int _defaultSortFn(Result a, Result b) => a.score.compareTo(b.score); @@ -59,8 +76,8 @@ class FuzzyOptions { /// Minimum number of characters that must be matched before a result is considered a match final int minMatchCharLength; - /// List of getters to properties that will be searched - final List> keys; + /// List of weighted getters to properties that will be searched + final List> keys; /// Whether to sort the result list, by score final bool shouldSort; diff --git a/lib/data/result.dart b/lib/data/result.dart index 2ba1dd7..ca563db 100644 --- a/lib/data/result.dart +++ b/lib/data/result.dart @@ -61,6 +61,7 @@ class Result { class ResultDetails { /// Instantiates it ResultDetails({ + this.key = '', this.arrayIndex, this.value, this.score, @@ -68,6 +69,9 @@ class ResultDetails { this.nScore, }); + /// Key ([WeightedKey.name]) used to create this + final String key; + /// Index of result in the original list final int arrayIndex; diff --git a/lib/fuzzy.dart b/lib/fuzzy.dart index 7a38cb4..2ae194c 100644 --- a/lib/fuzzy.dart +++ b/lib/fuzzy.dart @@ -1,3 +1,5 @@ +library fuzzy; + import 'dart:math'; import 'bitap/bitap.dart'; @@ -69,12 +71,13 @@ class Fuzzy { final results = >[]; final resultMap = >{}; -// Check the first item in the list, if it's a string, then we assume + // Check the first item in the list, if it's a string, then we assume // that every item in the list is also a string, and thus it's a flattened array. if (list[0] is String) { // Iterate over every item for (var i = 0, len = list.length; i < len; i += 1) { _analyze( + key: '', value: list[i].toString(), record: list[i], index: i, @@ -95,10 +98,14 @@ class Fuzzy { final item = list[i]; // Iterate over every key for (var j = 0; j < options.keys.length; j += 1) { - final value = options.keys[j](item); - weights.update(value, (_) => 1.0, ifAbsent: () => 1.0); + final key = options.keys[j].name; + final value = options.keys[j].getter(item); + + final weight = 1.0 - options.keys[j].weight ?? 0.0; + weights.update(key, (_) => weight, ifAbsent: () => weight); _analyze( + key: key, value: value, record: list[i], index: i, @@ -114,6 +121,7 @@ class Fuzzy { } List> _analyze({ + String key = '', int arrayIndex = -1, String value, T record, @@ -195,6 +203,7 @@ class Fuzzy { // Use the lowest score // existingResult.score, bitapResult.score existingResult.matches.add(ResultDetails( + key: key, arrayIndex: arrayIndex, value: value, score: finalScore, @@ -206,6 +215,7 @@ class Fuzzy { item: record, matches: [ ResultDetails( + key: key, arrayIndex: arrayIndex, value: value, score: finalScore, @@ -238,9 +248,10 @@ class Fuzzy { var bestScore = 1.0; for (var j = 0; j < scoreLen; j += 1) { - final weight = weights[matches[j].value] ?? 1; - final score = - weight == 1 ? matches[j].score : (matches[j].score ?? 0.001); + final weight = weights[matches[j].key] ?? 1.0; + final score = weight == 1.0 + ? matches[j].score + : (matches[j].score == 0.0 ? 0.001 : matches[j].score); final nScore = score * weight; if (weight != 1) { @@ -251,9 +262,7 @@ class Fuzzy { } } - results[i].score = bestScore == 1 ? currScore : bestScore; - - _log('${results[i]}'); + results[i].score = bestScore == 1.0 ? currScore : bestScore; } } diff --git a/test/fixtures/books.dart b/test/fixtures/books.dart new file mode 100644 index 0000000..d4e673c --- /dev/null +++ b/test/fixtures/books.dart @@ -0,0 +1,37 @@ +class Book { + Book({ + this.title, + this.author, + this.tags = const [], + }); + + final String title; + final String author; + final List tags; + + @override + String toString() => '$title, $author'; +} + +final customBookList = [ + Book( + title: 'Old Man\'s War fiction', + author: 'John X', + tags: ['war'], + ), + Book( + title: 'Right Ho Jeeves', + author: 'P.D. Mans', + tags: ['fiction', 'war'], + ), + Book( + title: 'The life of Jane', + author: 'John Smith', + tags: ['john', 'smith'], + ), + Book( + title: 'John Smith', + author: 'Steve Pearson', + tags: ['steve', 'pearson'], + ), +]; diff --git a/test/fuzzy_test.dart b/test/fuzzy_test.dart index 634c942..218b000 100644 --- a/test/fuzzy_test.dart +++ b/test/fuzzy_test.dart @@ -1,6 +1,8 @@ import 'package:fuzzy/fuzzy.dart'; import 'package:test/test.dart'; +import 'fixtures/books.dart'; + final defaultList = ['Apple', 'Orange', 'Banana']; final defaultOptions = FuzzyOptions( location: 0, @@ -19,7 +21,7 @@ final defaultOptions = FuzzyOptions( ); Fuzzy setup({ - List itemList, + List itemList, FuzzyOptions overwriteOptions, }) { return Fuzzy( @@ -102,12 +104,87 @@ void main() { }); }); - group('Weighted search', () { - Fuzzy fuse; - setUp(() { - fuse = setup(); + group('Weighted search on typed list', () { + test('When searching for the term "John Smith" with author weighted higher', + () { + final fuse = Fuzzy( + customBookList, + options: FuzzyOptions(keys: [ + WeightedKey(getter: (i) => i.title, weight: 0.3, name: 'title'), + WeightedKey(getter: (i) => i.author, weight: 0.7, name: 'author'), + ]), + ); + final result = fuse.search('John Smith'); + + expect(result[0].item, customBookList[2], + reason: 'We get the the exactly matching object'); }); - }, skip: true); + + test('When searching for the term "John Smith" with title weighted higher', + () { + final fuse = Fuzzy( + customBookList, + options: FuzzyOptions(keys: [ + WeightedKey(getter: (i) => i.title, weight: 0.7, name: 'title'), + WeightedKey(getter: (i) => i.author, weight: 0.3, name: 'author'), + ]), + ); + final result = fuse.search('John Smith'); + + expect(result[0].item, customBookList[3], + reason: 'We get the the exactly matching object'); + }); + + test( + 'When searching for the term "Man", where the author is weighted higher than title', + () { + final fuse = Fuzzy( + customBookList, + options: FuzzyOptions(keys: [ + WeightedKey(getter: (i) => i.title, weight: 0.3, name: 'title'), + WeightedKey(getter: (i) => i.author, weight: 0.7, name: 'author'), + ]), + ); + final result = fuse.search('Man'); + + expect(result[0].item, customBookList[1], + reason: 'We get the the exactly matching object'); + }); + + test( + 'When searching for the term "Man", where the title is weighted higher than author', + () { + final fuse = Fuzzy( + customBookList, + options: FuzzyOptions(keys: [ + WeightedKey(getter: (i) => i.title, weight: 0.7, name: 'title'), + WeightedKey(getter: (i) => i.author, weight: 0.3, name: 'author'), + ]), + ); + final result = fuse.search('Man'); + + expect(result[0].item, customBookList[0], + reason: 'We get the the exactly matching object'); + }); + + test( + 'When searching for the term "War", where tags are weighted higher than all other keys', + () { + final fuse = Fuzzy( + customBookList, + options: FuzzyOptions(keys: [ + WeightedKey(getter: (i) => i.title, weight: 0.8, name: 'title'), + WeightedKey(getter: (i) => i.author, weight: 0.3, name: 'author'), + WeightedKey( + getter: (i) => i.tags.join(' '), weight: 0.9, name: 'tags'), + ]), + ); + final result = fuse.search('War'); + + expect(result[0].item, customBookList[0], + reason: 'We get the the exactly matching object'); + }); + }); group('Search with match all tokens', () { Fuzzy fuse;