第17章 クエリー

17.1. クエリー

Infinispan Query は Lucene クエリーを実行し、ドメインオブジェクトを Red Hat JBoss Data Grid キャッシュから取得できます。

クエリーの準備および実行

  1. 以下のように、インデックス化が有効になっているキャッシュの SearchManager を取得します。

    SearchManager manager = Search.getSearchManager(cache);
  2. 以下のように、QueryBuilder を作成して Myth.class のクエリーを構築します。

    final org.hibernate.search.query.dsl.QueryBuilder queryBuilder =
        manager.buildQueryBuilderForClass(Myth.class).get();
  3. 以下のように、Myth.class クラスの属性をクエリーする Apache Lucene クエリーを作成します。

    org.apache.lucene.search.Query query = queryBuilder.keyword()
        .onField("history").boostedTo(3)
        .matching("storm")
        .createQuery();
    
    // wrap Lucene query in a org.infinispan.query.CacheQuery
    CacheQuery cacheQuery = manager.getQuery(query);
    
    // Get query result
    List<Object> result = cacheQuery.list();

17.2. クエリーの構築

17.2.1. クエリーの構築

クエリーモジュールは Lucene クエリー上で構築されるため、ユーザーはすべての Lucene クエリータイプを使用できます。クエリーが構築されると、Infinispan Query は org.infinispan.query.CacheQuery をクエリー操作 API として使用して、さらにクエリー処理を続行します。

17.2.2. Lucene ベースのクエリー API を使用した Lucene クエリーの構築

Lucene API では、クエリーパーサー (簡単なクエリー) または Lucene プログラム API (複雑なクエリー) を使用します。詳細は、Lucene のオンラインドキュメント Lucene in Action または Hibernate Search in Action を参照してください。

17.2.3. Lucene クエリーの構築

17.2.3.1. Lucene クエリーの構築

Lucene プログラム API を使用すると、フルテキストクエリーを書くことができます。しかし、Lucene プログラム API を使用する場合はパラメーターを同等の文字列に変換し、さらに正しいアナライザーを適切なフィールドに適用する必要があります。たとえば、N-gram アナライザーは複数の N-gram を指定の言葉のトークンとして使用し、そのように検索する必要があります。この作業には QueryBuilder の使用が推奨されます。

Lucene ベースのクエリー API は流動的です。この API には以下の特徴があります。

  • メソッド名は英語になります。そのため、API 操作は一連の英語のフレーズや指示として読み取りおよび理解されます。
  • 入力した接頭辞の補完を可能にし、ユーザーが適切なオプションを選択できるようにする IDE 自動補完を使用します。
  • チェイニングメソッドパターンを頻繁に使用します。
  • API 操作の使用および読み取りは簡単です。

API を使用するには、最初に指定のインデックスタイプにアタッチされるクエリービルダーを作成します。この QueryBuilder は、使用するアナライザーと適用するフィールドブリッジを認識します。複数の QueryBuilder を作成できます (クエリーのルートに関係する書くタイプごとに 1 つ)。QueryBuilderSearchManager から派生します。

Search.getSearchManager(cache).buildQueryBuilderForClass(Myth.class).get();

指定のフィールドに使用されるアナライザーをオーバーライドすることもできます。

SearchManager searchManager = Search.getSearchManager(cache);
    QueryBuilder mythQB = searchManager.buildQueryBuilderForClass(Myth.class)
        .overridesForField("history","stem_analyzer_definition")
        .get();

Lucene クエリーの構築にクエリービルダーが使用されるようになります。

17.2.3.2. キーワードクエリー

以下の例は特定の単語を検索する方法を示しています。

キーワード検索

Query luceneQuery = mythQB.keyword().onField("history").matching("storm").createQuery();

表17.1 キーワードクエリーパラメーター

パラメーター説明

keyword()

このパラメーターを使用して特定の単語を見つけます。

onField()

このパラメーターを使用して単語を検索する Lucene フィールドを指定します。

matching()

このパラメーターを使用して検索する文字列の一致を指定します。

createQuery()

Lucene クエリーオブジェクトを作成します。

  • 値「storm」は「history」FieldBridge から渡されます。これは、数字や日付が関係する場合に便利です。
  • その後、フィールドブリッジの値はフィールド「history」をインデックス化するために使用されるアナライザーへ渡されます。これにより、クエリーがインデックス化と同じ用語変換を使用するようにします (小文字、N-gram、ステミングなど)。分析プロセスが指定の単語に対して複数の用語を生成する場合、ブール値クエリーは SHOULD ロジック (おおよそ OR ロジックと同様) とともに使用されます。

文字列型でないプロパティーを検索します。

@Indexed
public class Myth {
    @Field(analyze = Analyze.NO)
    @DateBridge(resolution = Resolution.YEAR)
    public Date getCreationDate() { return creationDate; }
    public void setCreationDate(Date creationDate) { this.creationDate = creationDate; }
    private Date creationDate;
}

Date birthdate = ...;
Query luceneQuery = mythQb.keyword()
    .onField("creationDate")
    .matching(birthdate)
    .createQuery();
注記

プレーンな Lucene では、Date オブジェクトは文字列の表現に変換する必要がありました (この例では年)。

