MongoDB'de metin araması yapmak için koleksiyonda bir text index oluşturur, ardından sorguda $text operatörünü kullanırsınız: db.makaleler.createIndex( { icerik: "text" } ) ve db.makaleler.find( { $text: { $search: "yedekleme" } } ). Text index, alanlardaki metni kelimelere ayırır, dile özgü kök ayırma (stemming) uygular, durak kelimeleri (stop words) atar ve her benzersiz kökü indekse yazar. Sorgu da aynı kurallarla işlendiği için "yedeklemeler" gibi çekimli biçimler aynı köke indirgenebilir.

Bu yazı kendi sunucunuzda (self-managed) çalışan MongoDB içindir. Text index'i oluşturmayı, Türkçe dahil dil seçimini, tam ifade ve hariç tutma aramasını, alan ağırlıklarını, alaka puanına göre sıralamayı ve en önemlisi kısıtları anlatıyoruz. Not olarak: MongoDB, Atlas üzerinde çalışan veriler için Atlas Search ve Atlas Vector Search'ü önerir; bu yazı onları kapsamaz.

Text index oluşturma

Text index, db.collection.createIndex() ile oluşturulur; anahtarın değeri olarak "text" dizesi yazılır. Alan bir dize ya da dize dizisi tutabilir. Bir koleksiyonda en fazla bir text index olabilir, ama bu tek index birden çok alanı kapsayabilir; belgelere göre bir text index en fazla 32 alan içerebilir.

javascript
use blog
db.makaleler.insertMany( [
   { _id: 1, baslik: "VPS yedekleme rehberi", icerik: "Günlük yedekleme planını cron ile kurun", etiketler: [ "yedek", "linux" ] },
   { _id: 2, baslik: "Nginx ayarları", icerik: "Ters vekil ve önbellek ayarları", etiketler: [ "nginx" ] },
   { _id: 3, baslik: "Veritabanı bakımı", icerik: "Index bakımı ve yedekleme kontrolleri", etiketler: [ "mongodb" ] }
] )

// Tek alanli text index
db.makaleler.createIndex( { icerik: "text" } )

Bu index yalnızca icerik alanını kapsar. "nginx" kelimesi ikinci belgede baslik ve etiketler alanlarında geçse bile $text araması onu bulamaz, çünkü o alanlar indekste yoktur. Belgelerdeki örnek de tam olarak bu noktayı vurgular: text index dışındaki alanlar $text sonucunu etkilemez.

Birden çok alanı aramak istiyorsanız önce mevcut text index'i silip bileşik bir text index oluşturmalısınız, çünkü ikinci bir text index eklenemez:

javascript
db.makaleler.dropIndex( "icerik_text" )
db.makaleler.createIndex(
   { baslik: "text", icerik: "text", etiketler: "text" },
   { name: "MakaleMetin", default_language: "turkish" }
)

Index adını bilmiyorsanız db.makaleler.getIndexes() ile listeleyip oradaki name değerini kullanın; index adlandırma ve silme ayrıntıları MongoDB index oluşturma yazısında. Alan adlarını önceden bilmediğiniz durumlar için { "$**": "text" } biçiminde bir wildcard text index de vardır: dize içeren her alanı indeksler. Belgeler bunun ekleme ve güncelleme performansını olumsuz etkilediğini ve hedefli bir text index kadar iyi çalışmadığını belirtir; yalnızca alanlar gerçekten bilinmiyorsa ya da değişkense kullanın. Wildcard text index, normal wildcard index'ten farklıdır; $text'i yalnızca ilki destekler.

$text operatörünün sözdizimi

