fuzzy-rater
Version:
Tooling for fuzzily rating text according to some query
234 lines • 18.8 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: { 2: 1, 5: 2 },
skipPenalty: 10,
missingPenalty: 10,
typoPenalty: 10,
extraBonus: 5,
...config,
};
if (typeof query == "string")
query = query.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,
...input,
}));
return { query: normalizedQuery, config: normalizedConfig };
}
/**
* Initializes the word matches
* @returns The word matchers
*/
initializeWordMatchers() {
return this.query.reduce((matchers, { word, maxDistance }, i) => {
const alreadyContains = matchers.find(({ matcher }) => matcher.word == word);
if (alreadyContains)
matchers.map(({ count, matcher }) => matcher.word == word ? { count: count + 1, matcher } : { count, matcher });
return [
...matchers,
{ count: 1, matcher: new FuzzyMultiWordMatcher_1.FuzzyMultiWordMatcher(word, maxDistance) },
];
}, []);
}
/**
* 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;
}
/**
* Computes all relevant match data for the given text
* @param text The text to get match data for
* @returns
*/
getMatch(text) {
const { missingCost, extraBonus, matches } = this.wordMatchers.reduce(({ missingCost, extraBonus, matches: prevMatches }, { count: requireWordCount, matcher }) => {
// 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 * this.config.typoPenalty,
}));
return {
missingCost: missingCost +
(matches.length == 0 ? this.config.missingPenalty : 0),
// Note: It doesn't consider distance or typos in extra matches
extraBonus: extraBonus +
Math.max(0, matches.length - requireWordCount) *
this.config.extraBonus,
matches: this.mergeMatches(prevMatches, normalized),
};
}, {
missingCost: 0,
extraBonus: 0,
matches: [],
});
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 large 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 }) => {
// Retrieve the matches for this matcher
const matchData = matcher.getMatchData(text);
const word = matcher.word;
const matches = matchData.alterations.reduce((matches, alteration) => {
if (alteration.query.index == 0) {
const distance = matchData.distances[matches.length];
return [
...matches,
{
word,
index: alteration.target.index,
endIndex: alteration.target.index + word.length - distance,
cost: distance * this.config.typoPenalty,
},
];
}
return matches;
}, []);
return {
missingCost: missingCost +
(matchData.distances.length == 0
? this.config.missingPenalty
: 0),
// Note: It doesn't consider distance or typos in extra matches
extraBonus: extraBonus +
Math.max(0, matchData.distances.length - requireWordCount) *
this.config.extraBonus,
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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiRnV6enlSYXRlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9mdXp6eVJhdGUvRnV6enlSYXRlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSxzRUFBaUU7QUFFakUsK0VBQTBFO0FBTTFFLDJEQUFzRDtBQUd0RDs7R0FFRztBQUNILE1BQWEsVUFBVTtJQU9uQjs7OztPQUlHO0lBQ0gsWUFBbUIsS0FBaUMsRUFBRSxNQUF5QjtRQUMzRSxNQUFNLFVBQVUsR0FBRyxJQUFJLENBQUMsZ0JBQWdCLENBQUMsS0FBSyxFQUFFLE1BQU0sQ0FBQyxDQUFDO1FBQ3hELElBQUksQ0FBQyxNQUFNLEdBQUcsVUFBVSxDQUFDLE1BQU0sQ0FBQztRQUNoQyxJQUFJLENBQUMsS0FBSyxHQUFHLFVBQVUsQ0FBQyxLQUFLLENBQUM7UUFDOUIsSUFBSSxDQUFDLFlBQVksR0FBRyxJQUFJLENBQUMsc0JBQXNCLEVBQUUsQ0FBQztRQUNsRCxJQUFJLENBQUMsWUFBWSxHQUFHLElBQUksQ0FBQyxzQkFBc0IsRUFBRSxDQUFDO0lBQ3RELENBQUM7SUFFRDs7Ozs7T0FLRztJQUNPLGdCQUFnQixDQUN0QixLQUFpQyxFQUNqQyxNQUF5QjtRQUV6QixNQUFNLGdCQUFnQixHQUFHO1lBQ3JCLFNBQVMsRUFBRSxFQUFDLENBQUMsRUFBRSxDQUFDLEVBQUUsQ0FBQyxFQUFFLENBQUMsRUFBQztZQUN2QixXQUFXLEVBQUUsRUFBRTtZQUNmLGNBQWMsRUFBRSxFQUFFO1lBQ2xCLFdBQVcsRUFBRSxFQUFFO1lBQ2YsVUFBVSxFQUFFLENBQUM7WUFDYixHQUFHLE1BQU07U0FDWixDQUFDO1FBRUYsSUFBSSxPQUFPLEtBQUssSUFBSSxRQUFRO1lBQUUsS0FBSyxHQUFHLEtBQUssQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsQ0FBQyxFQUFDLElBQUksRUFBQyxDQUFDLENBQUMsQ0FBQztR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