js-worker-search-x
Version:
JavaScript client-side search API with web-worker support
563 lines (436 loc) • 16.9 kB
JavaScript
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId])
/******/ return installedModules[moduleId].exports;
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ exports: {},
/******/ id: moduleId,
/******/ loaded: false
/******/ };
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/ // Load entry module and return exports
/******/ return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {
"use strict";
var _util = __webpack_require__(1);
/**
* Search entry point to web worker.
* Builds search index and performs searches on separate thread from the ui.
*/
var searchUtility = new _util.SearchUtility();
self.addEventListener("message", function (event) {
var data = event.data;
var method = data.method;
switch (method) {
case "indexDocument":
var uid = data.uid,
text = data.text;
searchUtility.indexDocument(uid, text);
break;
case "search":
var callbackId = data.callbackId,
query = data.query;
var results = searchUtility.search(query);
self.postMessage({ callbackId: callbackId, results: results });
break;
case "setCaseSensitive":
var caseSensitive = data.caseSensitive;
searchUtility.setCaseSensitive(caseSensitive);
break;
case "setIndexMode":
var indexMode = data.indexMode;
searchUtility.setIndexMode(indexMode);
break;
case "setMatchAnyToken":
var matchAnyToken = data.matchAnyToken;
searchUtility.setMatchAnyToken(matchAnyToken);
break;
case "setTokenizePattern":
var tokenizePattern = data.tokenizePattern;
searchUtility.setTokenizePattern(tokenizePattern);
break;
}
}, false);
/***/ },
/* 1 */
/***/ function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.SearchUtility = exports.INDEX_MODES = undefined;
var _SearchUtility = __webpack_require__(2);
var _SearchUtility2 = _interopRequireDefault(_SearchUtility);
var _constants = __webpack_require__(3);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
exports.default = _SearchUtility2.default;
exports.INDEX_MODES = _constants.INDEX_MODES;
exports.SearchUtility = _SearchUtility2.default;
/***/ },
/* 2 */
/***/ function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _constants = __webpack_require__(3);
var _SearchIndex = __webpack_require__(4);
var _SearchIndex2 = _interopRequireDefault(_SearchIndex);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
/**
* Synchronous client-side full-text search utility.
* Forked from JS search (github.com/bvaughn/js-search).
*/
var SearchUtility = function () {
/**
* Constructor.
*
* @param indexMode See #setIndexMode
* @param tokenizePattern See #setTokenizePattern
* @param caseSensitive See #setCaseSensitive
* @param matchAnyToken See #setMatchAnyToken
*/
function SearchUtility() {
var _this = this;
var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
_ref$caseSensitive = _ref.caseSensitive,
caseSensitive = _ref$caseSensitive === undefined ? false : _ref$caseSensitive,
_ref$indexMode = _ref.indexMode,
indexMode = _ref$indexMode === undefined ? _constants.INDEX_MODES.ALL_SUBSTRINGS : _ref$indexMode,
_ref$matchAnyToken = _ref.matchAnyToken,
matchAnyToken = _ref$matchAnyToken === undefined ? false : _ref$matchAnyToken,
_ref$tokenizePattern = _ref.tokenizePattern,
tokenizePattern = _ref$tokenizePattern === undefined ? /\s+/ : _ref$tokenizePattern;
_classCallCheck(this, SearchUtility);
this.indexDocument = function (uid, text) {
_this._uids[uid] = true;
var fieldTokens = _this._tokenize(_this._sanitize(text));
fieldTokens.forEach(function (fieldToken) {
var expandedTokens = _this._expandToken(fieldToken);
expandedTokens.forEach(function (expandedToken) {
_this._searchIndex.indexDocument(expandedToken, uid);
});
});
return _this;
};
this.search = function (query) {
if (!query) {
return Object.keys(_this._uids);
} else {
var tokens = _this._tokenize(_this._sanitize(query));
return _this._searchIndex.search(tokens, _this._matchAnyToken);
}
};
this.terminate = function () {};
this._caseSensitive = caseSensitive;
this._indexMode = indexMode;
this._matchAnyToken = matchAnyToken;
this._tokenizePattern = tokenizePattern;
this._searchIndex = new _SearchIndex2.default();
this._uids = {};
}
/**
* Returns a constant representing the current case-sensitive bit.
*/
_createClass(SearchUtility, [{
key: "getCaseSensitive",
value: function getCaseSensitive() {
return this._caseSensitive;
}
/**
* Returns a constant representing the current index mode.
*/
}, {
key: "getIndexMode",
value: function getIndexMode() {
return this._indexMode;
}
/**
* Returns a constant representing the current match-any-token bit.
*/
}, {
key: "getMatchAnyToken",
value: function getMatchAnyToken() {
return this._matchAnyToken;
}
/**
* Returns a constant representing the current tokenize pattern.
*/
}, {
key: "getTokenizePattern",
value: function getTokenizePattern() {
return this._tokenizePattern;
}
/**
* 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.
*
* @param uid Uniquely identifies a searchable object
* @param text Text to associate with uid
*/
/**
* Searches the current index for the specified query text.
* Only uids matching all of the words within the text will be accepted,
* unless matchAny is set to true.
* If an empty query string is provided all indexed uids will be returned.
*
* Document searches are case-insensitive by default (e.g. "search" will match "Search").
* Document searches use substring matching by default (e.g. "na" and "me" will both match "name").
*
* @param query Searchable query text
* @return Array of uids
*/
}, {
key: "setCaseSensitive",
/**
* Sets a new case-sensitive bit
*/
value: function setCaseSensitive(caseSensitive) {
this._caseSensitive = caseSensitive;
}
/**
* Sets a new index mode.
* See util/constants/INDEX_MODES
*/
}, {
key: "setIndexMode",
value: function setIndexMode(indexMode) {
if (Object.keys(this._uids).length > 0) {
throw Error("indexMode cannot be changed once documents have been indexed");
}
this._indexMode = indexMode;
}
/**
* Sets a new match-any-token bit
*/
}, {
key: "setMatchAnyToken",
value: function setMatchAnyToken(matchAnyToken) {
this._matchAnyToken = matchAnyToken;
}
/**
* Sets a new tokenize pattern (regular expression)
*/
}, {
key: "setTokenizePattern",
value: function setTokenizePattern(pattern) {
this._tokenizePattern = pattern;
}
/**
* Added to make class adhere to interface. Add cleanup code as needed.
*/
}, {
key: "_expandToken",
/**
* Index strategy based on 'all-substrings-index-strategy.ts' in github.com/bvaughn/js-search/
*
* @private
*/
value: function _expandToken(token) {
switch (this._indexMode) {
case _constants.INDEX_MODES.EXACT_WORDS:
return [token];
case _constants.INDEX_MODES.PREFIXES:
return this._expandPrefixTokens(token);
case _constants.INDEX_MODES.ALL_SUBSTRINGS:
default:
return this._expandAllSubstringTokens(token);
}
}
}, {
key: "_expandAllSubstringTokens",
value: function _expandAllSubstringTokens(token) {
var 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 (var i = 0, length = token.length; i < length; ++i) {
var substring = "";
for (var j = i; j < length; ++j) {
substring += token.charAt(j);
expandedTokens.push(substring);
}
}
} catch (error) {
console.error("Unable to parse token \"" + token + "\" " + error);
}
return expandedTokens;
}
}, {
key: "_expandPrefixTokens",
value: function _expandPrefixTokens(token) {
var 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 (var 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;
}
/**
* @private
*/
}, {
key: "_sanitize",
value: function _sanitize(string) {
return this._caseSensitive ? string.trim() : string.trim().toLocaleLowerCase();
}
/**
* @private
*/
}, {
key: "_tokenize",
value: function _tokenize(text) {
return text.split(this._tokenizePattern).filter(function (text) {
return text;
}); // Remove empty tokens
}
}]);
return SearchUtility;
}();
exports.default = SearchUtility;
/***/ },
/* 3 */
/***/ function(module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var INDEX_MODES = exports.INDEX_MODES = {
// Indexes for all substring searches (e.g. the term "cat" is indexed as "c", "ca", "cat", "a", "at", and "t").
// Based on 'all-substrings-index-strategy' from js-search;
// github.com/bvaughn/js-search/blob/master/source/index-strategy/all-substrings-index-strategy.ts
ALL_SUBSTRINGS: "ALL_SUBSTRINGS",
// Indexes for exact word matches only.
// Based on 'exact-word-index-strategy' from js-search;
// github.com/bvaughn/js-search/blob/master/source/index-strategy/exact-word-index-strategy.ts
EXACT_WORDS: "EXACT_WORDS",
// Indexes for prefix searches (e.g. the term "cat" is indexed as "c", "ca", and "cat" allowing prefix search lookups).
// Based on 'prefix-index-strategy' from js-search;
// github.com/bvaughn/js-search/blob/master/source/index-strategy/prefix-index-strategy.ts
PREFIXES: "PREFIXES"
};
/***/ },
/* 4 */
/***/ function(module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
/**
* Maps search tokens to uids using a trie structure.
*/
var SearchIndex = function () {
function SearchIndex() {
_classCallCheck(this, SearchIndex);
this.tokenToUidMap = {};
}
/**
* Maps the specified token to a uid.
*
* @param token Searchable token (e.g. "road")
* @param uid Identifies a document within the searchable corpus
*/
_createClass(SearchIndex, [{
key: "indexDocument",
value: function indexDocument(token, uid) {
if (!this.tokenToUidMap[token]) {
this.tokenToUidMap[token] = {};
}
this.tokenToUidMap[token][uid] = uid;
}
/**
* Finds uids that have been mapped to the set of tokens specified.
* Only uids that have been mapped to all tokens will be returned.
*
* @param tokens Array of searchable tokens (e.g. ["long", "road"])
* @param matchAnyToken Whether to match any token. Default is false.
* @return Array of uids that have been associated with the set of search tokens
*/
}, {
key: "search",
value: function search(tokens, matchAnyToken) {
var _this = this;
var uidMap = {};
var uidMatches = {};
var initialized = false;
tokens.forEach(function (token) {
var currentUidMap = _this.tokenToUidMap[token] || {};
if (!initialized) {
initialized = true;
for (var _uid in currentUidMap) {
uidMap[_uid] = currentUidMap[_uid];
uidMatches[_uid] = 1;
}
} else {
// Delete existing matches if using and AND query (the default)
// Otherwise add new results to the matches
if (!matchAnyToken) {
for (var _uid2 in uidMap) {
if (!currentUidMap[_uid2]) {
delete uidMap[_uid2];
}
}
} else {
for (var _uid3 in currentUidMap) {
uidMap[_uid3] = currentUidMap[_uid3];
uidMatches[_uid3] = (uidMatches[_uid3] || 0) + 1;
}
}
}
});
var uids = [];
for (var _uid4 in uidMap) {
uids.push(uidMap[_uid4]);
}
// Sort according to most matches, if match any token is set.
if (matchAnyToken) {
uids.sort(function (a, b) {
return uidMatches[b] - uidMatches[a];
});
}
return uids;
}
}]);
return SearchIndex;
}();
exports.default = SearchIndex;
/***/ }
/******/ ]);