fuzzy-rater
Version:
Tooling for fuzzily rating text according to some query
209 lines • 17 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.FuzzyMultiWordMatcher = void 0;
const createFuzzyNFATemplate_1 = require("./createFuzzyNFATemplate");
const NFADFA_1 = require("../DFA/NFADFA/NFADFA");
/**
* A fuzzy word matcher that can be used to find a word in a number of items.
* Initial setup time is relatively long, but matching per string happens in linear time.
*/
class FuzzyMultiWordMatcher {
/**
* Constructs a new fuzzy word rater
* @param word The word to look for
* @param maxDistance The maximum error
*/
constructor(word, maxDistance) {
this.initialize(word, maxDistance);
}
/**
* Initializes the data structures used for rating
* @param word The word to look for
* @param maxDistance The maximal allowed distance
*/
initialize(word, maxDistance) {
const nfaTemplate = createFuzzyNFATemplate_1.createFuzzyNFATemplate(word, maxDistance, true);
this.NFA = new NFADFA_1.NFADFA(nfaTemplate, {
// Add the best combined fuzzy meta to ever node
nodeMeta: nodes => {
var _a;
return ((_a = this.getBestMatch(nodes, n => n.metadata)) === null || _a === void 0 ? void 0 : _a.metadata) || {
matched: false,
distance: 0,
};
},
// No need to augment the transitions
transitionMeta: transition => ({}),
});
}
/**
* Finds the best match in a set of NFA nodes
* @param matches The nodes to find the best match in
* @param getNode Retrieves the node data
* @returns The best match
*/
getBestMatch(matches, getNode) {
const best = matches.reduce((best, m) => {
const node = getNode(m);
return node.matched && (best == null || node.distance < best.distance)
? { item: m, distance: node.distance }
: best;
}, undefined);
return best === null || best === void 0 ? void 0 : best.item;
}
/**
* Retrieves the best match in the given text
* @param text The text to find the query word in
* @returns Whether the text matched, and the distance from the query word
*/
getMatch(text) {
// Execute the DFA
const trace = this.NFA.executeDFATraced(text);
// Extract the matches from the data
const lastMatched = trace.final.matched;
const matchData = trace.path.reduceRight(({ matches, bestConsecutiveMatch }, { fromNode: { matched, distance } }, endIndex) => {
if (!matched) {
return {
matches: bestConsecutiveMatch
? [bestConsecutiveMatch, ...matches]
: matches,
bestConsecutiveMatch: null,
};
}
else {
// When a node matched, reduce it to the best option of a sequence before adding the match
return {
matches,
bestConsecutiveMatch: !bestConsecutiveMatch ||
bestConsecutiveMatch.distance > distance
? { endIndex, distance }
: bestConsecutiveMatch,
};
}
}, {
matches: lastMatched
? [{ endIndex: text.length, distance: trace.final.distance }]
: [],
bestConsecutiveMatch: null,
});
// Return the matches
return matchData.bestConsecutiveMatch
? [matchData.bestConsecutiveMatch, ...matchData.matches]
: matchData.matches;
}
/**
* Retrieves the best NFA trace given a DFA match
* @param nfaDfaTrace The simplified NFA-DFA trace to obtain the best NFA trace in (text matches with lowest distance)
* @returns The NFA trace
*/
getBestTrace(nfaDfaTrace) {
const trace = nfaDfaTrace.getPath(dfaTrace => {
// Obtain the indices of transitions where to choose the best
const matchIndices = dfaTrace.path
.reduceRight(({ matches, bestConsecutive }, { fromNode }, index) => {
if (fromNode.matched) {
return {
matches,
bestConsecutive: !bestConsecutive ||
bestConsecutive.distance > fromNode.distance
? { index: index - 1, distance: fromNode.distance }
: bestConsecutive,
};
}
return {
matches: bestConsecutive
? [bestConsecutive, ...matches]
: matches,
bestConsecutive: null,
};
}, {
matches: [],
bestConsecutive: dfaTrace.final.matched
? {
index: dfaTrace.path.length - 1,
distance: dfaTrace.final.distance,
}
: null,
})
.matches.map(({ index }) => index);
// Return a function that chooses the transition from (/to since moving backwards) a node with the lowest distance, if we found that this leads to the best match
return (to, transitions, index, nodes) => {
const possibleTrans = transitions.filter(transition => transition.to == to.ID);
// Look for the best transition that came from a match, and choose that if it exists
const matching = matchIndices.includes(index) &&
possibleTrans.reduce((best, transition) => {
var _a;
const md = (_a = nodes[transition.from]) === null || _a === void 0 ? void 0 : _a.metadata;
if (md.matched && md.distance < best.distance)
return { transition, distance: md.distance };
return best;
}, { transition: null, distance: Infinity }).transition;
if (matching)
return matching;
return possibleTrans[0];
};
});
return trace;
}
/**
* Retrieves the best match in the given text, and data of how to obtain it
* @param text The text to find the query word in
* @returns The distances from the query word, for each match (no distances = no matches), and how the text differed
*/
getMatchData(text) {
const matches = this.NFA.executeTraced(text);
const best = this.getBestMatch(matches, ({ final }) => final);
const trace = best && this.getBestTrace(best);
// Convert the NFA trace to an alterations array
if (best && trace) {
const result = trace.reduce(({ alterations, index, distances, prevNode }, { transition, fromNode }) => {
var _a;
// Skip restart transitions, since they have no relevance in either the query or target
if (transition.type == "restart")
return {
alterations,
index,
distances: [...distances, fromNode.distance],
prevNode,
};
// Obtain the query and target data
const target = {
index,
character: transition.type == "skip" ? "" : text[index],
};
const query = {
index: transition.index,
character: (_a = transition.character) !== null && _a !== void 0 ? _a : "",
};
// Add any distances and alterations
const matched = fromNode.matched;
const newMatch = (prevNode === null || prevNode === void 0 ? void 0 : prevNode.matched) && !matched; // Make sure we don't include different distances of the same match
return {
alterations: [
...alterations,
{ target, query, type: transition.type },
],
distances: newMatch && prevNode
? [...distances, prevNode.distance]
: distances,
index: transition.type == "skip" ? index : index + 1,
prevNode: fromNode,
};
}, {
alterations: [],
distances: [],
index: 0,
prevNode: null,
});
return {
alterations: result.alterations,
distances: best.final.matched
? [...result.distances, best.final.distance]
: result.distances,
};
}
return { distances: [], alterations: [] };
}
}
exports.FuzzyMultiWordMatcher = FuzzyMultiWordMatcher;
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiRnV6enlNdWx0aVdvcmRNYXRjaGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2Z1enp5UmF0ZS9GdXp6eU11bHRpV29yZE1hdGNoZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEscUVBQWdFO0FBSWhFLGlEQUE0QztBQUc1Qzs7O0dBR0c7QUFDSCxNQUFhLHFCQUFxQjtJQUc5Qjs7OztPQUlHO0lBQ0gsWUFBbUIsSUFBWSxFQUFFLFdBQW1CO1FBQ2hELElBQUksQ0FBQyxVQUFVLENBQUMsSUFBSSxFQUFFLFdBQVcsQ0FBQyxDQUFDO0lBQ3ZDLENBQUM7SUFFRDs7OztPQUlHO0lBQ08sVUFBVSxDQUFDLElBQVksRUFBRSxXQUFtQjtRQUNsRCxNQUFNLFdBQVcsR0FBRywrQ0FBc0IsQ0FBQyxJQUFJLEVBQUUsV0FBVyxFQUFFLElBQUksQ0FBQyxDQUFDO1FBQ3BFLElBQUksQ0FBQyxHQUFHLEdBQUcsSUFBSSxlQUFNLENBQUMsV0FBVyxFQUFFO1lBQy9CLGdEQUFnRDtZQUNoRCxRQUFRLEVBQUUsS0FBSyxDQUFDLEVBQUU7O2dCQUNkLE9BQUEsT0FBQSxJQUFJLENBQUMsWUFBWSxDQUFDLEtBQUssRUFBRSxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsMENBQUUsUUFBUSxLQUFJO29CQUNuRCxPQUFPLEVBQUUsS0FBSztvQkFDZCxRQUFRLEVBQUUsQ0FBQztpQkFDZCxDQUFBO2FBQUE7WUFDTCxxQ0FBcUM7WUFDckMsY0FBYyxFQUFFLFVBQVUsQ0FBQyxFQUFFLENBQUMsQ0FBQyxFQUFFLENBQUM7U0FDckMsQ0FBQyxDQUFDO0lBQ1AsQ0FBQztJQUVEOzs7OztPQUtHO0lBQ08sWUFBWSxDQUNsQixPQUFZLEVBQ1osT0FBb0M7UUFFcEMsTUFBTSxJQUFJLEdBQUcsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQUFDLElBQUksRUFBRSxDQUFDLEVBQUUsRUFBRTtZQUNwQyxNQUFNLElBQUksR0FBRyxPQUFPLENBQUMsQ0FBQyxDQUFDLENBQUM7WUFDeEIsT0FBTyxJQUFJLENBQUMsT0FBTyxJQUFJLENBQUMsSUFBSSxJQUFJLElBQUksSUFBSSxJQUFJLENBQUMsUUFBUSxHQUFHLElBQUksQ0FBQyxRQUFRLENBQUM7Z0JBQ2xFLENBQUMsQ0FBQyxFQUFDLElBQUksRUFBRSxDQUFDLEVBQUUsUUFBUSxFQUFFLElBQUksQ0FBQyxRQUFRLEVBQUM7Z0JBQ3BDLENBQUMsQ0FBQyxJQUFJLENBQUM7UUFDZixDQUFDLEVBQUUsU0FBb0QsQ0FBQyxDQUFDO1FBQ3pELE9BQU8sSUFBSSxhQUFKLElBQUksdUJBQUosSUFBSSxDQUFFLElBQUksQ0FBQztJQUN0QixDQUFDO0lBRUQ7Ozs7T0FJRztJQUNJLFFBQVEsQ0FBQyxJQUFZO1FBQ3hCLGtCQUFrQjtRQUNsQixNQUFNLEtBQUssR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFDLGdCQUFnQixDQUFDLElBQUksQ0FBQyxDQUFDO1FBRTlDLG9DQUFvQztRQUNwQyxNQUFNLFdBQVcsR0FBRyxLQUFLLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQztRQUN4QyxNQUFNLFNBQVMsR0FBRyxLQUFLLENBQUMsSUFBSSxDQUFDLFdBQVcsQ0FDcEMsQ0FDSSxFQUFDLE9BQU8sRUFBRSxvQkFBb0IsRUFBQyxFQUMvQixFQUFDLFFBQVEsRUFBRSxFQUFDLE9BQU8sRUFBRSxRQUFRLEVBQUMsRUFBQyxFQUMvQixRQUFRLEVBQ1YsRUFBRTtZQUNBLElBQUksQ0FBQyxPQUFPLEVBQUU7Z0JBQ1YsT0FBTztvQkFDSCxPQUFPLEVBQUUsb0JBQW9CO3dCQUN6QixDQUFDLENBQUMsQ0FBQyxvQkFBb0IsRUFBRSxHQUFHLE9BQU8sQ0FBQzt3QkFDcEMsQ0FBQyxDQUFDLE9BQU87b0JBQ2Isb0JBQW9CLEVBQUUsSUFBSTtpQkFDN0IsQ0FBQzthQUNMO2lCQUFNO2dCQUNILDBGQUEwRjtnQkFDMUYsT0FBTztvQkFDSCxPQUFPO29CQUNQLG9CQUFvQixFQUNoQixDQUFDLG9CQUFvQjt3QkFDckIsb0JBQW9CLENBQUMsUUFBUSxHQUFHLFFBQVE7d0JBQ3BDLENBQUMsQ0FBQyxFQUFDLFFBQVEsRUFBRSxRQUFRLEVBQUM7d0JBQ3RCLENBQUMsQ0FBQyxvQkFBb0I7aUJBQ2pDLENBQUM7YUFDTDtRQUNMLENBQUMsRUFDRDtZQUNJLE9BQU8sRUFBRSxXQUFXO2dCQUNoQixDQUFDLENBQUMsQ0FBQyxFQUFDLFFBQVEsRUFBRSxJQUFJLENBQUMsTUFBTSxFQUFFLFFBQVEsRUFBRSxLQUFLLENBQUMsS0FBSyxDQUFDLFFBQVEsRUFBQyxDQUFDO2dCQUMzRCxDQUFDLENBQUMsRUFBRTtZQUNSLG9CQUFvQixFQUFFLElBQW1EO1NBQzVFLENBQ0osQ0FBQztRQUVGLHFCQUFxQjtRQUNyQixPQUFPLFNBQVMsQ0FBQyxvQkFBb0I7WUFDakMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLG9CQUFvQixFQUFFLEdBQUcsU0FBUyxDQUFDLE9BQU8sQ0FBQztZQUN4RCxDQUFDLENBQUMsU0FBUyxDQUFDLE9BQU8sQ0FBQztJQUM1QixDQUFDO0lBRUQ7Ozs7T0FJRztJQUNPLFlBQVksQ0FDbEIsV0FBK0U7UUFPL0UsTUFBTSxLQUFLLEdBQUcsV0FBVyxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUMsRUFBRTtZQUN6Qyw2REFBNkQ7WUFDN0QsTUFBTSxZQUFZLEdBQUcsUUFBUSxDQUFDLElBQUk7aUJBQzdCLFdBQVcsQ0FDUixDQUFDLEVBQUMsT0FBTyxFQUFFLGVBQWUsRUFBQyxFQUFFLEVBQUMsUUFBUSxFQUFDLEVBQUUsS0FBSyxFQUFFLEVBQUU7Z0JBQzlDLElBQUksUUFBUSxDQUFDLE9BQU8sRUFBRTtvQkFDbEIsT0FBTzt3QkFDSCxPQUFPO3dCQUNQLGVBQWUsRUFDWCxDQUFDLGVBQWU7NEJBQ2hCLGVBQWUsQ0FBQyxRQUFRLEdBQUcsUUFBUSxDQUFDLFFBQVE7NEJBQ3hDLENBQUMsQ0FBQyxFQUFDLEtBQUssRUFBRSxLQUFLLEdBQUcsQ0FBQyxFQUFFLFFBQVEsRUFBRSxRQUFRLENBQUMsUUFBUSxFQUFDOzRCQUNqRCxDQUFDLENBQUMsZUFBZTtxQkFDNUIsQ0FBQztpQkFDTDtnQkFDRCxPQUFPO29CQUNILE9BQU8sRUFBRSxlQUFlO3dCQUNwQixDQUFDLENBQUMsQ0FBQyxlQUFlLEVBQUUsR0FBRyxPQUFPLENBQUM7d0JBQy9CLENBQUMsQ0FBQyxPQUFPO29CQUNiLGVBQWUsRUFBRSxJQUFJO2lCQUN4QixDQUFDO1lBQ04sQ0FBQyxFQUNEO2dCQUNJLE9BQU8sRUFBRSxFQUF5QztnQkFDbEQsZUFBZSxFQUFFLFFBQVEsQ0FBQyxLQUFLLENBQUMsT0FBTztvQkFDbkMsQ0FBQyxDQUFDO3dCQUNJLEtBQUssRUFBRSxRQUFRLENBQUMsSUFBSSxDQUFDLE1BQU0sR0FBRyxDQUFDO3dCQUMvQixRQUFRLEVBQUUsUUFBUSxDQUFDLEtBQUssQ0FBQyxRQUFRO3FCQUNwQztvQkFDSCxDQUFDLENBQUUsSUFBaUQ7YUFDM0QsQ0FDSjtpQkFDQSxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsRUFBQyxLQUFLLEVBQUMsRUFBRSxFQUFFLENBQUMsS0FBSyxDQUFDLENBQUM7WUFFckMsaUtBQWlLO1lBQ2pLLE9BQU8sQ0FBQyxFQUFFLEVBQUUsV0FBVyxFQUFFLEtBQUssRUFBRSxLQUFLLEVBQUUsRUFBRTtnQkFDckMsTUFBTSxhQUFhLEdBQUcsV0FBVyxDQUFDLE1BQU0sQ0FDcEMsVUFBVSxDQUFDLEVBQUUsQ0FBQyxVQUFVLENBQUMsRUFBRSxJQUFJLEVBQUUsQ0FBQyxFQUFFLENBQ3ZDLENBQUM7Z0JBRUYsb0ZBQW9GO2dCQUNwRixNQUFNLFFBQVEsR0FDVixZQUFZLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQztvQkFDNUIsYUFBYSxDQUFDLE1BQU0sQ0FDaEIsQ0FBQyxJQUFJLEVBQUUsVUFBVSxFQUFFLEVBQUU7O3dCQUNqQixNQUFNLEVBQUUsU0FBRyxLQUFLLENBQUMsVUFBVSxDQUFDLElBQUksQ0FBQywwQ0FBRSxRQUFRLENBQUM7d0JBQzVDLElBQUksRUFBRSxDQUFDLE9BQU8sSUFBSSxFQUFFLENBQUMsUUFBUSxHQUFHLElBQUksQ0FBQyxRQUFROzRCQUN6QyxPQUFPLEVBQUMsVUFBVSxFQUFFLFFBQVEsRUFBRSxFQUFFLENBQUMsUUFBUSxFQUFDLENBQUM7d0JBQy9DLE9BQU8sSUFBSSxDQUFDO29CQUNoQixDQUFDLEVBQ0QsRUFBQyxVQUFVLEVBQUUsSUFBSSxFQUFFLFFBQVEsRUFBRSxRQUFRLEVBQUMsQ0FDekMsQ0FBQyxVQUFVLENBQUM7Z0JBQ2pCLElBQUksUUFBUTtvQkFBRSxPQUFPLFFBQVEsQ0FBQztnQkFFOUIsT0FBTyxhQUFhLENBQUMsQ0FBQyxDQUFDLENBQUM7WUFDNUIsQ0FBQyxDQUFDO1FBQ04sQ0FBQyxDQUFDLENBQUM7UUFFSCxPQUFPLEtBQUssQ0FBQztJQUNqQixDQUFDO0lBRUQ7Ozs7T0FJRztJQUNJLFlBQVksQ0FDZixJQUFZO1FBRVosTUFBTSxPQUFPLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxhQUFhLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDN0MsTUFBTSxJQUFJLEdBQUcsSUFBSSxDQUFDLFlBQVksQ0FBQyxPQUFPLEVBQUUsQ0FBQyxFQUFDLEtBQUssRUFBQyxFQUFFLEVBQUUsQ0FBQyxLQUFLLENBQUMsQ0FBQztRQUM1RCxNQUFNLEtBQUssR0FBRyxJQUFJLElBQUksSUFBSSxDQUFDLFlBQVksQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUU5QyxnREFBZ0Q7UUFDaEQsSUFBSSxJQUFJLElBQUksS0FBSyxFQUFFO1lBQ2YsTUFBTSxNQUFNLEdBQUcsS0FBSyxDQUFDLE1BQU0sQ0FDdkIsQ0FBQyxFQUFDLFdBQVcsRUFBRSxLQUFLLEVBQUUsU0FBUyxFQUFFLFFBQVEsRUFBQyxFQUFFLEVBQUMsVUFBVSxFQUFFLFFBQVEsRUFBQyxFQUFFLEVBQUU7O2dCQUNsRSx1RkFBdUY7Z0JBQ3ZGLElBQUksVUFBVSxDQUFDLElBQUksSUFBSSxTQUFTO29CQUM1QixPQUFPO3dCQUNILFdBQVc7d0JBQ1gsS0FBSzt3QkFDTCxTQUFTLEVBQUUsQ0FBQyxHQUFHLFNBQVMsRUFBRSxRQUFRLENBQUMsUUFBUSxDQUFDO3dCQUM1QyxRQUFRO3FCQUNYLENBQUM7Z0JBRU4sbUNBQW1DO2dCQUNuQyxNQUFNLE1BQU0sR0FBRztvQkFDWCxLQUFLO29CQUNMLFNBQVMsRUFBRSxVQUFVLENBQUMsSUFBSSxJQUFJLE1BQU0sQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDO2lCQUMxRCxDQUFDO2dCQUNGLE1BQU0sS0FBSyxHQUFHO29CQUNWLEtBQUssRUFBRSxVQUFVLENBQUMsS0FBSztvQkFDdkIsU0FBUyxRQUFFLFVBQVUsQ0FBQyxTQUFTLG1DQUFJLEVBQUU7aUJBQ3hDLENBQUM7Z0JBRUYsb0NBQW9DO2dCQUNwQyxNQUFNLE9BQU8sR0FBRyxRQUFRLENBQUMsT0FBTyxDQUFDO2dCQUNqQyxNQUFNLFFBQVEsR0FBRyxDQUFBLFFBQVEsYUFBUixRQUFRLHVCQUFSLFFBQVEsQ0FBRSxPQUFPLEtBQUksQ0FBQyxPQUFPLENBQUMsQ0FBQyxtRUFBbUU7Z0JBQ25ILE9BQU87b0JBQ0gsV0FBVyxFQUFFO3dCQUNULEdBQUcsV0FBVzt3QkFDZCxFQUFDLE1BQU0sRUFBRSxLQUFLLEVBQUUsSUFBSSxFQUFFLFVBQVUsQ0FBQyxJQUFJLEVBQUM7cUJBQ3pDO29CQUNELFNBQVMsRUFDTCxRQUFRLElBQUksUUFBUTt3QkFDaEIsQ0FBQyxDQUFDLENBQUMsR0FBRyxTQUFTLEVBQUUsUUFBUSxDQUFDLFFBQVEsQ0FBQzt3QkFDbkMsQ0FBQyxDQUFDLFNBQVM7b0JBQ25CLEtBQUssRUFBRSxVQUFVLENBQUMsSUFBSSxJQUFJLE1BQU0sQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxLQUFLLEdBQUcsQ0FBQztvQkFDcEQsUUFBUSxFQUFFLFFBQVE7aUJBQ3JCLENBQUM7WUFDTixDQUFDLEVBQ0Q7Z0JBQ0ksV0FBVyxFQUFFLEVBQXVCO2dCQUNwQyxTQUFTLEVBQUUsRUFBRTtnQkFDYixLQUFLLEVBQUUsQ0FBQztnQkFDUixRQUFRLEVBQUUsSUFBNkI7YUFDMUMsQ0FDSixDQUFDO1lBRUYsT0FBTztnQkFDSCxXQUFXLEVBQUUsTUFBTSxDQUFDLFdBQVc7Z0JBQy9CLFNBQVMsRUFBRSxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU87b0JBQ3pCLENBQUMsQ0FBQyxDQUFDLEdBQUcsTUFBTSxDQUFDLFNBQVMsRUFBRSxJQUFJLENBQUMsS0FBSyxDQUFDLFFBQVEsQ0FBQztvQkFDNUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxTQUFTO2FBQ3pCLENBQUM7U0FDTDtRQUVELE9BQU8sRUFBQyxTQUFTLEVBQUUsRUFBRSxFQUFFLFdBQVcsRUFBRSxFQUFFLEVBQUMsQ0FBQztJQUM1QyxDQUFDO0NBQ0o7QUFuUEQsc0RBbVBDIn0=