ElasticsearchにおけるJavaによる効率的なページング手法

通常の検索フローでは、最初の10件を取得する場合、クライアントがノードにリクエストを送信し、各シャードが上位10件を返し、ノードがそれらを集約して最終的にトップ10を返します。

では、11件目から20件目を取得したい場合はどうすれば良いでしょうか?ここで登場するのがページング処理です。

「浅いページング」は最も単純な方法で、先頭20件を取得した後、最初の10件を捨てて残り10件を返す方式です。この方法では、不要な前半データの取得・処理が発生し、無駄な負荷がかかります。

サンプルデータの準備

@Test
public void generateSampleData() throws IOException {
    for (int idx = 1; idx <= 100; idx++) {
        XContentBuilder content = jsonBuilder()
            .startObject()
                .field("bookTitle", "書籍" + idx)
                .field("writer", "著者" + idx)
                .field("recordId", idx)
                .field("summary", "宇宙論に関する概要文...(省略)")
            .endObject();

        IndexResponse resp = client.prepareIndex("library", "publication")
            .setSource(content.string(), XContentType.JSON)
            .get();

        System.out.printf("インデックス: %s, タイプ: %s, ID: %s, バージョン: %d%n",
            resp.getIndex(), resp.getType(), resp.getId(), resp.getVersion());
    }
}

基本ページング(浅いページング)

@Test
public void basicPagination() {
    SearchRequestBuilder baseQuery = client.prepareSearch("library")
        .setTypes("publication")
        .setQuery(QueryBuilders.matchAllQuery());

    long totalRecords = baseQuery.get().getHits().getTotalHits();
    int itemsPerPage = 10;
    int totalPages = (int) Math.ceil((double) totalRecords / itemsPerPage);

    for (int pageNum = 0; pageNum < totalPages; pageNum++) {
        System.out.println("=== ページ " + (pageNum + 1) + " ===");

        SearchResponse response = baseQuery
            .setFrom(pageNum * itemsPerPage)
            .setSize(itemsPerPage)
            .get();

        for (SearchHit hit : response.getHits()) {
            System.out.println(hit.getSourceAsString());
        }
    }
}

深層ページング(Scroll APIによる最適化)

大規模なページングでは、従来の方法では不要なドキュメントを多数読み込むため非効率です。Scroll APIは初期検索時にスナップショットを作成し、以降はそのスナップショットに対してカーソル移動でデータを取得します。これにより、挿入・更新された新規データは結果に含まれず、高速かつ安定したページングが可能になります。

@Test
public void optimizedScrollPagination() {
    SearchResponse initialResp = client.prepareSearch("library")
        .setTypes("publication")
        .setSize(10)
        .setScroll(TimeValue.timeValueSeconds(60))
        .get();

    long total = initialResp.getHits().getTotalHits();
    int totalPages = (int) Math.ceil((double) total / 10);

    System.out.println("総ページ数: " + totalPages);

    String scrollId = initialResp.getScrollId();
    for (int page = 1; page <= totalPages; page++) {
        System.out.println("=== Scroll ページ " + page + " ===");

        SearchResponse scrollResp = client.prepareSearchScroll(scrollId)
            .setScroll(TimeValue.timeValueSeconds(60))
            .execute()
            .actionGet();

        scrollId = scrollResp.getScrollId();
        for (SearchHit hit : scrollResp.getHits()) {
            System.out.println(hit.getSourceAsString());
        }
    }
}

タグ: Elasticsearch Java ScrollAPI ページング 検索最適化

5月21日 13:12 投稿