この変換は、FieldBridgeobjectToString メソッドがあれば (組み込みの FieldBridge 実装にはすべてこのメソッドがあります) どのオブジェクトに対しても実行できます。

次の例は、N-gram アナライザーを使用するフィールドを検索します。N-gram アナライザーは単語の N-gram の連続をインデックス化します。これは、ユーザーによる誤字を防ぐのに役立ちます。たとえば、単語 hibernate の 3-gram は hib、ibe、ber、rna、nat、ate になります。

N-gram アナライザーを使用した検索

@AnalyzerDef(name = "ngram",
    tokenizer = @TokenizerDef(factory = StandardTokenizerFactory.class),
    filters = {
        @TokenFilterDef(factory = StandardFilterFactory.class),
        @TokenFilterDef(factory = LowerCaseFilterFactory.class),
        @TokenFilterDef(factory = StopFilterFactory.class),
        @TokenFilterDef(factory = NGramFilterFactory.class,
            params = {
                @Parameter(name = "minGramSize", value = "3"),
                @Parameter(name = "maxGramSize", value = "3")})
    })
public class Myth {
    @Field(analyzer = @Analyzer(definition = "ngram"))
    public String getName() { return name; }
    public String setName(String name) { this.name = name; }
    private String name;
}

Date birthdate = ...;
Query luceneQuery = mythQb.keyword()
    .onField("name")
    .matching("Sisiphus")
    .createQuery();

一致する単語「Sisiphus」は小文字に変換され、3-gram (sis、isi、sip、phu、hus) に分割されます。各 N-gram はクエリーの一部になります。その後、ユーザーは Sysiphus (i ではなく y) myth (シーシュポスの神話) を検索できます。ユーザーに対してすべてが透過的に行われます。

注記

特定のフィールドがフィールドブリッジまたはアナライザーを使用しないようにするには、ignoreAnalyzer() または ignoreFieldBridge() 関数を呼び出すことができます。

同じフィールドで可能な単語を複数検索し、すべてを一致する句に追加します。

複数の単語の検索

//search document with storm or lightning in their history
Query luceneQuery =
    mythQB.keyword().onField("history").matching("storm lightning").createQuery();

複数のフィールドで同じ単語を検索するには、onFields メソッドを使用します。

複数のフィールドでの検索

Query luceneQuery = mythQB
    .keyword()
    .onFields("history","description","name")
    .matching("storm")
    .createQuery();

同じ用語を検索する場合でも、あるフィールドに必要な処理が他のフィールドとは異なることがあります。このような場合は andField() メソッドを使用します。

andField メソッドの使用

Query luceneQuery = mythQB.keyword()
    .onField("history")
    .andField("name")
    .boostedTo(5)
    .andField("description")
    .matching("storm")
    .createQuery();

前述の例では、フィールド名のみが 5 にブーストされます。

17.2.3.3. ファジークエリー

レーベンシュタイン距離 (Levenshtein Distance) アルゴリズムを基にしてファジークエリーを実行するには、keyword クエリーと同様に起動して fuzzy フラグを追加します。

ファジークエリー

Query luceneQuery = mythQB.keyword()
    .fuzzy()
    .withEditDistanceUpTo(1)
    .withPrefixLength(1)
    .onField("history")
    .matching("starm")
    .createQuery();

withEditDistanceUpTo は、2 つの用語の一致を考慮するための編集距離 (レーベンシュタイン距離) の最大値です。この値は 0 から 2 までの整数値で、デフォルト値は 2 になります。prefixLength は「ファジーの度合い」によって無視される接頭辞の長さになります。デフォルト値は 0 ですが、異なる用語が大量に含まれるインデックスではゼロ以外の値を指定することが推奨されます。

17.2.3.4. ワイルドカードクエリー

ワイルドカードクエリーを実行することもできます (単語の一部が不明のクエリー)。? は単一の文字を表し、* は連続する文字を表します。パフォーマンス上の理由で、クエリーの最初に ? または \* を使用しないことが推奨されます。

ワイルドカードクエリー

Query luceneQuery = mythQB.keyword()
    .wildcard()
    .onField("history")
    .matching("sto*")
    .createQuery();

注記

ワイルドカードクエリーは、アナライザーを一致する用語に適用しません。これは、\* または ? が適切に処理されないリスクが大変高くなるためです。

17.2.3.5. フレーズクエリー

これまでは、単語または単語のセットを検索しましたが、完全一致の文または近似の文を検索することも可能です。文の検索には phrase() を使用します。

フレーズクエリー

Query luceneQuery = mythQB.phrase()
    .onField("history")
    .sentence("Thou shalt not kill")
    .createQuery();

おおよその文はスロップ (slop) 係数を追加すると検索可能になります。スロップ係数は、その文で許可される別の単語の数を表します。これは、within または near 演算子と同様に動作します。

スロップ係数の追加

Query luceneQuery = mythQB.phrase()
    .withSlop(3)
    .onField("history")
    .sentence("Thou kill")
    .createQuery();

17.2.3.6. 範囲クエリー

範囲クエリーは指定の境界の間、上、または下で値を検索します。

範囲クエリー

//look for 0 <= starred < 3
Query luceneQuery = mythQB.range()
    .onField("starred")
    .from(0).to(3).excludeLimit()
    .createQuery();

