← Kursa Dön
📄 Text · 35 min

Java ile Arama — SearchRequest ve Aggregation

Giriş — Kütüphaneci ve Katalog Sistemi

Kütüphaneye gidip "yapay zeka hakkında, son 2 yılda basılmış, Türkçe kitaplar" diye sorduğunuzu düşünün. Kütüphaneci katalog sistemine bakar: "yapay zeka" kelimesini arar (match query), tarih filtresini uygular (range), dili kontrol eder (term), sonuçları basım tarihine göre sıralar (sort), ilk 10'u gösterir (pagination). Sonra der ki: "Toplam 47 kitap buldum, en çok bilgisayar bilimleri kategorisinde" (aggregation).

Elasticsearch Java Client ile arama tam olarak bu akışta çalışır. Önceki derslerde document'ları CRUD ile yönettik — şimdi onları arayacağız. Query DSL'in Java karşılığı olan builder API'sini, aggregation'ları, pagination'ı ve highlight'ı öğreneceğiz.


1. İlk Arama — Temel SearchRequest

1.1 Match All — Tüm Documentlar

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.TotalHits;

public class SearchBasics {

    public static void matchAll(ElasticsearchClient client) throws Exception {
        SearchResponse<Product> response = client.search(s -> s
            .index("products")
            .query(q -> q.matchAll(m -> m)),
            Product.class
        );

        TotalHits total = response.hits().total();
        System.out.println("Toplam sonuç: " + total.value());
        System.out.println("Relation: " + total.relation()); // Eq veya Gte

        for (Hit<Product> hit : response.hits().hits()) {
            System.out.printf("  [%.2f] %s: %s%n",
                hit.score(), hit.id(), hit.source().getName());
        }
    }
}

1.2 Match Query — Full-Text Arama

public static void matchQuery(ElasticsearchClient client) throws Exception {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .query(q -> q
            .match(m -> m
                .field("description")
                .query("profesyonel dizüstü bilgisayar")
                .fuzziness("AUTO")       // Yazım hatası toleransı
                .minimumShouldMatch("2") // En az 2 kelime eşleşmeli
            )
        ),
        Product.class
    );

    System.out.println("Bulunan: " + response.hits().total().value());
    for (Hit<Product> hit : response.hits().hits()) {
        System.out.printf("  [%.4f] %s — %s%n",
            hit.score(), hit.source().getName(), hit.source().getDescription());
    }
}

1.3 Response Yapısı

SearchResponse<Product> response = client.search(/* ... */, Product.class);

long totalHits = response.hits().total().value();  // Toplam sonuç
long took       = response.took();                  // Süre (ms)
boolean timedOut = response.timedOut();             // Timeout?

for (Hit<Product> hit : response.hits().hits()) {
    String id       = hit.id();       // Document ID
    Double score    = hit.score();    // Relevance skoru
    Product source  = hit.source();   // Document içeriği
}

2. Query Tipleri

2.1 Term Query — Exact Match

// Keyword alanda birebir eşleşme
SearchResponse<Product> response = client.search(s -> s
    .index("products")
    .query(q -> q
        .term(t -> t
            .field("category")
            .value("electronics")
        )
    ),
    Product.class
);

⚠️ term vs match: term analiz edilmemiş (keyword) alanlarda kullanılır — text alanda kullanmak beklenmeyen sonuçlar verir çünkü text alanlar lowercase'e dönüştürülür ama term query dönüştürmez.

2.2 Range Query

// Fiyatı 10.000 - 50.000 arası ürünler
SearchResponse<Product> response = client.search(s -> s
    .index("products")
    .query(q -> q
        .range(r -> r
            .field("price")
            .gte(co.elastic.clients.json.JsonData.of(10000))
            .lte(co.elastic.clients.json.JsonData.of(50000))
        )
    ),
    Product.class
);

// Tarih aralığı — aynı mantık
SearchResponse<Product> dateRange = client.search(s -> s
    .index("products")
    .query(q -> q.range(r -> r.field("created_at")
        .gte(co.elastic.clients.json.JsonData.of("2024-01-01"))
        .lt(co.elastic.clients.json.JsonData.of("2024-07-01"))
    )),
    Product.class
);

2.4 Multi-Match Query

