Spring Data Elasticsearchにおける高度な検索・設定機能の活用ガイド

インデックス設定の細かな制御

リポジトリインターフェース経由では直接操作できない設定を適用する場合、アノテーションによる宣言的アプローチが推奨されます。インデックス作成時に@Settingを用いることで、シャード数やレプリカ数、リフレッシュ間隔などを定義可能です。主な設定項目には以下のものがあります。

  • useServerConfiguration: サーバーデフォルト設定を優先し、クライアント側からのパラメータ送信をスキップ
  • settingPath: クラスパス上に配置したJSON設定ファイルの参照パス
  • shards: プライマリシャード数(初期値: 1)
  • replicas: レプリカ数(初期値: 1)
  • refreshInterval: データの反映頻度(初期値: 1s)
  • indexStoreType: ストレージエンジンタイプ(初期値: fs)

さらに、ソート順をインデックスレベルで指定することもできます。以下の実装例では、日付と優先度の降順ソートを定義しています。ソートモードや欠損値の扱いも配列として設定可能ですが、要素数はソート対象フィールドと一致させる必要があります。


@Document(indexName = "product_catalog")
@Setting(
  shards = 3,
  replicas = 1,
  refreshInterval = "2s",
  sortFields = { "releaseDate", "priorityRank" },
  sortModes = { Setting.SortMode.min, Setting.SortMode.max },
  sortOrders = { Setting.SortOrder.desc, Setting.SortOrder.asc },
  sortMissingValues = { Setting.SortMissing._first, Setting.SortMissing._last })
public class ProductDocument {
    @Nullable @Id private String productId;
    @Nullable @Field(name = "release_date", type = FieldType.Date) private LocalDate releaseDate;
    @Nullable @Field(name = "priority_rank", type = FieldType.Integer) private Integer priorityRank;
    // アクセサメソッドは省略
}

マッピング定義の外部ファイル化と拡張

自動マッピング生成をバイパスし、独自定義を適用したい場合は@Mappingアノテーションを利用します。mappingPath属性にクラスパス上のJSONリソースを指定すると、Spring Data Elasticsearchはその内容をそのままインデックス定義として使用します。また、enabled = falseを設定することで特定タイプのインデックス化を無効化し、dateDetectionnumericDetectionで自動型推論を制御可能です。dynamicDateFormatsには複数の日付パターン配列を渡せます。

ランタイムフィールドの定義ファイルを外部化する場合は、runtimeFieldsPathにJSONパスを指定します。


@Document(indexName = "access_logs")
@Mapping(runtimeFieldsPath = "/es-config/runtime-definitions.json")
public class AccessLogEntity {
    @Id @Nullable private String logId;
    @Field(type = FieldType.Keyword) @Nullable private String endpoint;
    // その他のフィールド
}

フィルターコンテキストの最適化

スコアリング計算を不要とする場合は、クエリをフィルターコンテキストに移動させることでキャッシュ効率が向上し、検索速度が改善します。NativeQueryビルダーのwithFilterメソッドを利用すると、通常のクエリと分離して定義できます。


ElasticsearchOperations esClient = ...; // DI経由で取得
IndexCoordinates targetIndex = IndexCoordinates.of("transaction_data");

NativeQueryBuilder reqBuilder = NativeQuery.builder();
reqBuilder.withQuery(q -> q.matchAll(m -> m));
Query searchReq = reqBuilder
    .withFilter(f -> f.bool(b -> b
        .filter(term -> term
            .field("accountStatus")
            .value("VERIFIED")
        )
        .must(term2 -> term2
            .term(t -> t.field("region").value("JP"))
        )
    ))
    .build();

SearchHits<TransactionDoc> results = esClient.search(searchReq, TransactionDoc.class, targetIndex);

大容量データ取得のためのスクロール処理

一度に大量のレコードを取得する際、メモリ負荷を抑制するためにスクロールAPIが利用されます。Spring DataではsearchForStreamが内部でスクロール機構を抽象化しています。


Query streamReq = NativeQuery.builder()
    .withQuery(q -> q.matchAll(m -> m))
    .withFields("payload", "createdAt")
    .withPageable(PageRequest.of(0, 500))
    .build();

try (SearchHitsIterator<LogEntry> logStream = esClient.searchForStream(streamReq, LogEntry.class, targetIndex)) {
    List<LogEntry> aggregatedData = new ArrayList<>();
    while (logStream.hasNext()) {
        aggregatedData.add(logStream.next());
    }
    // 取得データの処理
}

スクロールIDを直接制御したい場合は、基盤となるテンプレートメソッドを呼び出します。スクロールセッションの開始、継続、クリアを明示的に管理可能です。


AbstractElasticsearchTemplate baseTemplate = (AbstractElasticsearchTemplate) esClient;
String sessionToken = null;

SearchScrollHits<LogEntry> initialScroll = baseTemplate.searchScrollStart(
    Duration.ofSeconds(30), streamReq, LogEntry.class, targetIndex);

sessionToken = initialScroll.getScrollId();
List<LogEntry> batchResult = new ArrayList<>();

