UNPKG

fz-search

Version:

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

378 lines (296 loc) 10.5 kB
// // - - - - - - - - - - - - // Prepare Query // - - - - - - - - - - - - // extend(FuzzySearch.prototype, /** @lends {FuzzySearch.prototype} */ { /** * Input: a user search string * Output a query object * * Perform a few transformation to allw faster searching. * String is set to lowercase, some accents removed, split into tokens. * Token too small are filtered out, token too large are trimmed. * Token are packed in group of 32 char, each token is processed to extract an alphabet map. * * If score_test_fused is enabled, we do an extra pass disregarding tokens. * IF score_per_token is disabled this is the only pass we do. * * @param query_string * @returns {Query} * @private */ _prepQuery: function (query_string) { var options = this.options; var opt_tok = options.score_per_token; var opt_fuse = options.score_test_fused; var opt_fuselen = options.token_fused_max_length; var opt_qmin = options.token_field_min_length; var opt_qmax = options.token_field_max_length; var tags = this.tags; var tags_re = this.tags_re; var nb_tags = tags.length; var token_re = this.token_re; var norm, fused, fused_map, children, has_tags, group, words; if (opt_tok && nb_tags && tags_re) { var start = 0, end; var q_index = 0; var q_parts = new Array(nb_tags + 1); var match = tags_re.exec(query_string); has_tags = (match !== null); while (match !== null) { end = match.index; q_parts[q_index] = query_string.substring(start, end); start = end + match[0].length; q_index = tags.indexOf(match[1]) + 1; match = tags_re.exec(query_string); } q_parts[q_index] = query_string.substring(start); children = []; for (var i = -1; ++i < nb_tags;) { var qp = q_parts[i + 1]; if (!qp || !qp.length) continue; norm = options.normalize(qp); fused = norm.substring(0, opt_fuselen); fused_map = (opt_fuse || !opt_tok) ? FuzzySearch.alphabet(fused) : {}; words = FuzzySearch.filterSize(norm.split(token_re), opt_qmin, opt_qmax); group = FuzzySearch.pack_tokens(words); children[i] = new Query(norm, words, group, fused, fused_map, false, []); } norm = options.normalize(q_parts[0]); words = FuzzySearch.filterSize(norm.split(token_re), opt_qmin, opt_qmax); group = FuzzySearch.pack_tokens(words); } else { norm = options.normalize(query_string); words = FuzzySearch.filterSize(norm.split(token_re), opt_qmin, opt_qmax); group = opt_tok ? FuzzySearch.pack_tokens(words) : []; has_tags = false; children = new Array(nb_tags); } fused = norm.substring(0, opt_fuselen); fused_map = (opt_fuse || !opt_tok) ? FuzzySearch.alphabet(fused) : {}; return new Query(norm, words, group, fused, fused_map, has_tags, children) } }); // // Query objects // /** * Hold a query * * @param {string} normalized * @param {Array.<string>} words * @param {Array.<PackInfo>} tokens_groups * @param {string} fused_str * @param {Object} fused_map * @param {boolean} has_children * @param {Array<Query>} children * * @constructor */ function Query(normalized, words, tokens_groups, fused_str, fused_map, has_children, children) { this.normalized = normalized; this.words = words; this.tokens_groups = tokens_groups; this.fused_str = fused_str; this.fused_map = fused_map; this.fused_score = 0; this.has_children = has_children; this.children = children; } // // Query hold some memory to keep score of it's tokens. // Used in search methods /** * Loop tru each item score and reset to 0, apply to child query */ Query.prototype.resetItem = function () { var groups = this.tokens_groups; for (var group_index = -1, nb_groups = groups.length; ++group_index < nb_groups;) { var score_item = groups[group_index].score_item; for (var i = -1, l = score_item.length; ++i < l;) score_item[i] = 0 } this.fused_score = 0; if (this.has_children) { var children = this.children; for (var child_index = -1, nb_child = children.length; ++child_index < nb_child;) { var child = children[child_index]; if (child) child.resetItem(); } } }; /** * Sum each item score and add to child score */ Query.prototype.scoreItem = function () { var query_score = 0; var groups = this.tokens_groups; for (var group_index = -1, nb_groups = groups.length; ++group_index < nb_groups;) { var group_scores = groups[group_index].score_item; for (var score_index = -1, nb_scores = group_scores.length; ++score_index < nb_scores;) { query_score += group_scores[score_index] } } if (this.fused_score > query_score) query_score = this.fused_score; if (this.has_children) { var children = this.children; for (var child_index = -1, nb_child = children.length; ++child_index < nb_child;) { var child = children[child_index]; if (child) query_score += child.scoreItem(); } } return query_score; }; /** * Hold a group of token for parallel scoring * * @param {Array.<string>} group_tokens * @param {Object} group_map * @param {number} gate * @constructor */ function PackInfo(group_tokens, group_map, gate) { this.tokens = group_tokens; this.map = group_map; this.gate = gate; var t = group_tokens.length, i = -1; var scores = new Array(t); while (++i < t) scores[i] = 0; this.score_item = scores.slice(); this.score_field = scores.slice(); this.field_pos = scores; } // // - - - - - - - - - - - - - - - - - // Prepare Token for search // - - - - - - - - - - - - - - - - - // a normal string can be view as an array of char. // so we map ( position -> char). // // we reverse that relation to map // char -> positions /** * Record position of each character in a token. * If token is small, position is recorded by position of a single bit in an int. * If token is larger than INT_SIZE, position is recorder as array of number. * * @param {string} token * @returns {Object} key value map char->positions (as array of position or single int (can be seen as an array of bit) ) */ FuzzySearch.alphabet = function (token) { var len = token.length; if (len > INT_SIZE) return FuzzySearch.posVector(token); else return FuzzySearch.bitVector(token, {}, 0); }; /** * Apply FuzzySearch.alphabet on multiple tokens * * @param {Array.<string>} tokens * @returns {Array.<Object>} */ FuzzySearch.mapAlphabet = function (tokens) { var outlen = tokens.length; var out = new Array(outlen), i = -1; while (++i < outlen) { var t = tokens[i]; if (t.length > INT_SIZE) out[i] = FuzzySearch.posVector(t); else out[i] = FuzzySearch.bitVector(t, {}, 0); } return out; }; /** * Record position of each char using a single bit * * @param {string} token * @param {Object} map - Existing map to modify, can init with {} * @param offset - used for packing multiple word in a single map, can init with 0 * @returns {Object} Key value map char -> int */ FuzzySearch.bitVector = function (token, map, offset) { var len = token.length; var i = -1, c; var b = offset; while (++i < len) { c = token[i]; if (c in map) map[c] |= (1 << b++); else map[c] = (1 << b++); } return map; }; /** * Record position of each char in a token using an array * Append Infinity as a stop marker for llcs_large * * map = posVector("position") * map["p"] -> [0,Inf] * map["o"] -> [1,6,Inf] * * @param {string} pattern * @returns {Object} - key value map char->array of position (as number) */ FuzzySearch.posVector = function (pattern) { var map = {}, c; var m = pattern.length, i = -1; while (++i < m) { c = pattern[i]; if (c in map) map[c].push(i); else map[c] = [i]; } for (c in map) { if (map.hasOwnProperty(c)) { map[c].push(Infinity); } } return map; }; /** * Given a list of tokens, pack them into group of upto INT_SIZE(32) chars. * If a single token is bigger than INT_SIZE create a groupe of a single item * And use posVector instead of bitVector to prepare fallback algorithm. * * @param {Array.<string>} tokens * @returns {Array.<PackInfo>} */ FuzzySearch.pack_tokens = function (tokens) { var token_index = -1; var nb_tokens = tokens.length; var large; var groups = []; //For each group while (token_index < nb_tokens) { var group_tokens = []; var group_map = {}; var offset = 0; var gate = 0; //For each token in the group while (++token_index < nb_tokens) { var token = tokens[token_index]; var l = token.length; if (l >= INT_SIZE) { large = new PackInfo([token], FuzzySearch.posVector(token), 0xFFFFFFFF); break; } else if (l + offset >= INT_SIZE) { token_index--; break; } else { group_tokens.push(token); FuzzySearch.bitVector(token, group_map, offset); gate |= ( (1 << ( token.length - 1) ) - 1 ) << offset; offset += l } } if (group_tokens.length > 0) { groups.push(new PackInfo(group_tokens, group_map, gate)); } if (large) { groups.push(large); large = null; } } return groups; };