sherlock 0.2.0 copy "sherlock: ^0.2.0" to clipboard
sherlock: ^0.2.0 copied to clipboard

Perform efficient and customized searches on local data. Built-in autocomplete feature for search bars.

sherlock #

Perform efficient and customized searches on local data. Built-in autocomplete feature for search bars.

Sherlock in the new SearchBar widget ! (Flutter 3.10.0)
See this example here.

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 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,
    })
    

    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 and queryMatch. 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 function SortResults.sorted, then unwrap thanks to the other extension function UnwrapResults.unwrap which returns a List<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 = sherlock./* query */.sorted();
    

    Unwrapping results means getting just the element object from the Result object.

    final sherlock = Sherlock(/*...*/);
    List<Result> results = sherlock./* query */.sorted();
    List<Map> foundElements = results.unwrap();
    

    Getting results unsorted means the results will be in the order they were found.

    final sherlock = Sherlock(/*...*/);
    List<Result> results = sherlock./* query */;
    

    Also, the results can be sorted at the end after all queries are done :

    final sherlock = Sherlock(/*...*/);
    
    List<Result> allResults [];
    allResults += sherlock./* query */;
    allResults += sherlock./* query */;
    
    allResults = allResults.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].
      ),
    );
    
  • 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<String>> input(
      String input,
      bool caseSensitive = false,
      bool? caseSensitiveFurtherSearches,
      int minResults = -1,
      int maxResults = -1,
    )
    

    Usage

    // Find all the names starting with 'fr'.
    await completer.input(input: 'fr');
    
    // Find all the 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 names matching with 'fr'.
    await completer.input(input: 'fr', minResults: 4);
    
    // Try to find at least 3 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]
    
  • Results #

    Prototypes

    // Completions for the given input.
    Future<List<String>> input(...);
    

    Usage

    List<String> resultNames = await completion.input(input: 'fr');
    print('names: $resultNames')
    
    names: [Fruits and vegetables market, Fresh fish store]
    
  • Unchanged ranges of the results #

    Prototype

    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 = completion.input(input: input, minResults: 4);
    
    // The case is ignored.
    List<Range> unchangedRanges = completion.unchangedRanges(
      input: input, 
      results: results,
    );
    
    print(await 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]]
    
17
likes
0
pub points
68%
popularity

Publisher

verified publisherantoninhrlt.com

Perform efficient and customized searches on local data. Built-in autocomplete feature for search bars.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

diacritic, flutter

More

Packages that depend on sherlock