//look for myths strictly BC
Date beforeChrist = ...;
Query luceneQuery = mythQB.range()
    .onField("creationDate")
    .below(beforeChrist).excludeLimit()
    .createQuery();

17.2.3.7. クエリーの組み合わせ

クエリーを集合 (組み合わせ) するとさらに複雑なクエリーを作成できます。以下の集合演算子を使用できます。

  • SHOULD: クエリーにはサブクエリーの一致要素が含まれるはずです。
  • MUST: クエリーにはサブクエリーの一致要素が含まれていなければなりません。
  • MUST NOT: クエリーにサブクエリーの一致要素が含まれてはなりません。

サブクエリーはブール値クエリー自体を含む Lucene クエリーです。例を以下に示します。

サブクエリーの組み合わせ

//look for popular modern myths that are not urban
Date twentiethCentury = ...;
Query luceneQuery = mythQB.bool()
    .must(mythQB.keyword().onField("description").matching("urban").createQuery())
    .not()
    .must(mythQB.range().onField("starred").above(4).createQuery())
    .must(mythQB.range()
        .onField("creationDate")
        .above(twentiethCentury)
        .createQuery())
    .createQuery();

//look for popular myths that are preferably urban
Query luceneQuery = mythQB
    .bool()
    .should(mythQB.keyword()
        .onField("description")
        .matching("urban")
        .createQuery())
    .must(mythQB.range().onField("starred").above(4).createQuery())
    .createQuery();

//look for all myths except religious ones
Query luceneQuery = mythQB.all()
    .except(mythQB.keyword()
        .onField("description_stem")
        .matching("religion")
        .createQuery())
    .createQuery();

17.2.3.8. クエリーオプション

クエリー型およびフィールドのクエリーオプションの概要は次のとおりです。

  • boostedTo (クエリー型およびフィールド上) は、クエリーまたはフィールドを指定の係数にブーストします。
  • withConstantScore (クエリー上) は、クエリーと一致し、ブーストと等しくなる定数スコアを持つすべての結果を返します。
  • filteredBy(Filter) (クエリー上) は Filter インスタンスを使用してクエリー結果をフィルターします。
  • ignoreAnalyzer (フィールド上) は、このフィールドの処理時にアナライザーを無視します。
  • ignoreFieldBridge (フィールド上) は、このフィールドの処理時にフィールドブリッジを無視します。

以下の例は、これらのオプションの使用方法を表しています。

クエリーオプション

Query luceneQuery = mythQB
    .bool()
    .should(mythQB.keyword().onField("description").matching("urban").createQuery())
    .should(mythQB
        .keyword()
        .onField("name")
        .boostedTo(3)
        .ignoreAnalyzer()
        .matching("urban").createQuery())
    .must(mythQB
        .range()
        .boostedTo(5)
        .withConstantScore()
        .onField("starred")
        .above(4).createQuery())
    .createQuery();

17.2.4. Infinispan Query でのクエリーの構築

17.2.4.1. 一般論

Lucene クエリーを構築したら、Infinispan CacheQuery クエリー内でラップします。クエリーは、インデックス化されたエンティティーをすべて検索し、インデックス化されたクラスのすべての型を返します。この挙動を変更するには、明示的に設定する必要があります。

Infinispan CacheQuery での Lucene クエリーのラッピング

CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery);

パフォーマンスを向上するには、以下のように戻り値の型を制限します。

エンティティー型での検索結果のフィルター

CacheQuery cacheQuery =
    Search.getSearchManager(cache).getQuery(luceneQuery, Customer.class);
// or
CacheQuery cacheQuery =
    Search.getSearchManager(cache).getQuery(luceneQuery, Item.class, Actor.class);

2 つ目の例の最初の部分は、一致する Customer インスタンスのみを返します。同じ例の 2 番目の部分は、一致する Actor および Item インスタンスを返します。型制限は多形です。このため、ベースクラス Person の 2 つのサブクラスである Salesman および Customer が返される場合は、Person.class を指定して結果の型に基づいてフィルターします。

17.2.4.2. ページネーション

パフォーマンスの劣化を防ぐため、クエリーごとに返されるオブジェクトの数を制限することが推奨されます。ユーザーがあるページから別のページへ移動するユースケースは大変一般的です。ページネーションを定義する方法は、プレーン HQL または Criteria クエリーでページネーションを定義する方法に似ています。

検索クエリーに対するページネーションの定義

CacheQuery cacheQuery = Search.getSearchManager(cache)
                              .getQuery(luceneQuery, Customer.class);
cacheQuery.firstResult(15); //start from the 15th element
cacheQuery.maxResults(10); //return 10 elements

注記

ページネーションに関係なく、一致する要素の合計数は cacheQuery.getResultSize() で取得できます。

17.2.4.3. ソート

Apache Lucene には、柔軟で強力な結果ソートメカニズムが含まれています。デフォルトでは関連性でソートされます。他のプロパティーでソートするようソートメカニズムを変更するには、Lucene Sort オブジェクトを使用して Lucene ソートストラテジーを適用します。

Lucene Sort の指定

org.infinispan.query.CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery, Book.class);
org.apache.lucene.search.Sort sort = new Sort(
    new SortField("title", SortField.STRING_FIRST));