// Birden fazla alanda aynı anda ara
SearchResponse<Product> response = client.search(s -> s
    .index("products")
    .query(q -> q
        .multiMatch(mm -> mm
            .query("apple laptop")
            .fields("name^3", "description", "tags^2") // name 3x boost
            .type(co.elastic.clients.elasticsearch._types.query_dsl
                .TextQueryType.BestFields)
            .fuzziness("AUTO")
        )
    ),
    Product.class
);

3. Bool Query — Karmaşık Sorgular

Bool query Elasticsearch'ün en güçlü silahıdır — birden fazla koşulu mantıksal operatörlerle birleştirir.

public class BoolQueryExamples {

    public static void complexSearch(ElasticsearchClient client) throws Exception {
        // "electronics" kategorisinde,
        // fiyatı 10.000-80.000 arası,
        // "apple" veya "samsung" etiketli,
        // stoku 0 OLMAYAN ürünler
        SearchResponse<Product> response = client.search(s -> s
            .index("products")
            .query(q -> q
                .bool(b -> b
                    // must — ZORUNLU, skora katkı sağlar
                    .must(m -> m
                        .term(t -> t.field("category").value("electronics"))
                    )
                    // filter — ZORUNLU, skora katkı sağlamaz (cache'lenir)
                    .filter(f -> f
                        .range(r -> r
                            .field("price")
                            .gte(co.elastic.clients.json.JsonData.of(10000))
                            .lte(co.elastic.clients.json.JsonData.of(80000))
                        )
                    )
                    // should — EN AZ BİR tanesi eşleşmeli (minimum_should_match)
                    .should(sh -> sh
                        .term(t -> t.field("tags").value("apple"))
                    )
                    .should(sh -> sh
                        .term(t -> t.field("tags").value("samsung"))
                    )
                    .minimumShouldMatch("1")
                    // must_not — OLMAMALI
                    .mustNot(mn -> mn
                        .term(t -> t.field("stock").value(0))
                    )
                )
            ),
            Product.class
        );

        System.out.println("Sonuç: " + response.hits().total().value());
        for (Hit<Product> hit : response.hits().hits()) {
            Product p = hit.source();
            System.out.printf("  [%.2f] %s — %.2f TL (stok: %d)%n",
                hit.score(), p.getName(), p.getPrice(), p.getStock());
        }
    }
}

must vs filter Farkı

// ❌ Her koşul must'ta — gereksiz skor hesaplaması
.bool(b -> b
    .must(m -> m.term(t -> t.field("category").value("electronics")))
    .must(m -> m.range(r -> r.field("price").gte(JsonData.of(1000))))
    .must(m -> m.range(r -> r.field("stock").gt(JsonData.of(0))))
)

// ✅ Sadece skor gereken koşullar must'ta, geri kalanlar filter'da
.bool(b -> b
    .must(m -> m.match(t -> t.field("name").query("laptop")))  // Skor önemli
    .filter(f -> f.term(t -> t.field("category").value("electronics")))  // Cache
    .filter(f -> f.range(r -> r.field("price").gte(JsonData.of(1000))))  // Cache
    .filter(f -> f.range(r -> r.field("stock").gt(JsonData.of(0))))      // Cache
)

💡 filter içindeki koşullar Elasticsearch tarafından cache'lenir ve skor hesaplanmaz — performans açısından büyük fark yaratır.


4. Sorting (Sıralama)

4.1 Tekli ve Çoklu Sıralama

import co.elastic.clients.elasticsearch._types.SortOrder;

SearchResponse<Product> response = client.search(s -> s
    .index("products")
    .query(q -> q.matchAll(m -> m))
    .sort(so -> so.field(f -> f.field("price").order(SortOrder.Asc)))
    .sort(so -> so.field(f -> f.field("created_at").order(SortOrder.Desc))),
    Product.class
);

// Sıralama değerleri
for (Hit<Product> hit : response.hits().hits()) {
    System.out.printf("  %s — %.2f TL (sort: %s)%n",
        hit.source().getName(), hit.source().getPrice(), hit.sort());
}

Relevance skoru ile field sıralamasını birleştirebilirsiniz: .sort(so -> so.score(sc -> sc.order(SortOrder.Desc))) + .sort(so -> so.field(f -> f.field("price").order(SortOrder.Asc))) — önce skora, eşitse fiyata göre sıralar.


5. Pagination

5.1 from/size — Basit Pagination

public class PaginationExamples {

