recLuceneその2

2018年5月27日

recLuceneは、前述した目的のためにできる限り簡単に使用できるように構成してある。ここではいかに簡単に扱えるかを見ていく。

データベースを作成する。あるいはオープンする。

データベースを作成し、テーブル定義を追加するには以下のようにする。

    // データベースを作成し、テーブル定義を追加する
    RlDatabase db = new RlDatabase.Ram();
    db.add(FooRecord.class);

ここではディスクではなくメモリ上に作成しているが、実際にディスクに作成するには以下のようにする。

    File folder = フォルダの位置
    RlDatabase db = new RlDatabase.Dir(folder);
    db.add(FooRecord.class);

フォルダが存在しなければ新たなDBが作成され、既にあるならそれがオープンされる。

このDBにaddでテーブル定義を設定する必要がある。これはJavaクラス名であり、つまり通常のORマッピングの際に指定するような「レコードにマッピングする対象のJavaクラスを指定する」ということである。

ただし、これを指定したときに、Luceneデータベースに何かが格納されるわけではなく、「DBにどのようにアクセスするか」を定義するものにすぎない。したがって、既存のDBには無かった「テーブル」を追加してもよいし、以前には存在した「テーブル」を省略してもよい。

マッピング用のJavaクラス

マッピング用のJavaクラスは以下のようなものである。

public class FooRecord {

  /** ID */
  @RlFieldAttr(pk = true, converter = IdConverter.class)
  public Long id;

  /** 自由文字列 */
  @RlFieldAttr(store = false)
  public String content;

static, transient以外のフィールドがマッピングされることになるのだが、基本的に@RlFieldAttrアノテーションをつけてそのフィールドの属性を指定しないといけない。

プライマリキーの指定

プライマリキーとなるフィールドにはpk=trueをつける。pk=trueのフィールドは0個か1個のみが許される。プライマリキーのある場合は当然ながら、同じキーを持つ「レコード」は、DB中で0個か1個になる。

通常の利用方法としては、リレーショナルDBのキーと同じものをここに格納し、Lucene側の検索ではプライマリキーのみを取得して、実際のレコードはRDBのものを用いるという利用方法を想定している。

すべてが文字列であること

Luceneに格納できるものは文字列だけである。他もあるかもしれないが、その機能は使用していない。すべてを文字列として格納する。

したがって、文字列以外のものを格納する際には、その「コンバータ」を指定しなければならない。これが「converter = IdConverter.class」の指定である。これは単純にRlFieldConverter.Abstractから派生した以下のようなものである。単純にLong/String変換をするだけだ。

  public static class IdConverter extends RlFieldConverter.Abstract<Long> {
    public IdConverter() {
      super(Long.class);
    }

    @Override
    public String toString(Long id) {
      return id.toString();
    }

    @Override
    public Long fromString(String string) {
      return Long.parseLong(string);
    }
  }

storeしない指定

実際の検索データはcontentに格納する。

  @RlFieldAttr(store = false)
  public String content;

ここではstore=falseを指定しているのだが、この意味としては、contentの中身をLuceneデータベースに格納しないという意味である。

「検索対象データをLuceneデータベースに格納しない」とは、どういう意味かと言えば、こういうことである。

  • 元々のデータはLuceneには格納しない
  • しかし、検索用にトークン化されたデータは当然格納する

「トークン化された」とは、この例の場合には、3gramで分割された日本語である。これはどういう意味かと言えば、例えば「吾輩は猫である。名前はまだ無い。」という元の文字列があるとすれば、トークン化されたものは、「吾、吾輩、吾輩は、輩、輩は、輩は猫、。。。」などと最大三文字に分割された複数の文字列になる。

これらのトークン化された文字列を検索用としてLuceneに格納するのだが、元々の「吾輩は猫である。名前はまだ無い。」という文字列は格納しておく(保存しておく)必要は無いという意味である。

アナライザの指定

なお、ここではアナライザを指定していないが、これは「トークン化」を行うクラスである。先にも書いたように3gramでトークン化しているのだが、これはデフォルトの動作であり、このデフォルトは以下の部分で指定している。

