package edu.stanford.nlp.sempre.tables.match; import java.util.*; import edu.stanford.nlp.sempre.*; import edu.stanford.nlp.sempre.FuzzyMatchFn.FuzzyMatchFnMode; import edu.stanford.nlp.sempre.tables.*; import fig.basic.*; /** * Perform fuzzy matching on the table knowledge graph. * * @author ppasupat */ public class EditDistanceFuzzyMatcher extends FuzzyMatcher { public static class Options { @Option(gloss = "Verbosity") public int verbose = 0; @Option(gloss = "Also return the union of matched formulas") public boolean alsoReturnUnion = false; @Option(gloss = "Also return fb:row.consecutive...") public boolean alsoReturnConsecutive = false; @Option(gloss = "Also match parts") public boolean alsoMatchPart = false; @Option(gloss = "Also add normalization to matched binaries") public boolean alsoAddNormalization = false; @Option(gloss = "Maximum edit distance ratio") public double fuzzyMatchMaxEditDistanceRatio = 0; @Option(gloss = "Allow the query phrase to match part of the table cell content") public boolean fuzzyMatchSubstring = false; @Option(gloss = "Minimum query phrase length (number of characters) to invoke substring matching") public int fuzzyMatchSubstringMinQueryLength = 3; @Option(gloss = "If the number of cells matching the query exceeds this, don't return individual matches (but still return the union)") public int fuzzyMatchMaxTotalMatches = 5; @Option(gloss = "If the number of cells having the query as a substring exceeds this, don't perform substring matches") public int fuzzyMatchMaxSubstringMatches = 5; @Option(gloss = "Ignore cells with more than this number of characters when doing substring matching") public int fuzzyMatchSubstringMaxCellLength = 70; } public static Options opts = new Options(); public EditDistanceFuzzyMatcher(TableKnowledgeGraph graph) { super(graph); precompute(); } protected final Map<String, Set<Formula>> phraseToEntityFormulas = new HashMap<>(), phraseToUnaryFormulas = new HashMap<>(), phraseToBinaryFormulas = new HashMap<>(); protected final Map<String, Set<Formula>> substringToEntityFormulas = new HashMap<>(), substringToUnaryFormulas = new HashMap<>(), substringToBinaryFormulas = new HashMap<>(); protected final Set<Formula> allEntityFormulas = new HashSet<>(), allUnaryFormulas = new HashSet<>(), allBinaryFormulas = new HashSet<>(); protected void precompute() { // unary and binary for (TableColumn column : graph.columns) { Formula unary = getUnaryFormula(column); Formula binary = getBinaryFormula(column); Formula consecutive = (opts.alsoReturnConsecutive && column.hasConsecutive()) ? getConsecutiveBinaryFormula(column) : null; List<Formula> normalizedBinaries = opts.alsoAddNormalization ? getNormalizedBinaryFormulas(column) : null; allUnaryFormulas.add(unary); allBinaryFormulas.add(binary); if (consecutive != null) allBinaryFormulas.add(consecutive); if (normalizedBinaries != null) allBinaryFormulas.addAll(normalizedBinaries); for (String s : getAllCollapsedForms(column.originalString)) { MapUtils.addToSet(phraseToUnaryFormulas, s, unary); MapUtils.addToSet(phraseToBinaryFormulas, s, binary); if (consecutive != null) MapUtils.addToSet(phraseToBinaryFormulas, s, consecutive); if (normalizedBinaries != null) for (Formula f : normalizedBinaries) MapUtils.addToSet(phraseToBinaryFormulas, s, f); } if (opts.fuzzyMatchSubstring) { for (String s : getAllSubstringCollapsedForms(column.originalString)) { MapUtils.addToSet(substringToUnaryFormulas, s, unary); MapUtils.addToSet(substringToBinaryFormulas, s, binary); if (consecutive != null) MapUtils.addToSet(substringToBinaryFormulas, s, consecutive); if (normalizedBinaries != null) for (Formula f : normalizedBinaries) MapUtils.addToSet(substringToBinaryFormulas, s, f); } } } // entity for (TableCellProperties properties : graph.cellProperties) { Formula entity = getEntityFormula(properties); allEntityFormulas.add(entity); for (String s : getAllCollapsedForms(properties.originalString)) MapUtils.addToSet(phraseToEntityFormulas, s, entity); if (opts.fuzzyMatchSubstring) { for (String s : getAllSubstringCollapsedForms(properties.originalString)) { MapUtils.addToSet(substringToEntityFormulas, s, entity); } } } // part (treated as extra entities) if (opts.alsoMatchPart) { for (NameValue value : graph.cellParts) { Formula partEntity = getEntityFormula(value); allEntityFormulas.add(partEntity); for (String s : getAllCollapsedForms(value.description)) MapUtils.addToSet(phraseToEntityFormulas, s, partEntity); if (opts.fuzzyMatchSubstring) { for (String s : getAllSubstringCollapsedForms(value.description)) { MapUtils.addToSet(substringToEntityFormulas, s, partEntity); } } } } // debug print if (opts.verbose >= 5) { debugPrint("phrase Entity", phraseToEntityFormulas); debugPrint("phrase Unary", phraseToUnaryFormulas); debugPrint("phrase Binary", phraseToBinaryFormulas); debugPrint("substring Entity", substringToEntityFormulas); debugPrint("substring Unary", substringToUnaryFormulas); debugPrint("substring Binary", substringToBinaryFormulas); } } void debugPrint(String message, Map<String, Set<Formula>> target) { LogInfo.begin_track("%s", message); for (Map.Entry<String, Set<Formula>> entry : target.entrySet()) { LogInfo.logs("[%s] %s", entry.getKey(), entry.getValue()); } LogInfo.end_track(); } // ============================================================ // Collapse Strings + Compute Substrings // ============================================================ static Collection<String> getAllSubstringCollapsedForms(String original) { // Compute all substrings (based on spaces) Set<String> collapsedForms = new HashSet<>(); if (original.length() > opts.fuzzyMatchSubstringMaxCellLength) return collapsedForms; String[] tokens = original.trim().split("[^A-Za-z0-9]+"); StringBuilder sb = new StringBuilder(); for (int i = 0; i < tokens.length; i++) { sb.setLength(0); for (int j = i; j < tokens.length; j++) { String phrase = sb.append(" " + tokens[j]).toString(); collapsedForms.addAll(getAllCollapsedForms(phrase)); } } return collapsedForms; } static Collection<String> getAllCollapsedForms(String original) { Set<String> collapsedForms = new HashSet<>(); collapsedForms.add(StringNormalizationUtils.collapseNormalize(original)); String normalized = StringNormalizationUtils.aggressiveNormalize(original); collapsedForms.add(StringNormalizationUtils.collapseNormalize(normalized)); collapsedForms.remove(""); return collapsedForms; } static String getCanonicalCollapsedForm(String original) { return StringNormalizationUtils.collapseNormalize(original); } // ============================================================ // Edit Distance // ============================================================ static int editDistance(String a, String b) { int m = a.length() + 1, n = b.length() + 1; int[] dists = new int[m], newDists = new int[m]; for (int i = 0; i < m; i++) dists[i] = i; for (int j = 1; j < n; j++) { newDists[0] = j; for (int i = 1; i < m; i++) { newDists[i] = Math.min(Math.min( dists[i] + 1, // Insert newDists[i-1] + 1), // Delete dists[i-1] + (a.charAt(i-1) == b.charAt(j-1) ? 0 : 1)); // Replace } int[] swap = dists; dists = newDists; newDists = swap; } return dists[m-1]; } static double editDistanceRatio(String a, String b) { if (a.isEmpty() && b.isEmpty()) return 0.0; return editDistance(a, b) * 2.0 / (a.length() + b.length()); } // ============================================================ // Caching fuzzy matches of a whole sentence // ============================================================ Map<Pair<String, FuzzyMatchFnMode>, FuzzyMatchCache> cacheMap = new HashMap<>(); protected FuzzyMatchCache cacheSentence(List<String> sentence, FuzzyMatchFnMode mode) { String joined = String.join(" ", sentence); FuzzyMatchCache cache = cacheMap.get(new Pair<>(joined, mode)); if (cache != null) return cache; // Compute a new FuzzyMatchCache cache = new FuzzyMatchCache(); // aggregateCache[i,j] = all formulas matched by sentence[i'<=i:j'>=j], (i',j') != (i,j) FuzzyMatchCache aggregateCache = new FuzzyMatchCache(); for (int s = sentence.size(); s >= 1; s--) { for (int i = 0; i + s <= sentence.size(); i++) { int j = i + s; String term = String.join(" ", sentence.subList(i, j)); Collection<Formula> formulas = new HashSet<>(); if (!(FuzzyMatcher.opts.ignorePunctuationBoundedQueries && !checkPunctuationBoundaries(term))) { String normalized = getCanonicalCollapsedForm(term); // Exact matches Collection<Formula> exactMatched = getFuzzyExactMatchedFormulas(normalized, mode); if (exactMatched != null) formulas.addAll(exactMatched); // Substring matches if (opts.fuzzyMatchSubstring && normalized.length() >= opts.fuzzyMatchSubstringMinQueryLength) { Collection<Formula> substringMatched = getFuzzySubstringMatchedFormulas(normalized, mode); if (substringMatched != null && substringMatched.size() <= opts.fuzzyMatchMaxSubstringMatches) formulas.addAll(substringMatched); } } cache.put(i, j, formulas); if (s > 1) { aggregateCache.addAll(i + 1, j, formulas); aggregateCache.addAll(i + 1, j, aggregateCache.get(i, j)); aggregateCache.addAll(i, j - 1, formulas); aggregateCache.addAll(i, j - 1, aggregateCache.get(i, j)); } } } if (opts.verbose >= 3) { LogInfo.begin_track("Caching[%s] %s", mode, sentence); for (int s = 1; s <= sentence.size(); s++) { for (int i = 0; i + s <= sentence.size(); i++) { int j = i + s; Collection<Formula> formulas = cache.get(i, j); if (formulas == null || formulas.isEmpty()) continue; LogInfo.logs("{%s:%s} %s", i, j, sentence.subList(i, j)); LogInfo.logs("%s", formulas); } } LogInfo.end_track(); } // Filter: If sentence[i:j] and sentence[i'<=i:j'>=j], (i',j') != (i,j), // both matches formula f, remove f from sentence[i:j] // This is done to reduce over-generation for (int s = sentence.size(); s >= 1; s--) { for (int i = 0; i + s <= sentence.size(); i++) { int j = i + s; cache.removeAll(i, j, aggregateCache.get(i, j)); Collection<Formula> allMatched = cache.get(i, j); if (allMatched == null) continue; Collection<Formula> unions = opts.alsoReturnUnion ? getUnions(allMatched) : null; if (allMatched.size() > opts.fuzzyMatchMaxTotalMatches) cache.clear(i, j); if (unions != null) cache.addAll(i, j, unions); } } if (opts.verbose >= 3) { LogInfo.begin_track("Caching[%s] %s", mode, sentence); for (int s = 1; s <= sentence.size(); s++) { for (int i = 0; i + s <= sentence.size(); i++) { int j = i + s; Collection<Formula> formulas = cache.get(i, j); if (formulas == null || formulas.isEmpty()) continue; LogInfo.logs("[%s:%s] %s", i, j, sentence.subList(i, j)); LogInfo.logs("%s", formulas); } } LogInfo.end_track(); } cacheMap.put(new Pair<>(joined, mode), cache); return cache; } // ============================================================ // Main fuzzy matching interface // ============================================================ /** * Helper: Get the union of all fb:cell... and the union of all fb:part... */ protected Collection<Formula> getUnions(Collection<Formula> formulas) { Collection<Formula> unions = new ArrayList<>(); Formula formula; // fb:cell... if ((formula = getUnion(formulas, TableTypeSystem.CELL_NAME_PREFIX)) != null) unions.add(formula); // fb:part... if ((formula = getUnion(formulas, TableTypeSystem.PART_NAME_PREFIX)) != null) unions.add(formula); return unions; } protected Formula getUnion(Collection<Formula> formulas, String prefix) { if (formulas == null || formulas.size() <= 1) return null; List<Formula> sortedFormulaList = new ArrayList<>(); for (Formula formula : formulas) if (prefix == null || formula.toString().startsWith(prefix)) sortedFormulaList.add(formula); if (sortedFormulaList.size() <= 1) return null; Collections.sort(sortedFormulaList, (Formula v1, Formula v2) -> v1.toString().compareTo(v2.toString())); Formula union = sortedFormulaList.get(0); for (int i = 1; i < sortedFormulaList.size(); i++) { if (union.toString().compareTo(sortedFormulaList.get(i).toString()) <= 0) union = new MergeFormula(MergeFormula.Mode.or, union, sortedFormulaList.get(i)); else union = new MergeFormula(MergeFormula.Mode.or, sortedFormulaList.get(i), union); } return union; } @Override protected Collection<Formula> getFuzzyMatchedFormulasInternal(String term, FuzzyMatchFnMode mode) { String normalized = getCanonicalCollapsedForm(term); Collection<Formula> allMatched = new HashSet<>(); // Exact matches Collection<Formula> exactMatched = getFuzzyExactMatchedFormulas(normalized, mode); if (exactMatched != null) allMatched.addAll(exactMatched); // Substring matches if (opts.fuzzyMatchSubstring && normalized.length() >= opts.fuzzyMatchSubstringMinQueryLength) { Collection<Formula> substringMatched = getFuzzySubstringMatchedFormulas(normalized, mode); if (substringMatched != null && substringMatched.size() <= opts.fuzzyMatchMaxSubstringMatches) { allMatched.addAll(substringMatched); } } Formula union = opts.alsoReturnUnion ? getUnion(allMatched, null) : null; if (allMatched.size() > opts.fuzzyMatchMaxTotalMatches) allMatched.clear(); if (union != null) allMatched.add(union); return allMatched; } protected Collection<Formula> getFuzzyExactMatchedFormulas(String normalized, FuzzyMatchFnMode mode) { Map<String, Set<Formula>> target; switch (mode) { case ENTITY: target = phraseToEntityFormulas; break; case UNARY: target = phraseToUnaryFormulas; break; case BINARY: target = phraseToBinaryFormulas; break; default: throw new RuntimeException("Unknown FuzzyMatchMode " + mode); } Set<Formula> filtered = filterFuzzyMatched(normalized, target); // Debug print if (opts.verbose >= 3 && filtered != null && !filtered.isEmpty()) { LogInfo.begin_track("(EXACT) Normalized: %s (%d)", normalized, filtered.size()); for (Formula formula : filtered) LogInfo.logs("%s", formula); LogInfo.end_track(); } return filtered; } protected Collection<Formula> getFuzzySubstringMatchedFormulas(String normalized, FuzzyMatchFnMode mode) { Map<String, Set<Formula>> target; switch (mode) { case ENTITY: target = substringToEntityFormulas; break; case UNARY: target = substringToUnaryFormulas; break; case BINARY: target = substringToBinaryFormulas; break; default: throw new RuntimeException("Unknown FuzzyMatchMode " + mode); } Set<Formula> filtered = filterFuzzyMatched(normalized, target); // Debug print if (opts.verbose >= 3 && filtered != null && !filtered.isEmpty()) { LogInfo.begin_track("(SUBSTRING) Normalized: %s (%d)", normalized, filtered.size()); for (Formula formula : filtered) LogInfo.logs("%s", formula); LogInfo.end_track(); } return filtered; } protected Set<Formula> filterFuzzyMatched(String normalized, Map<String, Set<Formula>> phraseToFormulas) { Set<Formula> filtered; if (opts.fuzzyMatchMaxEditDistanceRatio == 0) { filtered = phraseToFormulas.get(normalized); } else { filtered = new HashSet<>(); for (Map.Entry<String, Set<Formula>> entry : phraseToFormulas.entrySet()) { if (editDistanceRatio(entry.getKey(), normalized) < opts.fuzzyMatchMaxEditDistanceRatio) { filtered.addAll(entry.getValue()); } } } return filtered; } @Override protected Collection<Formula> getAllFormulasInternal(FuzzyMatchFnMode mode) { switch (mode) { case ENTITY: return allEntityFormulas; case UNARY: return allUnaryFormulas; case BINARY: return allBinaryFormulas; default: throw new RuntimeException("Unknown FuzzyMatchMode " + mode); } } // ============================================================ // Test // ============================================================ public static void main(String[] args) { System.out.println(editDistance("unionist", "unionists")); } }