while (initialScroll.hasSearchHits()) {
    batchResult.addAll(initialScroll.getSearchHits());
    sessionToken = initialScroll.getScrollId();
    initialScroll = baseTemplate.searchScrollContinue(sessionToken, Duration.ofSeconds(30), LogEntry.class);
}
baseTemplate.searchScrollClear(sessionToken);

リポジトリ層でストリームを返す場合、戻り値をStream<T>として宣言するだけで、フレームワークが自動的にスクロール実装に切り替えます。

高度なソート条件の指定

標準のソートオプションに加え、地理空間情報に基づく距離ソートや、Elasticsearch固有のパラメータを付与したカスタムOrderクラスが提供されています。位置情報フィールドを含むエンティティに対してはGeoDistanceOrderが利用可能です。


GeoPoint referencePoint = new GeoPoint(35.681236, 139.767125);
Sort distanceSort = Sort.by(new GeoDistanceOrder("storeLocation", referencePoint).order(Sort.Direction.ASC));
Query geoQuery = CriteriaQuery.builder().all().withSort(distanceSort).build();

クエリ実行時およびマッピングレベルでのランタイムフィールド

Elasticsearch 7.12以降で導入されたランタイムフィールドは、インデックス再構成なしに動的な値計算を可能にします。Spring Dataでは、インデックス定義時と検索クエリ実行時の2つのコンテキストでサポートされています。

クエリ実行時にランタイムフィールドを適用する場合、RuntimeFieldオブジェクトを生成しクエリに追加します。以下の例では、基本価格に税率を乗算したフィールドをその場で生成し、検索条件に利用しています。


RuntimeField calcField = new RuntimeField(
    "taxIncludedPrice", 
    "double", 
    "emit(doc['basePrice'].value * doc['taxMultiplier'].value)"
);

CriteriaQuery priceQuery = new CriteriaQuery(new Criteria("taxIncludedPrice").greaterThanEqual(10000.0));
priceQuery.addRuntimeField(calcField);

SearchHits<ItemDoc> hits = esClient.search(priceQuery, ItemDoc.class);

Point in Time (PIT) による整合性維持

長時間にわたるページング処理中にデータが更新され、結果の重複や欠落が発生するのを防ぐため、PITスナップショットが利用されます。最初に保持期間(Keep-alive)を指定してPIT IDを取得し、以降の検索に付与します。


String pitIdentifier = esClient.openPointInTime(IndexCoordinates.of("user_activity"), Duration.ofMinutes(5));

Query firstPage = CriteriaQuery.builder()
    .where("eventType").is("LOGIN")
    .withPointInTime(new Query.PointInTime(pitIdentifier, Duration.ofMinutes(5)))
    .build();

SearchHits<UserLog> pageResult = esClient.search(firstPage, UserLog.class);
String nextPitId = pageResult.getPointInTimeId();

// 後続ページでも同様に新しいIDを使用
esClient.closePointInTime(nextPitId);

検索テンプレート(Mustache)の活用

複雑なクエリ構造を再利用可能なテンプレートとして保存する場合、Script APIとSearch Template機能が役立ちます。まずMustache形式でテンプレートを登録します。


ScriptDefinition tmplScript = Script.builder()
    .id("employee_search_v2")
    .lang("mustache")
    .source("""
      {
        "query": { "match": { "department": "{{dept}}" } },
        "size": {{maxResults}},
        "sort": [{ "joinDate": "desc" }]
      }
      """)
    .build();

esClient.scriptOps().putScript(tmplScript);

登録済みテンプレートはSearchTemplateQueryを通じて呼び出せます。パラメータはMap形式でバインドされ、ページング情報もテンプレート内に展開可能です。


SearchTemplateQuery templateQuery = SearchTemplateQuery.builder()
    .id("employee_search_v2")
    .params(Map.of(
        "dept", "engineering",
        "maxResults", pageable.getPageSize(),
        "offset", pageable.getOffset()
    ))
    .build();

SearchHits<Employee> tmplResults = esClient.search(templateQuery, Employee.class);

ネストされたオブジェクトのソート

配列フィールドやネスト型オブジェクト内の値を基準にソートを行う場合は、ネストパスとフィルターを明示する必要があります。Javaのフィールド名とElasticsearchの実際のフィールド名が異なる場合は注意が必要です。フィルタークエリにはCriteriaQueryが変換される関係上、StringQueryまたはNativeQueryの利用が推奨されます。


String filterJson = """
  { "bool": { "filter": { "term": { "courses.students.enrolled": true } } } }
  """;

StringQuery baseQuery = StringQuery.builder(filterJson).build();

Order nestedOrder = new Order(Sort.Direction.DESC, "courses.students.finalScore")
    .withNested(
        Nested.builder("courses")
            .withNested(
                Nested.builder("courses.students")
                    .filter(baseQuery)
                    .build()
            )
            .build()
    );

Query courseQuery = Query.findAll().addSort(Sort.by(nestedOrder));

タグ: spring-data-elasticsearch index-settings search-templates point-in-time-api nested-sorting

8月10日 09:23 投稿