Last updated:
0 purchases
sherlock
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();
copied to clipboard
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 the Sherlock 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,
})
copied to clipboard
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];
},
);
copied to clipboard
Create a Sherlock instance. #
Prototype
Sherlock(
List<Map<String, dynamic>> elements,
Map<String, int> priorities = {'*': 1},
NormalizationSettings normalization = /* defaults */
)
copied to clipboard
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)
copied to clipboard
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);
copied to clipboard
Specifying normalization :
final normalization = NormalizationSettings(
normalizeCase: true,
normalizeCaseType: false,
removeDiacritics: true,
);
final sherlock = Sherlock(elements: users, normalization: normalization);
copied to clipboard
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,
];
copied to clipboard
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;
copied to clipboard
/// 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,
)
copied to clipboard
These settings are only used by query and queryMatch. The smart search uses its own normalization settings, which is :
NormalizationSettings(
normalizeCase: true,
normalizeCaseType: false,
removeDiacritics: true,
);
copied to clipboard
Results #
Every query function returns its research findings. These results are returned as List<Result> and can be sorted thanks to the extension function SortResults.sorted, then unwrap thanks to the other extension function UnwrapResults.unwrap which returns a List<Map>.
Import
import 'package:sherlock/result.dart';
copied to clipboard
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();
}
copied to clipboard
Usages
Results are sorted following the priorities map.
final sherlock = Sherlock(/*...*/);
List<Result> results = (await sherlock./* query */).sorted();
copied to clipboard
Unwrapping results means getting just the element object from the Result object.
final sherlock = Sherlock(/*...*/);
List<Result> results = (await sherlock./* query */).sorted();
List<Map> foundElements = results.unwrap();
copied to clipboard
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();
copied to clipboard
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 */,
)
copied to clipboard
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).*');
copied to clipboard
Prototype
/// Searches for elements where [what] exists (is not null) in the column [where].
Future<List<Result>> queryExist(String where, String what)
copied to clipboard
Usage
/// All activities where monday is specified in the opening hours.
sherlock.queryExist(where: 'openingHours', what: 'monday');
copied to clipboard
Prototypes
Future<List<Result>> queryBool(
String where = '*',
bool Function(dynamic value) fn,
)
Future<List<Result>> queryMatch(
String where = '*',
dynamic match,
NormalizationSettings specificNormalization = /* this.normalization */,
)
copied to clipboard
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,
);
copied to clipboard
/// 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].
),
);
copied to clipboard
/// 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].
),
);
copied to clipboard
Smart search #
Prototype
Future<List<Result>> search(
dynamic where = '*',
String input,
List<String> stopWords = StopWords.en,
)
copied to clipboard
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');
copied to clipboard
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,
)
copied to clipboard
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);
copied to clipboard
Input #
Prototype
Future<List<Result>> input(
String input,
bool caseSensitive = false,
bool? caseSensitiveFurtherSearches,
int minResults = -1,
int maxResults = -1,
)
copied to clipboard
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);
copied to clipboard
[Fruits and vegetables market, Fresh fish store]
[Fruits and vegetables market, Fresh fish store]
copied to clipboard
// 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,
);
copied to clipboard
[Fruits and vegetables market, Fresh fish store, Best place to find fruits, Museum of Africa]
[Fruits and vegetables market, Fresh fish store]
copied to clipboard
// Find maximum 1 name matching with 'fr'.
completion.input(input: 'fr', maxResults: 1);
copied to clipboard
[Fruits and vegetables market]
copied to clipboard
Important note: as you can see in the prototype, the input function
retuerns a list of Result, 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());
copied to clipboard
Results #
Prototypes
Future<List<String>> getStrings(
List<Result> fromResults
);
copied to clipboard
Usage
List<Result> results = await completion.input(input: 'fr'));
List<String> resultNames = await completer.getStrings(fromResults: results);
print('names: $resultNames');
copied to clipboard
names: [Fruits and vegetables market, Fresh fish store]
copied to clipboard
Unchanged ranges of the string results #
Prototype
Future<List<Range>> unchangedRanges({
String input,
List<String> results,
)
copied to clipboard
class Range {
int start;
int end;
}
copied to clipboard
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);
copied to clipboard
[Fruits and vegetables market, Fresh fish store, Best place to find fruits, Museum of Africa]
[[0, 2], [0, 2], [19, 21], [11, 13]]
copied to clipboard
For personal and professional use. You cannot resell or redistribute these repositories in their original state.
There are no reviews.