← Kursa Dön
📄 Text · 30 min

Full-Text Queries — match, multi_match, match_phrase

match, multi_match, match_phrase

Google'da arama yaparken ne olur? "En iyi Java kitabı" yazarsın, Google bunu kelimelere ayırır, her kelimeyi milyarlarca sayfada arar ve en alakalı sonuçları döner. "En iyi" yazmana rağmen "Java Programlama — Kapsamlı Rehber" çıkar çünkü Google anlamsal yakınlığı değerlendirir. Elasticsearch'ün full-text query'leri de tam olarak bunu yapar.

Full-text query'ler, arama terimini analiz eder (tokenize + normalize), sonra inverted index'te eşleşen dökümanları bulur ve relevance scoring ile sıralar. Bu derste match, multi_match ve match_phrase sorgularını tüm detaylarıyla öğreneceğiz.


match Query — Temel Full-Text Arama

En sık kullanılan sorgu tipi. Verdiğin metni analiz eder, çıkan token'ları inverted index'te arar.

Basit match

GET /products/_search
{
  "query": {
    "match": {
      "name": "kablosuz kulaklık"
    }
  }
}

Perde arkasında:

  1. "kablosuz kulaklık" → Analiz → ["kablosuz", "kulaklık"]

  2. Inverted index'te "kablosuz" ve "kulaklık" terimlerini ara

  3. Eşleşen dökümanları BM25 ile skorla

  4. Varsayılan operatör OR → "kablosuz" VEYA "kulaklık" içeren tüm dökümanlar döner

operator Parametresi

// Varsayılan: OR — herhangi bir kelime eşleşse yeter
GET /products/_search
{
  "query": {
    "match": {
      "name": {
        "query": "kablosuz kulaklık",
        "operator": "or"        // Varsayılan
      }
    }
  }
}
// "kablosuz mouse" da sonuçta çıkar (sadece "kablosuz" eşleşti)

// AND operatörü — tüm kelimeler eşleşmeli
GET /products/_search
{
  "query": {
    "match": {
      "name": {
        "query": "kablosuz kulaklık",
        "operator": "and"
      }
    }
  }
}
// Sadece HEM "kablosuz" HEM "kulaklık" içeren dökümanlar

minimum_should_match

OR operatörü çok gevşek, AND çok katı. Ortayolu minimum_should_match ile bul:

// En az 2 kelime eşleşmeli (3 kelimelik sorguda)
GET /products/_search
{
  "query": {
    "match": {
      "description": {
        "query": "hafif dayanıklı kablosuz kulaklık",
        "minimum_should_match": "75%"
      }
    }
  }
}
// 4 kelimeden en az 3'ü (75%) eşleşmeli

// Sayısal değer de olabilir
{
  "match": {
    "description": {
      "query": "hafif dayanıklı kablosuz kulaklık",
      "minimum_should_match": 2    // En az 2 kelime
    }
  }
}

fuzziness — Yazım Hatası Toleransı

Kullanıcılar yazım hatası yapar: "laptob", "kulalık", "elektonik". Fuzzy matching bunu tolere eder:

GET /products/_search
{
  "query": {
    "match": {
      "name": {
        "query": "laptob",        // Yanlış yazım
        "fuzziness": "AUTO"       // Otomatik fuzzy
      }
    }
  }
}
// "laptop" sonuçlarını bulur!

fuzziness değerleri:

  • 0 — Exact match (fuzzy kapalı)

  • 1 — 1 karakter farkı tolere et (ekleme, silme, değiştirme, yer değiştirme)

  • 2 — 2 karakter farkı tolere et

  • AUTO — Kelime uzunluğuna göre otomatik:

- 0-2 karakter → 0 (fuzzy yok) - 3-5 karakter → 1 edit distance - 6+ karakter → 2 edit distance

// AUTO en yaygın ve önerilen seçenek
{
  "match": {
    "name": {
      "query": "elektronik",     // "elektronik" → doğru
      "fuzziness": "AUTO"
    }
  }
}

// prefix_length ile ilk N karakterin doğru olmasını zorunlu kıl
{
  "match": {
    "name": {
      "query": "elektonik",
      "fuzziness": "AUTO",
      "prefix_length": 2    // İlk 2 karakter doğru olmalı: "el..."
    }
  }
}

