Term-Level Queries — term, terms, range, wildcard
term, range, exists, wildcard, regexp
Bir gardırobun gözlerini düşün. "Kırmızı" gözünü açarsan sadece kırmızı kıyafetleri görürsün — tam eşleşme. "M-XL" arasını istersen, o boyut aralığındaki gözleri açarsın — range. "Ceket gözü var mı?" diye sorarsan, ceket gözünün varlığını kontrol edersin — exists.
Term-level queries metni analiz etmez. Arama terimini olduğu gibi inverted index'te arar. Full-text query'lerin aksine, burada "anlam" veya "yakınlık" yok — birebir eşleşme, aralık kontrolü veya pattern matching var.
Full-Text vs Term-Level — Kritik Fark
Bu ayrım Elasticsearch'teki en yaygın hata kaynağıdır:
// Senaryo: "status" alanı "Active" değerine sahip dökümanı bul
// 1. match (full-text) → Metni ANALİZ EDER
GET /users/_search
{
"query": {
"match": { "status": "Active" }
}
}
// "Active" → analiz → "active" (küçük harf)
// Inverted index'te "active" aranır
// text field'da → ✅ Bulur (çünkü "Active" da "active" olarak index'lendi)
// 2. term (term-level) → Metni ANALİZ ETMEZ
GET /users/_search
{
"query": {
"term": { "status": "Active" }
}
}
// "Active" olduğu gibi inverted index'te aranır
// text field'da → ❌ Bulamaz! (çünkü "Active" → "active" olarak index'lendi)
// keyword field'da → ✅ Bulur! (çünkü keyword analiz edilmez, "Active" aynen saklanır)Altın Kural:
termquery → keyword field'larda kullanmatchquery → text field'larda kullan
// ✅ DOĞRU kullanım
{ "term": { "status.keyword": "Active" } } // keyword field + term query
{ "match": { "description": "hızlı laptop" } } // text field + match query
// ❌ YANLIŞ kullanım
{ "term": { "description": "Hızlı Laptop" } } // text field + term query → BULAMAZ
{ "match": { "status.keyword": "Active" } } // Çalışır ama gereksiz analizterm Query — Birebir Eşleşme
Verdiğin değeri olduğu gibi inverted index'te arar. Analiz yok, dönüşüm yok.
// Keyword field'da exact match
GET /products/_search
{
"query": {
"term": {
"category.keyword": "Laptop"
}
}
}
// Boolean field
GET /products/_search
{
"query": {
"term": {
"in_stock": true
}
}
}
// Numeric field
GET /products/_search
{
"query": {
"term": {
"price": 15000
}
}
}Case Sensitivity
term query büyük-küçük harf duyarlıdır (keyword field'larda):
// "Laptop" vs "laptop" vs "LAPTOP"
{ "term": { "category.keyword": "Laptop" } } // ✅ Eşleşir (veri "Laptop" ise)
{ "term": { "category.keyword": "laptop" } } // ❌ Eşleşmez
{ "term": { "category.keyword": "LAPTOP" } } // ❌ EşleşmezCase-insensitive term query:
{
"term": {
"category.keyword": {
"value": "laptop",
"case_insensitive": true // Elasticsearch 7.10+
}
}
}
// "Laptop", "laptop", "LAPTOP" — hepsi eşleşirBoost ile Scoring
{
"term": {
"brand.keyword": {
"value": "Apple",
"boost": 2.0
}
}
}
// Apple ürünleri 2x skor alırterms Query — Birden Fazla Değer
SQL'deki IN operatörüne karşılık gelir:
// SQL: WHERE category IN ('Laptop', 'Tablet', 'Telefon')
GET /products/_search
{
"query": {
"terms": {
"category.keyword": ["Laptop", "Tablet", "Telefon"]
}
}
}Terms Lookup — Başka Dökümanın Değerleriyle Eşleştir
// user-1'in favori markalarını al, o markaların ürünlerini bul
GET /products/_search
{
"query": {
"terms": {
"brand.keyword": {
"index": "users",
"id": "user-1",
"path": "favorite_brands"
}
}
}
}
// users index'indeki user-1'in favorite_brands array'indeki değerleri alır
// O markalarla products index'inde term eşleşmesi yaparrange Query — Aralık Sorgusu
Sayısal, tarih ve string alanlarında aralık sorgusu:
Sayısal Range
// Fiyatı 5000-20000 arası
GET /products/_search
{
"query": {
"range": {
"price": {
"gte": 5000, // Greater Than or Equal (≥)
"lte": 20000 // Less Than or Equal (≤)
}
}
}
}| Operatör | Anlam | SQL Karşılığı |
|---|---|---|
gt | Greater than | > |
gte | Greater than or equal | >= |
lt | Less than | < |
lte | Less than or equal | <= |
// Sadece alt sınır
{ "range": { "rating": { "gte": 4.5 } } } // rating >= 4.5
// Sadece üst sınır
{ "range": { "stock_count": { "lt": 10 } } } // stock < 10 (düşük stok)
// Açık aralık (5000'den büyük, 20000'den küçük — sınırlar hariç)
{ "range": { "price": { "gt": 5000, "lt": 20000 } } }Tarih Range
// Son 30 gündeki ürünler
GET /products/_search
{
"query": {
"range": {
"created_at": {
"gte": "now-30d"
}
}
}
}
// Belirli tarih aralığı
{
"range": {
"created_at": {
"gte": "2025-01-01",
"lte": "2025-01-31"
}
}
}
// Tarih matematik ifadeleri
{
"range": {
"created_at": {
"gte": "now-1M/M", // 1 ay önce, ayın başına yuvarla
"lte": "now/M" // Şu anki ayın başı
}
}
}Tarih Matematik İfadeleri:
| İfade | Anlam | ||
|---|---|---|---|
now | Şu an | ||
now-1d | 1 gün önce | ||
now-1h | 1 saat önce | ||
now-30m | 30 dakika önce | ||
now-1M | 1 ay önce | ||
now-1y | 1 yıl önce | ||
now/d | Bugünün başlangıcı (00:00) | ||
now/M | Bu ayın başı | ||
now/y | Bu yılın başı | ||
| `2025-01-15\ | \ | +1M` | 15 Ocak + 1 ay = 15 Şubat |
// Son 7 günün verileri (tam günler)
{
"range": {
"@timestamp": {
"gte": "now-7d/d", // 7 gün önce, günün başından
"lt": "now/d" // Bugünün başından önce
}
}
}
// Bu yılın verileri
{
"range": {
"created_at": {
"gte": "now/y", // Yılın başı
"lte": "now"
}
}
}
// format parametresi ile özel tarih formatı
{
"range": {
"birth_date": {
"gte": "01-01-1990",
"lte": "31-12-1999",
"format": "dd-MM-yyyy"
}
}
}
// time_zone ile saat dilimi
{
"range": {
"created_at": {
"gte": "2025-01-15T00:00:00",
"lte": "2025-01-15T23:59:59",
"time_zone": "+03:00" // Türkiye saat dilimi
}
}
}exists Query — Alan Varlığı Kontrolü
Bir alanın var olup olmadığını kontrol eder:
// "discount" alanı olan ürünler (indirimli ürünler)
GET /products/_search
{
"query": {
"exists": {
"field": "discount"
}
}
}
// "discount" alanı OLMAYAN ürünler (bool + must_not)
GET /products/_search
{
"query": {
"bool": {
"must_not": [
{ "exists": { "field": "discount" } }
]
}
}
}exists alanı olmayan sayılan durumlar:
Alan hiç gönderilmemiş (mapping'de var ama değer yok)
Alan
nullolarak gönderilmişAlan boş array
[]olarak gönderilmiş
exists olan sayılan durumlar:
Herhangi bir değer (boş string
""dahil)false,0gibi "falsy" değerler → exists: true
// null_value ile null'ları aranabilir yap
PUT /products
{
"mappings": {
"properties": {
"discount": {
"type": "float",
"null_value": -1 // null → -1 olarak index'lenir
}
}
}
}
// Artık null gönderilen dökümanları bulabilirsin
GET /products/_search
{
"query": {
"term": { "discount": -1 }
}
}prefix Query — Ön Ek Eşleşmesi
Değerin belirli bir prefix ile başlayıp başlamadığını kontrol eder:
// "Mac" ile başlayan ürünler
GET /products/_search
{
"query": {
"prefix": {
"name.keyword": "Mac"
}
}
}
// "MacBook Pro", "MacBook Air", "Mac Mini" — hepsi eşleşir
// case_insensitive
{
"prefix": {
"name.keyword": {
"value": "mac",
"case_insensitive": true
}
}
}⚠️ Performans: prefix query text field'da kullanılırsa tüm terimleri tarar — yavaş olabilir. keyword field'da veya index-time optimization ile kullan.
wildcard Query — Joker Karakter Eşleşmesi
* (sıfır veya daha fazla karakter) ve ? (tek karakter) ile pattern matching:
// * → Sıfır veya daha fazla karakter
GET /products/_search
{
"query": {
"wildcard": {
"sku": {
"value": "MBP-*-2025"
}
}
}
}
// "MBP-16-2025", "MBP-14-M3-2025" — eşleşir
// ? → Tam olarak 1 karakter
{
"wildcard": {
"sku": {
"value": "MBP-1?-2025"
}
}
}
// "MBP-16-2025" → ✅
// "MBP-14-2025" → ✅
// "MBP-16-M3-2025" → ❌ (? sadece 1 karakter)
// case_insensitive
{
"wildcard": {
"name.keyword": {
"value": "*laptop*",
"case_insensitive": true
}
}
}⚠️ Performans Uyarısı: Başında * olan wildcard (*laptop) çok yavaş — tüm terimleri taramak zorunda. Mümkünse başına * koyma.
regexp Query — Düzenli İfade
Regular expression ile pattern matching:
// E-posta formatı kontrolü (basitleştirilmiş)
GET /users/_search
{
"query": {
"regexp": {
"email.keyword": {
"value": ".*@gmail\\.com"
}
}
}
}
// Ürün SKU pattern'i
{
"regexp": {
"sku": {
"value": "MBP-[0-9]{2}-[A-Z]{2}-2025",
"flags": "ALL"
}
}
}
// "MBP-16-M3-2025", "MBP-14-M2-2025" — eşleşirDesteklenen regex operatörleri:
.→ Herhangi bir karakter*→ Sıfır veya daha fazla tekrar+→ Bir veya daha fazla tekrar?→ Sıfır veya bir[abc]→ Karakter sınıfı[a-z]→ Karakter aralığı(abc)→ Gruplama|→ OR{n}→ Tam n tekrar{n,m}→ n ile m arası tekrar
⚠️ Performans: Regexp çok yavaş olabilir — production'da dikkatli kullan. Mümkünse prefix veya wildcard tercih et.
fuzzy Query — Yaklaşık Eşleşme
Edit distance ile yaklaşık eşleşme (yazım hatası toleransı):
// "laptob" → "laptop" bulur (1 edit distance)
GET /products/_search
{
"query": {
"fuzzy": {
"name.keyword": {
"value": "laptob",
"fuzziness": "AUTO"
}
}
}
}
// Daha fazla kontrol
{
"fuzzy": {
"brand.keyword": {
"value": "Samsng",
"fuzziness": 2,
"prefix_length": 2, // İlk 2 karakter doğru olmalı
"max_expansions": 50, // Maximum genişleme sayısı
"transpositions": true // Yer değiştirme (ab → ba) sayılır mı?
}
}
}💡 İpucu: Çoğu durumda fuzzy query yerine match query'nin fuzziness parametresini kullan — daha esnek ve doğal.
ids Query — ID ile Eşleşme
GET /products/_search
{
"query": {
"ids": {
"values": ["1", "5", "10", "42"]
}
}
}_mget'ten farkı: ids query scoring yapar ve diğer sorgularla bool içinde birleştirilebilir.
Gerçek Dünya: Filtre Paneli
E-ticaret sitelerindeki sol taraftaki filtre paneli term-level query'ler ile oluşturulur:
// Kullanıcı filtreleri:
// - Kategori: Laptop
// - Marka: Apple veya Lenovo
// - Fiyat: 10000-50000
// - Rating: 4+
// - Stokta olan
// - İndirimli (discount alanı var)
// - Renk: Gümüş
GET /products/_search
{
"query": {
"bool": {
"filter": [
{ "term": { "category.keyword": "Laptop" } },
{ "terms": { "brand.keyword": ["Apple", "Lenovo"] } },
{ "range": { "price": { "gte": 10000, "lte": 50000 } } },
{ "range": { "rating": { "gte": 4.0 } } },
{ "term": { "in_stock": true } },
{ "exists": { "field": "discount" } },
{ "term": { "attributes.color": "Gümüş" } }
]
}
},
"sort": [
{ "price": "asc" }
],
"aggs": {
"brands": {
"terms": { "field": "brand.keyword", "size": 20 }
},
"price_ranges": {
"range": {
"field": "price",
"ranges": [
{ "to": 5000 },
{ "from": 5000, "to": 15000 },
{ "from": 15000, "to": 30000 },
{ "from": 30000, "to": 50000 },
{ "from": 50000 }
]
}
},
"avg_rating": {
"avg": { "field": "rating" }
}
}
}Bu sorgu:
filter context — Tüm filtreler scoring yapmaz, cache'lenir
Sorting — Fiyata göre sıralama (scoring olmadığı için anlamlı)
Aggregations — Filtre panelindeki sayaçları güncelle (marka sayıları, fiyat aralıkları)
Java ile Term-Level Queries
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.SearchResponse;
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.Map;
class Main {
public static void main(String[] args) throws Exception {
RestClient restClient = RestClient.builder(
new HttpHost("localhost", 9200)
).build();
ElasticsearchClient client = new ElasticsearchClient(
new RestClientTransport(restClient, new JacksonJsonpMapper())
);
// 1. term query
var termResponse = client.search(s -> s
.index("products")
.query(q -> q
.term(t -> t.field("category.keyword").value("Laptop"))
),
Map.class
);
// 2. terms query (IN)
var termsResponse = client.search(s -> s
.index("products")
.query(q -> q
.terms(t -> t
.field("brand.keyword")
.terms(tv -> tv.value(
java.util.List.of("Apple", "Samsung", "Lenovo")
.stream()
.map(co.elastic.clients.elasticsearch._types.FieldValue::of)
.toList()
))
)
),
Map.class
);
// 3. range query
var rangeResponse = client.search(s -> s
.index("products")
.query(q -> q
.range(r -> r
.field("price")
.gte(JsonData.of(5000))
.lte(JsonData.of(50000))
)
),
Map.class
);
// 4. exists query
var existsResponse = client.search(s -> s
.index("products")
.query(q -> q
.exists(e -> e.field("discount"))
),
Map.class
);
// 5. Filtre paneli — bool + filter
var filterResponse = client.search(s -> s
.index("products")
.query(q -> q
.bool(b -> b
.filter(f -> f.term(t -> t.field("category.keyword").value("Laptop")))
.filter(f -> f.range(r -> r.field("price").gte(JsonData.of(10000)).lte(JsonData.of(50000))))
.filter(f -> f.term(t -> t.field("in_stock").value(true)))
)
)
.sort(so -> so.field(f -> f.field("price").order(co.elastic.clients.elasticsearch._types.SortOrder.Asc))),
Map.class
);
System.out.println("Term: " + termResponse.hits().total().value());
System.out.println("Terms: " + termsResponse.hits().total().value());
System.out.println("Range: " + rangeResponse.hits().total().value());
System.out.println("Exists: " + existsResponse.hits().total().value());
System.out.println("Filter panel: " + filterResponse.hits().total().value());
restClient.close();
}
}Best Practices
✅ term query'yi keyword field'larda kullan — text field'da term query yapma
✅ Tarih range'lerinde `now` math kullan — Hardcoded tarih yerine now-30d gibi dinamik ifadeler
✅ exists ile null kontrolü yap — null_value tanımlayarak null'ları da aranabilir kıl
✅ Filtre panellerini filter context'te yap — Cache'lenir, scoring yok, çok hızlı
✅ wildcard/regexp'te başa `*` koyma — Performans felaketi, tüm terimleri tarar
✅ terms query'de array boyutunu kontrol et — 65.000+ değer performans sorunlarına yol açar
Yaygın Hatalar
❌ "term query ile text field'da arama yapıyorum"
// ❌ Text field'da term — BULAMAZ
{ "term": { "name": "MacBook Pro" } }
// "MacBook Pro" analiz edilmedi ama index'te "macbook" + "pro" var
// ✅ Keyword field'da term
{ "term": { "name.keyword": "MacBook Pro" } }❌ "range query'de tarih formatını yanlış yazıyorum"
// ❌ Yanlış format
{ "range": { "created_at": { "gte": "15/01/2025" } } }
// ✅ Doğru (ISO 8601 veya mapping'deki format)
{ "range": { "created_at": { "gte": "2025-01-15" } } }❌ "exists query'de boş string'lerin olmadığını sanıyorum"
Boş string "" exists kontrolünden geçer. exists, alanın var olup olmadığını kontrol eder — boşluk kontrolü için ek filtreleme gerek.
❌ "wildcard'ı full-text search yerine kullanıyorum"
*laptop* gibi wildcard'lar match query'den çok daha yavaş. Full-text arama için match kullan.
Term-Level Query Performans Karşılaştırması
Hangi sorgunun ne kadar sürdüğünü bilmek, doğru seçim yapmana yardımcı olur:
Performans sıralaması (hızlıdan yavaşa):
1. term / terms → O(1) veya O(log n) — en hızlı
2. range → O(log n) — BKD tree üzerinde
3. prefix → O(k) — prefix uzunluğuna bağlı
4. exists → Hızlı — doc values/field names kontrolü
5. wildcard (sonunda) → O(k) — prefix + expansion
6. fuzzy → O(n×m) — edit distance hesaplama
7. wildcard (başında) → O(n) — tüm terimleri tara
8. regexp → O(n) — tüm terimleri tara
n = toplam terim sayısı, k = pattern uzunluğuPratik sonuç: term, terms, range ve exists production'da güvenle kullan. wildcard ve regexp'i başında * olmadan dikkatli kullan. Mümkünse mapping'de wildcard field tipi veya ngram analyzer kullanarak bu sorguların maliyetini index zamanına taşı.
Özet
term query metni analiz etmez — keyword field'larda exact match için kullan
terms query SQL'deki
INoperatörüdür — birden fazla değerle eşleştirrange query sayı ve tarih aralıkları için —
gte,lte,gt,ltoperatörleriexists query alanın var olup olmadığını kontrol eder — null ve eksik alanları bulur
prefix, wildcard, regexp pattern matching yapar — performansa dikkat et
fuzzy query yazım hatalarını tolere eder —
matchquery'ninfuzzinessparametresi daha pratikFiltre panelleri filter context'te term-level query'ler ile oluşturulur — cache'lenir, hızlı
Term query ile text field'da arama yapmak en yaygın hata — keyword field kullan
Bir sonraki derste Compound Queries öğreneceğiz — bool, boosting, dis_max!
AI Asistan
Sorularını yanıtlamaya hazır