fz-search
Version:
Fast aproximate string matching library for use in autocomplete, perform both search and highlight.
304 lines (221 loc) • 9.99 kB
JavaScript
// main entry of the algorithm (once settings are set)
// loop over everything and merge best scores
'use strict';
extend(FuzzySearch.prototype, /** @lends {FuzzySearch.prototype} */ {
/**
* Perform a search on the already indexed source.
*
* @param {string} query_string
* @returns {Array}
*/
search: function (query_string) {
var time_start = Date.now();
this.start_time = time_start;
var options = this.options;
// As long as lazy is set to false, we guarantee that making a search is read only.
if (this.dirty && options.lazy) {
this._buildIndexFromSource();
this.dirty = false;
}
var query = this.query = this._prepQuery(query_string);
var source = this.index;
var results = [];
if (options.use_index_store) {
source = this._storeSearch(query, source);
}
if (options.filter) {
source = options.filter.call(this, source);
}
// ---- MAIN SEARCH LOOP ---- //
var thresh_include = this._searchIndex(query, source, results);
//keep only results that are good enough compared to best
results = FuzzySearch.filterGTE(results, "score", thresh_include);
// sort by decreasing order of score
// equal rounded score: alphabetical order
if (typeof options.sorter === "function")
results = results.sort(options.sorter);
if (options.output_map || options.output_limit > 0) {
if (typeof options.output_map === "function")
results = FuzzySearch.map(results, options.output_map, this, options.output_limit);
else
results = FuzzySearch.mapField(results, options.output_map, options.output_limit);
}
var time_end = Date.now();
this.search_time = time_end - time_start;
this.results = results;
return results
},
/**
* Main search loop for a specified source
* This separation allow to search a different source, or a subset of source
*
* @param {Query} query
* @param {Array.<Indexed>} source
* @param {Array.<SearchResult>} results
* @returns {number} - thresh_include after this run.
*
* @private
*/
_searchIndex: function (query, source, results) {
var options = this.options;
var opt_bpd = options.bonus_position_decay;
var opt_fge = options.field_good_enough;
var opt_trb = options.thresh_relative_to_best;
var opt_score_tok = options.score_per_token;
var opt_round = options.score_round;
var thresh_include = options.thresh_include;
var best_item_score = 0;
var sub_query = query.children;
for (var item_index = -1, nb_items = source.length; ++item_index < nb_items;) {
//get indexed fields
var item = source[item_index];
var item_fields = item.fields;
//reset score
query.resetItem();
var item_score = 0;
var matched_field_index = -1;
var matched_node_index = -1;
var position_bonus = 1.0;
//
//Foreach field
//
for (var field_index = -1, nb_fields = item_fields.length; ++field_index < nb_fields;) {
var field_score = 0;
var field_node = -1;
var field = item_fields[field_index];
var child_query = sub_query[field_index]; //tag search
var tagged = !!child_query;
for (var node_index = -1, nb_nodes = field.length; ++node_index < nb_nodes;) {
var node_score, node = field[node_index];
if (opt_score_tok) {
node_score = this._scoreField(node, query);
if (tagged) node_score += this._scoreField(node, child_query);//tag search
}
else
node_score = FuzzySearch.score_map(query.fused_str, node.join(" "), query.fused_map, options);
if (node_score > field_score) {
field_score = node_score;
field_node = node_index;
}
}
field_score *= (1.0 + position_bonus);
position_bonus *= opt_bpd;
if (field_score > item_score) {
item_score = field_score;
matched_field_index = field_index;
matched_node_index = field_node;
if (field_score > opt_fge) break;
}
}
//
// Different query token match different fields ?
//
if (opt_score_tok) {
var query_score = query.scoreItem();
item_score = 0.5 * item_score + 0.5 * query_score;
}
//
// Keep track of best result, this control inclusion in the list
//
if (item_score > best_item_score) {
best_item_score = item_score;
var tmp = item_score * opt_trb;
if (tmp > thresh_include) thresh_include = tmp;
}
//
//candidate for best result ? push to list
//
if (item_score > thresh_include) {
item_score = Math.round(item_score / opt_round) * opt_round;
results.push(new SearchResult(
item.item,
item_fields,
item_score,
matched_field_index,
matched_node_index,
item_fields[0][0].join(" ")
));
}
}
return thresh_include
},
/**
* Internal loop that is run for each field in an item
*
* @param {Array} field_tokens
* @param {Query} query
* @returns {number}
* @private
*/
_scoreField: function (field_tokens, query) {
var groups = query.tokens_groups;
var nb_groups = groups.length;
var nb_tokens = field_tokens.length;
if (!nb_groups || !nb_tokens) return 0;
var field_score = 0, sc, bf;
var last_index = -1;
var options = this.options;
var bonus_order = options.bonus_token_order;
var minimum_match = options.minimum_match;
var token, scores, i;
for (var group_index = -1; ++group_index < nb_groups;) {
var group_info = groups[group_index];
var nb_scores = group_info.tokens.length;
// Each packinfo have their own reusable scratch pad
// to store best score information, reset them to 0
var best_of_field = group_info.score_field;
for (i = -1; ++i < nb_scores;) best_of_field[i] = 0
var best_index = group_info.field_pos;
for (i = -1; ++i < nb_scores;) best_index[i] = 0
for (var field_tk_index = -1; ++field_tk_index < nb_tokens;) {
token = field_tokens[field_tk_index];
scores = FuzzySearch.score_pack(group_info, token, options);
for (i = -1; ++i < nb_scores;) {
sc = scores[i];
bf = best_of_field[i];
//Score is an improvement OR
//Score is within a token order bonus from being better, but word are swapped
if (sc > bf || ( bf - sc < bonus_order && i > 0 && best_index[i] <= best_index[i - 1] )) {
best_of_field[i] = sc;
best_index[i] = field_tk_index;
}
}
}
var best_match_this_item = group_info.score_item;
for (i = -1; ++i < nb_scores;) {
sc = best_of_field[i];
field_score += sc;
// Give bonus for pair in consecutive order
// Only consider positive match for bonus
if (sc > minimum_match) {
var this_index = best_index[i];
//Bonus is diluted by the distance between words.
//Positive match, but out of order get half the bonus.
var d = this_index - last_index;
var bo = bonus_order * ( 1.0 / (1.0 + Math.abs(d)));
if (d > 0) bo *= 2;
field_score += bo;
sc += bo;
last_index = this_index;
}
if (sc > best_match_this_item[i])
best_match_this_item[i] = sc;
}
}
if (options.score_test_fused) {
// field_tokens.join(" "), remove last one if acronym
// performance of array.join(" ") and str concat look similar on modern browser.
var n = (options.score_acronym) ? nb_tokens - 1 : nb_tokens;
var fused_field = field_tokens[0], fi = 0;
while (++fi < n) fused_field += " " + field_tokens[fi];
// test "space bar is broken" no token match
var fused_score = FuzzySearch.score_map(query.fused_str, fused_field, query.fused_map, options);
fused_score += bonus_order; //fused cannot be out of order
field_score = fused_score > field_score ? fused_score : field_score;
if (fused_score > query.fused_score) {
query.fused_score = fused_score;
}
}
return field_score;
}
});