analyzer Parametresi

Sorgu analizinde kullanılacak analyzer'ı belirleyebilirsin:

// Varsayılan: index mapping'deki analyzer
{
  "match": {
    "name": {
      "query": "LAPTOP",
      "analyzer": "standard"    // Explicit analyzer
    }
  }
}
// "LAPTOP" → standard analyzer → "laptop" → inverted index'te ara

zero_terms_query

Tüm kelimeler stop word ise ne olur?

// "ve ile bir" → Türkçe analyzer stop word'leri kaldırır → boş sorgu
{
  "match": {
    "description": {
      "query": "ve ile bir",
      "zero_terms_query": "all"    // Tüm kelimeler stop word → match_all gibi davran
    }
  }
}
// zero_terms_query: "none" (varsayılan) → sonuç yok
// zero_terms_query: "all" → match_all gibi davran

lenient — Tip Uyumsuzluğunu Tolere Et

// Numeric field'a text sorgusu gönderirsen normalde hata alırsın
// lenient: true ile hatayı tolere et
{
  "match": {
    "price": {
      "query": "ucuz",
      "lenient": true    // Hata yerine sıfır sonuç dön
    }
  }
}

multi_match Query — Birden Fazla Alanda Arama

Aynı sorguyu birden fazla alanda aramak için:

GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "kablosuz kulaklık",
      "fields": ["name", "description", "tags"]
    }
  }
}

Field Boosting

// Başlıktaki eşleşme daha değerli
{
  "multi_match": {
    "query": "kablosuz kulaklık",
    "fields": ["name^3", "description^1", "tags^2"]
  }
}
// name alanında eşleşen döküman 3 kat skor, tags 2 kat

multi_match Tipleri

multi_match'in farklı skorlama stratejileri var:

1. best_fields (varsayılan)

En iyi eşleşen alanın skorunu kullanır. "Bu dökümanın EN ALAKALI alanı hangisi?"

{
  "multi_match": {
    "query": "kablosuz kulaklık",
    "fields": ["name", "description"],
    "type": "best_fields"           // Varsayılan
  }
}
// name'de 5.0 skor, description'da 2.0 skor → Sonuç: 5.0
// En yüksek alan skoru kullanılır

2. most_fields

Tüm alanların skorlarını toplar. "Bu döküman kaç alanda eşleşiyor?"

{
  "multi_match": {
    "query": "kablosuz kulaklık",
    "fields": ["name", "description", "tags"],
    "type": "most_fields"
  }
}
// name: 5.0, description: 2.0, tags: 1.5 → Sonuç: 8.5
// Daha fazla alanda eşleşen döküman daha yüksek skor alır

3. cross_fields

Kelimeler farklı alanlara dağılmış olabilir. "Ahmet Yılmaz" aramasında "Ahmet" first_name'de, "Yılmaz" last_name'de olabilir.

{
  "multi_match": {
    "query": "Ahmet Yılmaz",
    "fields": ["first_name", "last_name"],
    "type": "cross_fields",
    "operator": "and"
  }
}
// "Ahmet" first_name'de VE "Yılmaz" last_name'de → Eşleşir!
// best_fields ile bu çalışmazdı — her kelime aynı alanda aranırdı

4. phrase ve phrase_prefix

// phrase — match_phrase gibi davranır
{
  "multi_match": {
    "query": "kablosuz kulaklık",
    "fields": ["name", "description"],
    "type": "phrase"
  }
}

// phrase_prefix — autocomplete için
{
  "multi_match": {
    "query": "kablosuz kul",
    "fields": ["name", "description"],
    "type": "phrase_prefix"
  }
}
// "kablosuz kul..." ile başlayan ifadeleri bulur

Hangi Tipi Kullanmalıyım?

TipKullanımÖrnek
best_fieldsGenel arama — en iyi alan kazanırÜrün araması
most_fieldsÇok alanlı içerik — hepsinde eşleşme önemliMakale araması
cross_fieldsAlanlar birleşik anlam taşıyorİsim + soyisim
phraseTam ifade araması — sıra önemliSlogan, alıntı
phrase_prefixAutocomplete — yazmaya devam ederkenArama çubuğu

