JavaでコーディングしたスタンドアロンのCSVデータの読み込みプロセスがあります。これは、あいまいな文字列マッチングを使用する必要があります。これは明らかに理想的ではありませんが、あまり選択肢がありません。姓と名を使用して一致しています。実行の開始時にすべての可能性をキャッシュします。一致を見つけた後、実行中にそのpersonオブジェクトの複数の場所が必要です。グアバの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-> Michael)で正常に機能しますが、キャッシュ内の入力名の1つが完全に見つからないと、最終的に誤検出の一致が返されます。これを防ぐには、findClosetMatch()
のmin値を15に設定します(つまり、15文字以下にします)。それはほとんどの時間で機能しますが、いくつかの不一致が発生しました:_Mike Thompson
_で_Mike Thomas
_ヒットなど.
ロードされているファイルに主キーを取得する方法を理解する以外に、このプロセスを改善する方法を誰かが見ていますか?ここで役立つ他のマッチングアルゴリズムはありますか?
この問題を見ると、いくつかの改善の基礎となるいくつかの重要な事実に気づきました。
私はあなたの問題を次の2つが必要であると解釈しました:
PersonDO
オブジェクトがあります。 一意の名前ごとに1つ存在する既存のPersonDO
が必要であり、これも同じであるため、これを実行したいようですループ/ワークフローで名前が複数回現れることがあります。PersonDO
(言い換えれば、人の一意の識別子は名前です。これは明らかに現実の世界ではそうではありませんが、ここではうまくいくようです。次に、コードを改善する方法を見てみましょう。
1。クリーンアップ:不要なハッシュコード操作
ハッシュコードを自分で生成する必要はありません。これは少し問題を混乱させます。
あなたは単に名と姓の組み合わせのハッシュコードを生成しています。これは、連結文字列をキーとして指定した場合の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);
}
};
}
次に、総称名をスコアリングするためのメソッドを作成します。姓と名は別々にスコアリングする必要があります。ノイズが減るからです。たとえば、名が姓の一部と一致しているかどうかは関係ありません—姓が誤って姓フィールドにある場合、またはその逆の場合を除き、意図的に説明する必要はありません。誤って(後で対処します)。
「最大レーベンシュタイン距離」が不要になったことに注意してください。これは、それらを長さに正規化し、後で最も近い一致を選択するためです。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));
}
上記のように、サードパーティまたは他のワードマッチングアルゴリズムをプラグインして、それらすべての共有知識から得ることができます。
次に、リスト全体を調べて、すべての名前にスコアを付けます。 「微調整」のスポットを追加したことに注意してください。微調整には次のものがあります。
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);
}
..次に、元のゲッターを変更します。
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。 「あいまいさ」を別の方法で報告します。
最後に、findClosestMatch
は人を返すだけでなく、Match
を返すことがわかります。これは、プログラムを変更して、あいまい一致を完全一致とは異なる方法で処理できるようにするためです。
あなたがおそらくこれでしたいいくつかのこと:
ご覧のとおり、これを自分で行うにはコードが多すぎません。名前を予測し、データを自分で知ることができるライブラリが存在することは疑わしいです。
上記の例で行ったようにこれを分割してビルドすると、簡単に反復して微調整でき、サードパーティのライブラリをプラグインして、それらに完全に依存する代わりにスコアリング-障害とすべて。
最善の解決策はありません。とにかく、ある種のヒューリスティックに対処する必要があります。しかし、別のレーベンシュタイン距離の実装を探す(または自分で実装する)ことができます。この実装は、異なる文字の異なる文字操作(挿入、削除)に異なるスコアを与える必要があります。たとえば、キーボード上で近接している文字のペアのスコアを低くすることができます。また、文字列の長さに基づいて最大距離のしきい値を動的に計算することもできます。
そして、私はあなたのためにパフォーマンスのヒントを持っています。レーベンシュタイン距離を計算するたびに、n * m演算が実行されます。ここで、nおよびmは文字列の長さです。 Levenshtein automaton があり、一度構築すると、各文字列に対して非常に高速に評価されます。 NFAの評価には非常に費用がかかるため、最初にDFAに変換する必要があります。
Lucene をご覧ください。必要なファジー検索機能がすべて含まれているといいのですが。サポートされている場合は、DBMSフルテキスト検索を使用することもできます。たとえば、PostgreSQLはフルテキストをサポートしています。
Dbを使用して検索を実行しますか?選択で正規表現を使用するか、LIKE
演算子を使用する
データベースを分析し、ハフマンツリーまたは複数のテーブルを作成して、Key-Value検索を実行します。
これは私が同様のユースケースでやったことです:
distance( "ab"、 "ac")は33% max(distance( "a"、 "a")、distance( "b"、 "c"))は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
...
これらの2つは入力に対して機能するはずです。