UNPKG

fz-search

Version:

Fast aproximate string matching library for use in autocomplete, perform both search and highlight.

224 lines (172 loc) 7.1 kB
extend(FuzzySearch.prototype, /** @lends {FuzzySearch.prototype} */ { /** * Take a `source_item` (unprocessed item from source) and keys and produce * an `item` that's ready to be added to `this.index`. * * Preparation steps: * - Apply lowercase, accent removal * - Split field into token * - Remove small token eg "a" "of" and prefix large token */ _prepItem: function (source_item, keys) { var item_fields = FuzzySearch.generateFields(source_item, keys); var nb_fields = item_fields.length; for (var field_index = -1; ++field_index < nb_fields;) { var field = item_fields[field_index]; for (var node_index = -1, nb_nodes = field.length; ++node_index < nb_nodes;) { var norm = this.options.normalize(field[node_index]); var nodes = norm.split(this.token_re); //Filter size. (If total field length is very small, make an exception. // Eg some movie/Book have a single letter title, filter risk of removing everything ) if (norm.length > 2 * this.options.token_field_min_length) nodes = FuzzySearch.filterSize(nodes, this.options.token_field_min_length, this.options.token_field_max_length); if (this.options.score_acronym) nodes.push(norm.replace(this.acro_re, "$1")); field[node_index] = nodes; } } return new Indexed(source_item, item_fields); }, /** * Add an item to search index AND source collection. * It'll use identify_item to find if the item already exist. * If identify_item is null (default), calling this method is append-only with no duplicate detection * * To update the source, it use the assumption that this.source and this.index can be synced * by array index. That assumption will be true if source is a plain array, and always updated by this library. * Feel free to set `should_update_source` to false to manually manage source collection. * * Keeping source in sync is important to allow to recompute index from source. * This will happens with certain setting changes. * * @param {*} source_item - item to add to search index * @param {boolean=} should_update_source - set to false to skip updating the source. */ add: function(source_item, should_update_source){ // Default to keeping source in sync. if(should_update_source === undefined) should_update_source = true; var item_id = typeof this.options.identify_item === "function" ? this.options.identify_item(source_item) : null; // Find where to insert new item var idx; if (item_id === null) { // No identifier, append to end idx = this.nb_indexed; this.nb_indexed++; } else if (item_id in this.index_map) { // Item exist, update idx = this.index_map[item_id]; } else { // New identifier, append to end & record new this.index_map[item_id] = this.nb_indexed; idx = this.nb_indexed; this.nb_indexed++; } // Compute indexed item and update index var prepared = this._prepItem(source_item, this.keys); this.index[idx] = prepared; // Insert in source; if(should_update_source) this.source[idx] = source_item; if (this.options.use_index_store) { this._storeAdd(prepared, idx); } }, /** * Build (or rebuild) `this.index` from `this.source` * Flatten object into array using specified keys * * @private */ _buildIndexFromSource: function () { var nb_items = this.source.length; this.index = new Array(nb_items); this.index_map = {}; this.nb_indexed = 0; for (var item_index = -1; ++item_index < nb_items;) { var source_item = this.source[item_index]; // Add item to index. // Because we are iterating over source, do not attempt to modify it. this.add(source_item, false); } } }); /** * Original item with cached normalized field * * @param {*} source_item * @param {Array.<string>} fields * @constructor */ function Indexed(source_item, fields) { this.item = source_item; this.fields = fields; } // - - - - - - - - - - - - - - - - - - - - - - // Input stage: prepare field for search //- - - - - - - - - - - - - - - - - - - - - - /** * Given an object to index and a list of field to index * Return a flat list of the values. * * @param {Object} obj * @param {Array.<string>} fieldlist * @returns {Array} */ FuzzySearch.generateFields = function (obj, fieldlist) { if (!fieldlist || !fieldlist.length) return [[obj.toString()]]; var n = fieldlist.length; var indexed_fields = new Array(n); for (var i = -1; ++i < n;) indexed_fields[i] = _collectValues(obj, fieldlist[i].split("."), [], 0); return indexed_fields; }; /** * Traverse an object structure to collect item specified by parts. * If leaf node is an array or dictionary collect every children. * If key is wildcard '*' branch out the search process on each children. * * @param {*} obj - root to process * @param {Array.<string>} parts - array of subkey to direct object traversal "those.that.this"->["those","that","this"] * @param {Array} list - where to put collected items * @param {number} level - index of current position on parts list * @returns {Array} - return list * @private */ function _collectValues(obj, parts, list, level) { var key, i, olen; var nb_level = parts.length; while (level < nb_level) { key = parts[level++]; if (key === "*" || key === "") break; if (!(key in obj)) return list; obj = obj[key]; } var type = Object.prototype.toString.call(obj); var isArray = ( type === '[object Array]' ); var isObject = ( type === '[object Object]' ); if (level === nb_level) { if (isArray) for (i = -1, olen = obj.length; ++i < olen;) list.push(obj[i].toString()); else if (isObject) { for (key in obj) { if (obj.hasOwnProperty(key)) list.push(obj[key].toString()); } } else list.push(obj.toString()); } else if (key === "*") { if (isArray) for (i = -1, olen = obj.length; ++i < olen;) { _collectValues(obj[i], parts, list, level); } else if (isObject) for (key in obj) { if (obj.hasOwnProperty(key)) _collectValues(obj[key], parts, list, level); } } return list; }