fuzzy-rater
Version:
Tooling for fuzzily rating text according to some query
248 lines • 19.9 kB
JavaScript
;
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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