H2Databaseを追っかけていたりしたブログ

H2 database のリリースノートを読んだりとか。

H2のLuceneを使用した全文検索を試してみる

H2では、組み込みの全文検索関数のほかに、luceneを使った全文検索を組み込む事ができる。ちょっと試してみた。

なお、現状公式サイトに載っている手順では日本語を対象として全文検索を実行する事ができない。というのも、LuceneのAnalyzerとして、日本語に対応していないStandardAnalyzerが使用されているから。しかもハードコーディング。簡単にAnalyzerの設定を変えられるようにする要望は挙がっている。

Fulltext search Lucene: analyzer configuration.

http://www.h2database.com/html/roadmap.html

が、要望があがって1年半?くらいたって実装されていないし、動きもなさそうなので、たぶん当面実装はされないんだろうなと思われる。

ハードコーディングされているStandardAnalyzerの部分をCjkAnalyzerとかに変えてやれば、日本語に対応させる事もできるが、とりあえずStandardAnalyzerのままでやってみる。あと、luceneのバージョンは2.9系でなくては動かない。3.0系はダメ。

以下のjarを用意する。

  • h2-1.2.132.jar
  • lucene-core-2.9.2.jar

StandardAnalyzerを使うのであればこれだけでよい。

とりあえず何はなくともH2を起動する。luceneを使った全文検索(FullTextLucene)を使いたい場合、前述のjarもクラスパスに追加する。

java -cp h2-1.2.132.jar:lucene-core-2.9.2.jar org.h2.tools.Server -baseDir /home/hoge/h2/data &

baseDirは付けなくてもかまわないが、データベースのファイルがホームディレクトリの直下にできてしまったりするので、指定した方がいいと思う。あと、FullTextLuceneを使いたい場合は、オンメモリデータベースではだめだ。サポートしていない。

上記コマンドで実行すると、デフォルトのポートでWeb(8082)/TCP(9092)/PG(5435)の3つのサーバが起動する。Webコンソールが使いやすいので、同時に起動するブラウザでそのまま使ってもいい。コマンドラインのコンソールなら、次のコマンドの通り接続する。

java -cp h2-1.2.132.jar org.h2.tools.Shell -url jdbc:h2:tcp://localhost/test -user test -password test

testというデータベースが勝手にできて、testというユーザでtestというパスワードで接続する。先ほど指定したbaseDirの下にtest.*のファイル群が作成されていると思われる。できていなければ、baseDirが間違っているか、urlが間違っていると思われる。

で、FullTextLuceneの初期化を行う。以下のSQLを実行する(ここらへん、Tutorialのまま。)

CREATE ALIAS IF NOT EXISTS FTL_INIT FOR "org.h2.fulltext.FullTextLucene.init";
CALL FTL_INIT();

CREATE ALIASはユーザ定義関数を作成するSQL文。FT_INITがコールされたら、org.h2.fulltext.FullTextクラスのinitメソッドが呼ばれ、スキーマ(FTS)と、テーブル(FTS.INDEXES)と、ユーザ定義関数5つ(FTL_CREATE_INDEX,FTL_SEARCH,FTL_SEARCH_DATA,FTL_REINDEX,FTL_DROP_ALL)が作成される。これで、FullTextLuceneの初期化は終了。簡単。で、念のため、baseDirの下を見ると、test.*のh2のデータベースファイル群の他にtestというディレクトリができていて、そこにLuceneのインデックスができている。

で、早速テーブルを作ってみる(今回はドキュメント内に日本語は使わない)

CREATE TABLE DOCUMENT(ID INT PRIMARY KEY, TITLE VARCHAR, CONTENT VARCHAR,REG_DATE DATE);
INSERT INTO DOCUMENT VALUES( 1,'Python','Java Perl', CURRENT TIMESTAMP);
INSERT INTO DOCUMENT VALUES( 2,'Perl','Java Python', CURRENT TIMESTAMP);
INSERT INTO DOCUMENT VALUES( 3,'Perl','Java Ruby', CURRENT TIMESTAMP);
INSERT INTO DOCUMENT VALUES( 4,'Java','Perl Python', CURRENT TIMESTAMP);
INSERT INTO DOCUMENT VALUES( 5,'Java','Perl Ruby', CURRENT TIMESTAMP);
INSERT INTO DOCUMENT VALUES( 6,'Java','Python Ruby', CURRENT TIMESTAMP);
INSERT INTO DOCUMENT VALUES( 7,'Ruby','Java Perl', CURRENT TIMESTAMP);
INSERT INTO DOCUMENT VALUES( 8,'Ruby','Java Python', CURRENT TIMESTAMP);
INSERT INTO DOCUMENT VALUES( 9,'Python','Java Ruby', CURRENT TIMESTAMP);
INSERT INTO DOCUMENT VALUES(10,'Ruby','Perl Python', CURRENT TIMESTAMP);
INSERT INTO DOCUMENT VALUES(11,'Python','Perl Ruby', CURRENT TIMESTAMP);
INSERT INTO DOCUMENT VALUES(12,'Perl','Python Ruby', CURRENT TIMESTAMP);

