AND演算処理の概要

上の図から、ある2つの語の転置インデックスリストをA, Bとします。ここで、要素をそれぞれa, b(整数)とし演算結果を格納するリストをCとするとき、AND演算は主に以下の処理内容を繰り返します。
- if a < b then aの次の要素をaに代入
- if a = b then 要素aをCの末尾に追加しA, Bが指す要素を一つ進める
プログラムの主な処理内容
- 検索対象テキストを単語に分割。
- 単語を転置インデックスに登録。ここで、1単語あたりに格納する情報は、その単語の出現頻度とその文書ID。転置インデックスのデータ構造はTreeMapを使用しkeyに単語、valueはIndexRecordでputします。
- ユーザからの標準入力をパースしAND演算(Intersectメソッドで実現しています)。
以下に、ソースコードと実行結果を示します。
IndexRecord.java
1つのトークン(単語)に対するインデックス情報(docIDリストや出現頻度情報)
import java.util.ArrayList;
/*
* 1つのトークン(単語)に対するインデックス情報
*/
public class IndexRecord {
// 出現文書IDリスト(通常ソートの必要あり)
private ArrayList<integer> posts;
// 出現頻度(今回は同一doc内の頻度はカウントしない)
private int freq;
public IndexRecord(int id) {
posts = new ArrayList<integer>();
posts.add(id);
freq = 1;
}
/** docIDをリストに追加(今回は同一docIDのtermはカウントしない) */
public void addDocID(int id) {
if (!existDocID(id)) {
posts.add(id); // docIDの追加
freq++; // 出現頻度のカウントアップ
}
}
/** docIDが既にリスト中に存在するか否か */
public boolean existDocID(int id) {
return (posts.indexOf(id) != -1) ? true : false;
}
public String toString() {
String str = freq + ", "+ posts;
return str;
}
public ArrayList<integer> getPosts() {
return posts;
}
}
BooleanTest.java (AND演算のテストプログラム)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.StringTokenizer;
import java.util.TreeMap;
/**
* 検索エンジンのAND演算処理
* Web page: https://yukun.info/
* license GPL
*/
public class BooleanTest {
// 検索対象テキスト
static String doc0 = "It is meaningless only to think my long further aims idly. "+
"It is important to set my aims but at the same time I should confirm my present condition. "+
"Unless I set the standard where I am in any level, I'll be puzzled about what I should do from now on. It's in my case.";
static String doc1 = "Today, I enjoyed playing with friends daytime. "+
"After enjoying, I got back to my daily life with an vigorous power. "+
"I should think so, but why did I feel touch of uncertainty and regret? "+
"I wanna enjoy myself and another tremendously during the day when I've played. "+
"Well, As well as I commit play to quality, I'll choose such kinds of play.";
static String doc2 = "I'll manage the limited time in a day. "+
"I think that I divide the time into some intervals such as 5 minutes, "+
"15 minutes and more than one hour and so on. I'll make use of this character of the interval.";
public static void main(String[] args) {
ArrayList<string> docIDlist = new ArrayList<string>();
// 文書を格納
docIDlist.add(doc0);
docIDlist.add(doc1);
docIDlist.add(doc2);
StringTokenizer st[] = new StringTokenizer[3];
String stripChars = ".,:;?!"'[]{}()"; // 除外文字
// 文字列を空白で区切るよう設定
for (int i = 0; i < st.length; i++) {
st[i] = new StringTokenizer(docIDlist.get(i), " ");
}
// 転置インデックス用のMap
TreeMap<string, IndexRecord> termMap = new TreeMap<string , IndexRecord>();
// 分割されたトークンを取得
for (int i = 0; i < st.length; i++) {
// ここでのパラメータiはdocIDを指すことと同じ
while (st[i].hasMoreTokens()) {
// 文字列トークンの先頭・末尾の文字をフィルタリング
// org.apache.commons.lang.StringUtilsクラスを使用
// http://commons.apache.org/proper/commons-lang/
String term = StringUtils.strip(st[i].nextToken(), stripChars);
//System.out.println("値 : " + term);
if(termMap.containsKey(term)) {
// 登録されているtermならdocIDの追加とカウントアップ
IndexRecord ir = termMap.get(term);
ir.addDocID(i);
termMap.put(term, ir);
} else {
// termMapに登録されていないtermならdocIDと合わせて登録
termMap.put(term, new IndexRecord(i));
}
}
} // for loop ends
// termMapのデバッグプリント
System.out.println("単語 freq, docID");
for (String part : termMap.keySet()) {
System.out.printf("%-12s : %sn", part, termMap.get(part));
}
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String words = "";
while (true) {
ArrayList<arrayList<integer>> postsSet = new ArrayList<arrayList<integer>>();
System.out.print("検索語: ");
try {
// ユーザからの標準入力を受付
words = br.readLine();
if (words.equals("quit")) break;
// 入力文字列をパース
StringTokenizer parser = new StringTokenizer(words, " ");
while (parser.hasMoreTokens()) {
String term = StringUtils.strip(parser.nextToken(), stripChars);
// termMapに登録されている単語か否か
if (termMap.containsKey(term)) {
postsSet.add(termMap.get(term).getPosts());
} else {
postsSet = null;
break;
}
}
// AND演算処理
ArrayList<integer> result = intersect(postsSet);
System.out.print("結果 :");
if (result == null || result.size() == 0)
System.out.println("文書中に存在しません。");
else
System.out.println("文書ID "+ result +"に存在します。");
} catch (IOException e) {
e.printStackTrace();
}
}
} // main() ends
// AND演算処理メソッド
public static ArrayList<integer> intersect(ArrayList<arrayList<integer>> postsSet) {
if (postsSet == null) return null;
int len = postsSet.size();
if (len == 0) return null;
else if (len == 1) return postsSet.get(0);
// postsSetを昇順にソート(演算回数の削減)
Collections.sort(postsSet, new FreqComparator());
ArrayList< Integer > result = postsSet.get(0);
for (int i = 1; i < len; i++) {
result = intersect(result, postsSet.get(i));
}
return result;
}
public static ArrayList<integer> intersect(ArrayList<integer> p1, ArrayList<integer> p2) {
ArrayList<integer> answer = new ArrayList<integer>();
int len1 = p1.size();
int len2 = p2.size();
for (int i=0, j=0; i< len1 && j < len2; ) {
if (p1.get(i) == p2.get(j)) {
answer.add(p1.get(i));
i++; j++;
} else if (p1.get(i) < p2.get(j)) {
i++;
} else {
j++;
}
}
return answer;
}
}
[/java]
<h3>FreqComparator.java (ArrayList要素のソート用)</h3>
[java]
import java.util.ArrayList;
import java.util.Comparator;
public class FreqComparator implements Comparator<object>{
public int compare(Object o1, Object o2){
return ((ArrayList<integer>) o1).size() - ((ArrayList<integer>) o2).size();
}
}
実行結果
単語 freq, docID 15 : 1, [2] 5 : 1, [2] After : 1, [1] As : 1, [1] I : 3, [0, 1, 2] <中略> set : 1, [0] should : 2, [0, 1] so : 2, [1, 2] some : 1, [2] standard : 1, [0] such : 2, [1, 2] than : 1, [2] that : 1, [2] the : 3, [0, 1, 2] think : 3, [0, 1, 2] this : 1, [2] time : 2, [0, 2] to : 2, [0, 1] touch : 1, [1] <中略> 検索語: think that 結果 :文書ID [2]に存在します。 検索語: wanna play to 結果 :文書ID [1]に存在します。 検索語: the java 結果 :文書中に存在しません。 検索語: should so 結果 :文書ID [1]に存在します。 検索語: time to 結果 :文書ID [0]に存在します。 検索語: well only 結果 :文書中に存在しません。 検索語: the time 結果 :文書ID [0, 2]に存在します。 検索語: quit
おー、なんだか楽しくなってきましたね。
文字列の区切り方
今回は検索対象テキストを英文に絞ったため、テキスト中の空白文字で区切ることでトークンを抽出できました。対して、日本語テキストの場合は区切り記号等は無い為、n-gramか形態素辞書などを用いてトークンに区切ることで実現できます。日本語文の区切り方は色々ありますが、中でも簡単な方法は、文字種(英文字、記号、ひらがな、カタカナ、漢字)の違いを区切りの境界と捉える方法です。
余談ですが、ブラウザやエディタ等で文字の上でダブルクリックするとカーソル下の文字列が選択状態になりますが、その範囲を決定する際に上述の方法が応用されているようです。ソフトによってはトリプルクリックするとカーソル下の行全体が選択状態になります(使うと編集が楽です)。