UNPKG

fuzzy-rater

Version:

Tooling for fuzzily rating text according to some query

209 lines 17 kB
"use strict"; 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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiRnV6enlNdWx0aVdvcmRNYXRjaGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2Z1enp5UmF0ZS9GdXp6eU11bHRpV29yZE1hdGNoZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEscUVBQWdFO0FBSWhFLGlEQUE0QztBQUc1Qzs7O0dBR0c7QUFDSCxNQUFhLHFCQUFxQjtJQUc5Qjs7OztPQUlHO0lBQ0gsWUFBbUIsSUFBWSxFQUFFLFdBQW1CO1FBQ2hELElBQUksQ0FBQyxVQUFVLENBQUMsSUFBSSxFQUFFLFdBQVcsQ0FBQyxDQUFDO0lBQ3ZDLENBQUM7SUFFRDs7OztPQUlHO0lBQ08sVUFBVSxDQUFDLElBQVksRUFBRSxXQUFtQjtRQUNsRCxNQUFNLFdBQVcsR0FBRywrQ0FBc0IsQ0FBQyxJQUFJLEVBQUUsV0FBVyxFQUFFLElBQUksQ0FBQyxDQUFDO1FBQ3BFLElBQUksQ0FBQyxHQUFHLEdBQUcsSUFBSSxlQUFNLENBQUMsV0FBVyxFQUFFO1lBQy9CLGdEQUFnRDtZQUNoRCxRQUFRLEVBQUUsS0FBSyxDQUFDLEVBQUU7O2dCQUNkLE9BQUEsT0FBQSxJQUFJLENBQUMsWUFBWSxDQUFDLEtBQUssRUFBRSxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsMENBQUUsUUFBUSxLQUFJO29CQUNuRCxPQUFPLEVBQUUsS0FBSztvQkFDZCxRQUFRLEVBQUUsQ0FBQztpQkFDZCxDQUFBO2FBQUE7WUFDTCxxQ0FBcUM7WUFDckMsY0FBYyxFQUFFLFVBQVUsQ0FBQyxFQUFFLENBQUMsQ0FBQyxFQUFFLENBQUM7U0FDckMsQ0FBQyxDQUFDO0lBQ1AsQ0FBQztJQUVEOzs7OztPQUtHO0lBQ08sWUFBWSxDQUNsQixPQUFZLEVBQ1osT0FBb0M7UUFFcEMsTUFBTSxJQUFJLEdBQUcsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQUFDLElBQUksRUFBRSxDQUFDLEVBQUUsRUFBRTtZQUNwQyxNQUFNLElBQUksR0FB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