@itwin/core-frontend
Version:
iTwin.js frontend components
178 lines • 8.03 kB
JavaScript
"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Tools
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FuzzySearchResults = exports.FuzzySearch = void 0;
const fuse_js_1 = __importDefault(require("fuse.js"));
/** @public */
class FuzzySearch {
/** Override to provide non-standard FuseOptions for searches where the a single word pattern is used */
onGetSingleWordSearchOptions() {
return {
shouldSort: true,
threshold: 0.40,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 2,
includeMatches: true,
includeScore: true,
};
}
/** Override to provide non-standard FuseOptions for searches where the a multiple word pattern is used */
onGetMultiWordSearchOptions() {
return {
shouldSort: true,
threshold: 0.40,
tokenize: true,
matchAllTokens: true,
maxPatternLength: 32,
minMatchCharLength: 2,
includeMatches: true,
includeScore: true,
};
}
/** Call to conduct a fuzzy search of searchedObjects, looking at the 'key' member of each such object
* @param searchedObjects An array of objects to search.
* @param keys The name of the members to search in the searchedObjects.
* @param pattern The pattern for which each searchedObject is searched.
* @return FuzzySearchResults.
*/
search(searchedObjects, keys, pattern) {
if (!pattern || pattern.length < 2)
return new FuzzySearchResults(undefined);
// it is a multi-word pattern if there's a space other than at the end of the pattern.
const spaceIndex = pattern.indexOf(" ");
const multiWord = (-1 !== spaceIndex) && (spaceIndex !== (pattern.length - 1));
const options = multiWord ? this.onGetMultiWordSearchOptions() : this.onGetSingleWordSearchOptions();
options.keys = keys;
const fuse = new fuse_js_1.default(searchedObjects, options);
let results = fuse.search(pattern);
// We need to set the threshold fairly high to get results when the user misspells words (otherwise they are not returned),
// but doing that results in matches that don't make sense when there are "good" matches. So we discard matches where the match
// score increases by a large amount between results.
let checkScoreDelta = false;
let averageScoreDeltaThreshold = 1;
if (results.length > 30) {
averageScoreDeltaThreshold = ((results[results.length - 1].score - results[0].score) / results.length) * 10;
if (averageScoreDeltaThreshold > 0.01)
checkScoreDelta = true;
}
// Sometimes fuse returns results in the array where the matches array is empty. That seems like a bug to me, but it happens when
// the input is something like "fjt" and the string it matches is "fit". If we have more than three actual matches, we just truncate the set when we see one.
// The other use for this loop is to truncate when we see a dramatic increase in the score. The ones after are unlikely
// to be useful, so we truncate the results when we hit that point also.
for (let resultIndex = 0; resultIndex < results.length; resultIndex++) {
const thisResult = results[resultIndex];
if (0 === thisResult.matches.length) {
// here we have a result with no matches. If we have other matches, just discard this and the rest.
if (resultIndex > 2) {
results = results.slice(0, resultIndex);
break;
}
// otherwise we want to keep this result, but we have to add the matched value to the object because we can't get it from the matches array.
// we assume it came from the first key (usually there's only one anyway).
thisResult.matchedValue = thisResult.item[keys[0]];
thisResult.matchedKey = keys[0];
}
if (checkScoreDelta && (resultIndex > 0)) {
const resultScore = results[resultIndex].score;
if (resultScore < 0.101)
continue;
if ((resultScore - results[resultIndex - 1].score) > averageScoreDeltaThreshold) {
results = results.slice(0, resultIndex);
break;
}
}
}
// put the functions on each result so it fulfils the FuzzySearchResult interface.
for (const thisResult of results) {
thisResult.getResult = getResult.bind(thisResult);
thisResult.getBoldMask = getBoldMask.bind(thisResult);
thisResult.getMatchedKey = getMatchedKey.bind(thisResult);
thisResult.getMatchedValue = getMatchedValue.bind(thisResult);
}
return new FuzzySearchResults(results);
}
}
exports.FuzzySearch = FuzzySearch;
/** Added to each result to support the FuzzySearchResult interface. */
function getResult() {
return this.item;
}
/** Added to each result to support the FuzzySearchResult interface. */
function getMatchedKey() {
return (this.matches.length > 0) ? this.matches[0].key : this.matchedKey;
}
/** Added to each result to support the FuzzySearchResult interface. */
function getMatchedValue() {
return (this.matches.length > 0) ? this.matches[0].value : this.matchedValue;
}
/** this function is added to each result to support the FuzzySearchResult interface. */
function getBoldMask() {
if (this.boldMask)
return this.boldMask;
// if we had no matches, we return a bold mask with all false.
if (0 === this.matches.length) {
const noBoldMask = new Array(this.matchedValue.length);
noBoldMask.fill(false);
return this.boldMask = noBoldMask;
}
// we have some matched portions.
const thisMatchedString = this.matches[0].value;
const valueLength = thisMatchedString.length;
const boldMask = new Array(valueLength);
boldMask.fill(false);
const indicesArray = this.matches[0].indices;
indicesArray.forEach((set) => {
for (let start = set[0], end = set[1]; start <= end; start++) {
boldMask[start] = true;
}
});
// cache it so if someone asks again we don't have to recalculate it.
return this.boldMask = boldMask;
}
/**
* This class is used to return the results of FuzzySearch.search. It is iterable, with each iteration
* returning an object implementing the FuzzySearchResult interface.
* @public
*/
class FuzzySearchResults {
results;
constructor(results) {
this.results = [];
if (results)
this.results = results;
}
[Symbol.iterator]() { return new FuzzySearchResultsIterator(this); }
get length() { return this.results.length; }
getResult(resultIndex) {
if ((resultIndex < 0) || (resultIndex > this.results.length))
return undefined;
return this.results[resultIndex];
}
}
exports.FuzzySearchResults = FuzzySearchResults;
class FuzzySearchResultsIterator {
counter;
fsr;
constructor(fsr) {
this.fsr = fsr;
this.counter = 0;
}
next = () => {
return {
done: this.counter === this.fsr.results.length,
value: this.fsr.results[this.counter++],
};
};
}
//# sourceMappingURL=FuzzySearch.js.map