UNPKG

@itwin/core-frontend

Version:
178 lines 8.03 kB
"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