package edu.stanford.nlp.sempre.freebase;
import edu.stanford.nlp.io.IOUtils;
import edu.stanford.nlp.sempre.*;
import edu.stanford.nlp.sempre.cache.StringCache;
import edu.stanford.nlp.sempre.cache.StringCacheUtils;
import edu.stanford.nlp.sempre.freebase.index.FbEntitySearcher;
import edu.stanford.nlp.sempre.freebase.index.FbIndexField;
import edu.stanford.nlp.sempre.freebase.lexicons.EntrySource;
import edu.stanford.nlp.sempre.freebase.lexicons.LexicalEntry;
import edu.stanford.nlp.sempre.freebase.lexicons.LexicalEntry.EntityLexicalEntry;
import edu.stanford.nlp.sempre.freebase.lexicons.TokenLevelMatchFeatures;
import edu.stanford.nlp.sempre.freebase.utils.FileUtils;
import edu.stanford.nlp.stats.Counter;
import edu.stanford.nlp.util.ArrayUtils;
import edu.stanford.nlp.util.StringUtils;
import fig.basic.MapUtils;
import fig.basic.Option;
import org.apache.lucene.document.Document;
import org.apache.lucene.queryparser.classic.ParseException;
import java.io.IOException;
import java.util.*;
public final class EntityLexicon {
public enum SearchStrategy { exact, inexact, fbsearch }
public static class Options {
@Option(gloss = "Verbosity")
public int verbose = 0;
@Option(gloss = "Number of results return by the lexicon")
public int maxEntries = 1000;
@Option(gloss = "Number of documents queried from Lucene")
public int numOfDocs = 10000;
@Option(gloss = "Path to the exact match lucene index directory")
public String exactMatchIndex;
@Option(gloss = "Path to the inexact match lucene index directory")
public String inexactMatchIndex = "lib/lucene/4.4/inexact";
@Option(gloss = "Cache path to the mid-to-id path")
public String mid2idPath;
@Option(gloss = "Path to entity popularity file")
public String entityPopularityPath;
}
public static Options opts = new Options();
private static EntityLexicon entityLexicon;
public static EntityLexicon getInstance() {
if (entityLexicon == null) entityLexicon = new EntityLexicon();
return entityLexicon;
}
FbEntitySearcher exactSearcher; // Lucene
FbEntitySearcher inexactSearcher; // Lucene
FreebaseSearch freebaseSearch; // Google's API
StringCache mid2idCache; // Google's API spits back mids, which we need to convert to ids
Map<String, Double> entityPopularityMap;
private EntityLexicon() {
loadEntityPopularity();
}
public List<EntityLexicalEntry> lookupEntries(String query, SearchStrategy strategy) throws ParseException, IOException {
if (strategy == null)
throw new RuntimeException("No entity search strategy specified");
switch (strategy) {
case exact:
if (exactSearcher == null) exactSearcher = new FbEntitySearcher(opts.exactMatchIndex, opts.numOfDocs, "exact");
return lookupEntries(exactSearcher, query);
case inexact:
if (inexactSearcher == null) inexactSearcher = new FbEntitySearcher(opts.inexactMatchIndex, opts.numOfDocs, "inexact");
return lookupEntries(inexactSearcher, query);
case fbsearch:
if (freebaseSearch == null) freebaseSearch = new FreebaseSearch();
if (mid2idCache == null) mid2idCache = StringCacheUtils.create(opts.mid2idPath);
return lookupFreebaseSearchEntities(query);
default:
throw new RuntimeException("Unknown entity search strategy: " + strategy);
}
}
private void loadEntityPopularity() {
entityPopularityMap = new HashMap<>();
if (opts.entityPopularityPath == null) return;
for (String line : IOUtils.readLines(opts.entityPopularityPath)) {
String[] tokens = line.split("\t");
entityPopularityMap.put(tokens[0], Double.parseDouble(tokens[1]));
}
}
public List<EntityLexicalEntry> lookupFreebaseSearchEntities(String query) {
FreebaseSearch.ServerResponse response = freebaseSearch.lookup(query);
List<EntityLexicalEntry> entities = new ArrayList<>();
if (response.error != null) {
throw new RuntimeException(response.error.toString());
}
// num of words in query
int numOfQueryWords = query.split("\\s+").length;
for (FreebaseSearch.Entry e : response.entries) {
if (entities.size() >= opts.maxEntries) break;
// Note: e.id might not be the same one we're using (e.g., fb:en.john_f_kennedy_airport versus fb:en.john_f_kennedy_international_airport),
// so get the one from our canonical mid2idCache
String id = mid2idCache.get(e.mid);
if (id == null) continue; // Skip if no ID (probably not worth referencing)
// skip if it is a long phrase that is not an exact match
if (numOfQueryWords >= 4 && !query.toLowerCase().equals(e.name.toLowerCase())) {
continue;
}
int distance = editDistance(query.toLowerCase(), e.name.toLowerCase()); // Is this actually useful?
Counter<String> entityFeatures = TokenLevelMatchFeatures.extractFeatures(query, e.name);
double popularity = MapUtils.get(entityPopularityMap, id, 0d);
entityFeatures.incrementCount("text_popularity", Math.log(popularity + 1));
entities.add(new EntityLexicalEntry(query, query, Collections.singleton(e.name),
new ValueFormula<>(new NameValue(id, e.name)), EntrySource.FBSEARCH, e.score, distance,
new FreebaseTypeLookup().getEntityTypes(id), entityFeatures));
}
return entities;
}
public List<EntityLexicalEntry> lookupEntries(FbEntitySearcher searcher, String textDesc) throws ParseException, IOException {
List<EntityLexicalEntry> res = new ArrayList<>();
textDesc = textDesc.replaceAll("\\?", "\\\\?").toLowerCase();
List<Document> docs = searcher.searchDocs(textDesc);
for (Document doc : docs) {
Formula formula = Formula.fromString(doc.get(FbIndexField.ID.fieldName()));
String[] fbDescriptions = new String[]{doc.get(FbIndexField.TEXT.fieldName())};
String typesDesc = doc.get(FbIndexField.TYPES.fieldName());
Set<String> types = new HashSet<>();
if (typesDesc != null) {
String[] tokens = typesDesc.split(",");
Collections.addAll(types, tokens);
}
double popularity = Double.parseDouble(doc.get(FbIndexField.POPULARITY.fieldName()));
int distance = editDistance(textDesc.toLowerCase(), fbDescriptions[0].toLowerCase());
Counter<String> tokenEditDistanceFeatures = TokenLevelMatchFeatures.extractFeatures(textDesc, fbDescriptions[0]);
if ((popularity > 0 || distance == 0) && TokenLevelMatchFeatures.diffSetSize(textDesc, fbDescriptions[0]) < 4) {
res.add(new EntityLexicalEntry(textDesc, textDesc, ArrayUtils.asSet(fbDescriptions), formula, EntrySource.LUCENE, popularity, distance, types, tokenEditDistanceFeatures));
}
}
Collections.sort(res, new LexicalEntryComparator());
return res.subList(0, Math.min(res.size(), opts.maxEntries));
}
private int editDistance(String query, String name) {
String[] queryTokens = FileUtils.omitPunct(query).split("\\s+");
String[] nameTokens = FileUtils.omitPunct(name).split("\\s+");
StringBuilder querySb = new StringBuilder();
for (String queryToken : queryTokens)
querySb.append(queryToken).append(" ");
StringBuilder nameSb = new StringBuilder();
for (String nameToken : nameTokens)
nameSb.append(nameToken).append(" ");
return StringUtils.editDistance(querySb.toString().trim(), nameSb.toString().trim());
}
public static class LexicalEntryComparator implements Comparator<LexicalEntry> {
@Override
public int compare(LexicalEntry arg0, LexicalEntry arg1) {
if (arg0.popularity > arg1.popularity)
return -1;
if (arg0.popularity < arg1.popularity)
return 1;
if (arg0.distance < arg1.distance)
return -1;
if (arg0.distance > arg1.distance)
return 1;
return 0;
}
}
}