Pagination — from/size, search_after, Scroll, PIT
Giriş — Restoran Menüsü Benzetmesi
Bir restoranda 200 yemeklik bir menü olduğunu düşünün. Garson size hepsini bir anda getirmez — sayfa sayfa gösterir. İlk sayfa "Çorbalar", ikinci sayfa "Salatalar" gibi. Ama ya menü sürekli değişiyorsa? Siz 2. sayfaya bakarken biri yeni bir yemek eklerse? Veya 50. sayfaya atlamak istiyorsanız?
Elasticsearch'te de arama sonuçlarını sayfalamak (pagination) aynı soruları barındırır. Bu derste dört farklı sayfalama yöntemini öğreneceğiz: her birinin güçlü ve zayıf yanlarını, ne zaman hangisini kullanmanız gerektiğini derinlemesine inceleyeceğiz.
1. from/size — Klasik Sayfalama
En basit ve en yaygın sayfalama yöntemi. SQL'deki OFFSET/LIMIT mantığına benzer.
Temel Kullanım
// Sayfa 1: İlk 10 sonuç
GET products/_search
{
"from": 0,
"size": 10,
"query": {
"match": {
"category": "elektronik"
}
}
}
// Sayfa 2: 11-20 arası
GET products/_search
{
"from": 10,
"size": 10,
"query": {
"match": {
"category": "elektronik"
}
}
}
// Sayfa 5: 41-50 arası
GET products/_search
{
"from": 40,
"size": 10,
"query": {
"match": {
"category": "elektronik"
}
}
}Nasıl Çalışır?
from: 40, size: 10 dediğinizde Elasticsearch şunu yapar:
Her shard kendi içinde ilk
40 + 10 = 50dokümanı bulur ve sıralarCoordinating node tüm shard'lardan gelen sonuçları birleştirir
Global sıralamaya göre ilk 50'yi belirler
İlk 40'ı atar, son 10'u döndürür
5 shard'lı bir index'te from: 40, size: 10 sorgusu aslında 5 × 50 = 250 doküman toplayıp sıralayarak sadece 10 tanesini döndürür.
Deep Pagination Sorunu
// Sayfa 1000: from=9990, size=10
GET products/_search
{
"from": 9990,
"size": 10,
"query": {
"match_all": {}
}
}Bu sorgu her shard'dan 10.000 doküman toplar. 5 shard = 50.000 doküman coordinating node'da sıralanır → sadece 10 tanesi döndürülür.
Varsayılan limit: from + size <= 10.000
// ❌ Bu hata verir
GET products/_search
{
"from": 10000,
"size": 10
}
// Hata: "Result window is too large, from + size must be less than or equal to: [10000]"Limiti artırabilirsiniz ama yapmamalısınız:
// ⚠️ Performans sorunu yaratır — kullanmayın
PUT products/_settings
{
"index.max_result_window": 50000
}from/size Ne Zaman Kullanmalı?
✅ Kullanıcıya yönelik arayüzler (web, mobil) — ilk birkaç sayfa ✅ Toplam sonuç sayısı < 10.000 ✅ Sayfa atlama gerekiyorsa (1, 2, 3... 10 gibi sayfa numaraları)
❌ Deep pagination (10.000+ sonuç) ❌ Tüm sonuçları iterate etme ❌ Export/batch işlemleri
2. search_after — Cursor-Based Pagination
search_after, son döndürülen dokümanın sort değerlerini kullanarak bir sonraki sayfayı getirir. Deep pagination sorununu çözer.
Temel Kullanım
// İlk sayfa — sort zorunludur
GET products/_search
{
"size": 10,
"query": {
"match": {
"category": "elektronik"
}
},
"sort": [
{ "price": "desc" },
{ "_id": "asc" }
]
}Yanıt (son doküman):
{
"hits": {
"hits": [
...
{
"_id": "abc123",
"sort": [54999.99, "abc123"]
}
]
}
}// İkinci sayfa — son dokümanın sort değerlerini kullan
GET products/_search
{
"size": 10,
"query": {
"match": {
"category": "elektronik"
}
},
"sort": [
{ "price": "desc" },
{ "_id": "asc" }
],
"search_after": [54999.99, "abc123"]
}Neden sort'ta _id Gerekli?
Aynı fiyata sahip birden fazla ürün olabilir. _id (veya benzersiz bir alan) tiebreaker olarak eklenir — aynı sort değerlerine sahip dokümanların sıralamasını garantiler:
// ❌ Tiebreaker yok — aynı fiyata sahip dokümanlar arası kayıp olabilir
"sort": [{ "price": "desc" }]
// ✅ Tiebreaker ile — deterministik sıralama
"sort": [
{ "price": "desc" },
{ "_id": "asc" }
]search_after'ın Avantajları
10.000 limiti yok — istediğiniz kadar sayfa ilerleyebilirsiniz
Performans tutarlı — her sayfa aynı maliyette (deep pagination sorunu yok)
Gerçek zamanlı — yeni eklenen dokümanları görebilir
search_after'ın Sınırlamaları
Sayfa atlama yok — sadece "sonraki sayfa" alabilirsiniz (3. sayfadan 8.'ye atlayamazsınız)
Sort zorunlu —
search_aftersort değerlerine bağlıdırGerçek zamanlı = tutarsızlık riski — sayfalar arası yeni doküman eklenmesi tutarsızlığa yol açabilir
Point in Time (PIT) ile Tutarlı search_after
search_after'ın tutarsızlık sorununu PIT çözer:
// 1. PIT oluştur
POST products/_pit?keep_alive=5m
// Yanıt: { "id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4..." }
// 2. PIT ile search_after
GET _search
{
"size": 10,
"query": {
"match": {
"category": "elektronik"
}
},
"pit": {
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4...",
"keep_alive": "5m"
},
"sort": [
{ "price": "desc" },
{ "_shard_doc": "asc" }
]
}
// 3. Sonraki sayfalar — PIT + search_after
GET _search
{
"size": 10,
"query": {
"match": {
"category": "elektronik"
}
},
"pit": {
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4...",
"keep_alive": "5m"
},
"sort": [
{ "price": "desc" },
{ "_shard_doc": "asc" }
],
"search_after": [54999.99, 12]
}
// 4. İşiniz bitince PIT'i silin
DELETE _pit
{
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4..."
}PIT kullanıldığında:
indexparametresi belirtilmez (PIT zaten index'i biliyor)_shard_docotomatik tiebreaker olarak kullanılabilirVerinin belirli bir anının snapshot'ı üzerinde çalışılır — tutarlılık garanti
3. Scroll API — Toplu Veri İşleme (Legacy)
Scroll API, büyük veri kümelerini iterate etmek için tasarlanmıştır. Database cursor'a benzer bir yapıdadır.
⚠️ Not: Elasticsearch 7.x'ten itibaren scroll API yerine PIT + search_after önerilir. Scroll hâlâ çalışır ama yeni projelerde tercih etmeyin.
Temel Kullanım
// 1. İlk scroll isteği
POST products/_search?scroll=2m
{
"size": 100,
"query": {
"match_all": {}
}
}Yanıt:
{
"_scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==",
"hits": {
"total": { "value": 50000 },
"hits": [ ... ] // İlk 100 doküman
}
}// 2. Sonraki sayfalar — scroll_id ile
POST _search/scroll
{
"scroll": "2m",
"scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}
// 3. Tekrarla: hits boş dönene kadar
// 4. Scroll'u temizle
DELETE _search/scroll
{
"scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}Scroll Nasıl Çalışır?
İlk istek bir search context oluşturur — verinin o anki halinin snapshot'ı
Sonraki istekler bu snapshot üzerinden çalışır
Yeni eklenen/silinen dokümanlar görünmez — tutarlı veri
scrollparametresi context'in ne kadar açık kalacağını belirler
Scroll'un Sorunları
Kaynak tüketir — Her scroll context segment dosyalarını açık tutar, merge engellenebilir
Ölçeklenme sorunu — Çok sayıda eşzamanlı scroll cluster'ı zorlar
Gerçek zamanlı değil — Snapshot üzerinde çalışır
Sıralama esnekliği yok — İlk istekteki sort ile kilitlenir
4. Point in Time (PIT) — Modern Yaklaşım
PIT, scroll API'nin modern ve daha verimli alternatifidir. search_after ile birlikte kullanılır.
PIT Nedir?
PIT, index'in belirli bir andaki durumunu "dondurur". Açık kaldığı sürece verinin tutarlı bir görünümünü sağlar.
Tam PIT + search_after Workflow'u
// 1. PIT oluştur
POST products/_pit?keep_alive=5mYanıt:
{
"id": "gcSHBAEIcHJvZHVjdHMWYkd4X1RLQ1RRYXFqTk..."
}// 2. İlk sayfa
GET _search
{
"size": 100,
"query": {
"match_all": {}
},
"pit": {
"id": "gcSHBAEIcHJvZHVjdHMWYkd4X1RLQ1RRYXFqTk...",
"keep_alive": "5m"
},
"sort": [
{ "created_at": "desc" },
{ "_shard_doc": "asc" }
]
}
// 3. Sonraki sayfalar
GET _search
{
"size": 100,
"query": {
"match_all": {}
},
"pit": {
"id": "gcSHBAEIcHJvZHVjdHMWYkd4X1RLQ1RRYXFqTk...",
"keep_alive": "5m"
},
"sort": [
{ "created_at": "desc" },
{ "_shard_doc": "asc" }
],
"search_after": [1706745600000, 1542]
}
// 4. Tekrarla: hits.hits boş dönene kadar
// 5. PIT'i sil
DELETE _pit
{
"id": "gcSHBAEIcHJvZHVjdHMWYkd4X1RLQ1RRYXFqTk..."
}PIT'in Avantajları (Scroll'a Göre)
| Özellik | Scroll | PIT + search_after |
|---|---|---|
| Kaynak kullanımı | Yüksek | Düşük |
| Eşzamanlı kullanım | Sınırlı | Daha iyi |
| Sıralama esnekliği | İlk sorguya bağlı | Her sorguda değiştirilebilir |
| Önerilen | Hayır (legacy) | Evet |
5. Yöntem Karşılaştırması
| Özellik | from/size | search_after | scroll | PIT + search_after |
|---|---|---|---|---|
| Sayfa atlama | ✅ | ❌ | ❌ | ❌ |
| Deep pagination | ❌ (10K limit) | ✅ | ✅ | ✅ |
| Tutarlılık | ❌ | ❌ (PIT olmadan) | ✅ | ✅ |
| Performans (derin) | 📉 Kötüleşir | ✅ Tutarlı | ✅ Tutarlı | ✅ Tutarlı |
| Gerçek zamanlı | ✅ | ✅ | ❌ | ❌ |
| Kaynak tüketimi | Düşük | Düşük | Yüksek | Orta |
| Kullanım alanı | UI sayfalama | Sonsuz scroll, API | Toplu export | Toplu export, raporlama |
Karar Ağacı
Sayfalama ihtiyacın ne?
│
├── Kullanıcı arayüzü (web/mobil)?
│ ├── Sonuçlar < 10.000? → from/size
│ └── Sonsuz scroll / load more? → search_after
│
├── Toplu veri export?
│ └── PIT + search_after (modern)
│ └── (veya scroll — legacy sistemler)
│
└── Raporlama / analitik?
└── PIT + search_after6. Toplam Sonuç Sayısı ve track_total_hits
Varsayılan olarak Elasticsearch toplam sonuç sayısını 10.000'e kadar doğru sayar:
GET products/_search
{
"query": { "match_all": {} }
}Yanıt:
{
"hits": {
"total": {
"value": 10000,
"relation": "gte" // "10.000 veya daha fazla" anlamına gelir
}
}
}Tam sayı istiyorsanız:
GET products/_search
{
"track_total_hits": true,
"query": { "match_all": {} }
}
// "total": { "value": 158432, "relation": "eq" }Veya belirli bir limite kadar:
GET products/_search
{
"track_total_hits": 50000,
"query": { "match_all": {} }
}⚠️ Performans notu: track_total_hits: true büyük veri kümelerinde sorgu süresini artırır. Gerçekten ihtiyacınız yoksa kullanmayın.
7. Gerçek Dünya Örnekleri
7.1 E-Ticaret: Ürün Listeleme
// Test verisi oluştur
PUT ecommerce_pagination
{
"mappings": {
"properties": {
"name": { "type": "text", "analyzer": "turkish" },
"category": { "type": "keyword" },
"brand": { "type": "keyword" },
"price": { "type": "float" },
"rating": { "type": "float" },
"created_at": { "type": "date" },
"stock": { "type": "integer" }
}
}
}
POST ecommerce_pagination/_bulk
{"index":{"_id":"1"}}
{"name":"Samsung Galaxy S24 Ultra","category":"Telefon","brand":"Samsung","price":54999.99,"rating":4.8,"created_at":"2024-01-15","stock":150}
{"index":{"_id":"2"}}
{"name":"Apple iPhone 15 Pro","category":"Telefon","brand":"Apple","price":64999.99,"rating":4.9,"created_at":"2024-01-20","stock":200}
{"index":{"_id":"3"}}
{"name":"Xiaomi 14 Pro","category":"Telefon","brand":"Xiaomi","price":29999.99,"rating":4.5,"created_at":"2024-02-01","stock":300}
{"index":{"_id":"4"}}
{"name":"Samsung Galaxy Tab S9","category":"Tablet","brand":"Samsung","price":34999.99,"rating":4.6,"created_at":"2024-01-25","stock":100}
{"index":{"_id":"5"}}
{"name":"Apple MacBook Air M3","category":"Bilgisayar","brand":"Apple","price":42999.99,"rating":4.7,"created_at":"2024-02-10","stock":80}
{"index":{"_id":"6"}}
{"name":"Lenovo ThinkPad X1","category":"Bilgisayar","brand":"Lenovo","price":38999.99,"rating":4.4,"created_at":"2024-02-15","stock":60}
// Sayfa 1: En pahalıdan ucuza, sayfa başı 2 ürün
GET ecommerce_pagination/_search
{
"from": 0,
"size": 2,
"query": {
"match_all": {}
},
"sort": [
{ "price": "desc" },
{ "_id": "asc" }
]
}
// Sayfa 2
GET ecommerce_pagination/_search
{
"from": 2,
"size": 2,
"query": {
"match_all": {}
},
"sort": [
{ "price": "desc" },
{ "_id": "asc" }
]
}7.2 Sonsuz Scroll (search_after)
Mobil uygulamalar ve modern web UI'lar için:
// İlk yükleme
GET ecommerce_pagination/_search
{
"size": 2,
"query": { "match_all": {} },
"sort": [
{ "created_at": "desc" },
{ "_id": "asc" }
]
}
// Kullanıcı aşağı kaydırınca — son dokümanın sort değerleriyle
GET ecommerce_pagination/_search
{
"size": 2,
"query": { "match_all": {} },
"sort": [
{ "created_at": "desc" },
{ "_id": "asc" }
],
"search_after": ["2024-02-10T00:00:00.000Z", "5"]
}7.3 Toplu Export (PIT + search_after)
Raporlama veya veri migration'ı için:
// 1. PIT aç
POST ecommerce_pagination/_pit?keep_alive=10m
// 2. Tüm verileri iterate et (pseudo-code)
// İlk batch
GET _search
{
"size": 1000,
"query": { "match_all": {} },
"pit": { "id": "PIT_ID", "keep_alive": "10m" },
"sort": [{ "_shard_doc": "asc" }]
}
// Sonraki batch'ler — search_after ile devam et
// hits.hits boş dönene kadar tekrarla
// 3. PIT'i sil
DELETE _pit
{ "id": "PIT_ID" }8. Java ile Pagination
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.*;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.search.Hit;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.List;
import java.util.Map;
public class PaginationDemo {
// from/size pagination
public static void fromSizePagination(ElasticsearchClient client, int page, int pageSize)
throws Exception {
int from = (page - 1) * pageSize;
SearchResponse<ObjectNode> response = client.search(s -> s
.index("products")
.from(from)
.size(pageSize)
.query(q -> q.matchAll(m -> m))
.sort(so -> so.field(f -> f.field("price").order(SortOrder.Desc)))
.sort(so -> so.field(f -> f.field("_id").order(SortOrder.Asc))),
ObjectNode.class
);
System.out.printf("Sayfa %d (toplam: %d)%n", page, response.hits().total().value());
for (Hit<ObjectNode> hit : response.hits().hits()) {
System.out.printf(" %s - %s%n", hit.id(), hit.source().get("name").asText());
}
}
// search_after pagination
public static void searchAfterPagination(ElasticsearchClient client) throws Exception {
List<FieldValue> searchAfter = null;
int pageNum = 0;
while (true) {
final List<FieldValue> currentSearchAfter = searchAfter;
final int currentPage = ++pageNum;
SearchResponse<ObjectNode> response = client.search(s -> {
s.index("products")
.size(10)
.query(q -> q.matchAll(m -> m))
.sort(so -> so.field(f -> f.field("price").order(SortOrder.Desc)))
.sort(so -> so.field(f -> f.field("_id").order(SortOrder.Asc)));
if (currentSearchAfter != null) {
s.searchAfter(currentSearchAfter);
}
return s;
}, ObjectNode.class);
List<Hit<ObjectNode>> hits = response.hits().hits();
if (hits.isEmpty()) break;
System.out.printf("=== Sayfa %d ===%n", currentPage);
for (Hit<ObjectNode> hit : hits) {
System.out.printf(" %s%n", hit.source().get("name").asText());
}
// Son dokümanın sort değerlerini al
Hit<ObjectNode> lastHit = hits.get(hits.size() - 1);
searchAfter = lastHit.sort();
}
}
// PIT + search_after
public static void pitSearchAfter(ElasticsearchClient client) throws Exception {
// 1. PIT oluştur
OpenPointInTimeResponse pitResponse = client.openPointInTime(p -> p
.index("products")
.keepAlive(Time.of(t -> t.time("5m")))
);
String pitId = pitResponse.id();
try {
List<FieldValue> searchAfter = null;
int total = 0;
while (true) {
final List<FieldValue> currentSearchAfter = searchAfter;
SearchResponse<ObjectNode> response = client.search(s -> {
s.size(100)
.query(q -> q.matchAll(m -> m))
.pit(p -> p.id(pitId).keepAlive(Time.of(t -> t.time("5m"))))
.sort(so -> so.field(f -> f.field("_shard_doc").order(SortOrder.Asc)));
if (currentSearchAfter != null) {
s.searchAfter(currentSearchAfter);
}
return s;
}, ObjectNode.class);
List<Hit<ObjectNode>> hits = response.hits().hits();
if (hits.isEmpty()) break;
total += hits.size();
searchAfter = hits.get(hits.size() - 1).sort();
}
System.out.printf("Toplam %d doküman export edildi.%n", total);
} finally {
// 2. PIT'i sil
client.closePointInTime(c -> c.id(pitId));
}
}
}9. Best Practices
✅ Yapın
| Uygulama | Neden |
|---|---|
| UI pagination için from/size kullanın | Basit, sayfa atlama desteği var |
| Deep pagination için search_after kullanın | 10K+ sonuçlarda performans tutarlı |
| Toplu export için PIT + search_after kullanın | Tutarlı veri, düşük kaynak tüketimi |
| Sort'a tiebreaker ekleyin | _id veya _shard_doc — deterministik sıralama |
| PIT'i işiniz bitince silin | Kaynakları serbest bırakır |
❌ Yapmayın
| Uygulama | Neden |
|---|---|
max_result_window'u artırmayın | Deep pagination performans sorunu ortadan kalkmaz |
| Scroll API'yi yeni projelerde kullanmayın | PIT + search_after daha verimli |
track_total_hits: true'yu gereksiz yere kullanmayın | Performans maliyeti var |
| PIT'i saatlerce açık bırakmayın | Segment merge'i engeller, disk kullanımı artar |
| from/size ile tüm veriyi iterate etmeyin | Her sayfada maliyet artar |
10. Yaygın Hatalar
Hata 1: from/size ile Tüm Veriyi Çekmek
# ❌ Bu pattern performans felaketi
page = 0
while True:
results = search(from=page*100, size=100)
if not results: break
process(results)
page += 1
# Sayfa 50'de: from=5000, her shard 5100 doküman topluyor!# ✅ search_after veya PIT + search_after kullanın
search_after = None
while True:
results = search(size=100, search_after=search_after)
if not results: break
process(results)
search_after = results[-1].sortHata 2: search_after'da Tiebreaker Unutmak
// ❌ Aynı fiyata sahip dokümanlar arasında kayıp olabilir
"sort": [{ "price": "desc" }]
"search_after": [54999.99]
// ✅ Tiebreaker ekleyin
"sort": [{ "price": "desc" }, { "_id": "asc" }]
"search_after": [54999.99, "abc123"]Hata 3: PIT Timeout'u Çok Kısa Tutmak
// ❌ 30 saniye — batch işlem bitirmeden timeout olabilir
POST products/_pit?keep_alive=30s
// ✅ Makul bir süre
POST products/_pit?keep_alive=5m
// Her istekte keep_alive yenilenir
"pit": { "id": "...", "keep_alive": "5m" }Hata 4: Scroll Context'leri Temizlememek
// ❌ İşlem bitti ama scroll temizlenmedi — kaynak israfı
POST products/_search?scroll=30m
{ ... }
// ...işlem biter, scroll temizlenmez...
// ✅ Always clean up
DELETE _search/scroll
{ "scroll_id": "DXF1..." }
// Veya tüm scroll'ları temizle
DELETE _search/scroll/_all11. Performans Karşılaştırması
Simüle: 1 milyon doküman, 5 shard, 10 sonuç/sayfa
| Sayfa | from/size (ms) | search_after (ms) |
|---|---|---|
| 1 | 5 | 5 |
| 10 | 8 | 5 |
| 100 | 25 | 5 |
| 1.000 | 150 | 5 |
| 10.000 | Hata (limit) | 5 |
| 100.000 | — | 5-8 |
from/size maliyeti lineer artar, search_after sabit kalır.
Özet
from/size: En basit yöntem, UI sayfalama için ideal — ama 10.000 sonuç limitine takılır ve deep pagination'da performans düşer
search_after: Cursor-based pagination, deep pagination için tasarlanmış — sayfa atlama yapılamaz ama performans tutarlıdır
Scroll API: Legacy yöntem, toplu veri iterate etmek için — PIT + search_after lehine kullanımdan kaldırılıyor
PIT (Point in Time): Verinin belirli bir andaki snapshot'ını oluşturur, search_after ile birlikte tutarlı ve verimli pagination sağlar
Sort'a her zaman tiebreaker ekleyin (
_idveya_shard_doc)track_total_hits: trueperformans maliyeti taşır — sadece gerektiğinde kullanınPIT ve scroll context'lerini işiniz bitince mutlaka temizleyin
AI Asistan
Sorularını yanıtlamaya hazır