  RlDefaults.analyzerClass = JpnStandard3.class;

RlFieldAttrのパラメータ

このアノテーションRlFieldAttrのパラメータは以下のようになっている。ここでは簡単に記述している。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface RlFieldAttr {

  // プライマリキーを示す、強制的にstore=true, tokenized=falseになる。
  public boolean pk() default false;

  // Luceneデータベースに格納するかを示す
  public boolean store() default false;

  // トークン化するかを示す
  public boolean tokenized() default true;

  // フィールドコンバータの指定。文字列以外の場合は必須
  public Class<? extends RlFieldConverter<?>>converter() default RlFieldConverter.None.class;

  // アナライザの指定。デフォルトは「デフォルト」のアナライザ
  public Class<? extends RlAnalyzer>analyzer() default RlAnalyzer.Default.class;

}

ライターとサーチャーの取得

ライター(インデックス用レコード書き込み)とサーチャー(検索)は以下のように取得する。

    RlWriter writer = db.createWriter();
    RlSearcher<FooRecord> searcher = db.createSearcher(FooRecord.class);

これはあくまでサンプルである。通常はこのように二つが並ぶことは無いだろう。書き込み側は書き込みのみを行い、検索側は検索のみを行うはずだ。その場合は、それぞれ以下のように記述して欲しい。

  try (RlWriter writer = db.createWriter()) {
    // 書き込む
    writer.write(record);
  }
  try (RlSearcher<FooRecord>searcher = db.createSearcher(FooRecord.class)) {
    // 検索する
    searcher.search云々
  }

ライターの使い方

ライターは、単純に「レコード」を作成してそれをwriteで書き込むことだけである。通常のRDBと異なり、以下のような特徴がある。

  • commit/rollbackという操作は一切無い(元々のLuceneにはcommitがあるのだが、このライブラリではそれは意識せずに使うようになっている)
  • 従って、write時点で書き込みが完了する。この時点でサーチャー側にも反映される(できる限り即座に反映される設計になっている)
  • insert/updateという区別は無い。キーが存在しなければ挿入されるし、キーがあれば置換される。これをwriteのみで行う。
  • このライターはスレッドセーフではない(元々のLuceneのIndexWriterはスレッドセーフだが、RlWriterはそうではない)
  • 一度に使用できるライターは一つだけである。
  • ライターは最後に必ずclose()しなければならない(これはtry文でおこなっている)。
  • closeされていないライターが存在するときに、createWriter使用とすると、その実行はブロックされる。

RlWriterはスレッドセーフではなく、また一度に使えるのは一つだけであるため、以下のような使い方をしなければならない。

  • 必要になったらcreateWriter()で作成する。
  • 使い終わったら、ただちにclose()する。

この期間、別のスレッドで書き込みを行おうとしている者は終了するまでブロックされる。

サーチャーの使い方

サーチャーもまた、ライターと同じ制限がある。

  • サーチャーはスレッドセーフではない(これもまた元々のLuceneのIndexSearcherはスレッドセーフであるが、RlSearcherはそうではない)。
  • 最後に必ずclose()しなければならない(これはtry文で行っている)

ただし、ライターと異なり、サーチャーは同時に複数の存在が許される。以前に作成したサーチャーをクローズしないと次のものが作れない、ということは無い。

ただし、そうだからと言って、サーチャーをクローズせずに、どこかに保持しておいて使い回すことは推奨されない。必ず以下の使い方とする。

  • 必要になったらcreateSearcher()で作成する。
  • 使い終わったら、ただちにclose()する。

なお、サーチャー作成時には、どのレコードを検索するかを指定しなければならない。このシステムはRDBとは異なりSQLやらジョイン操作などは無い。あくまでも、「特定の一つのテーブル」を検索するのみである。そのテーブルを指定する必要がある。

検索のしかた

先の例では、検索条件を指定してプライマリキーの集合を取得するだけである。この検索条件としては以下のようなものである。

import static com.cm55.recLucene.RlQuery.*;

 new Word("content", "人間")

 new Word("content", "人間 種族")

 new And(new Word("content", "人間"), new Not(new Word("content", "書生")))

見てのとおり、最初のものは、contentフィールドに「人間」の含まれているものを検索し、次は、「人間」かつ「種族」が含まれているものである。三眼目は、「人間」が含まれているが、「書生」が含まれていないものである。

デフォルトのアナライザでは、空白によって語句を分割するようになっており、分割結果はANDとしてまとめられるため、二番目は、以下のように書いても同じ意味になる。

  new And(new Word("content", "人間"), new Word("content", "種族"))