cacheQuery.sort(sort);
List results = cacheQuery.list();

注記

ソートに使用されるフィールドはトークン化しないでください。トークン化に関する詳細は @Field を参照してください。

17.2.4.4. 射影

場合によってはプロパティーの小さなサブセットのみが必要になることがあります。以下のように、Infinispan Query を使用してプロパティーのサブセットを返します。

完全なドメインオブジェクトを返す代わりに射影を使用

SearchManager searchManager = Search.getSearchManager(cache);
CacheQuery cacheQuery = searchManager.getQuery(luceneQuery, Book.class);
cacheQuery.projection("id", "summary", "body", "mainAuthor.name");
List results = cacheQuery.list();
Object[] firstResult = (Object[]) results.get(0);
Integer id = (Integer) firstResult[0];
String summary = (String) firstResult[1];
String body = (String) firstResult[2];
String authorName = (String) firstResult[3];

クエリーモジュールは Lucene インデックスからプロパティーを抽出して、オブジェクト表現に変換してから Object[] のリストを返します。射影により、時間がかかるデータベースのラウンドトリップは回避されますが、以下の制約があります。

  • 射影されたプロパティーはインデックス (@Field(store=Store.YES)) に保存される必要があります。これにより、インデックスのサイズが大きくなります。
  • 射影されたプロパティーは org.infinispan.query.bridge.TwoWayFieldBridge または org.infinispan.query.bridge.TwoWayStringBridge を実装する FieldBridge を使用する必要があります。org.infinispan.query.bridge.TwoWayStringBridge はより簡単なバージョンです。

    注記

    Lucene ベースのクエリー API の組み込み型はすべて双方向です。

  • インデックス化されたエンティティーのシンプルなプロパティーまたは埋め込みされた関連のみを射影できます。埋め込みエンティティー全体は射影できません。
  • 射影は、@IndexedEmbedded を用いてインデックス化されたコレクションまたはマップでは動作しません。

Lucene はクエリー結果のメタデータ情報を提供します。射影定数を使用してメタデータを読み出します。

射影を使用したメタデータの読み出し

SearchManager searchManager = Search.getSearchManager(cache);
CacheQuery cacheQuery = searchManager.getQuery(luceneQuery, Book.class);
cacheQuery.projection("mainAuthor.name");
List results = cacheQuery.list();
Object[] firstResult = (Object[]) results.get(0);
float score = (Float) firstResult[0];
Book book = (Book) firstResult[1];
String authorName = (String) firstResult[2];

フィールドは、以下の射影定数と組み合わせることができます。

  • FullTextQuery.THIS は、射影されていないクエリーのように、初期化および管理されたエンティティーを返します。
  • FullTextQuery.DOCUMENT は、射影されたオブジェクトに関連する Lucene Document を返します。
  • FullTextQuery.OBJECT_CLASS は、インデックス化されたエンティティーのクラスを返します。
  • FullTextQuery.SCORE は、クエリーのドキュメントスコアを返します。スコアを使用して、指定のクエリーの結果の 1 つを他の結果と比較します。スコアは異なる 2 つのクエリーの結果を比較するためのものではありません。
  • FullTextQuery.ID は、射影されたオブジェクトの ID プロパティー値です。
  • FullTextQuery.DOCUMENT_ID は Lucene ドキュメント ID です。Lucene ドキュメント ID は開かれる 2 つの IndexReader の間で変更されます。
  • FullTextQuery.EXPLANATION は、クエリーの一致するオブジェクト/ドキュメントに対して Lucene Explanation オブジェクトを返します。これは、大量のデータの取得には適していません。FullTextQuery.EXPLANATION の実行コストは、一致する各要素に対して Lucene クエリーを実行するのに匹敵します。そのため、射影が推奨されます。

17.2.4.5. クエリー時間の制限

次のように、Infinispan Query でクエリーが要する時間を制限します。

  • 制限に達したら例外を発生します。
  • 時間制限に達したら取得された結果の数を制限します。

17.2.4.6. 時間制限での例外の発生

定義された時間を超えてクエリーが実行される場合に発生する、カスタム例外を定義できます。

CacheQuery API の使用時に制限を定義するには、以下の方法を使用します。

クエリー実行でのタイムアウトの定義

SearchManagerImplementor searchManager = (SearchManagerImplementor) Search.getSearchManager(cache);
searchManager.setTimeoutExceptionFactory(new MyTimeoutExceptionFactory());
CacheQuery cacheQuery = searchManager.getQuery(luceneQuery, Book.class);

//define the timeout in seconds
cacheQuery.timeout(2, TimeUnit.SECONDS);

try {
    cacheQuery.list();
}
catch (MyTimeoutException e) {
    //do something, too slow
}

private static class MyTimeoutExceptionFactory implements TimeoutExceptionFactory {
    @Override
    public RuntimeException createTimeoutException(String message, String queryDescription) {
        return new MyTimeoutException();
    }
}

public static class MyTimeoutException extends RuntimeException {
}

getResultSize()iterate()、および scroll() はメソッド呼び出しが終了するまでタイムアウトを考慮します。そのため、Iterable または ScrollableResults はタイムアウトを無視します。さらに、explain() はこのタイムアウト期間を考慮しません。このメソッドは、デバッグやクエリーのパフォーマンスが遅い理由をチェックするために使用されます。

