インデックス設定の細かな制御
リポジトリインターフェース経由では直接操作できない設定を適用する場合、アノテーションによる宣言的アプローチが推奨されます。インデックス作成時に@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を設定することで特定タイプのインデックス化を無効化し、dateDetectionやnumericDetectionで自動型推論を制御可能です。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));