UNPKG

fuzzy-rater

Version:

Tooling for fuzzily rating text according to some query

248 lines 19.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FuzzyRater = void 0; const WordOrderMatcher_1 = require("./orderMatcher/WordOrderMatcher"); const FuzzyMultiWordMatcher_1 = require("./wordMatcher/FuzzyMultiWordMatcher"); const addAlterationData_1 = require("./addAlterationData"); /** * The highest level fuzzy rater class, to rate how well text matches an input query */ class FuzzyRater { /** * Creates a new fuzzy rater * @param query The query to be rate with * @param config The configuration for how to rate matches */ constructor(query, config) { const normalized = this.initializeInputs(query, config); this.config = normalized.config; this.query = normalized.query; this.wordMatchers = this.initializeWordMatchers(); this.orderMatcher = this.initializeOrderMatcher(); } /** * Initializes the rater's configuration * @param query The query input * @param config The config input * @returns The normalized query and config with defaults */ initializeInputs(query, config) { const normalizedConfig = { fuzziness: { 3: 1, 5: 2 }, skipPenalty: 50, missingPenalty: 100, typoPenalty: 5, extraBonus: 5, ...config, }; if (typeof query == "string") query = query .trim() .split(" ") .map(word => ({ word })); const fuzziness = Object.entries(normalizedConfig.fuzziness) .map(([a, b]) => [Number(a), b]) .sort(([a], [b]) => a - b); const normalizedQuery = query.map(input => ({ extraBonus: normalizedConfig.extraBonus, maxDistance: fuzziness.reduce((cur, item) => (input.word.length > item[0] ? item[1] : cur), 0), skipPenalty: normalizedConfig.skipPenalty, missingPenalty: normalizedConfig.missingPenalty, typoPenalty: normalizedConfig.typoPenalty, ...input, })); return { query: normalizedQuery, config: normalizedConfig }; } /** * Initializes the word matches * @returns The word matchers */ initializeWordMatchers() { return this.query.reduce((matchers, word, i) => { const alreadyContains = matchers.find(({ matcher }) => matcher.word == word.word); if (alreadyContains) matchers.map(({ count, matcher, words }) => matcher.word == word.word ? { count: count + 1, matcher, words: [...words, word] } : { count, matcher, words }); return [ ...matchers, { count: 1, matcher: new FuzzyMultiWordMatcher_1.FuzzyMultiWordMatcher(word.word, word.maxDistance), words: [word], }, ]; }, []); } /** * Initializes the word order matcher * @returns The word matcher */ initializeOrderMatcher() { return new WordOrderMatcher_1.WordOrderMatcher(this.query.map(q => ({ word: q.word, penalty: q.skipPenalty }))); } /** * Merges two match lists * @param m1 The first (sorted) match list * @param m2 The second (sorted) match list * @returns The combined, sorted, match list */ mergeMatches(m1, m2) { const combined = []; let i = 0, j = 0; while (i < m1.length && j < m2.length) { if (m1[i].index < m2[j].index) combined.push(m1[i++]); else combined.push(m2[j++]); } while (i < m1.length) combined.push(m1[i++]); while (j < m2.length) combined.push(m2[j++]); return combined; } /** * Retrieves the word matches for a given string * @param text The text to get the matches for * @returns The matches as well as some additional costs */ getWordMatches(text) { return this.wordMatchers.reduce(({ missingCost, extraBonus, matches: prevMatches }, { count: requireWordCount, matcher, words: [{ missingPenalty, typoPenalty, extraBonus: wordExtraBonus }], }) => { // Retrieve the matches for this matcher const matches = matcher.getMatch(text); const word = matcher.word; const normalized = matches.map(match => ({ ...match, word, index: match.endIndex - word.length + match.distance, cost: match.distance * typoPenalty, })); return { missingCost: missingCost + (matches.length == 0 ? missingPenalty : 0), // Note: It doesn't consider distance or typos in extra matches extraBonus: extraBonus + Math.max(0, matches.length - requireWordCount) * wordExtraBonus, matches: this.mergeMatches(prevMatches, normalized), }; }, { missingCost: 0, extraBonus: 0, matches: [], }); } /** * Computes all relevant match data for the given text * @param text The text to get match data for * @returns The fuzzy match data, including score and individual components */ getMatch(text) { const { missingCost, extraBonus, matches } = this.getWordMatches(text); if (matches.length > 0) { const orderCost = this.orderMatcher.getMatch(matches); const combinedScore = orderCost + missingCost - extraBonus; return { score: combinedScore, orderCost, missingCost, extraBonus, }; } return { score: Infinity, orderCost: Infinity, missingCost: Infinity, extraBonus: 0, }; } /** * Retrieves the score for a given piece of text * @param text The text to be rated * @returns A score, where larger is worse */ getScore(text) { return this.getMatch(text).score; } /** * Retrieves the match data that can be used for highlighting * @param text The text to be matched * @returns The scores including match data */ getMatchData(text) { const { missingCost, extraBonus, matches, alterations } = this.wordMatchers.reduce(({ missingCost, extraBonus, matches: prevMatches, alterations }, { count: requireWordCount, matcher, words: [{ word, extraBonus: wordExtraBonus, skipPenalty, missingPenalty, typoPenalty, },], }) => { // Retrieve the matches for this matcher const matchData = matcher.getMatchData(text); const matches = matchData.alterations.reduce((matches, alteration) => { if (alteration.query.index == 0 && alteration.type != "ignore") { const distance = matchData.distances[matches.length]; return [ ...matches, { word, index: alteration.target.index, endIndex: alteration.target.index + word.length - distance, cost: distance * typoPenalty, }, ]; } return matches; }, []); return { missingCost: missingCost + (matchData.distances.length == 0 ? missingPenalty : 0), // Note: It doesn't consider distance or typos in extra matches extraBonus: extraBonus + Math.max(0, matchData.distances.length - requireWordCount) * wordExtraBonus, matches: this.mergeMatches(prevMatches, matches), alterations: [ ...alterations, { word, alterations: matchData.alterations }, ], }; }, { missingCost: 0, extraBonus: 0, alterations: [], matches: [], }); if (matches.length > 0) { const orderMatchData = this.orderMatcher.getMatchData(matches); const orderCost = orderMatchData.distance; // Combine all alterations data const words = this.query.map(({ word: text }, index) => { var _a; const match = orderMatchData.matches.find(m => m.wordIndex == index); return { text, index, matchedIndex: (_a = match === null || match === void 0 ? void 0 : match.match.index) !== null && _a !== void 0 ? _a : -1, }; }); const matchGroups = words.reduce((groups, word) => { const wordAlterations = alterations.find(({ word: w }) => w == word.text); if (wordAlterations) return addAlterationData_1.addAlterationData(groups, wordAlterations.alterations, word); return groups; }, []); // Return the final result const combinedScore = orderCost + missingCost - extraBonus; return { matchGroups, score: combinedScore, orderCost, missingCost, extraBonus, }; } return { matchGroups: [], score: Infinity, orderCost: Infinity, missingCost: Infinity, extraBonus: 0, }; } } exports.FuzzyRater = FuzzyRater; //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiRnV6enlSYXRlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9mdXp6eVJhdGUvRnV6enlSYXRlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSxzRUFBaUU7QUFFakUsK0VBQTBFO0FBTTFFLDJEQUFzRDtBQUd0RDs7R0FFRztBQUNILE1BQWEsVUFBVTtJQVduQjs7OztPQUlHO0lBQ0gsWUFBbUIsS0FBaUMsRUFBRSxNQUF5QjtRQUMzRSxNQUFNLFVBQVUsR0FBRyxJQUFJLENBQUMsZ0JBQWdCLENBQUMsS0FBSyxFQUFFLE1BQU0sQ0FBQyxDQUFDO1FBQ3hELElBQUksQ0FBQyxNQUFNLEdBQUcsVUFBVSxDQUFDLE1BQU0sQ0FBQztRQUNoQyxJQUFJLENBQUMsS0FBSyxHQUFHLFVBQVUsQ0FBQyxLQUFLLENBQUM7UUFDOUIsSUFBSSxDQUFDLFlBQVksR0FBRyxJQUFJLENBQUMsc0JBQXNCLEVBQUUsQ0FBQztRQUNsRCxJQUFJLENBQUMsWUFBWSxHQUFHLElBQUksQ0FBQyxzQkFBc0IsRUFBRSxDQUFDO0lBQ3RELENBQUM7SUFFRDs7Ozs7T0FLRztJQUNPLGdCQUFnQixDQUN0QixLQUFpQyxFQUNqQyxNQUF5QjtRQUV6QixNQUFNLGdCQUFnQixHQUFHO1lBQ3JCLFNBQVMsRUFBRSxFQUFDLENBQUMsRUFBRSxDQUFDLEVBQUUsQ0FBQyxFQUFFLENBQUMsRUFBQztZQUN2QixXQUFXLEVBQUUsRUFBRTtZQUNmLGNBQWMsRUFBRSxHQUFHO1lBQ25CLFdBQVcsRUFBRSxDQUFDO1lBQ2QsVUFBVSxFQUFFLENBQUM7WUFDYixHQUFHLE1BQU07U0FDWixDQUFDO1FBRUYsSUFBSSxPQUFPLEtBQUssSUFBSSxRQUFRO1lBQ3hCLEtBQUssR0FBRyxLQUFLO2lCQUNSLElBQUksRUFBRTtpQkFDTixLQUFLLENBQUMsR0FBRyxDQUFDO2lCQUNWLEdBQUcsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLENBQUMsRUFBQyxJQUFJLEVBQUMs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