UNPKG

fuzzy-rater

Version:

Tooling for fuzzily rating text according to some query

230 lines 19 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"); const merge_1 = require("../../utils/merge"); /** * 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 matcher * @param word The word to look for * @param maxDistance The maximum error */ constructor(word, maxDistance) { this.word = word; this.maxDistance = 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, searchIndex: 0, 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 * @param includeNonMatch Whether to fallback to the best non-match if no match was found * @returns The best match */ getBestMatch(matches, getNode, includeNoneMatch = false) { 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); if (best) return best.item; if (!includeNoneMatch) return; const bestNonMatch = matches.reduce((best, m) => { const node = getNode(m); return node.searchIndex == 0 && (best == null || node.distance < best.distance) ? { item: m, distance: node.distance } : best; }, undefined); return bestNonMatch === null || bestNonMatch === void 0 ? void 0 : bestNonMatch.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 ? merge_1.add(matches, bestConsecutiveMatch) // Would preferably add to the start, but that has a higher time complexity : 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: [], bestConsecutiveMatch: lastMatched ? { endIndex: text.length, distance: trace.final.distance } : null, }); // Return the matches const res = matchData.bestConsecutiveMatch ? merge_1.add(matchData.matches, matchData.bestConsecutiveMatch) : matchData.matches; return res.reverse(); // Reverse al matches at the end } /** * 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(({ indices, bestConsecutive }, { fromNode: { matched, distance } }, index) => { if (matched) { return { indices, bestConsecutive: !bestConsecutive || bestConsecutive.distance > distance ? { index: index - 1, distance } : bestConsecutive, }; } return { indices: bestConsecutive ? merge_1.add(indices, bestConsecutive.index) : indices, bestConsecutive: null, }; }, { indices: [], bestConsecutive: dfaTrace.final.matched ? { index: dfaTrace.path.length - 1, distance: dfaTrace.final.distance, } : null, }) .indices.reverse(); let nextMatchIndex = matchIndices.pop(); // Actually the previous match, but were iterating backwards // 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 if (nextMatchIndex == index) { nextMatchIndex = matchIndices.pop(); const matching = 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, true); 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: merge_1.add(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: merge_1.add(alterations, { target, query, type: transition.type, }), distances: newMatch && prevNode ? merge_1.add(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 ? merge_1.add(result.distances, best.final.distance) : result.distances, }; } return { distances: [], alterations: [] }; } } exports.FuzzyMultiWordMatcher = FuzzyMultiWordMatcher; //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiRnV6enlNdWx0aVdvcmRNYXRjaGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2Z1enp5UmF0ZS93b3JkTWF0Y2hlci9GdXp6eU11bHRpV29yZE1hdGNoZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEscUVBQWdFO0FBSWhFLG9EQUErQztBQUUvQyw2Q0FBc0M7QUFFdEM7OztHQUdHO0FBQ0gsTUFBYSxxQkFBcUI7SUFTOUI7Ozs7T0FJRztJQUNILFlBQW1CLElBQVksRUFBRSxXQUFtQjtRQUNoRCxJQUFJLENBQUMsSUFBSSxHQUFHLElBQUksQ0FBQztRQUNqQixJQUFJLENBQUMsV0FBVyxHQUFHLFdBQVcsQ0FBQztRQUMvQixJQUFJLENBQUMsVUFBVSxDQUFDLElBQUksRUFBRSxXQUFXLENBQUMsQ0FBQztJQUN2QyxDQUFDO0lBRUQ7Ozs7T0FJRztJQUNPLFVBQVUsQ0FBQyxJQUFZLEVBQUUsV0FBbUI7UUFDbEQsTUFBTSxXQUFXLEdBQUcsK0NBQXNCLENBQUMsSUFBSSxFQUFFLFdBQVcsRUFBRSxJQUFJLENBQUMsQ0FBQztRQUNwRSxJQUFJLENBQUMsR0FBRyxHQUFHLElBQUksZUFBTSxDQUFDLFdBQVcsRUFBRTtZQUMvQixnREFBZ0Q7WUFDaEQsUUFBUSxFQUFFLEtBQUssQ0FBQyxFQUFFOztnQkFDZCxPQUFBLE9BQUEsSUFBSSxDQUFDLFlBQVksQ0FBQyxLQUFLLEVBQUUsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLDBDQUFFLFFBQVEsS0FBSTtvQkFDbkQsT0FBTyxFQUFFLEtBQUs7b0JBQ2QsV0FBVyxFQUFFLENBQUM7b0JBQ2QsUUFBUSxFQUFFLENBQUM7aUJBQ2QsQ0FBQTthQUFBO1lBQ0wscUNBQXFDO1lBQ3JDLGNBQWMsRUFBRSxVQUFVLENBQUMsRUFBRSxDQUFDLENBQUMsRUFBRSxDQUFDO1NBQ3JDLENBQUMsQ0FBQztJQUNQLENBQUM7SUFFRDs7Ozs7O09BTUc7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