js-worker-search
Advanced tools
Comparing version 1.0.2 to 1.1.0
Changelog | ||
----- | ||
#### 1.1.0 | ||
Added support for custom index strategies. | ||
By default, a prefix matching strategy is still used but it can be overridden like so: | ||
```js | ||
import SearchApi, { INDEX_MODES } from 'js-worker-search' | ||
// all-substrings match by default; same as current | ||
// eg "c", "ca", "a", "at", "cat" match "cat" | ||
const searchApi = new SearchApi() | ||
// prefix matching (eg "c", "ca", "cat" match "cat") | ||
const searchApi = new SearchApi({ | ||
indexMode: INDEX_MODES.PREFIXES | ||
}) | ||
// exact words matching (eg only "cat" matches "cat") | ||
const searchApi = new SearchApi({ | ||
indexMode: INDEX_MODES.EXACT_WORDS | ||
}) | ||
``` | ||
#### 1.0.2 | ||
@@ -5,0 +27,0 @@ Wrapped `String.prototype.charAt` usage in a `try/catch` to avoid erroring when handling surrogate halves. |
{ | ||
"name": "js-worker-search", | ||
"version": "1.0.2", | ||
"version": "1.1.0", | ||
"description": "JavaScript client-side search API with web-worker support", | ||
@@ -5,0 +5,0 @@ "author": "Brian Vaughn (brian.david.vaughn@gmail.com)", |
@@ -26,2 +26,7 @@ js-worker-search | ||
##### `constructor ({ indexMode })` | ||
By default, `SearchApi` builds an index to match all substrings. | ||
You can override this behavior by passing an named `indexMode` parameter. | ||
Valid values are `INDEX_MODES.ALL_SUBSTRINGS`, `INDEX_MODES.EXACT_WORDS`, and `INDEX_MODES.PREFIXES`. | ||
##### `indexDocument (uid, text)` | ||
@@ -66,2 +71,23 @@ Adds or updates a uid in the search index and associates it with the specified text. Note that at this time uids can only be added or updated in the index, not removed. | ||
By default, `SearchApi` builds an index to match all substrings. | ||
You can override this behavior by passing an `indexMode` parameter to the constructor like so: | ||
```js | ||
import SearchApi, { INDEX_MODES } from 'js-worker-search' | ||
// all-substrings match by default; same as current | ||
// eg "c", "ca", "a", "at", "cat" match "cat" | ||
const searchApi = new SearchApi() | ||
// prefix matching (eg "c", "ca", "cat" match "cat") | ||
const searchApi = new SearchApi({ | ||
indexMode: INDEX_MODES.PREFIXES | ||
}) | ||
// exact words matching (eg only "cat" matches "cat") | ||
const searchApi = new SearchApi({ | ||
indexMode: INDEX_MODES.EXACT_WORDS | ||
}) | ||
``` | ||
Changelog | ||
@@ -68,0 +94,0 @@ --------- |
export default from './SearchApi' | ||
export { INDEX_MODES } from './util' |
@@ -0,1 +1,2 @@ | ||
import SearchUtility from './util' | ||
@@ -9,9 +10,9 @@ import SearchWorkerLoader from './worker' | ||
export default class SearchApi { | ||
constructor () { | ||
constructor ({ indexMode } = {}) { | ||
// Based on https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers | ||
// But with added check for Node environment | ||
if (typeof window !== 'undefined' && window.Worker) { | ||
this._search = new SearchWorkerLoader() | ||
this._search = new SearchWorkerLoader({ indexMode }) | ||
} else { | ||
this._search = new SearchUtility() | ||
this._search = new SearchUtility({ indexMode }) | ||
} | ||
@@ -18,0 +19,0 @@ |
export default from './SearchUtility' | ||
export { INDEX_MODES } from './constants' |
@@ -38,3 +38,3 @@ /** | ||
tokens.forEach(token => { | ||
for (let token of tokens) { | ||
let currentUidMap: {[uid: any]: any} = this.tokenToUidMap[token] || {} | ||
@@ -55,3 +55,3 @@ | ||
} | ||
}) | ||
} | ||
@@ -58,0 +58,0 @@ let uids: Array<any> = [] |
@@ -0,1 +1,2 @@ | ||
import { INDEX_MODES } from './constants' | ||
import SearchIndex from './SearchIndex' | ||
@@ -11,4 +12,10 @@ | ||
* Constructor. | ||
* | ||
* @param indexMode See #setIndexMode | ||
*/ | ||
constructor () { | ||
constructor ({ | ||
indexMode = INDEX_MODES.ALL_SUBSTRINGS | ||
} = {}) { | ||
this._indexMode = indexMode | ||
this.searchIndex = new SearchIndex() | ||
@@ -19,2 +26,9 @@ this.uids = {} | ||
/** | ||
* Returns a constant representing the current index mode. | ||
*/ | ||
getIndexMode (): string { | ||
return this._indexMode | ||
} | ||
/** | ||
* Adds or updates a uid in the search index and associates it with the specified text. | ||
@@ -31,9 +45,9 @@ * Note that at this time uids can only be added or updated in the index, not removed. | ||
fieldTokens.forEach(fieldToken => { | ||
for (let fieldToken of fieldTokens) { | ||
var expandedTokens: Array<string> = this._expandToken(fieldToken) | ||
expandedTokens.forEach(expandedToken => | ||
for (let expandedToken of expandedTokens) { | ||
this.searchIndex.indexDocument(expandedToken, uid) | ||
) | ||
}) | ||
} | ||
} | ||
@@ -65,2 +79,14 @@ return this | ||
/** | ||
* Sets a new index mode. | ||
* See util/constants/INDEX_MODES | ||
*/ | ||
setIndexMode (indexMode: string): void { | ||
if (Object.keys(this.uids).length > 0) { | ||
throw Error('indexMode cannot be changed once documents have been indexed') | ||
} | ||
this._indexMode = indexMode | ||
} | ||
/** | ||
* Index strategy based on 'all-substrings-index-strategy.ts' in github.com/bvaughn/js-search/ | ||
@@ -71,4 +97,16 @@ * | ||
_expandToken (token: string): Array<string> { | ||
var expandedTokens = [] | ||
switch (this._indexMode) { | ||
case INDEX_MODES.EXACT_WORDS: | ||
return [token] | ||
case INDEX_MODES.PREFIXES: | ||
return this._expandPrefixTokens(token) | ||
case INDEX_MODES.ALL_SUBSTRINGS: | ||
default: | ||
return this._expandAllSubstringTokens(token) | ||
} | ||
} | ||
_expandAllSubstringTokens (token: string): Array<string> { | ||
const expandedTokens = [] | ||
// String.prototype.charAt() may return surrogate halves instead of whole characters. | ||
@@ -82,7 +120,7 @@ // When this happens in the context of a web-worker it can cause Chrome to crash. | ||
for (let i = 0, length = token.length; i < length; ++i) { | ||
let prefixString: string = '' | ||
let substring: string = '' | ||
for (let j = i; j < length; ++j) { | ||
prefixString += token.charAt(j) | ||
expandedTokens.push(prefixString) | ||
substring += token.charAt(j) | ||
expandedTokens.push(substring) | ||
} | ||
@@ -97,2 +135,22 @@ } | ||
_expandPrefixTokens (token: string): Array<string> { | ||
const expandedTokens = [] | ||
// String.prototype.charAt() may return surrogate halves instead of whole characters. | ||
// When this happens in the context of a web-worker it can cause Chrome to crash. | ||
// Catching the error is a simple solution for now; in the future I may try to better support non-BMP characters. | ||
// Resources: | ||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charAt | ||
// https://mathiasbynens.be/notes/javascript-unicode | ||
try { | ||
for (let i = 0, length = token.length; i < length; ++i) { | ||
expandedTokens.push(token.substr(0, i + 1)) | ||
} | ||
} catch (error) { | ||
console.error(`Unable to parse token "${token}" ${error}`) | ||
} | ||
return expandedTokens | ||
} | ||
/** | ||
@@ -99,0 +157,0 @@ * @private |
import test from 'tape' | ||
import Immutable from 'immutable' | ||
import SearchUtility from './SearchUtility' | ||
import { INDEX_MODES } from './constants' | ||
@@ -18,28 +19,28 @@ const documentA = Immutable.fromJS({id: 1, name: 'One', description: 'The first document'}) | ||
function init () { | ||
const searchableDocuments = new SearchUtility() | ||
function init ({ indexMode } = {}) { | ||
const searchUtility = new SearchUtility({ indexMode }) | ||
documents.forEach(doc => { | ||
searchableDocuments.indexDocument(doc.get('id'), doc.get('name')) | ||
searchableDocuments.indexDocument(doc.get('id'), doc.get('description')) | ||
searchUtility.indexDocument(doc.get('id'), doc.get('name')) | ||
searchUtility.indexDocument(doc.get('id'), doc.get('description')) | ||
}) | ||
return searchableDocuments | ||
return searchUtility | ||
} | ||
test('SearchUtility should return documents ids for any searchable field matching a query', t => { | ||
const searchableDocuments = init() | ||
let ids = searchableDocuments.search('One') | ||
const searchUtility = init() | ||
let ids = searchUtility.search('One') | ||
t.equal(ids.length, 1) | ||
t.deepLooseEqual(ids, [1]) | ||
ids = searchableDocuments.search('Third') | ||
ids = searchUtility.search('Third') | ||
t.equal(ids.length, 1) | ||
t.deepLooseEqual(ids, [3]) | ||
ids = searchableDocuments.search('the') | ||
ids = searchUtility.search('the') | ||
t.equal(ids.length, 3) | ||
t.deepLooseEqual(ids, [1, 2, 3]) | ||
ids = searchableDocuments.search('楌') // Tests matching of other script systems | ||
ids = searchUtility.search('楌') // Tests matching of other script systems | ||
t.equal(ids.length, 2) | ||
@@ -51,8 +52,8 @@ t.deepLooseEqual(ids, [4, 5]) | ||
test('SearchUtility should return documents ids only if document matches all tokens in a query', t => { | ||
const searchableDocuments = init() | ||
let ids = searchableDocuments.search('the second') | ||
const searchUtility = init() | ||
let ids = searchUtility.search('the second') | ||
t.equal(ids.length, 1) | ||
t.equal(ids[0], 2) | ||
ids = searchableDocuments.search('three document') // Spans multiple fields | ||
ids = searchUtility.search('three document') // Spans multiple fields | ||
t.equal(ids.length, 1) | ||
@@ -64,4 +65,4 @@ t.equal(ids[0], 3) | ||
test('SearchUtility should return an empty array for query without matching documents', t => { | ||
const searchableDocuments = init() | ||
const ids = searchableDocuments.search('four') | ||
const searchUtility = init() | ||
const ids = searchUtility.search('four') | ||
t.equal(ids.length, 0) | ||
@@ -72,4 +73,4 @@ t.end() | ||
test('SearchUtility should return all uids for an empty query', t => { | ||
const searchableDocuments = init() | ||
const ids = searchableDocuments.search('') | ||
const searchUtility = init() | ||
const ids = searchUtility.search('') | ||
t.equal(ids.length, 5) | ||
@@ -80,6 +81,6 @@ t.end() | ||
test('SearchUtility should ignore case when searching', t => { | ||
const searchableDocuments = init() | ||
const searchUtility = init() | ||
const texts = ['one', 'One', 'ONE'] | ||
texts.forEach((text) => { | ||
const ids = searchableDocuments.search(text) | ||
const ids = searchUtility.search(text) | ||
t.equal(ids.length, 1) | ||
@@ -93,6 +94,6 @@ t.equal(ids[0], 1) | ||
test('SearchUtility should use substring matching', t => { | ||
const searchableDocuments = init() | ||
const searchUtility = init() | ||
let texts = ['sec', 'second', 'eco', 'cond'] | ||
texts.forEach((text) => { | ||
let ids = searchableDocuments.search(text) | ||
let ids = searchUtility.search(text) | ||
t.equal(ids.length, 1) | ||
@@ -104,3 +105,3 @@ t.equal(ids[0], 2) | ||
texts.forEach((text) => { | ||
let ids = searchableDocuments.search(text) | ||
let ids = searchUtility.search(text) | ||
t.equal(ids.length, 2) | ||
@@ -114,11 +115,11 @@ t.deepLooseEqual(ids, [4, 5]) | ||
test('SearchUtility should allow custom indexing via indexDocument', t => { | ||
const searchableDocuments = init() | ||
const searchUtility = init() | ||
const text = 'xyz' | ||
let ids = searchableDocuments.search(text) | ||
let ids = searchUtility.search(text) | ||
t.equal(ids.length, 0) | ||
const id = documentA.get('id') | ||
searchableDocuments.indexDocument(id, text) | ||
searchUtility.indexDocument(id, text) | ||
ids = searchableDocuments.search(text) | ||
ids = searchUtility.search(text) | ||
t.equal(ids.length, 1) | ||
@@ -128,1 +129,57 @@ t.equal(ids[0], 1) | ||
}) | ||
test('SearchUtility should recognize an :indexMode constructor param', t => { | ||
const searchUtility = new SearchUtility({ | ||
indexMode: INDEX_MODES.EXACT_WORDS | ||
}) | ||
t.equal(searchUtility.getIndexMode(), INDEX_MODES.EXACT_WORDS) | ||
t.end() | ||
}) | ||
test('SearchUtility should update its default :indexMode when :setIndexMode() is called', t => { | ||
const searchUtility = new SearchUtility() | ||
searchUtility.setIndexMode(INDEX_MODES.EXACT_WORDS) | ||
t.equal(searchUtility.getIndexMode(), INDEX_MODES.EXACT_WORDS) | ||
t.end() | ||
}) | ||
test('SearchUtility should should error if :setIndexMode() is called after an index has been created', t => { | ||
let errored = false | ||
const searchUtility = init() | ||
try { | ||
searchUtility.indexDocument | ||
searchUtility.setIndexMode(INDEX_MODES.EXACT_WORDS) | ||
} catch (error) { | ||
errored = true | ||
} | ||
t.equal(errored, true) | ||
t.end() | ||
}) | ||
test('SearchUtility should support PREFIXES :indexMode', t => { | ||
const searchUtility = init({ indexMode: INDEX_MODES.PREFIXES }) | ||
const match1 = ['fir', 'first'] | ||
const match2 = ['sec', 'second'] | ||
match1.forEach((token) => { | ||
t.deepLooseEqual(searchUtility.search(token), [1]) | ||
}) | ||
match2.forEach((token) => { | ||
t.deepLooseEqual(searchUtility.search(token), [2]) | ||
}) | ||
const noMatch = ['irst', 'rst', 'st', 'irs', 'ond', 'econd', 'eco'] | ||
noMatch.forEach((token) => { | ||
t.equal(searchUtility.search(token).length, 0) | ||
}) | ||
t.end() | ||
}) | ||
test('SearchUtility should support EXACT_WORDS :indexMode', t => { | ||
const searchUtility = init({ indexMode: INDEX_MODES.EXACT_WORDS }) | ||
t.deepLooseEqual(searchUtility.search('first'), [1]) | ||
t.deepLooseEqual(searchUtility.search('second'), [2]) | ||
const noMatch = ['irst', 'rst', 'st', 'irs', 'ond', 'econd', 'eco'] | ||
noMatch.forEach((token) => { | ||
t.equal(searchUtility.search(token).length, 0) | ||
}) | ||
t.end() | ||
}) |
@@ -12,3 +12,6 @@ import uuid from 'uuid' | ||
*/ | ||
constructor (WorkerClass) { | ||
constructor ({ | ||
indexMode, | ||
WorkerClass | ||
} = {}) { | ||
// Defer worker import until construction to avoid testing error: | ||
@@ -36,2 +39,10 @@ // Error: Cannot find module 'worker!./[workername]' | ||
} | ||
// Override default :indexMode if a specific one has been requested | ||
if (indexMode) { | ||
this.worker.postMessage({ | ||
method: 'setIndexMode', | ||
indexMode | ||
}) | ||
} | ||
} | ||
@@ -38,0 +49,0 @@ |
import test from 'tape' | ||
import SearchWorkerLoader from './SearchWorkerLoader' | ||
import { INDEX_MODES } from '../util' | ||
@@ -8,2 +9,3 @@ class StubWorker { | ||
this.searchQueue = [] | ||
this.setIndexModeQueue = [] | ||
} | ||
@@ -26,2 +28,6 @@ | ||
break | ||
case 'setIndexMode': | ||
const { indexMode } = props | ||
this.setIndexModeQueue.push({ indexMode }) | ||
break | ||
} | ||
@@ -42,3 +48,3 @@ } | ||
test('SearchWorkerLoader indexDocument should index a document with the specified text(s)', t => { | ||
const search = new SearchWorkerLoader(StubWorker) | ||
const search = new SearchWorkerLoader({ WorkerClass: StubWorker }) | ||
search.indexDocument('a', 'cat') | ||
@@ -57,3 +63,3 @@ search.indexDocument('a', 'dog') | ||
test('SearchWorkerLoader search should search for the specified text', t => { | ||
const search = new SearchWorkerLoader(StubWorker) | ||
const search = new SearchWorkerLoader({ WorkerClass: StubWorker }) | ||
search.search('cat') | ||
@@ -66,3 +72,3 @@ t.equal(search.worker.searchQueue.length, 1) | ||
test('SearchWorkerLoader search should resolve the returned Promise on search completion', async t => { | ||
const search = new SearchWorkerLoader(StubWorker) | ||
const search = new SearchWorkerLoader({ WorkerClass: StubWorker }) | ||
const promise = search.search('cat') | ||
@@ -77,3 +83,3 @@ search.worker.resolveSearch(0, ['a', 'b']) | ||
test('SearchWorkerLoader search should resolve multiple concurrent searches', async t => { | ||
const search = new SearchWorkerLoader(StubWorker) | ||
const search = new SearchWorkerLoader({ WorkerClass: StubWorker }) | ||
const promises = Promise.all([ | ||
@@ -90,3 +96,3 @@ search.search('cat'), | ||
test('SearchWorkerLoader search should resolve searches in the correct order', async t => { | ||
const search = new SearchWorkerLoader(StubWorker) | ||
const search = new SearchWorkerLoader({ WorkerClass: StubWorker }) | ||
const results = [] | ||
@@ -112,3 +118,3 @@ const promiseList = [ | ||
test('SearchWorkerLoader search should not reject all searches if one fails', async t => { | ||
const search = new SearchWorkerLoader(StubWorker) | ||
const search = new SearchWorkerLoader({ WorkerClass: StubWorker }) | ||
const errors = [] | ||
@@ -138,1 +144,17 @@ const results = [] | ||
}) | ||
test('SearchWorkerLoader should pass the specified :indexMode to the WorkerClass', t => { | ||
const search = new SearchWorkerLoader({ | ||
indexMode: INDEX_MODES.EXACT_WORDS, | ||
WorkerClass: StubWorker | ||
}) | ||
t.equal(search.worker.setIndexModeQueue.length, 1) | ||
t.equal(search.worker.setIndexModeQueue[0].indexMode, INDEX_MODES.EXACT_WORDS) | ||
t.end() | ||
}) | ||
test('SearchWorkerLoader should not override the default :indexMode in the WorkerClass if an override is not requested', t => { | ||
const search = new SearchWorkerLoader({ WorkerClass: StubWorker }) | ||
t.equal(search.worker.setIndexModeQueue.length, 0) | ||
t.end() | ||
}) |
@@ -27,3 +27,8 @@ import SearchUtility from '../util' | ||
break | ||
case 'setIndexMode': | ||
const { indexMode } = data | ||
searchUtility.setIndexMode(indexMode) | ||
break | ||
} | ||
}, false) |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
217453
23
2619
100