まぁここは普通。ただ、プライマリーキーが設定されていないテーブルは全文検索のインデックスに登録できなそうな感じ。

CALL FTL_CREATE_INDEX('PUBLIC', 'DOCUMENT', NULL);

PUBLICスキーマのDOCUMENTテーブルのすべてのカラムのデータで全文検索のインデックスを作成する。以降、データの変更があれば即時に全文検索のインデックスの方にも反映される。

全文検索を使った検索は、FTL_SEARCH/FTL_SEARCH_DATA関数のどちらかを使う。返り値が異なるが引数は同じ。全文検索のクエリ,limit,offsetの3つ。結果をテーブルと結合すると考えると、FTL_SEARCH_DATAを使うことのほうが多いだろうか。実行すると以下のような感じ。

SELECT * FROM FTL_SEARCH_DATA('Java', 0, 0);

SCHEMA  	TABLE  	COLUMNS  	KEYS  	SCORE  
PUBLIC	DOCUMENT	 (ID)	(1)	0.5172656774520874
PUBLIC	DOCUMENT	 (ID)	(2)	0.5172656774520874
PUBLIC	DOCUMENT	 (ID)	(3)	0.5172656774520874
PUBLIC	DOCUMENT	 (ID)	(4)	0.5172656774520874
PUBLIC	DOCUMENT	 (ID)	(5)	0.5172656774520874
PUBLIC	DOCUMENT	 (ID)	(6)	0.5172656774520874
PUBLIC	DOCUMENT	 (ID)	(7)	0.5172656774520874
PUBLIC	DOCUMENT	 (ID)	(8)	0.5172656774520874
PUBLIC	DOCUMENT	 (ID)	(9)	0.5172656774520874
(9 行, 8 ms)

DOCUMENTテーブルのID=1〜9とヒットしたよ、という結果がかえってくる。()がついているのは配列型なので、結果をDOCUMENTテーブルと結合するには以下のようなSQLになる。

SELECT DOC.TITLE,DOC.CONTENT FROM FTL_SEARCH_DATA('Java', 0, 0) FTL 
  JOIN DOCUMENT DOC ON DOC.ID = FTL.KEYS[0] 
 WHERE FTL.TABLE = 'DOCUMENT';

TITLE  	CONTENT  
Python	Java Perl
Perl	Java Python
Perl	Java Ruby
Java	Perl Python
Java	Perl Ruby
Java	Python Ruby
Ruby	Java Perl
Ruby	Java Python
Python	Java Ruby

Primary Keyが複合キーである場合には以下のようになる。下記の例は、(ID,SUB_ID)でPrimary Keyとなる場合。そう考えると、Primary Keyが1カラムで構成される場合についても、(DOC.ID) = FTL.KEYSとかけた方が自然なような気がするのだが、残念ながらそれは通らない。

SELECT DOC.TITLE,DOC.CONTENT FROM FTL_SEARCH_DATA('Java', 0, 0) FTL 
  JOIN SUB_DOCUMENT DOC ON (DOC.ID,DOC.SUB_ID) = FTL.KEYS 
 WHERE FTL.TABLE = 'SUB_DOCUMENT';

また、FullTextLuceneでは関数に渡されたクエリはそのままLuceneに渡される。なので、Luceneのクエリの書き方そのまま。「TITLEカラムにJavaを含み、カラム全体でPythonを含まない」というクエリは下記のようになる。

SELECT DOC.TITLE,DOC.CONTENT FROM FTL_SEARCH_DATA('TITLE:Java -Python', 0, 0) FTL 
  JOIN DOCUMENT DOC ON DOC.ID = FTL.KEYS[0] 
 WHERE FTL.TABLE = 'DOCUMENT';

TITLE  	CONTENT  
Java	Perl Ruby
(1 行, 21 ms)

事ほど左様にH2ではLuceneを簡単に組み込んで使う事ができる。ちょっと試しにLucene使いたいんだけど、という場合にH2経由で使ってみる、というのも意外とお手軽な気もする(Solrでもいいかもしれんが)