重要

サンプルコードは、クエリーが指定された結果の値で停止することを保証しません。

17.3. 結果の読み出し

17.3.1. 結果の読み出し

Infinispan Query は構築後に HQL や Criteria クエリーと同様に実行できます。同じパラダイムとオブジェクトセマンティックが Lucene クエリーおよび list() などの一般的な操作すべてに適用されます。

17.3.2. パフォーマンスに関する注意点

list() を使用すると妥当な数の結果を受信し (たとえば、ページネーションを使用する場合など)、すべてに対応することができます。 list()batch-size エンティティーが適切に設定されている場合に最適に動作します。list() が使用されると、クエリーモジュールはページネーション内ですべての Lucene Hits 要素を処理します。

17.3.3. 結果サイズ

ユースケースによっては、一致するドキュメントの合計数に関する情報が必要になります。以下の例について考えてみましょう。

一致するドキュメントをすべて読み出すには大量のリソースを消費します。Lucene ベースのクエリー API は、ページネーションのパラメーターに関係なく、一致するドキュメントをすべて読み出します。一致するドキュメントをすべて読み出すには大量のリソースが必要であるため、Lucene ベースのクエリー API はページネーションのパラメーターに関係なく、一致するドキュメントの合計数を読み出しできます。一致するすべての要素は、オブジェクトのロードを発生せずに読み出されます。

クエリーの結果サイズの決定

CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(luceneQuery,
                Book.class);
//return the number of matching books without loading a single one
assert 3245 == cacheQuery.getResultSize();

CacheQuery cacheQueryLimited =
        Search.getSearchManager(cache).getQuery(luceneQuery, Book.class);
cacheQuery.maxResults(10);
List results = cacheQuery.list();
assert 10 == results.size();
//return the total number of matching books regardless of pagination
assert 3245 == cacheQuery.getResultSize();

インデックスが適切にデータベースと同期されていない場合、結果の数は近似値になります。この場合の例の 1 つとして非同期クラスターが挙げられます。

17.3.4. 結果の理解

Luke を使用すると、想定されるクエリー結果で結果が表示される (または表示されない) 理由を判断できます。また、クエリーモジュールは指定の結果 (指定のクエリーの) に対する Lucene Explanation オブジェクトも提供します。 これは上級クラスです。次のように Explanation オブジェクトにアクセスします。

cacheQuery.explain(int) メソッド

このメソッドでは、パラメーターとするドキュメント ID が必要で、Explanation オブジェクトを返します。

注記

explanation オブジェクトの構築は、Lucene クエリーの実行と同様にリソースを大量に消費します。実装で必要になる場合のみ、explanation オブジェクトを構築してください。

17.4. フィルター

17.4.1. フィルター

Apache Lucene は、カスタムのフィルター処理に従ってクエリーの結果をフィルターすることができます。これは、フィルターをキャッシュおよび再使用できるため、データの制限を追加で適用する強力な方法です。該当するユースケースには以下が含まれます。

  • セキュリティー
  • 一時データ (例: 閲覧専用の先月のデータ)
  • 入力 (population) フィルター (例: 指定のカテゴリーに限定される検索)
  • その他多くのユースケース

17.4.2. フィルターの定義および実装

Lucene ベースのクエリー API には、パラメーターが含まれる filters という名前の透過キャッシュが含まれます。この API は Hibernate Core フィルターと似ています。

クエリーに対するフルテキストフィルターの有効化

cacheQuery = Search.getSearchManager(cache).getQuery(query, Driver.class);
cacheQuery.enableFullTextFilter("bestDriver");
cacheQuery.enableFullTextFilter("security").setParameter("login", "andre");
cacheQuery.list(); //returns only best drivers where andre has credentials

この例では、クエリーで 2 つのフィルターが有効になっています。フィルターを有効または無効にしてクエリーをカスタマイズします。

@FullTextFilterDef アノテーションを使用してフィルターを宣言します。このアノテーションは、フィルターのクエリーに関係なく @Indexed エンティティーに適用されます。フィルター定義はグローバルであるため、各フィルターに一意な名前を付ける必要があります。同じ名前を持つ 2 つの @FullTextFilterDef アノテーションが定義された場合、SearchException が発生します。名前の付いた各フィルターにはそのフィルター実装を指定する必要があります。

フィルターの定義および実装

@FullTextFilterDefs({
    @FullTextFilterDef(name = "bestDriver", impl = BestDriversFilter.class),
    @FullTextFilterDef(name = "security", impl = SecurityFilterFactory.class)
})
public class Driver { ... }

public class BestDriversFilter extends org.apache.lucene.search.Filter {

    public DocIdSet getDocIdSet(IndexReader reader) throws IOException {
        OpenBitSet bitSet = new OpenBitSet(reader.maxDoc());
        TermDocs termDocs = reader.termDocs(new Term("score", "5"));
        while (termDocs.next()) {
            bitSet.set(termDocs.doc());
        }
        return bitSet;
    }
}

BestDriversFilter はドライバーへの結果セットを減らす Lucene フィルターで、スコアは 5 になります。この例では、フィルターは直接 org.apache.lucene.search.Filter を実装し、引数がないコンストラクターが含まれます。

