Git Product home page Git Product logo

sherlock's Introduction

sherlock logo

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.

UsageOverviewCompletion toolExamples

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 = (await sherlock./* query */).sorted();

    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();

    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 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());
  • 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]]
    

sherlock's People

Contributors

antoninhrlt avatar fabienze avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

sherlock's Issues

Smart search getting silly when search input contains spaces

Searchable element example:

{
  "id": 29,
  "libelle": "Cheeseburger",
  "abbreviated": "cheeseburger",
  "abbreviatedClassification": "cheeseburger",
  "description": "",
  "price": "4.9",
  "category": {
    "id": 8,
    "libelle": "Burgers"
  }
}

Search query: Cheese burger
Sherlock usage:

Sherlock sherlock = Sherlock(
  elements: el,
  priorities: {
    'libelle': 3,
    'abbreviated': 2,
    'description': 2,
  },
);
var results = (await sherlock.search(input: searchQuery.value)).sorted().unwrap();

Results:

Desirable results:

Thanks for this package and the work you put on it. For more details on this issue, see our direct messages.

UnsupportedError (Unsupported operation: Cannot modify unmodifiable map)

When I allocate a Sherlock instance with a const priorities map, I get a runtime error :

const PriorityMap searchPriorities = {
  'name': 10,
  'description': 10,
  'address_formatted': 8,
  'contact_email': 5,
  'company_name': 2,
  'user_lastname': 2,
};
...
Sherlock(
    elements: elements,
    priorities: searchPriorities,
  );

Error occurs at sherlock.dart, line 95-96
when you set

if (priorities['*'] == null) {
      priorities['*'] = 1;
    }

You should better do this :

final PriorityMap defaultPriorities = { '*' : 1 };
final PriorityMap runPriorities = { ...defaultPriorities, ...priorities };

RangeError when the input length is greater than the column value length

In sherlock.dart, this :

// Calculates distance with the beginning of the string and the input.
final distance = levenshtein(
  a: normalizedValue.substring(0, input.length),
  b: input,
);

Should be changed to this :

// Calculates distance with the beginning of the string and the input.
final distance = levenshtein(
  a:  normalizedValue.substring(0, min(normalizedValue.length, input.length)),
  b: input,
);

Smart search return doublon

The use of Sherlock smart search, like this :

_searchResults = Sherlock(
    elements: _elementList,
    priorities: const {
        'title': 3,
        'description': 2,
    },
).search(input: value).sorted().unwrap();

is returning a list of doublon.

This could be linked to the use of _currentResults which is being reset at the end of each query even tho smart search is doing multiple queries.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.