    public static void fromSizePagination(ElasticsearchClient client,
                                           int page, int pageSize) throws Exception {
        int from = (page - 1) * pageSize;

        SearchResponse<Product> response = client.search(s -> s
            .index("products")
            .query(q -> q.matchAll(m -> m))
            .from(from)
            .size(pageSize)
            .sort(so -> so.field(f -> f.field("price").order(SortOrder.Asc))),
            Product.class
        );

        long totalHits = response.hits().total().value();
        int totalPages = (int) Math.ceil((double) totalHits / pageSize);

        System.out.printf("Sayfa %d/%d (toplam: %d)%n", page, totalPages, totalHits);
        for (Hit<Product> hit : response.hits().hits()) {
            System.out.println("  " + hit.source().getName());
        }
    }
}

⚠️ from + size ≤ 10.000 — Elasticsearch varsayılan limiti. Derin sayfalama için search_after kullanın.

5.2 search_after — Derin Sayfalama

public static void searchAfterPagination(ElasticsearchClient client) throws Exception {
    int pageSize = 20;
    List<String> searchAfter = null;

    for (int page = 1; page <= 5; page++) {
        var searchBuilder = new co.elastic.clients.elasticsearch.core
            .SearchRequest.Builder()
            .index("products")
            .query(q -> q.matchAll(m -> m))
            .size(pageSize)
            .sort(so -> so.field(f -> f.field("price").order(SortOrder.Asc)))
            .sort(so -> so.field(f -> f.field("_id").order(SortOrder.Asc)));

        // İlk sayfa hariç search_after ekle
        if (searchAfter != null) {
            searchBuilder.searchAfter(searchAfter);
        }

        SearchResponse<Product> response = client.search(
            searchBuilder.build(), Product.class);

        List<Hit<Product>> hits = response.hits().hits();
        if (hits.isEmpty()) break;

        System.out.printf("--- Sayfa %d ---%n", page);
        for (Hit<Product> hit : hits) {
            System.out.printf("  %s — %.2f TL%n",
                hit.source().getName(), hit.source().getPrice());
        }

        // Son hit'in sort değerlerini al — sonraki sayfa için
        Hit<Product> lastHit = hits.get(hits.size() - 1);
        searchAfter = lastHit.sort();
    }
}

💡 search_after ile sayfalama limitsizdir — milyonlarca sonuç arasında sayfalanabilir. Ama rastgele sayfaya atlayamazsınız, her zaman sırayla ilerlemelisiniz.


6. Source Filtering

// Sadece belirli alanları getir
SearchResponse<Product> response = client.search(s -> s
    .index("products")
    .query(q -> q.matchAll(m -> m))
    .source(src -> src
        .filter(f -> f
            .includes("name", "price", "category")
            .excludes("description", "tags")
        )
    ),
    Product.class
);

// Source tamamen kapatma — sadece ID ve score lazımsa
SearchResponse<Void> idsOnly = client.search(s -> s
    .index("products")
    .query(q -> q.matchAll(m -> m))
    .source(src -> src.fetch(false)),
    Void.class
);

for (Hit<Void> hit : idsOnly.hits().hits()) {
    System.out.println("ID: " + hit.id());
}

7. Highlight — Eşleşen Metni Vurgulama

import co.elastic.clients.elasticsearch.core.search.Highlight;

public class HighlightExample {

    public static void searchWithHighlight(ElasticsearchClient client) throws Exception {
        SearchResponse<Product> response = client.search(s -> s
            .index("products")
            .query(q -> q
                .match(m -> m.field("description").query("profesyonel bilgisayar"))
            )
            .highlight(h -> h
                .fields("description", hf -> hf
                    .preTags("<em>")
                    .postTags("</em>")
                    .fragmentSize(150)
                    .numberOfFragments(3)
                )
                .fields("name", hf -> hf
                    .preTags("<strong>")
                    .postTags("</strong>")
                )
            ),
            Product.class
        );

        for (Hit<Product> hit : response.hits().hits()) {
            System.out.println("Ürün: " + hit.source().getName());

            // Highlight sonuçları
            if (hit.highlight() != null) {
                hit.highlight().forEach((field, fragments) -> {
                    System.out.println("  " + field + ":");
                    fragments.forEach(fragment ->
                        System.out.println("    → " + fragment));
                });
            }
        }
    }
}

Çıktı:

Ürün: MacBook Pro 16
  description:
    → Apple M3 Max çipli <em>profesyonel</em> dizüstü <em>bilgisayar</em>