Wildcard Field Adları

// Tüm alanların hepsinde ara
{
  "multi_match": {
    "query": "laptop",
    "fields": ["*"]            // TÜM alanlar
  }
}

// Belirli pattern'e uyan alanlarda
{
  "multi_match": {
    "query": "laptop",
    "fields": ["*_name", "description*"]
  }
}

tie_breaker

best_fields'ta sadece en iyi alan kullanılır. Diğer alanların da bir miktar katkı sağlamasını istersen:

{
  "multi_match": {
    "query": "kablosuz kulaklık",
    "fields": ["name^3", "description"],
    "type": "best_fields",
    "tie_breaker": 0.3
  }
}
// Skor = en iyi alan skoru + 0.3 × (diğer alanların skorları toplamı)
// tie_breaker: 0.0 → sadece best field (varsayılan)
// tie_breaker: 1.0 → tüm alanların toplamı (most_fields gibi)
// tie_breaker: 0.3 → iyi bir orta yol

match_phrase Query — Tam İfade Eşleşmesi

match query kelimeleri bağımsız arar. match_phrase ise kelimelerin sırasını ve bitişikliğini korur.

// match: "hızlı arama" → "hızlı" VEYA "arama"
// Sonuçlar: "Hızlı ve güvenilir arama motoru", "Arama optimizasyonu hızlı sonuç"

// match_phrase: "hızlı arama" → "hızlı" ve "arama" yan yana ve bu sırada
// Sonuçlar: "Hızlı arama motoru" ✅, "Arama hızlı yapılır" ❌ (sıra yanlış)
GET /products/_search
{
  "query": {
    "match_phrase": {
      "description": "kablosuz bluetooth kulaklık"
    }
  }
}
// "kablosuz bluetooth kulaklık" ifadesi tam olarak bu sırada geçen dökümanlar

slop Parametresi — Kelimeler Arası Mesafe

Kelimelerin tam bitişik olması çok katı olabilir. slop ile aralarına kaç kelime girebileceğini belirle:

{
  "match_phrase": {
    "description": {
      "query": "kablosuz kulaklık",
      "slop": 1
    }
  }
}
// Eşleşir: "kablosuz kulaklık" (slop 0)
// Eşleşir: "kablosuz bluetooth kulaklık" (slop 1 — arada 1 kelime)
// Eşleşmez: "kablosuz aktif gürültü engelleyici kulaklık" (slop 3 gerekir)

// slop 2 ile daha esnek
{
  "match_phrase": {
    "description": {
      "query": "kablosuz kulaklık",
      "slop": 2
    }
  }
}
// "kablosuz aktif noise-cancelling kulaklık" → slop 2 ile eşleşir

slop ile Kelime Sırası

slop > 0 olduğunda kelimeler yer değiştirebilir:

{
  "match_phrase": {
    "description": {
      "query": "kulaklık kablosuz",
      "slop": 2
    }
  }
}
// "kablosuz bluetooth kulaklık" → Eşleşir!
// Çünkü "kulaklık" ve "kablosuz" 2 pozisyon farkla yer değiştirebilir
// Ama doğru sıradaki eşleşme daha yüksek skor alır

match_phrase_prefix — Autocomplete

// Kullanıcı yazmaya devam ederken
{
  "match_phrase_prefix": {
    "name": {
      "query": "macbook p",
      "max_expansions": 10
    }
  }
}
// "macbook p..." ile devam eden tüm ifadeler
// "MacBook Pro", "MacBook Pro 16", "MacBook Pro Max" — hepsi eşleşir

// max_expansions: Prefix'ten kaç farklı terime genişletilebileceği
// Düşük → daha hızlı, daha az sonuç
// Yüksek → daha yavaş, daha çok sonuç

⚠️ Dikkat: match_phrase_prefix basit autocomplete için çalışır ama production'da completion suggester veya search-as-you-type field tipi önerilir. match_phrase_prefix son kelimeyi prefix olarak arar — bazı durumlarda yavaş olabilir.


query_string ve simple_query_string