17.4.3. @Factory フィルター

フィルターの作成に追加の手順が必要であったり、フィルターに引数がないコンストラクターがない場合は、以下のファクトリーパターンを使用します。

ファクトリーパターンを使用したフィルターの作成

@FullTextFilterDef(name = "bestDriver", impl = BestDriversFilterFactory.class)
public class Driver { ... }

public class BestDriversFilterFactory {

    @Factory
    public Filter getFilter() {
        //some additional steps to cache the filter results per IndexReader
        Filter bestDriversFilter = new BestDriversFilter();
        return new CachingWrapperFilter(bestDriversFilter);
    }
}

Lucene ベースのクエリー API は @Factory アノテーションが付いたメソッドを使用してフィルターインスタンスを構築します。ファクトリーには引数がないコンストラクターが含まれる必要があります。

名前付きフィルターは、パラメーターをフィルターに渡す必要がある場合に便利です。たとえば、セキュリティーフィルターが、適用するセキュリティーレベルを認識する場合を考えてみます。

パラメーターを定義されたフィルターに渡す

cacheQuery = Search.getSearchManager(cache).getQuery(query, Driver.class);
cacheQuery.enableFullTextFilter("security").setParameter("level", 5);

対象となる名前付きフィルター定義のフィルターまたはフィルターファクトリーのいずれかで、関連付けられたセッターが各パラメーターに含まれる必要があります。

実際のフィルター実装でのパラメーターの使用

public class SecurityFilterFactory {
    private Integer level;

    /**
     * injected parameter
     */
    public void setLevel(Integer level) {
        this.level = level;
    }

    @Key
    public FilterKey getKey() {
        StandardFilterKey key = new StandardFilterKey();
        key.addParameter(level);
        return key;
    }

    @Factory
    public Filter getFilter() {
        Query query = new TermQuery(new Term("level", level.toString()));
        return new CachingWrapperFilter(new QueryWrapperFilter(query));
    }
}

@Key アノテーションの付いたメソッドは FilterKey オブジェクトを返します。返されたオブジェクトには特別なコントラクトがあります。キーオブジェクトは equals() / hashCode() を実装して、特定の Filter タイプが同じでパラメーターのセットが同じ場合でのみ 2 つのキーが等しくなるようにします。つまり、キーが生成されるフィルターが交換可能な場合のみ 2 つのフィルターキーは等しくなります。キーオブジェクトはキャッシュメカニズムでキーとして使用されます。

17.4.4. キーオブジェクト

@Key メソッドは以下の場合のみ必要です。

  • フィルターキャッシュシステムが有効である (デフォルトで有効)
  • フィルターにパラメーターが含まれる

StandardFilterKeyequals() / hashCode() 実装をパラメーター equals および hashcode メソッドに委譲します。

定義されたフィルターはデフォルトでキャッシュされ、キャッシュはハード参照とソフト参照の組み合わせを使用して必要な場合にメモリーの破棄を許可します。ハード参照キャッシュは最後に使用されたフィルターを追跡し、使用頻度が最も低いフィルターを必要に応じて SoftReferences に変換します。ハード参照キャッシュの制限に達すると、追加のフィルターは SoftReferencesと してキャッシュされます。ハード参照キャッシュのサイズを調整するには、 default.filter.cache_strategy.size (デフォルト値は 128) を使用します。フィルターキャッシュの高度な使用については、独自の FilterCachingStrategy を実装してください。クラス名は default.filter.cache_strategy によって定義されます。

このフィルターキャッシュメカニズムを実際のフィルター結果と混同しないでください。Lucene では、CachingWrapperFilterIndexReader を使用してフィルターをラップすることが一般的です。このラッパーは、コストがかかる再計算を回避するために getDocIdSet(IndexReader reader) メソッドから返された DocIdSet をキャッシュします。リーダーは開いたときのインデックスの状態を表すため、計算される DocIdSet は同じ IndexReader インスタンスに対してのみキャッシュできることに注意してください。ドキュメントリストは開いた IndexReader 内で変更できません。ただし、別または新しい IndexReader インスタンスが Document の別のセット (別のインデックスのもの、またはインデックスが変更されたため) で動作することがあります。この場合、キャッシュされた DocIdSet は再計算する必要があります。

17.4.5. フルテキストフィルター

Lucene ベースのクエリー API は @FullTextFilterDefcache フラグを使用し、FilterCacheModeType.INSTANCE_AND_DOCIDSETRESULTS に設定します。これは、フィルターインスタンスを自動的にキャッシュし、CachingWrapperFilter の Hibernate 固有の実装にフィルターをラッピングします。Lucene バージョンのこのクラスとは異なり、 SoftReference はハード参照数 (フィルターキャッシュに関する説明を参照) とともに使用されます。ハード参照数は、default.filter.cache_docidresults.size を使用して調整できます (デフォルト値は 5)。ラッピングは @FullTextFilterDef.cache パラメーターを使用して調整されます。このパラメーターには以下の 3 つの値があります。

Value定義

FilterCacheModeType.NONE

