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