query_string — Lucene Sorgu Söz Dizimi

İleri düzey kullanıcılar için güçlü ama tehlikeli:

GET /products/_search
{
  "query": {
    "query_string": {
      "query": "(laptop OR tablet) AND brand:Apple AND price:[10000 TO 80000]",
      "default_field": "name"
    }
  }
}

query_string operatörleri:

  • AND, OR, NOT — boolean operatörler

  • "" — tam ifade (phrase)

  • * — wildcard

  • ~ — fuzzy (laptop~2)

  • ^ — boost (laptop^3)

  • [] — range ([10 TO 100])

  • () — gruplama

// Karmaşık sorgu örnekleri
{
  "query_string": {
    "query": "name:(laptop OR \"gaming pc\") AND -brand:HP AND price:[5000 TO *]",
    "default_operator": "AND"
  }
}

⚠️ DİKKAT: query_string, geçersiz söz diziminde hata fırlatır. Kullanıcı girdisini doğrudan query_string'e KESİNLİKLE gönderme — injection riski var.

simple_query_string — Güvenli Alternatif

query_string'in güvenli versiyonu. Geçersiz söz diziminde hata vermez, sessizce yoksayar:

GET /products/_search
{
  "query": {
    "simple_query_string": {
      "query": "laptop + kablosuz | -HP",
      "fields": ["name^3", "description"],
      "default_operator": "and"
    }
  }
}

simple_query_string operatörleri:

  • + → AND

  • | → OR

  • - → NOT

  • "" → Phrase

  • * → Wildcard (sonuna)

  • ~N → Fuzzy

  • ~N (phrase sonunda) → Slop

{
  "simple_query_string": {
    "query": "\"kablosuz kulaklık\" + premium -ucuz",
    "fields": ["name", "description", "tags"]
  }
}
// "kablosuz kulaklık" ifadesi VE "premium" kelimesi olan
// AMA "ucuz" kelimesi olmayan dökümanlar

Kural: Kullanıcı girdisi → simple_query_string. Dahili/admin sorguları → query_string.


Gerçek Dünya: Arama Çubuğu Implementasyonu

Bir e-ticaret arama çubuğunun tam implementasyonu:

// Kullanıcı yazdı: "samsung kablosuz kulaklk" (yazım hatası var)
GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "samsung kablosuz kulaklk",
            "fields": [
              "name^5",
              "name.autocomplete^3",
              "brand^4",
              "description",
              "category^2",
              "tags^2"
            ],
            "type": "best_fields",
            "fuzziness": "AUTO",
            "prefix_length": 2,
            "tie_breaker": 0.3
          }
        }
      ],
      "filter": [
        { "term": { "in_stock": true } },
        { "term": { "active": true } }
      ],
      "should": [
        {
          "match_phrase": {
            "name": {
              "query": "samsung kablosuz kulaklık",
              "slop": 2,
              "boost": 3
            }
          }
        }
      ]
    }
  },
  "_source": ["name", "brand", "price", "rating", "thumbnail"],
  "highlight": {
    "fields": {
      "name": {},
      "description": { "fragment_size": 100 }
    }
  },
  "size": 20
}

Bu sorgudaki stratejiler:

  1. multi_match best_fields — Genel arama, en iyi alan kazanır

  2. fuzziness: AUTO — "kulaklk" → "kulaklık" bulunur

  3. match_phrase should — Tam ifade eşleşmesine bonus skor

  4. Field boosting — name > brand > category > description

  5. tie_breaker: 0.3 — Birden fazla alanda eşleşen dökümanlara ek skor

  6. Highlight — Eşleşen kelimeleri vurgula


