match-sorter
Simple, expected, and deterministic best-match sorting of an array in JavaScript
Top Related Projects
Quick Overview
match-sorter
is a JavaScript utility library that provides a simple and efficient way to sort and filter arrays of objects based on a given search query. It is designed to be used in scenarios where users need to quickly find and sort data, such as in autocomplete or search functionality.
Pros
- Simplicity: The library has a straightforward API and is easy to integrate into existing projects.
- Performance:
match-sorter
is optimized for performance, making it suitable for use in high-traffic applications. - Flexibility: The library supports a variety of sorting and filtering options, allowing developers to customize the behavior to fit their specific needs.
- Dependency-free:
match-sorter
is a standalone library and does not require any additional dependencies, making it easy to include in projects.
Cons
- Limited Functionality: While
match-sorter
is a powerful tool for sorting and filtering, it may not provide all the features that some developers might need, such as advanced search capabilities or complex data structures. - Lack of Internationalization: The library currently does not support internationalization, which may be a concern for projects targeting a global audience.
- Potential Performance Issues with Large Datasets: While
match-sorter
is optimized for performance, it may still struggle with extremely large datasets, especially on older or less powerful devices. - Lack of Comprehensive Documentation: The project's documentation, while helpful, could be more extensive, particularly for more advanced use cases.
Code Examples
Here are a few examples of how to use match-sorter
:
import matchSorter from 'match-sorter';
// Sorting an array of objects by a single property
const data = [
{ name: 'John Doe', age: 30 },
{ name: 'Jane Smith', age: 25 },
{ name: 'Bob Johnson', age: 35 },
];
const sortedData = matchSorter(data, 'John', { keys: ['name'] });
console.log(sortedData);
// Output: [{ name: 'John Doe', age: 30 }]
// Sorting an array of objects by multiple properties
const data = [
{ name: 'John Doe', email: 'john@example.com', age: 30 },
{ name: 'Jane Smith', email: 'jane@example.com', age: 25 },
{ name: 'Bob Johnson', email: 'bob@example.com', age: 35 },
];
const sortedData = matchSorter(data, 'john', { keys: ['name', 'email'] });
console.log(sortedData);
// Output: [{ name: 'John Doe', email: 'john@example.com', age: 30 }]
// Filtering an array of objects
const data = [
{ name: 'John Doe', email: 'john@example.com', age: 30 },
{ name: 'Jane Smith', email: 'jane@example.com', age: 25 },
{ name: 'Bob Johnson', email: 'bob@example.com', age: 35 },
];
const filteredData = matchSorter(data, 'john', { keys: ['name', 'email'] });
console.log(filteredData);
// Output: [{ name: 'John Doe', email: 'john@example.com', age: 30 }]
Getting Started
To get started with match-sorter
, you can install the package using npm or yarn:
npm install match-sorter
or
yarn add match-sorter
Once installed, you can import the matchSorter
function and use it in your code:
import matchSorter from 'match-sorter';
const data = [
{ name: 'John Doe', email: 'john@example.com', age: 30 },
{ name: 'Jane Smith', email: 'jane@example.com', age: 25 },
{ name: 'Bob Johnson', email: 'bob@example.com
Competitor Comparisons
Promise queue with concurrency control
Pros of p-queue
- Concurrency Control: p-queue provides a way to control the concurrency of asynchronous operations, ensuring that a maximum number of tasks are executed at a time.
- Prioritization: p-queue allows you to prioritize tasks, ensuring that more important tasks are executed before less important ones.
- Pause and Resume: p-queue offers the ability to pause and resume the queue, which can be useful in certain scenarios.
Cons of p-queue
- Complexity: p-queue is a more complex library compared to Match Sorter, which may be overkill for simpler use cases.
- Overhead: The additional features and functionality of p-queue may come with some overhead, which could be a concern in performance-critical applications.
Code Comparison
Match Sorter:
import matchSorter from 'match-sorter';
const items = ['apple', 'banana', 'cherry', 'date'];
const filteredItems = matchSorter(items, 'a');
console.log(filteredItems); // ['apple', 'banana']
p-queue:
import PQueue from 'p-queue';
const queue = new PQueue({ concurrency: 2 });
queue.add(() => doSomething(1));
queue.add(() => doSomething(2));
queue.add(() => doSomething(3));
Run multiple promise-returning & async functions with limited concurrency
Pros of p-limit
- Concurrency Control: p-limit allows you to limit the number of concurrent promises being executed, which can be useful in scenarios where you want to avoid overloading a system or API.
- Flexible API: The API of p-limit is simple and flexible, allowing you to easily integrate it into your existing codebase.
- Lightweight: p-limit is a lightweight library, with a small footprint and no external dependencies.
Cons of p-limit
- Limited Functionality: Compared to Match Sorter, p-limit has a more narrow focus on concurrency control, and does not provide the same level of functionality for sorting and filtering data.
- No Sorting or Filtering: Match Sorter provides a rich set of features for sorting and filtering data, which are not available in p-limit.
Code Comparison
Match Sorter:
import matchSorter from 'match-sorter';
const data = [
{ name: 'John Doe', age: 30 },
{ name: 'Jane Smith', age: 25 },
{ name: 'Bob Johnson', age: 35 }
];
const filteredData = matchSorter(data, 'Doe', { keys: ['name'] });
console.log(filteredData);
p-limit:
import pLimit from 'p-limit';
const limit = pLimit(2);
const tasks = [
() => doSomething(1),
() => doSomething(2),
() => doSomething(3),
() => doSomething(4)
];
Promise.all(tasks.map(task => limit(task))).then(() => {
console.log('All tasks completed');
});
Map over promises concurrently
Pros of p-map
- Concurrency Control: p-map allows you to control the concurrency level of your asynchronous operations, ensuring efficient resource utilization and preventing overloading.
- Flexible Mapping: The library provides a flexible mapping function, allowing you to transform the input data as needed during the parallel processing.
- Error Handling: p-map handles errors gracefully, providing the ability to handle individual errors without interrupting the entire operation.
Cons of p-map
- Limited Functionality: Compared to Match Sorter, p-map is a more specialized library focused on parallel mapping, lacking the broader functionality of a general-purpose utility like Match Sorter.
- Dependency Management: p-map has a dependency on the
p-limit
library, which may add complexity to your project's dependency management.
Code Comparison
Match Sorter:
import matchSorter from 'match-sorter';
const items = ['apple', 'banana', 'cherry', 'date'];
const filteredItems = matchSorter(items, 'a');
console.log(filteredItems); // ['apple', 'banana']
p-map:
import pMap from 'p-map';
const urls = ['https://example.com', 'https://example.org', 'https://example.net'];
const fetchData = async (url) => {
const response = await fetch(url);
return response.json();
};
const data = await pMap(urls, fetchData, { concurrency: 2 });
console.log(data);
Convert designs to code with AI
Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.
Try Visual CopilotREADME
match-sorter
Simple, expected, and deterministic best-match sorting of an array in JavaScript
The problem
- You have a list of dozens, hundreds, or thousands of items
- You want to filter and sort those items intelligently (maybe you have a filter input for the user)
- You want simple, expected, and deterministic sorting of the items (no fancy math algorithm that fancily changes the sorting as they type)
This solution
This follows a simple and sensible (user friendly) algorithm that makes it easy for you to filter and sort a list of items based on given input. Items are ranked based on sensible criteria that result in a better user experience.
To explain the ranking system, I'll use countries as an example:
- CASE SENSITIVE EQUALS: Case-sensitive equality trumps all. These will be
first. (ex.
France
would matchFrance
, but notfrance
) - EQUALS: Case-insensitive equality (ex.
France
would matchfrance
) - STARTS WITH: If the item starts with the given value (ex.
Sou
would matchSouth Korea
orSouth Africa
) - WORD STARTS WITH: If the item has multiple words, then if one of those
words starts with the given value (ex.
Repub
would matchDominican Republic
) - CONTAINS: If the item contains the given value (ex.
ham
would matchBahamas
) - ACRONYM: If the item's acronym is the given value (ex.
us
would matchUnited States
) - SIMPLE MATCH: If the item has letters in the same order as the letters
of the given value (ex.
iw
would matchZimbabwe
, but notKuwait
because it must be in the same order). Furthermore, if the item is a closer match, it will rank higher (ex.ua
matchesUruguay
more closely thanUnited States of America
, thereforeUruguay
will be ordered beforeUnited States of America
)
This ranking seems to make sense in people's minds. At least it does in mine. Feedback welcome!
- Installation
- Usage
- Advanced options
- Recipes
- Inspiration
- Other Solutions
- Issues
- Contributors â¨
- LICENSE
Installation
This module is distributed via npm which is bundled with node and
should be installed as one of your project's dependencies
:
npm install match-sorter
Usage
import {matchSorter} from 'match-sorter'
// or const {matchSorter} = require('match-sorter')
// or window.matchSorter.matchSorter
const list = ['hi', 'hey', 'hello', 'sup', 'yo']
matchSorter(list, 'h') // ['hello', 'hey', 'hi']
matchSorter(list, 'y') // ['yo', 'hey']
matchSorter(list, 'z') // []
Advanced options
keys: [string]
Default: undefined
By default it just uses the value itself as above. Passing an array tells match-sorter which keys to use for the ranking.
const objList = [
{name: 'Janice', color: 'Green'},
{name: 'Fred', color: 'Orange'},
{name: 'George', color: 'Blue'},
{name: 'Jen', color: 'Red'},
]
matchSorter(objList, 'g', {keys: ['name', 'color']})
// [{name: 'George', color: 'Blue'}, {name: 'Janice', color: 'Green'}, {name: 'Fred', color: 'Orange'}]
matchSorter(objList, 're', {keys: ['color', 'name']})
// [{name: 'Jen', color: 'Red'}, {name: 'Janice', color: 'Green'}, {name: 'Fred', color: 'Orange'}, {name: 'George', color: 'Blue'}]
Array of values: When the specified key matches an array of values, the best match from the values of in the array is going to be used for the ranking.
const iceCreamYum = [
{favoriteIceCream: ['mint', 'chocolate']},
{favoriteIceCream: ['candy cane', 'brownie']},
{favoriteIceCream: ['birthday cake', 'rocky road', 'strawberry']},
]
matchSorter(iceCreamYum, 'cc', {keys: ['favoriteIceCream']})
// [{favoriteIceCream: ['candy cane', 'brownie']}, {favoriteIceCream: ['mint', 'chocolate']}]
Nested Keys: You can specify nested keys using dot-notation.
const nestedObjList = [
{name: {first: 'Janice'}},
{name: {first: 'Fred'}},
{name: {first: 'George'}},
{name: {first: 'Jen'}},
]
matchSorter(nestedObjList, 'j', {keys: ['name.first']})
// [{name: {first: 'Janice'}}, {name: {first: 'Jen'}}]
const nestedObjList = [
{name: [{first: 'Janice'}]},
{name: [{first: 'Fred'}]},
{name: [{first: 'George'}]},
{name: [{first: 'Jen'}]},
]
matchSorter(nestedObjList, 'j', {keys: ['name.0.first']})
// [{name: {first: 'Janice'}}, {name: {first: 'Jen'}}]
// matchSorter(nestedObjList, 'j', {keys: ['name[0].first']}) does not work
This even works with arrays of multiple nested objects: just specify the key
using dot-notation with the *
wildcard instead of a numeric index.
const nestedObjList = [
{aliases: [{name: {first: 'Janice'}},{name: {first: 'Jen'}}]},
{aliases: [{name: {first: 'Fred'}},{name: {first: 'Frederic'}}]},
{aliases: [{name: {first: 'George'}},{name: {first: 'Georgie'}}]},
]
matchSorter(nestedObjList, 'jen', {keys: ['aliases.*.name.first']})
// [{aliases: [{name: {first: 'Janice'}},{name: {first: 'Jen'}}]}]
matchSorter(nestedObjList, 'jen', {keys: ['aliases.0.name.first']})
// []
Property Callbacks: Alternatively, you may also pass in a callback function that resolves the value of the key(s) you wish to match on. This is especially useful when interfacing with libraries such as Immutable.js
const list = [{name: 'Janice'}, {name: 'Fred'}, {name: 'George'}, {name: 'Jen'}]
matchSorter(list, 'j', {keys: [item => item.name]})
// [{name: 'Janice'}, {name: 'Jen'}]
For more complex structures, expanding on the nestedObjList
example above, you
can use map
:
const nestedObjList = [
{
name: [
{first: 'Janice', last: 'Smith'},
{first: 'Jon', last: 'Doe'},
],
},
{
name: [
{first: 'Fred', last: 'Astaire'},
{first: 'Jenny', last: 'Doe'},
{first: 'Wilma', last: 'Flintstone'},
],
},
]
matchSorter(nestedObjList, 'doe', {
keys: [
item => item.name.map(i => i.first),
item => item.name.map(i => i.last),
],
})
// [name: [{ first: 'Janice', last: 'Smith' },{ first: 'Jon', last: 'Doe' }], name: [{ first: 'Fred', last: 'Astaire' },{ first: 'Jenny', last: 'Doe' },{ first: 'Wilma', last: 'Flintstone' }]]
Threshold: You may specify an individual threshold for specific keys. A key will only match if it meets the specified threshold. For more information regarding thresholds see below
const list = [
{name: 'Fred', color: 'Orange'},
{name: 'Jen', color: 'Red'},
]
matchSorter(list, 'ed', {
keys: [{threshold: matchSorter.rankings.STARTS_WITH, key: 'name'}, 'color'],
})
//[{name: 'Jen', color: 'Red'}]
Min and Max Ranking: You may restrict specific keys to a minimum or maximum ranking by passing in an object. A key with a minimum rank will only get promoted if there is at least a simple match.
const tea = [
{tea: 'Earl Grey', alias: 'A'},
{tea: 'Assam', alias: 'B'},
{tea: 'Black', alias: 'C'},
]
matchSorter(tea, 'A', {
keys: ['tea', {maxRanking: matchSorter.rankings.STARTS_WITH, key: 'alias'}],
})
// without maxRanking, Earl Grey would come first because the alias "A" would be CASE_SENSITIVE_EQUAL
// `tea` key comes before `alias` key, so Assam comes first even though both match as STARTS_WITH
// [{tea: 'Assam', alias: 'B'}, {tea: 'Earl Grey', alias: 'A'},{tea: 'Black', alias: 'C'}]
const tea = [
{tea: 'Milk', alias: 'moo'},
{tea: 'Oolong', alias: 'B'},
{tea: 'Green', alias: 'C'},
]
matchSorter(tea, 'oo', {
keys: ['tea', {minRanking: matchSorter.rankings.EQUAL, key: 'alias'}],
})
// minRanking bumps Milk up to EQUAL from CONTAINS (alias)
// Oolong matches as STARTS_WITH
// Green is missing due to no match
// [{tea: 'Milk', alias: 'moo'}, {tea: 'Oolong', alias: 'B'}]
threshold: number
Default: MATCHES
Thresholds can be used to specify the criteria used to rank the results. Available thresholds (from top to bottom) are:
- CASE_SENSITIVE_EQUAL
- EQUAL
- STARTS_WITH
- WORD_STARTS_WITH
- CONTAINS
- ACRONYM
- MATCHES (default value)
- NO_MATCH
const fruit = ['orange', 'apple', 'grape', 'banana']
matchSorter(fruit, 'ap', {threshold: matchSorter.rankings.NO_MATCH})
// ['apple', 'grape', 'orange', 'banana'] (returns all items, just sorted by best match)
const things = ['google', 'airbnb', 'apple', 'apply', 'app'],
matchSorter(things, 'app', {threshold: matchSorter.rankings.EQUAL})
// ['app'] (only items that are equal)
const otherThings = ['fiji apple', 'google', 'app', 'crabapple', 'apple', 'apply']
matchSorter(otherThings, 'app', {threshold: matchSorter.rankings.WORD_STARTS_WITH})
// ['app', 'apple', 'apply', 'fiji apple'] (everything that matches with "word starts with" or better)
keepDiacritics: boolean
Default: false
By default, match-sorter will strip diacritics before doing any comparisons. This is the default because it makes the most sense from a UX perspective.
You can disable this behavior by specifying keepDiacritics: true
const thingsWithDiacritics = [
'jalapeño',
'Ã la carte',
'café',
'papier-mâché',
'Ã la mode',
]
matchSorter(thingsWithDiacritics, 'aa')
// ['jalapeño', 'à la carte', 'à la mode', 'papier-mâché']
matchSorter(thingsWithDiacritics, 'aa', {keepDiacritics: true})
// ['jalapeño', 'à la carte']
matchSorter(thingsWithDiacritics, 'Ã ', {keepDiacritics: true})
// ['Ã la carte', 'Ã la mode']
baseSort: function(itemA, itemB): -1 | 0 | 1
Default: (a, b) => String(a.rankedValue).localeCompare(b.rankedValue)
By default, match-sorter uses the String.localeCompare
function to tie-break
items that have the same ranking. This results in a stable, alphabetic sort.
const list = ['C apple', 'B apple', 'A apple']
matchSorter(list, 'apple')
// ['A apple', 'B apple', 'C apple']
You can customize this behavior by specifying a custom baseSort
function:
const list = ['C apple', 'B apple', 'A apple']
// This baseSort function will use the original index of items as the tie breaker
matchSorter(list, 'apple', {baseSort: (a, b) => (a.index < b.index ? -1 : 1)})
// ['C apple', 'B apple', 'A apple']
sorter: function(rankedItems): rankedItems
Default:
matchedItems => matchedItems.sort((a, b) => sortRankedValues(a, b, baseSort))
By default, match-sorter uses an internal sortRankedValues
function to sort
items after matching them.
You can customize the core sorting behavior by specifying a custom sorter
function:
Disable sorting entirely:
const list = ['appl', 'C apple', 'B apple', 'A apple', 'app', 'applebutter']
matchSorter(list, 'apple', {sorter: rankedItems => rankedItems})
// ['C apple', 'B apple', 'A apple', 'applebutter']
Return the unsorted rankedItems, but in reverse order:
const list = ['appl', 'C apple', 'B apple', 'A apple', 'app', 'applebutter']
matchSorter(list, 'apple', {sorter: rankedItems => [...rankedItems].reverse()})
// ['applebutter', 'A apple', 'B apple', 'C apple']
Recipes
Match PascalCase, camelCase, snake_case, or kebab-case as words
By default, match-sorter
assumes spaces to be the word separator. However, if
your data has a different word separator, you can use a property callback to
replace your separator with spaces. For example, for snake_case
:
const list = [
{name: 'Janice_Kurtis'},
{name: 'Fred_Mertz'},
{name: 'George_Foreman'},
{name: 'Jen_Smith'},
]
matchSorter(list, 'js', {keys: [item => item.name.replace(/_/g, ' ')]})
// [{name: 'Jen_Smith'}, {name: 'Janice_Kurtis'}]
Match many words across multiple fields (table filtering)
By default, match-sorter
will return matches from objects where one of the
properties matches the entire search term. For multi-column data sets it can
be beneficial to split words in search string and match each word separately.
This can be done by chaining match-sorter
calls.
The benefit of this is that a filter string of "two words" will match both "two" and "words", but will return rows where the two words are found in different columns as well as when both words match in the same column. For single-column matches it will also return matches out of order (column = "wordstwo" will match just as well as column="twowords", the latter getting a higher score).
function fuzzySearchMultipleWords(
rows, // array of data [{a: "a", b: "b"}, {a: "c", b: "d"}]
keys, // keys to search ["a", "b"]
filterValue: string, // potentially multi-word search string "two words"
) {
if (!filterValue || !filterValue.length) {
return rows
}
const terms = filterValue.split(' ')
if (!terms) {
return rows
}
// reduceRight will mean sorting is done by score for the _first_ entered word.
return terms.reduceRight(
(results, term) => matchSorter(results, term, {keys}),
rows,
)
}
Inspiration
Actually, most of this code was extracted from the very first library I ever wrote: genie!
Other Solutions
You might try Fuse.js. It uses advanced math fanciness to get the closest match. Unfortunately what's "closest" doesn't always really make sense. So I extracted this from genie.
Issues
Looking to contribute? Look for the Good First Issue label.
ð Bugs
Please file an issue for bugs, missing documentation, or unexpected behavior.
ð¡ Feature Requests
Please file an issue to suggest new features. Vote on feature requests by adding a ð. This helps maintainers prioritize what to work on.
Contributors â¨
Thanks goes to these people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!
LICENSE
MIT
Top Related Projects
Convert designs to code with AI
Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.
Try Visual Copilot