フィルターインスタンスなしと結果なしは Hibernate Search によってキャッシュされます。フィルターの呼び出しごとに、新しいフィルターインスタンスが作成されます。この設定は、頻繁に変更するデータやメモリーの制約が大きい環境に役に立つことがあります。

FilterCacheModeType.INSTANCE_ONLY

フィルターインスタンスはキャッシュされ、同時 Filter.getDocIdSet() 呼び出しで再使用されます。DocIdSet の結果はキャッシュされません。この設定は、アプリケーション固有のイベントにより DocIdSet のキャッシュが不必要になったことが原因で、フィルターが独自のキャッシュメカニズムを使用する場合やフィルター結果が動的に変更される場合に役に立ちます。

FilterCacheModeType.INSTANCE_AND_DOCIDSETRESULTS

フィルターインスタンスの結果と DocIdSet の結果の両方がキャッシュされます。これはデフォルト値です。

フィルターは次の状況でキャッシュされる必要があります。

  • システムが対象となるエンティティーインデックスを頻繁に更新しない (つまり、IndexReader が頻繁に再利用される)
  • フィルターの DocIdSet の計算のコストが高い (クエリーを実行するのにかかる時間と比較して)

17.4.6. シャード化された環境におけるフィルターの使用

シャード化された環境にて、利用可能なシャードのサブセットでクエリーを実行するには、以下の手順にしたがいます。

  1. フィルター設定に応じて IndexManager のサブセットを選択するため、シャードストラテジーを作成します。
  2. クエリーの実行時にフィルターをアクティベートします。

以下は、customer フィルターがアクティベートされた場合に特定のシャードをクエリーするシャードストラテジーの例になります。

特定シャードのクエリー

public class CustomerShardingStrategy implements IndexShardingStrategy {

    // stored IndexManagers in a array indexed by customerID
    private IndexManager[] indexManagers;

    public void initialize(Properties properties, IndexManager[] indexManagers) {
        this.indexManagers = indexManagers;
    }

    public IndexManager[] getIndexManagersForAllShards() {
        return indexManagers;
    }

    public IndexManager getIndexManagerForAddition(
        Class<?> entity, Serializable id, String idInString, Document document) {
        Integer customerID = Integer.parseInt(document.getFieldable("customerID")
                                                      .stringValue());
        return indexManagers[customerID];
    }

    public IndexManager[] getIndexManagersForDeletion(
        Class<?> entity, Serializable id, String idInString) {
        return getIndexManagersForAllShards();
    }

    /**
     * Optimization; don't search ALL shards and union the results; in this case, we
     * can be certain that all the data for a particular customer Filter is in a single
     * shard; return that shard by customerID.
     */
    public IndexManager[] getIndexManagersForQuery(
        FullTextFilterImplementor[] filters) {
        FullTextFilter filter = getCustomerFilter(filters, "customer");
        if (filter == null) {
            return getIndexManagersForAllShards();
        }
        else {
            return new IndexManager[] { indexManagers[Integer.parseInt(
                filter.getParameter("customerID").toString())] };
        }
    }

    private FullTextFilter getCustomerFilter(FullTextFilterImplementor[] filters,
                                             String name) {
        for (FullTextFilterImplementor filter: filters) {
            if (filter.getName().equals(name)) return filter;
        }
        return null;
    }
}

この例では、customer フィルターが存在する場合、クエリーはカスタマー専用のシャードのみを使用します。customer フィルターが見つからない場合は、クエリーはすべてのシャードを返します。シャードストラテジーは提供されたパラメーターに応じて各フィルターに反応します。

クエリーの実行が必要なときに、フィルターをアクティベートします。フィルターは、クエリーの後に Lucene の結果をフィルターする通常のフィルターです (フィルターにて定義)。この代わりに、シャードストラテジーに渡され、クエリーの実行中は無視される特別なフィルターを使用できます。ShardSensitiveOnlyFilter クラスを使用してそのフィルターを宣言します。

ShardSensitiveOnlyFilter クラスの使用

@Indexed
@FullTextFilterDef(name = "customer", impl = ShardSensitiveOnlyFilter.class)
public class Customer {
   ...
}

CacheQuery cacheQuery = Search.getSearchManager(cache).getQuery(query,
    Customer.class);
cacheQuery.enableFullTextFilter("customer").setParameter("CustomerID", 5);
@SuppressWarnings("unchecked")
List results = cacheQuery.list();

ShardSensitiveOnlyFilter フィルターが使用される場合、Lucene フィルターを実装する必要はありません。これらのフィルターに反応するフィルターとシャードストラテジーを使用して、シャード化された環境でクエリーを迅速に実行します。

17.5. 継続的クエリー

17.5.1. 継続的クエリー

継続的クエリーは、アプリケーションが現在クエリーと一致するエントリーを受信できるようにし、クエリーされたデータセットへの変更が継続して通知されるようにします。これには、追加のキャッシュ操作による、受信の一致 (セットに参加した値に対する) と送信の一致 (セットから離脱した値に対する) の両方が含まれます。継続的なクエリーを使用すると、アプリケーションは安定してイベントを受信するため、変更を発見するために同じクエリーを繰り返し実行しません。そのため、リソースをより効率的に使用できます。

