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
Sherlockinstance. The function parameters are constructed like theSherlockconstructor 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
Sherlockinstance. #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
queryandqueryMatch. 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.unwrapwhich 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
prioritiesmap.final sherlock = Sherlock(/*...*/); List<Result> results = (await sherlock./* query */).sorted();Unwrapping results means getting just the
elementobject from theResultobject.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
SherlockCompletioninstance #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
inputfunction 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]]