Lucene簡易利用ラッパライブラリrecLuceneの紹介

2018年8月1日

※弊社製オープンソースソフトはオープンソースソフトウェアとしてまとめているので参照されたい。

Luceneは検索エンジンとして有名なところなのだが、使い方はややこしく、しかもバージョンによってコロコロ仕様が変わる。これを、ある特定の目的のために簡単に使えるようなライブラリを作ってみた。

想定する利用状況

特にLuceneの利用に際して大きく注意すべきことは、どれだけ頻繁にインデックスが作成され、どれだけ頻繁に検索が行われるかを想定することかと思われる。インデックス書き込みが常に行われ、リアルタイムで検索に反映させることは非常にコストがかかるようだ。一口にLuceneを利用すると言っても、どういった状況でそれを使うのかにより、利用の仕方も変わってくる。

しかし、recLuceneでは以下のような想定をしている。

  • インデックスは滅多に書き込まれない
  • 検索は頻繁に行われる、インデックス書き込みをした者がすぐにそれを検索で確認する可能性が高い
  • 検索対象となるのは、商品名、個人名、電話番号、メールアドレス等の非常に短いデータである。

さら、以下の想定をしている

  • 元々のデータはRDBに格納されており、その検索だけにLuceneを利用する
  • このため、Luceneから得る検索結果としては、RDBレコードのプライマリキーのみでよい。

以上の目的のために簡単に使える物として作成した。

githubのリポジトリはhttps://github.com/ysugimura/recLucene
アーティファクトのMavenリポジトリはhttps://ysugimura.github.io/mavenになる。

簡単なサンプル

何はともあれ、簡単なサンプルを示す。

package com.cm55.recLucene.sample;

import java.util.*;
import java.util.stream.*;

import org.junit.*;
import static org.junit.Assert.*;

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

public class SampleMain {

  static FooRecord[]recs0 = new FooRecord[] {
      new FooRecord(1L, "吾輩は猫である。名前はまだ無い。"),
      new FooRecord(2L, "どこで生れたかとんと見当がつかぬ。"),
      new FooRecord(3L, "何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。"),
      new FooRecord(4L, "吾輩はここで始めて人間というものを見た"),
      new FooRecord(5L, "しかもあとで聞くとそれは書生という人間中で一番獰悪な種族であったそうだ。")
  };
  static FooRecord[]recs1 = new FooRecord[] {
      new FooRecord(6L, "この書生というのは時々我々を捕えて煮て食うという話である。"),
      new FooRecord(7L, "しかしその当時は何という考もなかったから別段恐しいとも思わなかった。"),
      new FooRecord(8L, "ただ彼の掌に載せられてスーと持ち上げられた時何だかフワフワした感じがあったばかりである。"),
      new FooRecord(9L, "掌の上で少し落ちついて書生の顔を見たのがいわゆる人間というものの見始であろう。"),
      new FooRecord(10L, "この時妙なものだと思った感じが今でも残っている。第一毛をもって装飾されべきはずの顔がつるつるしてまるで薬缶だ。")
  };

  @Test
  public void test()  {

    // デフォルトの日本語アナライザを3gramにする
    RlDefaults.analyzerClass = JpnStandard3.class;

    // データベースを作成する
    RlDatabase db = new RlDatabase.Ram().add(FooRecord.class);
    RlWriter writer = db.createWriter();
    RlSearcher<FooRecord> searcher = db.createSearcher(FooRecord.class);

    // 前半の書き込み
    Arrays.stream(recs0).forEach(r->writer.write(r));

    // 検索
    checkIds(searcher, new Word("content", "人間"), 4L, 5L);
    checkIds(searcher, new Word("content", "人間 種族"), 5L);
    checkIds(searcher, new And(new Word("content", "人間"), new Word("content", "種族")), 5L);

    // 後半の書き込み
    Arrays.stream(recs1).forEach(r->writer.write(r));

    // 検索
    checkIds(searcher, new Word("content", "人間"), 4L, 5L, 9L);

    // 削除
    writer.delete("id", 5L);

    // 検索
    checkIds(searcher, new Word("content", "人間"), 4L, 9L);
    checkIds(searcher, new And(new Word("content", "人間"), new Not(new Word("content", "書生"))), 4L);
  }

  void checkIds(RlSearcher<FooRecord>searcher, RlQuery query, Long...ids) {
    Set<Long>set = searcher.searchPkSet(query);
    assertEquals(Arrays.stream(ids).collect(Collectors.toSet()), set);
  }
}

比較的短い文字列を持つレコードを作成し、そこに1,2,3,4…というプライマリキーをつける。
これを書き込み、検索する。検索結果は、プライマリキーの集合として取得し、それが正しいかを確認している。

各レコードは以下のようなものである。先も書いたように、元々これらのデータはRDBレコードとして格納されていることを想定しており、
そこからインデックス付けが必要なデータを抜き出したものという想定である。

package com.cm55.recLucene.sample;

import com.cm55.recLucene.*;

public class FooRecord {

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

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

  public FooRecord() {    
  }

  public FooRecord(Long id, String content) {
    this.id = id;
    this.content = content;
  }

  @Override
  public String toString() {
    return "id:" + id + ",content:" + content;
  }

  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);
    }
  }
}

インデックス書き込みと検索結果のタイミングを見てもらえればわかるのだが、インデックス書き込みの直後にそれが検索結果に返ってくる。
引き続き、recLuceneの使い方を説明する予定である。