sherlock 0.2.3 sherlock: ^0.2.3 copied to clipboard
Perform efficient and customized searches on local data. Built-in autocomplete feature for search bars.
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
Usage #
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.
Overview #
See also the search completion tool.
-
Quick Sherlock #
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]; }, );
-
Create a
Sherlock
instance. #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);
-
Priorities #
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, ];
-
Normalization settings #
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, );
-
Results #
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();
-
Queries #
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]. ), );
-
Smart search #
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');
Search completion tool #
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.
Overview #
-
Create a
SherlockCompletion
instance #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);
-
Input #
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());
-
Results #
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]
-
Unchanged ranges of the string results #
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]]