たとえば、以下のユースケースはすべて継続的なクエリーを利用できます。

  1. 年齢が 18 歳から 25 歳までの人を全員返します (Person エンティティーに age プロパティーがあり、ユーザーアプリケーションによって更新されることを前提とします)。
  2. $2000 を超える取引をすべて返します。
  3. 1:45.00s 未満である F1 レーサーのラップタイムをすべて返します (キャッシュに Lap エントリーが含まれ、レース中にラップタイムがリアルタイムで入力されることを前提とします)。

17.5.2. 継続的クエリーの評価

継続的クエリーは以下の場合に通知を受け取るリスナーを使用します。

  • エントリーが指定クエリーの一致を開始したとき。Join イベントによって表されます。
  • エントリーが指定クエリーの一致を停止したとき。Leave イベントによって表されます。

クライアントが継続的クエリーリスナーを登録した直後、現在クエリーと一致する結果の受信が開始されます。前述のとおり、これは Join イベントとして受信されます。さらに、通常は creation、modification、removal、または expiration イベントを生成するキャッシュ操作の結果として、他のエントリーがクエリーに一致すると、Join イベントとして後続の通知も受信され、クエリーの一致が停止されると Leave イベントとして受信されます。

リスナーが Join または Leave イベントを受信するかを判断するため、以下の論理が使用されます。

  1. 新旧両方の値でクエリーが false と評価された場合、イベントは抑制されます。
  2. 新旧両方の値でクエリーが true と評価された場合、イベントは抑制されます。
  3. 古い値でクエリーが false と評価され、新しい値でクエリーが true と評価された場合、Join イベントが送信されます。
  4. 古い値でクエリーが true と評価され、新しい値でクエリーが false と評価された場合、Leave イベントが送信されます。
  5. 古い値でクエリーが true と評価され、エントリーが削除された場合、Leave イベントが送信されます。
注記

継続的クエリーはグループ化、集計、およびソート操作を使用できません。

17.5.3. 継続的クエリーの使用

以下の手順は、ライブラリーモードとリモートクライアントサーバーモードの両方に適用されます。

継続的クエリーの追加

継続的クエリーを作成するため、他のクエリーメソッドと同様に Query オブジェクトが作成されますが、Queryorg.infinispan.query.api.continuous.ContinuousQuery と登録されるようにし、org.infinispan.query.api.continuous.ContinuousQueryListener が使用されているようにしてください。

キャッシュに関連付けられた ContinuousQuery オブジェクトを取得するには、クライアントサーバーモードで実行している場合は静的メソッド org.infinispan.client.hotrod.Search.getContinuousQuery(RemoteCache<K, V> cache) を呼び出し、ライブラリーモードで実行している場合は org.infinispan.query.Search.getContinuousQuery(Cache<K, V> cache) を呼び出します。

ContinuousQueryListener が定義されたら、ContinuousQueryaddContinuousQueryListener メソッドを使用して追加できます。

continuousQuery.addContinuousQueryListener(query, listener)

以下の例は、ライブラリーモードで継続的クエリーを実装および追加する簡単なメソッドを表しています。

継続的クエリーの定義および追加

import org.infinispan.query.api.continuous.ContinuousQuery;
import org.infinispan.query.api.continuous.ContinuousQueryListener;
import org.infinispan.query.Search;
import org.infinispan.query.dsl.QueryFactory;
import org.infinispan.query.dsl.Query;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

[...]

// To begin we create a ContinuousQuery instance on the cache
ContinuousQuery<Integer, Person> continuousQuery = Search.getContinuousQuery(cache);

// Define our query. In this case we will be looking for any
// Person instances under 21 years of age.
QueryFactory queryFactory = Search.getQueryFactory(cache);
Query query = queryFactory.from(Person.class)
    .having("age").lt(21)
    .build();

final Map<Integer, Person> matches = new ConcurrentHashMap<Integer, Person>();

// Define the ContinuousQueryListener
ContinuousQueryListener<Integer, Person> listener = new ContinuousQueryListener<Integer, Person>() {
    @Override
    public void resultJoining(Integer key, Person value) {
        matches.put(key, value);
    }

    @Override
    public void resultLeaving(Integer key) {
        matches.remove(key);
    }
};

// Add the listener and generated query
continuousQuery.addContinuousQueryListener(query, listener);

[...]

// Remove the listener to stop receiving notifications
continuousQuery.removeContinuousQueryListener(listener);

Person インスタンスが 21 未満の Age が含まれるキャッシュへ追加されると matches に配置されます。これらのエントリーがキャッシュから削除されると、matches からも削除されます。

継続的クエリーの削除

クエリーの実行を停止するには、リスナーを削除します。

continuousQuery.removeContinuousQueryListener(listener);

17.5.4. 継続的クエリーでのパフォーマンスに関する注意点

継続的クエリーは、アプリケーションが常に最新の状態を保持するよう設計されているため、特に広範囲のクエリーに対して生成されたイベントの数が多くなる可能性があります。さらに、各イベントに対して新たにメモリーが割り当てられます。注意してクエリーを作成しないと、この挙動が原因でメモリーの負荷が発生し、エラーなどを引き起こす可能性があります。

この問題を防ぐため、各クエリーには必要な情報のみを指定し、各 ContinuousQueryListener が受信したすべてのイベントを迅速に処理できるようにすることが強く推奨されます。