8. Aggregation — Veri Analizi

Aggregation, arama sonuçları üzerinde istatistik ve gruplama yapar. Dashboard'lar, raporlar ve analitik için kullanılır.

8.1 Terms Aggregation — Kategoriye Göre Gruplama

import co.elastic.clients.elasticsearch._types.aggregations.*;

public class AggregationExamples {

    public static void termsAggregation(ElasticsearchClient client) throws Exception {
        SearchResponse<Void> response = client.search(s -> s
            .index("products")
            .size(0)  // Document'ları getirme, sadece aggregation
            .aggregations("categories", a -> a
                .terms(t -> t
                    .field("category")
                    .size(20)  // En fazla 20 bucket
                )
            ),
            Void.class
        );

        // Aggregation sonuçlarını parse et
        StringTermsAggregate termsAgg = response.aggregations()
            .get("categories")
            .sterms();

        System.out.println("Kategoriler:");
        for (StringTermsBucket bucket : termsAgg.buckets().array()) {
            System.out.printf("  %s: %d ürün%n",
                bucket.key().stringValue(), bucket.docCount());
        }
    }
}

8.2 Metric Aggregations — İstatistikler

public static void metricAggregations(ElasticsearchClient client) throws Exception {
    SearchResponse<Void> response = client.search(s -> s
        .index("products")
        .size(0)
        .aggregations("avg_price", a -> a.avg(av -> av.field("price")))
        .aggregations("max_price", a -> a.max(mx -> mx.field("price")))
        .aggregations("min_price", a -> a.min(mn -> mn.field("price")))
        .aggregations("price_stats", a -> a.stats(st -> st.field("price"))),
        Void.class
    );

    double avgPrice = response.aggregations().get("avg_price").avg().value();
    double maxPrice = response.aggregations().get("max_price").max().value();
    double minPrice = response.aggregations().get("min_price").min().value();

    System.out.printf("Fiyat: min=%.2f, max=%.2f, avg=%.2f%n",
        minPrice, maxPrice, avgPrice);

    // Stats — hepsi bir arada
    StatsAggregate stats = response.aggregations().get("price_stats").stats();
    System.out.printf("Stats: count=%d, sum=%.2f, avg=%.2f%n",
        stats.count(), stats.sum(), stats.avg());
}

8.3 Date Histogram

public static void dateHistogram(ElasticsearchClient client) throws Exception {
    SearchResponse<Void> response = client.search(s -> s
        .index("products")
        .size(0)
        .aggregations("monthly", a -> a
            .dateHistogram(dh -> dh
                .field("created_at")
                .calendarInterval(CalendarInterval.Month)
                .format("yyyy-MM")
                .minDocCount(0)
            )
        ),
        Void.class
    );

    DateHistogramAggregate dateAgg = response.aggregations()
        .get("monthly").dateHistogram();

    System.out.println("Aylık ürün sayısı:");
    for (DateHistogramBucket bucket : dateAgg.buckets().array()) {
        System.out.printf("  %s: %d ürün%n",
            bucket.keyAsString(), bucket.docCount());
    }
}

8.4 Nested Aggregation — Sub-Aggregation

public static void nestedAggregation(ElasticsearchClient client) throws Exception {
    // Her kategorinin ortalama fiyatı
    SearchResponse<Void> response = client.search(s -> s
        .index("products")
        .size(0)
        .aggregations("categories", a -> a
            .terms(t -> t.field("category").size(20))
            .aggregations("avg_price", sub -> sub
                .avg(av -> av.field("price"))
            )
            .aggregations("price_range", sub -> sub
                .stats(st -> st.field("price"))
            )
        ),
        Void.class
    );

    StringTermsAggregate cats = response.aggregations()
        .get("categories").sterms();

    for (StringTermsBucket bucket : cats.buckets().array()) {
        double avgPrice = bucket.aggregations().get("avg_price").avg().value();
        StatsAggregate priceStats = bucket.aggregations()
            .get("price_range").stats();

        System.out.printf("  %s: %d ürün, avg=%.2f, min=%.2f, max=%.2f%n",
            bucket.key().stringValue(),
            bucket.docCount(),
            avgPrice,
            priceStats.min(),
            priceStats.max());
    }
}

9. Multi-Search — Tek İstekte Birden Fazla Sorgu