Java ile Full-Text Queries

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.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. Basit match
        var matchResponse = client.search(s -> s
            .index("products")
            .query(q -> q
                .match(m -> m
                    .field("name")
                    .query("kablosuz kulaklık")
                    .operator(co.elastic.clients.elasticsearch._types.query_dsl.Operator.And)
                    .fuzziness("AUTO")
                )
            ),
            Map.class
        );

        // 2. multi_match
        var multiMatchResponse = client.search(s -> s
            .index("products")
            .query(q -> q
                .multiMatch(mm -> mm
                    .query("kablosuz kulaklık")
                    .fields("name^3", "description", "tags^2")
                    .type(co.elastic.clients.elasticsearch._types.query_dsl.TextQueryType.BestFields)
                    .fuzziness("AUTO")
                    .tieBreaker(0.3)
                )
            ),
            Map.class
        );

        // 3. match_phrase
        var phraseResponse = client.search(s -> s
            .index("products")
            .query(q -> q
                .matchPhrase(mp -> mp
                    .field("description")
                    .query("kablosuz bluetooth kulaklık")
                    .slop(1)
                )
            ),
            Map.class
        );

        // 4. simple_query_string (kullanıcı girdisi için güvenli)
        var simpleResponse = client.search(s -> s
            .index("products")
            .query(q -> q
                .simpleQueryString(sqs -> sqs
                    .query("kablosuz + kulaklık -ucuz")
                    .fields("name^3", "description")
                    .defaultOperator(co.elastic.clients.elasticsearch._types.query_dsl.Operator.And)
                )
            ),
            Map.class
        );

        System.out.println("Match: " + matchResponse.hits().total().value());
        System.out.println("Multi-match: " + multiMatchResponse.hits().total().value());
        System.out.println("Phrase: " + phraseResponse.hits().total().value());
        System.out.println("Simple QS: " + simpleResponse.hits().total().value());

        for (Hit<Map> hit : multiMatchResponse.hits().hits()) {
            System.out.printf("%.3f | %s%n", hit.score(), hit.source().get("name"));
        }

        restClient.close();
    }
}

Best Practices

multi_match kullan — Tek field yerine birden fazla alanda ara

Field boosting yap — Başlık > açıklama > tags

fuzziness: AUTO kullan — Yazım hatalarını tolere et

match_phrase ile phrase boost yap — Tam ifade eşleşmesine bonus skor

Kullanıcı girdisi için simple_query_string kullan — query_string yerine, güvenli

tie_breaker: 0.3 dene — Birden fazla alanda eşleşen dökümanlara ek skor

Analyze API ile test et — Sorgunun nasıl analiz edildiğini kontrol et


Yaygın Hatalar

❌ "match query ile exact match yapmaya çalışıyorum"

match query metni analiz eder. "Apple" → "apple" olur, "MacBook Pro" → "macbook" + "pro" olur. Exact match istiyorsan term query (keyword field üzerinde) kullan.

❌ "query_string'e kullanıcı girdisini doğrudan gönderiyorum"

query_string geçersiz söz diziminde hata fırlatır. "query": "name:(" gibi bir girdi uygulamayı kırar. Kullanıcı girdisi → simple_query_string.

❌ "match_phrase çok katı, sonuç gelmiyor"

slop parametresi ekle. slop: 1 veya slop: 2 ile kelimeler arasına boşluk tanı. Veya match_phrase'i should'a koyup bonus scoring olarak kullan.

❌ "Tüm alanlarda wildcard (*) ile arıyorum"

"fields": ["*"] tüm alanları tarar — çok yavaş. İlgili alanları açıkça belirt.

❌ "fuzziness'ı 3-4 yapıyorum"

Yüksek fuzziness değeri alakasız sonuçlar getirir. "cat" ile "dog" 3 edit distance — tam kaos. AUTO kullan, 2'den fazla gerekli değil.


Özet

  • match query, en temel full-text arama — metni analiz eder, inverted index'te arar

  • operator (or/and) ve minimum_should_match ile eşleşme katılığı ayarlanır

  • fuzziness: AUTO yazım hatalarını tolere eder — production'da mutlaka kullan

  • multi_match birden fazla alanda arar — best_fields, most_fields, cross_fields tipleri var

  • Field boosting (^) ile alanları önceliklendir — başlık > açıklama

  • match_phrase kelimelerin sırasını ve bitişikliğini korur — slop ile esneklik ekle

  • simple_query_string kullanıcı girdisi için güvenli — query_string yerine kullan

  • Arama çubuğunda multi_match + fuzziness + match_phrase boost kombinasyonu etkili

Bir sonraki derste Term-Level Queries öğreneceğiz — term, range, exists, wildcard, regexp!