$text operatörünün sözdizimi: $search, $language, $caseSensitive, $diacriticSensitive
javascript
{
  $text: {
    $search: <string>,
    $language: <string>,
    $caseSensitive: <boolean>,
    $diacriticSensitive: <boolean>
  }
}
AlanGörevVarsayılan
$searchAranacak terimler; boşlukla ayrılan terimler arasında mantıksal VEYAZorunlu
$languageDurak kelime, kök ayırıcı ve tokenizer kurallarını seçerIndex dili
$caseSensitiveBüyük-küçük harf duyarlılığını açarfalse
$diacriticSensitiveAksan (şapka, nokta gibi) duyarlılığını açar, sürüm 3 indekslerdefalse

Varsayılan olarak $text sonuçları alaka puanına göre sıralamaz. Sıralama için aşağıda anlatılan $meta: "textScore" ifadesini kullanmanız gerekir.

Arama dizesinin kuralları

Terimler arasında VEYA

$search: "yedekleme nginx" yazdığınızda MongoDB, iki terimden herhangi birini (kök biçimiyle) içeren belgeleri döndürür. Yani boşluk "ve" değil "veya" anlamına gelir. Bu, ilk kez kullananların en çok şaşırdığı davranıştır.

Tam ifade araması

Birden fazla kelimeyi yan yana, tam ifade olarak aramak için ifadeyi kaçışlı çift tırnak içine alırsınız. Arama dizesinde hem tam ifade hem de tek tek terimler varsa, $text yalnızca tam ifadeyi içeren belgeleri eşleştirir; diğer terimler bu durumda sonucu genişletmez.

javascript
db.makaleler.find( { $text: { $search: "\"index bakımı\"" } } )

// Tam ifade + ek terimler: yalnızca tam ifadeyi içeren belgeler eşleşir
db.makaleler.find( { $text: { $search: "\"index bakımı\" yedekleme" } } )

Hariç tutma (negation)

Bir kelimenin başına eksi işareti koyarsanız o kelimeyi içeren belgeler sonuçtan çıkarılır. Tüm hariç tutmalar mantıksal VE ile uygulanır. Yalnızca hariç tutulan kelimelerden oluşan bir arama hiçbir belge döndürmez. Tire ile yazılmış bileşik kelimeler hariç tutma sayılmaz: MongoDB tireyi ayırıcı olarak görür. Belgelerdeki örnekle, pre-market içinde "market"i dışlamak için pre -market yazmanız gerekir.

javascript
// "yedekleme" gecen ama "cron" gecmeyen belgeler
db.makaleler.find( { $text: { $search: "yedekleme -cron" } } )

Kök ayırma ve durak kelimeler

Büyük-küçük harf ve aksan duyarsız modda $text, kelimenin kök biçiminin tamamıyla eşleşir. Belgelerin İngilizce örneğinde, alanda "blueberry" varsa "blue" araması eşleşmez; "blueberry" ya da "blueberries" eşleşir. Yani text index bir önek veya alt dize araması değildir. Kelimenin bir parçasını aramanız gerekiyorsa $regex daha uygun olabilir; farklarını MongoDB regex sorguları yazısında karşılaştırıyoruz. Durak kelimeler (İngilizcede "the", "and" gibi) hem indekslenmez hem de aramada yok sayılır.

Türkçe ve diğer diller

Text index'in varsayılan dili english değeridir. Türkçe metinler için index oluştururken default_language: "turkish" (ya da iki harfli kod "tr") vermelisiniz; aksi halde İngilizce kök ayırma ve İngilizce durak kelime listesi kullanılır. Desteklenen diller arasında Danca, Felemenkçe, İngilizce, Fince, Fransızca, Almanca, Macarca, İtalyanca, Norveççe, Portekizce, Romence, Rusça, İspanyolca, İsveççe ve Türkçe yer alır.

javascript
db.yorumlar.createIndex(
   { metin: "text" },
   { default_language: "turkish" }
)

// Sorgu dilini ayrica belirtmek mumkun
db.yorumlar.find( { $text: { $search: "sunucu", $language: "tr" } } )

Dil değeri olarak none verirseniz index, durak kelimeler dahil her kelimeyi olduğu gibi işler ve kök ayırma yapmaz. Bu, ürün kodu ya da kullanıcı adı gibi dil kuralı istemeyen metinlerde işe yarar.