import co.elastic.clients.elasticsearch.core.MsearchResponse;
import co.elastic.clients.elasticsearch.core.msearch.MultiSearchResponseItem;

public class MultiSearchExample {

    public static void multiSearch(ElasticsearchClient client) throws Exception {
        MsearchResponse<Product> response = client.msearch(ms -> ms
            .searches(s -> s
                .header(h -> h.index("products"))
                .body(b -> b.query(q -> q
                    .match(m -> m.field("category").query("electronics")))
                    .size(5))
            )
            .searches(s -> s
                .header(h -> h.index("products"))
                .body(b -> b.query(q -> q
                    .range(r -> r.field("price")
                        .gte(co.elastic.clients.json.JsonData.of(50000))))
                    .size(5))
            ),
            Product.class
        );

        int i = 0;
        for (MultiSearchResponseItem<Product> item : response.responses()) {
            if (item.isResult()) {
                var result = item.result();
                System.out.printf("Sorgu %d: %d sonuç%n",
                    ++i, result.hits().total().value());
                result.hits().hits().forEach(hit ->
                    System.out.println("  → " + hit.source().getName()));
            } else {
                System.out.printf("Sorgu %d: HATA — %s%n",
                    ++i, item.failure().error().reason());
            }
        }
    }
}

💡 Multi-search birden fazla bağımsız aramayı tek HTTP isteğinde toplar — dashboard sayfaları için ideal.


10. Bütünleşik Örnek — E-Ticaret Arama Servisi

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.aggregations.*;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import java.util.List;

public class ProductSearchService {

    private final ElasticsearchClient client;
    private static final String INDEX = "products";

    public ProductSearchService(ElasticsearchClient client) {
        this.client = client;
    }

    /**
     * Genel ürün arama — filtre, sıralama, sayfalama, highlight destekli
     */
    public SearchResponse<Product> search(String keyword, String category,
                                           Double minPrice, Double maxPrice,
                                           int page, int pageSize,
                                           String sortField, SortOrder sortOrder)
                                           throws Exception {

        return client.search(s -> {
            s.index(INDEX)
             .from((page - 1) * pageSize)
             .size(pageSize);

            // Query
            s.query(q -> q.bool(b -> {
                // Keyword arama (varsa)
                if (keyword != null && !keyword.isBlank()) {
                    b.must(m -> m.multiMatch(mm -> mm
                        .query(keyword)
                        .fields("name^3", "description", "tags^2")
                        .fuzziness("AUTO")
                    ));
                }

                // Kategori filtresi
                if (category != null) {
                    b.filter(f -> f.term(t -> t
                        .field("category").value(category)));
                }

                // Fiyat aralığı
                if (minPrice != null || maxPrice != null) {
                    b.filter(f -> f.range(r -> {
                        r.field("price");
                        if (minPrice != null) r.gte(JsonData.of(minPrice));
                        if (maxPrice != null) r.lte(JsonData.of(maxPrice));
                        return r;
                    }));
                }

                // Stokta olanlar
                b.filter(f -> f.range(r -> r
                    .field("stock").gt(JsonData.of(0))));

                return b;
            }));

            // Sıralama
            if (sortField != null) {
                s.sort(so -> so.field(f -> f
                    .field(sortField).order(sortOrder)));
            }

            // Highlight
            if (keyword != null) {
                s.highlight(h -> h
                    .fields("name", hf -> hf
                        .preTags("<mark>").postTags("</mark>"))
                    .fields("description", hf -> hf
                        .preTags("<mark>").postTags("</mark>")
                        .fragmentSize(200))
                );
            }

            // Aggregations — facet'ler
            s.aggregations("categories", a -> a
                .terms(t -> t.field("category").size(20)));
            s.aggregations("price_stats", a -> a
                .stats(st -> st.field("price")));
            s.aggregations("price_ranges", a -> a
                .range(r -> r.field("price")
                    .ranges(rng -> rng.to("10000").key("budget"))
                    .ranges(rng -> rng.from("10000").to("50000").key("mid"))
                    .ranges(rng -> rng.from("50000").key("premium"))
                ));

            return s;
        }, Product.class);
    }

