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
| Uygulama | Neden |
|---|---|
Filtre koşullarını filter context'e koyun | Cache'lenir, skor hesaplanmaz — hızlı |
size(0) ile aggregation-only sorgu | Document'ları çekmeye gerek yoksa bant genişliği kazanırsınız |
| Source filtering kullanın | Büyük document'larda gereksiz alan transfer etmeyin |
Derin sayfalamada search_after kullanın | from/size 10.000 limiti var |
| Multi-search ile bağımsız sorguları birleştirin | Dashboard'da 5 ayrı sorgu = 5 HTTP yerine 1 |
❌ Yapmayın
| Uygulama | Neden |
|---|---|
text alanda term query kullanmayın | Text alanlar analiz edilir — eşleşmez |
from: 9999, size: 100 yapmayın | Deep pagination performans katilidir |
Her koşulu must'a koymayın | Filtreler filter'da olmalı — gereksiz skor hesaplaması |
Aggregation sonucunda size: 10000 yapmayın | Bellek 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 matchHata 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'tahits,score,totalkontrol edilirBool 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ı ekleyinHighlight eşleşen metni
<em>ile vurgular — birden fazla alan ve fragment desteklerAggregation ile veri analizi:
terms(gruplama),avg/sum/stats(istatistik),date_histogram(zaman serisi), nested sub-aggregation'larMulti-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ı
filtercontext'e koyun — cache'lenir, performans artar
AI Asistan
Sorularını yanıtlamaya hazır