synchronous-autocomplete
Advanced tools
Comparing version 1.0.0 to 2.0.0-alpha.1
29
build.js
'use strict'; | ||
var roundTo = function roundTo(v, p) { | ||
return parseFloat(v.toFixed(p)); | ||
}; | ||
// todo: handle props like `toString` | ||
var buildIndexes = function buildIndexes(tokenize, items) { | ||
var originalIds = []; | ||
var tokens = Object.create(null); | ||
var weights = Object.create(null); | ||
var nrOfTokens = Object.create(null); | ||
var weights = []; | ||
var nrOfTokens = []; | ||
var currentId = 0; | ||
var generateId = function generateId(oldId) { | ||
var newId = currentId++; | ||
originalIds[newId] = oldId; | ||
return newId; | ||
}; | ||
var _iteratorNormalCompletion = true; | ||
@@ -16,2 +29,3 @@ var _didIteratorError = false; | ||
var id = generateId(item.id); | ||
var tokensOfItem = tokenize(item.name); | ||
@@ -27,3 +41,3 @@ var _iteratorNormalCompletion2 = true; | ||
if (!Array.isArray(tokens[_token])) tokens[_token] = []; | ||
if (!tokens[_token].includes(item.id)) tokens[_token].push(item.id); | ||
if (!tokens[_token].includes(id)) tokens[_token].push(id); | ||
} | ||
@@ -45,4 +59,4 @@ } catch (err) { | ||
weights[item.id] = item.weight; | ||
nrOfTokens[item.id] = tokensOfItem.length; | ||
weights[id] = item.weight; | ||
nrOfTokens[id] = tokensOfItem.length; | ||
} | ||
@@ -67,8 +81,9 @@ } catch (err) { | ||
var nrOfItemsForToken = tokens[token].length; | ||
scores[token] = nrOfItemsForToken / items.length; | ||
var score = nrOfItemsForToken / items.length; | ||
scores[token] = roundTo(score, 6 - Math.log10(score) | 0); | ||
} | ||
return { tokens: tokens, scores: scores, weights: weights, nrOfTokens: nrOfTokens }; | ||
return { tokens: tokens, scores: scores, weights: weights, nrOfTokens: nrOfTokens, originalIds: originalIds }; | ||
}; | ||
module.exports = buildIndexes; |
13
index.js
@@ -6,3 +6,3 @@ 'use strict'; | ||
var createAutocomplete = function createAutocomplete(tokens, scores, weights, nrOfTokens, tokenize) { | ||
var createAutocomplete = function createAutocomplete(tokens, scores, weights, nrOfTokens, originalIds, tokenize) { | ||
var byFragment = function byFragment(fragment, completion, fuzzy) { | ||
@@ -12,5 +12,7 @@ var results = {}; | ||
// todo: hasProp check? | ||
if (tokens[fragment]) { | ||
var relevance = 1 + scores[fragment] + Math.sqrt(fragment.length); | ||
// todo: for i loop | ||
var _iteratorNormalCompletion = true; | ||
@@ -46,2 +48,3 @@ var _didIteratorError = false; | ||
for (var t in tokens) { | ||
// todo: hasProp check? | ||
if (fragment === t) continue; // has been dealt with above | ||
@@ -59,2 +62,3 @@ | ||
// todo: for i loop | ||
var _iteratorNormalCompletion2 = true; | ||
@@ -143,3 +147,8 @@ var _didIteratorError2 = false; | ||
var score = relevance * Math.pow(weights[id], 1 / 3); | ||
results[id] = { id: id, relevance: relevance, score: score, weight: weights[id] }; | ||
results[id] = { | ||
id: originalIds[id], | ||
relevance: relevance, | ||
score: score, | ||
weight: weights[id] | ||
}; | ||
} | ||
@@ -146,0 +155,0 @@ } |
{ | ||
"name": "synchronous-autocomplete", | ||
"description": "Fast, simple autocompletion.", | ||
"version": "1.0.0", | ||
"version": "2.0.0-alpha.1", | ||
"main": "index.js", | ||
@@ -6,0 +6,0 @@ "files": [ |
@@ -9,2 +9,3 @@ # synchronous-autocomplete | ||
[![chat on gitter](https://badges.gitter.im/derhuerst.svg)](https://gitter.im/derhuerst) | ||
[![support me on Patreon](https://img.shields.io/badge/support%20me-on%20patreon-fa7664.svg)](https://patreon.com/derhuerst) | ||
@@ -33,3 +34,3 @@ | ||
}, { | ||
id: 'pomegranate', | ||
id: 'pome', | ||
name: 'Sour Pomegranate', | ||
@@ -45,35 +46,42 @@ weight: 5 | ||
- *token*: A word from the fully processed search query. For example, to find an item named `Hey There!`, you may process its name into the *tokens* `hey` & `there`. | ||
- *fragment*: A word from the process search query, which may partially match a token. E.g. the *fragment* `ther` (from the search query `Hey There!`) partially matches the *token* `there`. | ||
- *fragment*: A word from the process search query, which may partially match a token. E.g. the *fragment* `ther` (from the search query `Hey Ther`) partially matches the *token* `there`. | ||
- *relevance*: How well an item is matched by the search query. | ||
- *score*: A combination of an item's *weight* and *relevance*. Use it to sort search results. | ||
In order to be as fast an disk-space-efficient as possible, `synchronous-autocomplete` requires four indexes to be prebuilt from the list of items. They look like this for our example: | ||
In order to be as fast and disk-space-efficient as possible, `synchronous-autocomplete` requires five indexes to be prebuilt from the list of items. For our example, they would look like this: | ||
```js | ||
const tokens = { | ||
juicy: ['apple', 'banana'], | ||
sour: ['apple', 'pomegranate'], | ||
apple: ['apple'], | ||
sweet: ['banana'], | ||
banana: ['banana'], | ||
pomegranate: ['pomegranate'] | ||
const tokens = { // internal item IDs, by token | ||
juicy: [0, 1], | ||
sour: [0, 3], | ||
apple: [0], | ||
sweet: [1], | ||
banana: [1], | ||
pomegranate: [3] | ||
} | ||
const weights = { | ||
apple: 3, | ||
banana: 2, | ||
pomegranate: 5 | ||
const weights = [ // item weights, by internal item ID | ||
3, // apple | ||
2, // banana | ||
5 // pome | ||
] | ||
const nrOfTokens = [ // nr of tokens, by internal item ID | ||
3, // apple | ||
3, // banana | ||
2 // pome | ||
] | ||
const scores = { // "uniqueness" of each token, by token | ||
juicy: 2 / 3, // 2 out of 3 items have the token "juicy" | ||
sour: 2 / 3, | ||
apple: 1 / 3, | ||
sweet: 1 / 3, | ||
banana: 1 / 3, | ||
pomegranate: 1 / 3 | ||
} | ||
const nrOfTokens = { | ||
apple: 3, | ||
banana: 3, | ||
pomegranate: 2 | ||
} | ||
const scores = { | ||
juicy: 2 / 3, // 2 out of 3 items | ||
sour: 2 / 3, // 2 out of 3 items | ||
apple: 1 / 3, // 1 out of 3 items | ||
sweet: 1 / 3, // 1 out of 3 items | ||
banana: 1 / 3, // 1 out of 3 items | ||
pomegranate: 1 / 3 // 1 out of 3 items | ||
} | ||
// In order to create smaller search indexes, we use numerical item IDs | ||
// internally and maintain a mapping to their "real"/original IDs. | ||
const originalIds = [ | ||
'apple', | ||
'banana', | ||
'pome' | ||
] | ||
``` | ||
@@ -116,10 +124,11 @@ | ||
```js | ||
const autocomplete = create(tokens, scores, weights, nrOfTokens, tokenize) | ||
const autocomplete = create(tokens, scores, weights, nrOfTokens, originalIds, tokenize) | ||
autocomplete(query, limit = 6, fuzzy = false, completion = true) | ||
``` | ||
`tokens` must be an object with an array of *item* IDs per *token*. | ||
`tokens` must be an object with an array of internal *item* IDs per *token*. | ||
`scores` must be an object with a *token* score per *token*. | ||
`weights` must be an object with an *item* weight per *item* ID. | ||
`nrOfTokens` must be an object with the number of *tokens* per *item* ID. | ||
`weights` must be an array with an *item* weight per internal *item* ID. | ||
`nrOfTokens` must be an array with the number of *tokens* per internal *item* ID. | ||
`originalIds` must be an array with the (real) *item* ID per internal *item* ID. | ||
`tokenize` must be a function that, given a search query, returns an array of *fragments*. | ||
@@ -126,0 +135,0 @@ |
'use strict' | ||
const roundTo = (v, p) => parseFloat(v.toFixed(p)) | ||
// todo: handle props like `toString` | ||
const buildIndexes = (tokenize, items) => { | ||
const originalIds = [] | ||
const tokens = Object.create(null) | ||
const weights = Object.create(null) | ||
const nrOfTokens = Object.create(null) | ||
const weights = [] | ||
const nrOfTokens = [] | ||
let currentId = 0 | ||
const generateId = (oldId) => { | ||
const newId = currentId++ | ||
originalIds[newId] = oldId | ||
return newId | ||
} | ||
for (let item of items) { | ||
const id = generateId(item.id) | ||
const tokensOfItem = tokenize(item.name) | ||
for (let token of tokensOfItem) { | ||
if (!Array.isArray(tokens[token])) tokens[token] = [] | ||
if (!tokens[token].includes(item.id)) tokens[token].push(item.id) | ||
if (!tokens[token].includes(id)) tokens[token].push(id) | ||
} | ||
weights[item.id] = item.weight | ||
nrOfTokens[item.id] = tokensOfItem.length | ||
weights[id] = item.weight | ||
nrOfTokens[id] = tokensOfItem.length | ||
} | ||
@@ -22,8 +34,9 @@ | ||
const nrOfItemsForToken = tokens[token].length | ||
scores[token] = nrOfItemsForToken / items.length | ||
const score = nrOfItemsForToken / items.length | ||
scores[token] = roundTo(score, 6 - Math.log10(score) | 0) | ||
} | ||
return {tokens, scores, weights, nrOfTokens} | ||
return {tokens, scores, weights, nrOfTokens, originalIds} | ||
} | ||
module.exports = buildIndexes |
@@ -6,3 +6,3 @@ 'use strict' | ||
const createAutocomplete = (tokens, scores, weights, nrOfTokens, tokenize) => { | ||
const createAutocomplete = (tokens, scores, weights, nrOfTokens, originalIds, tokenize) => { | ||
const byFragment = (fragment, completion, fuzzy) => { | ||
@@ -12,5 +12,7 @@ const results = {} | ||
// todo: hasProp check? | ||
if (tokens[fragment]) { | ||
const relevance = 1 + scores[fragment] + Math.sqrt(fragment.length) | ||
// todo: for i loop | ||
for (let id of tokens[fragment]) { | ||
@@ -25,2 +27,3 @@ if (!results[id] || !results[id] > relevance) { | ||
for (let t in tokens) { | ||
// todo: hasProp check? | ||
if (fragment === t) continue // has been dealt with above | ||
@@ -38,2 +41,3 @@ | ||
// todo: for i loop | ||
for (let id of tokens[t]) { | ||
@@ -76,3 +80,8 @@ if (!results[id] || !results[id] > relevance) { | ||
const score = relevance * Math.pow(weights[id], 1/3) | ||
results[id] = {id, relevance, score, weight: weights[id]} | ||
results[id] = { | ||
id: originalIds[id], | ||
relevance, | ||
score, | ||
weight: weights[id] | ||
} | ||
} | ||
@@ -79,0 +88,0 @@ } |
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
16044
317
136
0
1