← Kursa Dön
📄 Text · 25 min

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:

  • term query → keyword field'larda kullan

  • match query → 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 analiz

term 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şmez

Case-insensitive term query:

{
  "term": {
    "category.keyword": {
      "value": "laptop",
      "case_insensitive": true    // Elasticsearch 7.10+
    }
  }
}
// "Laptop", "laptop", "LAPTOP" — hepsi eşleşir

Boost ile Scoring

{
  "term": {
    "brand.keyword": {
      "value": "Apple",
      "boost": 2.0
    }
  }
}
// Apple ürünleri 2x skor alır

terms 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 yapar

range 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örAnlamSQL Karşılığı
gtGreater than>
gteGreater than or equal>=
ltLess than<
lteLess 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:

İfadeAnlam
nowŞu an
now-1d1 gün önce
now-1h1 saat önce
now-30m30 dakika önce
now-1M1 ay önce
now-1y1 yıl önce
now/dBugünün başlangıcı (00:00)
now/MBu ayın başı
now/yBu 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 null olarak gönderilmiş

  • Alan boş array [] olarak gönderilmiş

exists olan sayılan durumlar:

  • Herhangi bir değer (boş string "" dahil)

  • false, 0 gibi "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şir

Desteklenen 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:

  1. filter context — Tüm filtreler scoring yapmaz, cache'lenir

  2. Sorting — Fiyata göre sıralama (scoring olmadığı için anlamlı)

  3. 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ğu

Pratik 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 IN operatörüdür — birden fazla değerle eşleştir

  • range query sayı ve tarih aralıkları için — gte, lte, gt, lt operatörleri

  • exists 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 — match query'nin fuzziness parametresi daha pratik

  • Filtre 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!