Koleksiyonunuzda farklı dillerde belgeler varsa her belgeye language adlı bir alan ekleyip dilini yazabilirsiniz; index o belge için bu alandaki dili kullanır. Dil bilgisini farklı adlı bir alanda tutuyorsanız index oluştururken language_override seçeneğiyle o alanı gösterirsiniz. Bu alanı içermeyen belgelerde index İngilizceyi kullanır.

javascript
db.icerikler.createIndex(
   { govde: "text" },
   { language_override: "dil" }
)
db.icerikler.insertOne( { govde: "Sunucu bakımı tamamlandı", dil: "turkish" } )

Büyük-küçük harf ve aksan davranışı

Sürüm 3 text index (MongoDB 3.2'den beri varsayılan) büyük-küçük harfe ve aksana duyarsızdır. Harf katlama için Unicode 8.0 kurallarını kullanır; belgeler bu kurallar arasında Türk dillerine özgü özel katlama kurallarının da bulunduğunu, aksanlı Latin harflerinin ve Kiril gibi Latin olmayan alfabelerin de desteklendiğini listeler. Aksan duyarsızlığında ise Unicode'da aksan olarak sınıflanan işaretler çıkarılır; belgelerdeki İspanyolca örnekte "punal" araması "puñal" kelimesini bulur. Türkçe metinlerde ç, ş, ğ, ö, ü gibi harflerin aramada nasıl davrandığından emin olmak istiyorsanız kendi verinizle deneyin; gerekirse $diacriticSensitive: true ile aksan duyarlılığını açabilirsiniz.

$caseSensitive: true ya da $diacriticSensitive: true verildiğinde MongoDB önce duyarsız index'i sorgular, sonra sonuçları istenen harf ve aksan biçimine göre süzer. Bu ek süzme performansı düşürebilir. Eski index sürümleri her zaman aksana duyarlıdır ve bu seçenek onlarda etki yapmaz.

Alaka puanı ve alan ağırlıkları

$text her eşleşen belgeye bir alaka puanı verir. Bu puana { $meta: "textScore" } ifadesiyle erişirsiniz; projection içinde göstermek, sort() içinde sıralamak için kullanabilirsiniz. Sıralamada kullanmak için projection'a eklemeniz şart değildir.

javascript
db.makaleler.find(
   { $text: { $search: "yedekleme index" } },
   { puan: { $meta: "textScore" }, baslik: 1 }
).sort( { puan: { $meta: "textScore" } } ).limit( 10 )

Bileşik bir text index'te alanlara farklı ağırlık verebilirsiniz. Varsayılan ağırlık 1'dir. Örneğin başlıktaki eşleşmenin içerikteki eşleşmeden daha önemli olmasını istiyorsanız başlığa daha yüksek ağırlık verirsiniz. Belgelere göre bir terim birden çok alanda eşleştiğinde MongoDB her alandaki eşleşme sayısını o alanın ağırlığıyla çarpar ve sonuçları toplar.

javascript
db.makaleler.createIndex(
   { baslik: "text", icerik: "text", etiketler: "text" },
   {
      weights: { baslik: 10, etiketler: 5 },
      default_language: "turkish",
      name: "MakaleMetinAgirlikli"
   }
)

Bu tanımda baslik 10, etiketler 5, adı yazılmayan icerik ise varsayılan 1 ağırlığa sahiptir. Ağırlıkları index oluşturulduktan sonra değiştirmek koleksiyonun yeniden indekslenmesini gerektirir; büyük koleksiyonlarda bu ciddi bir yüktür. Ağırlıkları baştan düşünerek belirleyin.

Kısıtlar: neyi yapamazsınız?

Kısıtlar: neyi yapamazsınız? Bir sorguda yalnızca bir $text ifadesi olabilir., $text, $nor içinde ve $elemMatch sorgu ya da projection ifadelerinde..., $or ile birlikte...
  • Bir sorguda yalnızca bir $text ifadesi olabilir.
  • $text, $nor içinde ve $elemMatch sorgu ya da projection ifadelerinde kullanılamaz.
  • $or ile birlikte kullanıyorsanız tüm $or kollarının indeksli olması gerekir.
  • $text içeren sorguda hint() ile index seçemezsiniz; $natural sıralaması da kullanılamaz.
  • $text, başka bir özel index gerektiren operatörle birleştirilemez; örneğin $near ile aynı sorguda olamaz.
  • View'lar $text aramasını desteklemez. Stable API V1, index oluşturmada $text'i desteklemez.
  • Text index sıralama performansını artırmaz ve bir sorguyu karşılayamaz (covered query olamaz).
  • Text index collation desteklemez, yalnızca ikili karşılaştırma yapar. Varsayılan collation'ı basit olmayan bir koleksiyonda text index oluştururken { collation: { locale: "simple" } } belirtmeniz gerekir.
  • Time series koleksiyonlarında text index oluşturulamaz.

Aggregation içinde $text kullanırken ek kurallar vardır: $text içeren $match aşaması pipeline'ın ilk aşaması olmalıdır, aşamada $text yalnızca bir kez geçebilir ve $or ya da $not ifadeleri içinde yer alamaz. Puana göre sıralamak için $sort aşamasında $meta kullanılır. Pipeline mantığını MongoDB aggregation pipeline yazısında anlatıyoruz.

Bileşik text index kuralları

Text anahtarını artan veya azalan anahtarlarla birleştirebilirsiniz, ancak bileşik text index multikey ya da coğrafi index alanı içeremez ve tüm text anahtarları index tanımında yan yana yazılmalıdır. Text anahtarından önce gelen anahtarlar varsa, $text kullanabilmek için sorgunun bu anahtarlar üzerinde eşitlik koşulu içermesi gerekir. Örneğin { kategori: 1, icerik: "text" } index'i, sorguda kategori: "linux" gibi bir eşitlik olmadan $text için kullanılamaz. Bu yapı, taranan index girdisini kategoriyle sınırlamanın belgelenmiş yoludur. Genel bileşik index mantığı için MongoDB compound index ve ESR kuralı yazısına bakın.

Depolama ve performans maliyeti

Text index'ler hafife alınmamalıdır. Her belgedeki her indeksli alan için, her benzersiz kök kelimeye bir index girdisi yazılır; bu yüzden ciddi miktarda RAM tüketebilirler. Text index oluşturmak, aynı veri üzerinde sıradan bir index oluşturmaktan daha uzun sürer ve büyük bir multikey index oluşturmaya benzer. Büyük text index'ler oluştururken işletim sisteminin dosya tanımlayıcı (file descriptor) sınırlarının yeterli olduğundan emin olun; önerilen ayarlar için MongoDB production notları yazısına bakabilirsiniz. Yazma tarafında da maliyet vardır: her yeni belgede tüm benzersiz kökler için index girdisi eklenir.

Text index kelimeleri tek tek saklar; kelime grupları veya kelimelerin birbirine yakınlığı bilgisi tutulmaz. Çok kelimeli sorgular, koleksiyonun tamamı RAM'e sığdığında daha hızlı çalışır. MongoDB 8.3 ile, text puanı okuyan sorguları işleyen TextOr aşamasının bellek kullanımı 100 MB ile sınırlanmıştır: allowDiskUse açıksa ara sonuçlar diske yazılır, kapalıysa sorgu bellek sınırı hatasıyla başarısız olur. MongoDB 9.0'daki işlem başına bellek sınırı için MongoDB 9.0 yenilikleri yazısına bakın.

Uçtan uca örnek: blog içi arama

Şimdi parçaları birleştirelim. Bir blogda ziyaretçinin yazdığı kelimeyi başlık, içerik ve etiketlerde arayan, yalnızca yayında olan yazıları getiren, en alakalı on sonucu sıralayan bir sorgu kuruyoruz. Index olarak yukarıdaki ağırlıklı MakaleMetinAgirlikli index'ini kullandığımızı varsayalım.

javascript
const aranan = "yedekleme -windows"

db.makaleler.find(
   { $text: { $search: aranan }, yayinda: true },
   { baslik: 1, puan: { $meta: "textScore" } }
).sort( { puan: { $meta: "textScore" } } ).limit( 10 )

Bu sorgu "yedekleme" kökünü içeren ama "windows" geçmeyen, yayinda alanı true olan belgeleri bulur. Başlıktaki eşleşmeler ağırlık 10 olduğu için puanı en çok yükseltir, etiketler ondan sonra gelir. $text koşulunu başka sorgu koşullarıyla aynı filtrede birleştirmek belgelerde gösterilen bir kullanımdır. Kullanıcıdan gelen metni doğrudan $search içine koyduğunuzda, kullanıcının yazdığı eksi işaretleri ve tırnaklar da arama sözdizimi olarak yorumlanır; bu bir özellik olabilir ama beklemiyorsanız girdiyi uygulama tarafında temizleyin. Sorgunun hangi planla çalıştığını görmek için sonuna .explain("executionStats") ekleyebilirsiniz.

Sık yapılan hatalar

Sık yapılan hatalar: Türkçe içerikte dil vermemek, İkinci bir text index eklemeye çalışmak, Boşluğu VE sanmak, Önek araması beklemek
  • Türkçe içerikte dil vermemek: Varsayılan İngilizce kurallar Türkçe metinde beklenmedik kök ayırma ve durak kelime davranışına yol açar.
  • İkinci bir text index eklemeye çalışmak: Koleksiyon başına yalnızca bir text index olabilir; alanları tek bileşik index'te toplayın.
  • Boşluğu VE sanmak: Terimler VEYA ile birleşir; tümünün geçmesini istiyorsanız tam ifade kullanın ya da sonuçları ek koşulla süzün.
  • Önek araması beklemek: "yede" yazarak "yedekleme"yi bulamazsınız; text index kök eşleşmesi yapar.
  • Sonuçların puana göre geldiğini varsaymak: Sıralama için $meta: "textScore" şarttır.
  • Ağırlıkları sık değiştirmek: Her değişiklik yeniden indeksleme demektir.

Sık Sorulan Sorular

Text index ile $regex arasında hangisini seçmeliyim?

Kelime bazlı arama, kök eşleşmesi ve alaka puanı istiyorsanız text index uygundur. Bir alanın belirli bir desenle başlaması veya içinde belirli bir parça geçmesi gibi karakter düzeyinde eşleşme istiyorsanız $regex gerekir. İkisi farklı problemleri çözer.

Text index sparse midir?

Evet, text index her zaman sparse davranır ve sparse seçeneği yok sayılır. İndeksli alanı olmayan, alanı null ya da boş dizi olan belgeler için index girdisi oluşturulmaz.

Text index sürümünü değiştirmem gerekir mi?

Normalde hayır. MongoDB 3.2'den beri varsayılan sürüm 3'tür ve bu yazıdaki harf ve aksan davranışları sürüm 3'e aittir. Belgeler eski bir sürümü textIndexVersion seçeneğiyle zorlamanın mümkün olduğunu gösterir, ama yeni projelerde buna gerek yoktur.

Arama sonuçlarını sayfalamak mümkün mü?

Evet. Puana göre sıraladıktan sonra limit() ve skip() kullanabilirsiniz. Büyük sonuç kümelerinde sayfalama stratejileri için MongoDB sort, limit ve skip yazısına bakın.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: MongoDB öğrenme rehberi · MongoDB regex sorguları · MongoDB index oluşturma · Compound index ve ESR · MongoDB explain