技術ネタ‎ > ‎

compass+JDOを使ってappengine上で全文検索

2.3.0-betaでの情報です。

使い方

  1. モジュールのダウンロード
    1. http://build.compass-project.org/ のCompass TrunkのNightlyビルドを選択する。
    2. ビルド結果ページの"Artifacts"をクリックして、"Release"を選ぶ。
    3. ファイルのリストが表示されるはずなので、その中から compass-2.3.0-beta1.zip をダウンロードする。
  2. モジュールの配置
    1. ダウンロードしたモジュールを解凍し、以下のファイルをCLASSPATHに追加する(Google Pluginを使う場合は"war/WEB-INF/lib"にコピーし、EclipseのプロジェクトのBuild pathにも追加する)。
      1. commons-logging.jar
      2. compass-2.3.0-beta1.jar
      3. lucene-core.jar

初期化

デモ動画では、PMF.javaのスタティックイニシャライザを使って初期化していたのでそれをマネする。

PMF.javaの一部

private static final Compass compass;

private static final CompassGps compassGps;

static {
  compass = new CompassConfiguration()
      .setConnection("gae://index")
      .setSetting(
          CompassEnvironment.ExecutorManager.EXECUTOR_MANAGER_TYPE,
          "disabled").addScan(
          "jp.co.topgate.sandbox.compass/model").buildCompass();

  compassGps = new SingleCompassGps(getCompass());
  compassGps.addGpsDevice(new Jdo2GpsDevice("appengine", INSTANCE));
  compassGps.start();
  compassGps.index();
}

public static Compass getCompass() {
  return compass;
}


addScan()では、JDOのEntityクラスが格納されているパッケージ名を指定する。
Entityクラス
  • クラスを"@Searchable"で修飾する。
  • Entityの主キーとして使用できるフィールドを"@SearchableId"で修飾する。
    • ただし、"com.google.appengine.api.datastore.Key"はそのままではバインドできないので、以下のようにしてみた。
      @SearchableId
      public Long getKeyValue() {
          return key.getId();
      }

  • 検索対象として使用するフィールドを"@SearchableProperty"で修飾する。
    • ただし、"com.google.appengine.api.datastore.Text"はそのままではバインドできないので、以下のようにしてみた。
      @SearchableProperty
      public String getContent1String() {
          if (content1 == null) {
              return "";
          }
          return content1.getValue();
      }

  • @SearchableProperty(name="Field名") でEntityクラスの属性名とは違う名称を使う事も出来る。
    • 他にも、org.apache.lucene.document.Fieldの作成時に使用するような属性を指定できる。

検索

検索結果を取得する例

@SuppressWarnings("serial")
public static class SearchResult implements Serializable {
  public final String keyValue;
  public final String content;
  public final String nickname;
  public final String email;

  public SearchResult(Resource resource) {
    this.keyValue = resource.getId();
    this.content = (String) resource.getProperty("content1String").getObjectValue();
    this.nickname = (String) resource.getProperty("nickname").getObjectValue();
    this.email = (String) resource.getProperty("email").getObjectValue();
  }
}

private List<SearchResult> search(String keyWords) {
  CompassSearchSession search = PMF.getCompass().openSearchSession();
  CompassHits hits = search.find(keyWords);
  int length = hits.length();
  List<SearchResult> result = new ArrayList<SearchResult>(length);
  for (int i = 0; i < length; i++) {
    Resource resource = hits.resource(i);
    result.add(new SearchResult(resource));
  }
  return result;
}

検索対象のField名を指定する場合

  • 検索条件に field名:検索キーワード

検索対象のKindを指定する場合

  • 検索条件に alias:Kind名(Class.getSimpleName())

  • JDO経由で保存する際に、@SearchablePropertyが付加されたFieldやgetterの値を使ってIndexを作成する、という動作をしている模様。
    • Entityクラスに、アノテーションを使ってIndexのための情報を付加してやる方法の場合(つまりこのページに書いた内容)、必ずJDO経由で保存する必要がある。LowLevelAPI等で保存したEntityに対してはIndexが作成されない(当たり前か…)。
    • Unownedな関係で他のEntityへの参照を保持し、そのfetch後の値に対するIndexを作成する事も出来る。が、永続化のタイミングで常にその値を返すgetterが実際に値を返す必要があるので、「保存前に参照先のEntityをfetchして保持しておく」必要がある。

      unowned

      @Persistence private Key otherEntityKey;

      @NotPersistence private OtherEntity otherEntity;

      @SearchablePropperty(name="otherEntity")
      public Sting getOtherEntityValue() {
        return otherEntity != null? otherEntity.getValue() : "";
      }
  • Index作成用のEntityを別途用意するというのも手かも?
    • Task QueueがJavaプラットフォームにも実装されたら、Index作成用のEntityだけ遅延させて…といった動作ができて良いかも。
      • Compass側でTaskQueue対応する可能性も十分期待できるかな。
  • Indexの作成を細かく制御したい
    • CompassGpsを使わずに、Compass.getSearchEngineIndexManager() を呼び出し制御する。
      • subinterfaceにlucene用のものがあるので、それを使うとより細かいことができるかも?
    • 個別のEntityをindexするにはCompass.openIndexSession() で呼び出し制御する
  •  queryを動的に作りたい
    • Compass.queryBuilder()でquerybuilderを取得。後は良しなに。
    • between,eq,ge,gt,le,le,and,or,like,wildcardなど一通りそろっている。
    •  CompassMultiPropertyQueryStringBuilder で複数のプロパティに一気に検索をかけたり
    • CompassSearchSession.find(queryString)は内部的にCompassSearchSession.queryBuilder().queryString(queryString).toQuery().hits()をしている。
       
  • 検索にソートを追加したい
  • limit,offsetを追加したい
    • 探したけど、よくわからなかった。CompassHitsdetach(int from, int size)があるので、これを1-2回すればData自体は減らせるだろう。意味あまりないけど。
  • 検索にヒットした文字列をハイライトしたい
    • 一般的な検索エンジンではハイライト表示のテキストは、一番検索単語がマッチしているあたりをフラグメントとして表示している(たとえば、数百文字位)luceneではbestFragment等と呼んでいる。
    • で、このフラグメントを得るメソッドはCompassHits.highlighter(i).fragment(propertyName) 

      ハイライトのフラグメントを取得する

          CompassSearchSession searchSession = getCompass().openSearchSession();
          CompassHits hits = searchSession.find(text);

          int length = hits.getLength();

          if (length > 0) {

           for (int i = 0; i < length; i++) {

            //ここではPRの文字列にハイライトを利かせている。
            String fragment = hits.highlighter(i).fragment("pr");
            Resource resource = hits.resource(i);
           }
          }
          return results;
    •  Wicketで表示する場合にはIDataの中でhighlighterを取得してフラグメントをとるような形になるでしょう。
Comments