我有一个独立的CSV数据加载过程,我用Java编码,必须使用一些模糊字符串匹配。这绝对不是理想的,但我没有太多选择。我使用名字和姓氏进行匹配,并在运行开始时缓存所有可能性。找到匹配后,我需要该人在运行期间对多个位置。我使用guava的Objects.hashCode()
来创建名字和姓氏的散列。
缓存机制如下所示:
Map<Integer,PersonDO> personCache = Maps.newHashMap();
for(PersonDO p: dao.getPeople()) {
personCache.put(Objects.hashCode(p.getFirstName(),p.getLastName()), p);
}
大多数时候我会在firstname + lastname上点击,但是当它错过时我会使用Apache的StringUtils.getLevenshteinDistance()
来尝试匹配它。这就是匹配逻辑流程的方式:
person = personCache.get(Objects.hashCode(firstNameFromCSV,lastNameFromCSV));
if(person == null) {//fallback to fuzzy matching
person = findClosetMatch(firstNameFromCSV+lastNameFromCSV);
}
这是findClosetMatch()
方法:
private PersonDO findClosetMatch(String name) {
int min = 15;//initial value
int testVal=0;
PersonDO matchedPerson = null;
for(PersonDO person: personCache.values()) {
testVal = StringUtils.getLevenshteinDistance(name,person.getFirstName()+person.getLastName());
if( testVal < min ) {
min = testVal;
matchedPerson = person;
}
}
if(matchedPerson == null) {
throw new Exception("Unable to find person: " + name)
}
return matchedPerson;
}
这适用于简单的拼写错误,拼写错误和缩写名称(即Mike-&gt; Michael),但是当我完全错过缓存中的一个传入名称时,我最终会返回误报。为了防止这种情况发生,我将findClosetMatch()
中的最小值设置为15(即不超过15个字符);它大部分时间都有效,但我仍然发生了一些不匹配:Mike Thompson
点击Mike Thomas
等。
除了找出将主键放入正在加载的文件的方法之外,有没有人看到改进此过程的方法?任何其他可以帮助的匹配算法?
答案 0 :(得分:40)
当我看到这个问题时,我注意到一些关键事实可以作为一些改进:
我已将您的问题解释为需要以下两项内容:
PersonDO
个对象。听起来你想这样做是因为你需要一个预先存在的PersonDO
,其中每个唯一名称存在一个,并且你的循环/工作流程中可能会出现同一个名字PersonDO
(换句话说,一个人& #39;唯一的标识符是他们的名字,这在现实生活中显然不是这样,但在这里似乎对你有用。)接下来,让我们看一下代码的一些改进:
<强> 1。清理:不必要的哈希码操作。
您不需要自己生成哈希码。这有点混淆了这个问题。
您只是为firstname + lastname的组合生成哈希码。如果你将连接字符串作为键给它,那么这正是HashMap
的作用。所以,就这样做(并添加一个空格,以防万一我们想要稍后从密钥中解析出第一个/最后一个)。
Map<String, PersonDO> personCache = Maps.newHashMap();
public String getPersonKey(String first, String last) {
return first + " " + last;
}
...
// Initialization code
for(PersonDO p: dao.getPeople()) {
personCache.put(getPersonKey(p.getFirstName(), p.getLastName()), p);
}
<强> 2。清理:构建检索功能以执行查找。
由于我们已经更改了地图中的键,因此我们需要更改查找功能。我们将其构建为迷你API。如果我们总是完全知道密钥(即唯一ID),我们当然只会使用Map.get
。所以我们从那开始,但是因为我们知道我们需要添加模糊匹配,所以我们会在可能发生这种情况的情况下添加一个包装器:
public PersonDO findPersonDO(String searchFirst, String searchLast) {
return personCache.get(getPersonKey(searchFirst, searchLast));
}
第3。使用评分自己构建模糊匹配算法。
请注意,由于您使用的是Guava,我在这里使用了一些便利(Ordering
,ImmutableList
,Doubles
等。)
首先,我们希望保留我们所做的工作,以确定匹配的接近程度。使用POJO执行此操作:
class Match {
private PersonDO candidate;
private double score; // 0 - definitely not, 1.0 - perfect match
// Add candidate/score constructor here
// Add getters for candidate/score here
public static final Ordering<Match> SCORE_ORDER =
new Ordering<Match>() {
@Override
public int compare(Match left, Match right) {
return Doubles.compare(left.score, right.score);
}
};
}
接下来,我们创建一个评估通用名称的方法。我们应该分别对名和姓进行评分,因为它可以降低噪音。例如,我们不关心名字是否与姓氏的任何部分匹配 - ,除非您的名字可能意外地在姓氏字段中,反之亦然,您应该有意识地而不是偶然地考虑(我们&# 39;稍后会解决这个问题)。
请注意,我们不再需要&#34; max levenshtein距离&#34;。这是因为我们将它们标准化为长度,我们将在稍后选择最接近的匹配。 15个字符添加/编辑/删除似乎非常高,并且因为我们通过评分最小化了空白的名字/姓氏问题如果你想要的话,我们现在可以选择最多3-4个(将其他任何东西评分为0)。
// Typos on first letter are much more rare. Max score 0.3
public static final double MAX_SCORE_FOR_NO_FIRST_LETTER_MATCH = 0.3;
public double scoreName(String searchName, String candidateName) {
if (searchName.equals(candidateName)) return 1.0
int editDistance = StringUtils.getLevenshteinDistance(
searchName, candidateName);
// Normalize for length:
double score =
(candidateName.length() - editDistance) / candidateName.length();
// Artificially reduce the score if the first letters don't match
if (searchName.charAt(0) != candidateName.charAt(0)) {
score = Math.min(score, MAX_SCORE_FOR_NO_FIRST_LETTER_MATCH);
}
// Try Soundex or other matching here. Remember that you don't want
// to go above 1.0, so you may want to create a second score and
// return the higher.
return Math.max(0.0, Math.min(score, 1.0));
}
如上所述,您可以插入第三方或其他字匹配算法,并从所有这些算法的共享知识中获益。
现在,我们浏览整个列表并对每个名称进行评分。请注意,我已经为&#34; tweaks&#34;添加了一个位置。调整可能包括:
checkForReversal
,该方法将反向对该名称进行评分,并在该值高得多时获取该分数。 如果完全匹配,则会给出1.0分。正如您所看到的,将其构建为一系列API,为我们提供了合理的位置,可以轻松地将其调整为我们心中的内容。
用alogrithm:
public static final double MIN_SCORE = 0.3;
public List<Match> findMatches(String searchFirst, String searchLast) {
List<Match> results = new ArrayList<Match>();
// Keep in mind that this doesn't scale well.
// With only 1000 names that's not even a concern a little bit, but
// thinking ahead, here are two ideas if you need to:
// - Keep a map of firstnames. Each entry should be a map of last names.
// Then, only iterate through last names if the firstname score is high
// enough.
// - Score each unique first or last name only once and cache the score.
for(PersonDO person: personCache.values()) {
// Some of my own ideas follow, you can tweak based on your
// knowledge of the data)
// No reason to deal with the combined name, that just makes things
// more fuzzy (like your problem of too-high scores when one name
// is completely missing).
// So, score each name individually.
double scoreFirst = scoreName(searchFirst, person.getFirstName());
double scoreLast = scoreName(searchLast, person.getLastName());
double score = (scoreFirst + scoreLast)/2.0;
// Add tweaks or alternate scores here. If you do alternates, in most
// cases you'll probably want to take the highest, but you may want to
// average them if it makes more sense.
if (score > MIN_SCORE) {
results.add(new Match(person, score));
}
}
return ImmutableList.copyOf(results);
}
现在我们修改你的findClosestMatch以获得所有匹配中最高的一个(如果列表中没有,则抛出NoSuchElementException
。)
可能的调整:
代码:
public Match findClosestMatch(String searchFirst, String searchLast) {
List<Match> matches = findMatch(searchFirst, searchLast);
// Tweak here
return Match.SCORE_ORDER.max(list);
}
..然后修改我们原来的getter:
public PersonDO findPersonDO(String searchFirst, String searchLast) {
PersonDO person = personCache.get(getPersonKey(searchFirst, searchLast));
if (person == null) {
Match match = findClosestMatch(searchFirst, searchLast);
// Do something here, based on score.
person = match.getCandidate();
}
return person;
}
<强> 4。报告&#34;模糊&#34;不同。强>
最后,您会注意到findClosestMatch
并不仅仅返回一个人,它会返回Match
- 这样我们就可以修改程序来区别对待模糊匹配完全匹配。
您可能想要做的一些事情:
正如您所看到的那样,自己不需要太多代码。令人怀疑的是,有一个图书馆可以预测名称,也可以自己了解数据。
正如我在上面的示例中所做的那样,将允许您轻松地进行迭代和调整,甚至插入第三方库以改善您的得分而不是依赖于它们完全 - 缺点和所有。
答案 1 :(得分:2)
使用db来执行搜索?在您的选择中使用正则表达式,或使用LIKE
运算符
分析您的数据库并尝试构建或使用Huffman-tree或多个表来执行键值搜索。
答案 2 :(得分:2)
没有最好的解决方案,无论如何你必须处理某种启发式方法。但是你可以寻找另一个Levenshtein距离实现(或者自己实现)。此实现必须为不同字符的不同字符操作(插入,删除)提供不同的分数。例如,您可以为键盘上的字符对提供较低的分数。此外,您可以根据字符串长度动态计算最大距离阈值。
我有一个性能提示给你。每次计算Levenshtein距离时,都会执行n * m次运算,其中 n 和 m 是字符串的长度。您构建一次Levenshtein automaton,然后对每个字符串进行非常快速的评估。请注意,因为评估NFA非常昂贵,所以首先需要将其转换为DFA。
你应该看看Lucene。我希望它包含您需要的所有模糊搜索功能。如果支持,你甚至可以使用你的DBMS全文搜索。例如,PostgreSQL支持全文。
答案 3 :(得分:2)
这就是我对类似用例所做的事情:
distance("a b", "a c") is 33% max(distance("a", "a"), distance("b", "c")) is 100%
min
距离标准,即0
适用于短于2个符号的字符串,1
适用于短于3个符号的字符串。int length = Math.min(s1.length(), s2.length);
int min;
if(length <= 2) min = 0; else
if(length <= 4) min = 1; else
if(length <= 6) min = 2; else
...
这两个应该适用于您的输入。