    /** Sonuçları yazdır */
    public void printResults(SearchResponse<Product> response, int page, int size) {
        long total = response.hits().total().value();
        System.out.printf("%n=== Sayfa %d (%d ms, %d sonuç) ===%n",
            page, response.took(), total);

        for (Hit<Product> hit : response.hits().hits()) {
            Product p = hit.source();
            System.out.printf("[%.2f] %s — %.2f TL%n",
                hit.score(), p.getName(), p.getPrice());
            if (hit.highlight() != null) {
                hit.highlight().forEach((field, frags) ->
                    frags.forEach(f -> System.out.println("  💡 " + f)));
            }
        }

        // Aggregation sonuçları
        response.aggregations().get("categories").sterms()
            .buckets().array().forEach(b ->
                System.out.printf("  %s: %d%n", b.key().stringValue(), b.docCount()));
    }

    public static void main(String[] args) throws Exception {
        RestClient rest = RestClient.builder(
            new HttpHost("localhost", 9200, "http")).build();
        var transport = new RestClientTransport(rest, new JacksonJsonpMapper());
        var service = new ProductSearchService(new ElasticsearchClient(transport));

        var resp = service.search("laptop", "electronics",
            null, 80000.0, 1, 10, "price", SortOrder.Asc);
        service.printResults(resp, 1, 10);

        transport.close();
        rest.close();
    }
}

11. Best Practices

✅ Yapın

UygulamaNeden
Filtre koşullarını filter context'e koyunCache'lenir, skor hesaplanmaz — hızlı
size(0) ile aggregation-only sorguDocument'ları çekmeye gerek yoksa bant genişliği kazanırsınız
Source filtering kullanınBüyük document'larda gereksiz alan transfer etmeyin
Derin sayfalamada search_after kullanınfrom/size 10.000 limiti var
Multi-search ile bağımsız sorguları birleştirinDashboard'da 5 ayrı sorgu = 5 HTTP yerine 1

❌ Yapmayın

UygulamaNeden
text alanda term query kullanmayınText alanlar analiz edilir — eşleşmez
from: 9999, size: 100 yapmayınDeep pagination performans katilidir
Her koşulu must'a koymayınFiltreler filter'da olmalı — gereksiz skor hesaplaması
Aggregation sonucunda size: 10000 yapmayınBellek patlatır, composite aggregation kullanın

12. Yaygın Hatalar

Hata 1: text Alanda term Query

// ❌ "Electronics" text alanda "electronics" olarak index'lenmiş (lowercase)
.term(t -> t.field("name").value("MacBook"))     // Bulamaz!

// ✅ text alanda match kullan
.match(m -> m.field("name").query("MacBook"))     // Bulur

// ✅ Veya keyword sub-field kullan
.term(t -> t.field("name.keyword").value("MacBook Pro 16"))  // Exact match

Hata 2: Aggregation Tipi Yanlış Cast

// ❌ String terms'ü long terms olarak cast etme
LongTermsAggregate wrong = response.aggregations()
    .get("categories").lterms();  // ClassCastException!

// ✅ Doğru tip
StringTermsAggregate correct = response.aggregations()
    .get("categories").sterms();

Hata 3: Search After'da Sıralama Eksikliği

// ❌ Sort olmadan search_after çalışmaz
.searchAfter(List.of("value1"))  // Hata!

// ✅ Unique bir sıralama alanı olmalı (genellikle _id ekleyin)
.sort(so -> so.field(f -> f.field("price").order(SortOrder.Asc)))
.sort(so -> so.field(f -> f.field("_id").order(SortOrder.Asc)))
.searchAfter(lastHit.sort())

Özet

  • SearchRequest ile full-text arama: match, multi_match, term, range — response'ta hits, score, total kontrol edilir

  • Bool query karmaşık koşulları birleştirir: must (skor + zorunlu), filter (cache + zorunlu), should (opsiyonel), must_not (hariç tutma)

  • Pagination: from/size (basit, max 10.000), search_after (derin, limitsiz) — her zaman unique sort alanı ekleyin

  • Highlight eşleşen metni <em> ile vurgular — birden fazla alan ve fragment destekler

  • Aggregation ile veri analizi: terms (gruplama), avg/sum/stats (istatistik), date_histogram (zaman serisi), nested sub-aggregation'lar

  • Multi-search bağımsız sorguları tek HTTP isteğinde toplar — dashboard'lar için ideal

  • Source filtering ile sadece gerekli alanları çekin — bant genişliği ve bellek tasarrufu

  • Filtre koşullarını filter context'e koyun — cache'lenir, performans artar