sherlock is a library to perform efficient and customized searches on local data, for Flutter.
It provides a search engine, a tool to complete search inputs and can be easily integrated in a search bar widget.
Sherlock in the new SearchBar
widget ! (Flutter 3.10.0)
See this example here.
Usage • Overview • Completion tool • Examples
Sherlock needs the elements in which it (he?) will search. Priorities can be specified for results sorting, but it is not mandatory.
final foo = [
{
'col1': 'foo',
'col2': ['foo1', 'foo2'],
'col3': <non-string value>,
},
// Other elements...
];
// The bigger it is, the more important it is.
final priorities = {
'col2': 4,
'col1': 3,
// '*': 1,
};
final sherlock = Sherlock(elements: foo, priorities: priorities);
var results = sherlock
.query(where: '<column>', regex: r'<regex expression>')
.sorted()
.unwrap();
Note : this package is designed for researches on local data retrieved after an API call or something. It avoids requiring Internet during the search.
See the examples.
See also the search completion tool.
-
Use to execute any task with a unique
Sherlock
instance. The function parameters are constructed like theSherlock
constructor plus a callback in which tasks are executed.Prototype
Future<List<Element>> processUnique( List<Element> elements, PriorityMap priorities = const {'*': 1}, NormalizationSettings normalization = /* default */, void Function(Sherlock sherlock) queries, })
Usage
final users = [ { 'firstName': 'Finn', 'lastName': 'Thornton', 'city': 'Edinburgh', 'id': 1, }, { 'firstName': 'Suz', 'lastName': 'Judy', 'city': 'Paris', 'id': 2, }, { 'firstName': 'Suz', 'lastName': 'Crystal', 'city': 'Edinburgh', 'id': 3, }, ]; final results = await Sherlock.processUnique( elements: users, fn: (sherlock) async { final resultsName = sherlock.queryMatch(where: 'firstName', match: 'Finn'); final resultsCity = sherlock.queryMatch(where: 'city', match: 'Edinburgh'); return [...await resultsName, ...await resultsCity]; }, );
-
Prototype
Sherlock( List<Map<String, dynamic>> elements, Map<String, int> priorities = {'*': 1}, NormalizationSettings normalization = /* defaults */ )
Usage
/// Users with their first and last name, and the city where they live. /// They also have an ID. List<Map<String, dynamic>> users = [ { 'firstName': 'Finn', 'lastName': 'Thornton', 'city': 'Edinburgh', 'id': 1, // other types than string can be used. }, { 'firstName': 'Suz', 'lastName': 'Judy', 'city': 'Paris', 'id': 2, }, { 'firstName': 'Suz', 'lastName': 'Crystal', 'city': 'Edinburgh', 'hobbies': ['sport', 'programming'], // string lists can be used. 'id': 3, }, ]; final sherlock = Sherlock(elements: users)
Specifying
priorities
:// First and last name have the same priority. // The city is less important. // The default priority is `1`. Map<String, int> priorities = [ 'firstName': 3, 'lastName': 3, 'city': 2, ]; final sherlock = Sherlock(elements: users, priorities: priorities);
Specifying
normalization
:final normalization = NormalizationSettings( normalizeCase: true, normalizeCaseType: false, removeDiacritics: true, ); final sherlock = Sherlock(elements: users, normalization: normalization);
-
The priority map (also known as "priorities") is used to define the priority of each column. If there is no priority set for a column, the default priority will be used instead.
The default priority value can be specified, otherwise it will be set to
1
:// The city is the least important. Map<String, int> priorities = [ 'firstName': 3, 'lastName': 3, 'city': 1, '*': 2, ];
-
The normalization settings are used to define the type of normalization that will be performed on the strings during searches.
Prototype
NormalizationSettings normalization;
/// Out of the [Sherlock] class. NormalizationSettings( // If `true` : case insensitive. // If `false` : case sensitive. bool normalizeCase, // If `true` : no matter if it is snake or camel cased. // If `false` : it matters to be snake or camel cased. bool normalizeCaseType, // If `true` : keeps the diacritics. // If `false` : remove all the diacritics. bool removeDiacritics, )
These settings are only used by
query
andqueryMatch
. The smart search uses its own normalization settings, which is :NormalizationSettings( normalizeCase: true, normalizeCaseType: false, removeDiacritics: true, );
-
Every query function returns its research findings. These results are returned as
List<Result>
and can be sorted thanks to the extension functionSortResults.sorted
, then unwrap thanks to the other extension functionUnwrapResults.unwrap
which returns aList<Map>
.Import
import 'package:sherlock/result.dart';
Prototypes
class Result { Map<String, dynamic> element; int priority; } extension SortResults on List<Result> { List<Result> sorted(); } extension UnwrapResults on List<Result> { List<Map<String, dynamic>> unwrap(); }
Usages
Results are sorted following the
priorities
map.final sherlock = Sherlock(/*...*/); List<Result> results = (await sherlock./* query */).sorted();
Unwrapping results means getting just the
element
object from theResult
object.final sherlock = Sherlock(/*...*/); List<Result> results = (await sherlock./* query */).sorted(); List<Map> foundElements = results.unwrap();
Note: Getting results unsorted means the results will be in the order they were found.
Also, the results can be sorted at the end after all queries are done :
final sherlock = Sherlock(/*...*/); final Future<List<Result>> results1 = sherlock./* query */; final Future<List<Result>> results2 = sherlock./* query */; final allResults = [...await results1, ...await results2].sorted();
-
Every query returns its research findings (results) but they are not sorted. Click here to learn how to manage them.
Prototypes
Future<List<Result>> query( String where = '*', String regex, NormalizationSettings specificNormalization = /* this.normalization */, )
Usage
/// All elements having a title, which contains the word 'game' or 'vr'. sherlock.query(where: 'title', regex: r'(game|vr)'); /// All elements with in at least one of their fields which contain the word /// 'cat'. final catsResults = sherlock.query(regex: r'cat'); /// All elements having a title, which is equal to 'movie theatre'. sherlock.query(where: 'title', regex: r'^Movie Theatre$'); /// All elements having a title, which is equal to 'Movie Theatre', the case /// matters. sherlock.query( where: 'title', regex: r'^Movie Theatre$', specificNormalization: NormalizationSettings( normalizeCase: false, // other normalization settings are the one of [this.normalization]. ) ); /// All elements with both words 'world' and 'pretty' in their descriptions. sherlock.query(where: 'description', regex: r'(?=.*pretty)(?=.*world).*');
Prototype
/// Searches for elements where [what] exists (is not null) in the column [where]. Future<List<Result>> queryExist(String where, String what)
Usage
/// All activities where monday is specified in the opening hours. sherlock.queryExist(where: 'openingHours', what: 'monday');
Prototypes
Future<List<Result>> queryBool( String where = '*', bool Function(dynamic value) fn, ) Future<List<Result>> queryMatch( String where = '*', dynamic match, NormalizationSettings specificNormalization = /* this.normalization */, )
Usages
/// All activities having a title which does not correspond to 'Parc'. sherlock.queryBool(where: 'title', fn: (value) => value != 'Parc'); /// All activities starting at 7'o on tuesday. sherlock.queryBool( where: 'openingHours', fn: (value) => value['tuesday'][0] == 7, );
/// All activities having a title corresponding to 'Parc', the case matters. sherlock.queryMatch( where: 'title', match: 'Parc', specificNormalization: NormalizationSettings( normalizeCase: false, // other normalization settings are the one of [this.normalization]. ), );
/// All activities having a title corresponding to 'parc', no matter the case. sherlock.queryMatch( where: 'title', match: 'pArC', specificNormalization: NormalizationSettings( normalizeCase: true, // other normalization settings are the one of [this.normalization]. ), );
-
Prototype
Future<List<Result>> search( dynamic where = '*', String input, List<String> stopWords = StopWords.en, )
Usages
Perfect matches are searched first, it means they will be on top of the results if they exist.
/// All elements having at least one of their field containing the word 'cats' sherlock.search(input: 'cAtS'); /// Elements having their title or their categories containing the word 'cat' sherlock.search(where: ['title', 'categories'], input: 'cat');
When doing searches from an user's input, it might be useful to help them completing their search. That's why SherlockCompletion
exists.
The results could be used in a search widget for example.
-
Prototype
SherlockCompletion( String where, List<Map<String, dynamic>> elements, )
Usage
final places = [ { 'name': 'Africa discovery', }, { 'name': 'Fruits and vegetables market', 'description': 'A cool place to buy fruits and vegetables', }, { 'name': 'Fresh fish store', }, { 'name': 'Ball pool', }, { 'name': 'Finland discovery', }, ]; final completer = SherlockCompletion(where: 'name', elements: places);
-
Prototype
Future<List<Result>> input( String input, bool caseSensitive = false, bool? caseSensitiveFurtherSearches, int minResults = -1, int maxResults = -1, )
Usage
// Find all the elements with names starting with 'fr'. await completer.input(input: 'fr'); // Find all the elements with names starting with 'Fr', and the case matters. await completer.input(input: 'Fr', caseSensitive: true);
[Fruits and vegetables market, Fresh fish store] [Fruits and vegetables market, Fresh fish store]
// Try to find at least 4 elements with names matching with 'fr'. await completer.input(input: 'fr', minResults: 4); // Try to find at least 3 elements with names matching with 'Fr', and the // case matters only for the searches that might be performed if there is // less than 3 results. await completer.input( input: 'Fr', minResults: 3, caseSensitiveFurtherSearches: true, );
[Fruits and vegetables market, Fresh fish store, Best place to find fruits, Museum of Africa] [Fruits and vegetables market, Fresh fish store]
// Find maximum 1 name matching with 'fr'. completion.input(input: 'fr', maxResults: 1);
[Fruits and vegetables market]
Important note: as you can see in the prototype, the
input
function retuerns a list ofResult
, not strings. To print the output seen above, the following has been done:final results = await completer.input(...); // Only get the completion strings from the results. final stringResults = completer.getStrings(fromResults: results); debugPrint(stringResults.toString());
-
Prototypes
Future<List<String>> getStrings( List<Result> fromResults );
Usage
List<Result> results = await completion.input(input: 'fr')); List<String> resultNames = await completer.getStrings(fromResults: results); print('names: $resultNames');
names: [Fruits and vegetables market, Fresh fish store]
-
Prototype
Future<List<Range>> unchangedRanges({ String input, List<String> results, )
class Range { int start; int end; }
Usage
This can be used to highlight the unchanged part while displaying the possible completions.
What it could look like :
const input = 'Fr'; final results = await completer.input(input: input, minResults: 4); final stringResults = completer.getStrings(fromResults: results); // The case is ignored. List<Range> unchangedRanges = await completer.unchangedRanges( input: input, results: stringResults, ); print(results); print(unchangedRanges);
[Fruits and vegetables market, Fresh fish store, Best place to find fruits, Museum of Africa] [[0, 2], [0, 2], [19, 21], [11, 13]]