C# üzerinde NEST paketi kullanarak farklı yapılarda ihtiyaçlarınıza göre sorgular oluşturabilirsiniz. Fakat ihtiyacınıza en uygun şekilde oluşturmak için bilmeniz gereken Query yapısı bulunuyor.
Bunu bir örnek üzerinden anlatmak daha iyi olur. C# üzerinde bir model oluşturalım. Güncel geliştirdiğim bir uygulama üzerinden örnek olan mevzuatlar.org.tr üzerinde olan mevzuatları saklayacak bir model geliştirelim. Amacımız ise sakladığımız binlerce veri içerisinde doğru sorgular oluşturmak olacak.
[ElasticsearchType(Name = "regulation")]
public class Regulation
{
public int Id { get; set; }
public string RegulationNo { get; set; }
public string Title { get; set; }
public int TypeId { get; set; }
public List<int> RelatedItems { get; set; }
public string Content { get; set; }
public System.DateTime? ReleaseDate { get; set; }
public System.DateTime AddedDate { get; set; }
public bool IsActive { get; set; }
public bool IsDeleted { get; set; }
}Projenize NEST paketini (bağımlılık olarak Elasticsearch gibi paketler otomatik kurulur) kurmalısınız. NEST üzerinden ElasticClient oluşturun. xı almak için bir fabrika da oluşturdum, basitleştirmek için client.SearchAsync()’i çağırdığımda onu zaten örnekleştirmiş ve hazırlamış olacağımı unutmayın.
Yapısal (Structured) ve Yapısal Olmayan (Unstructured) Arama Çeşitleri
Bir aramanın yapısal veya yapısal olmayan türde olması için filtreleri nasıl uyguladığınızla ilgilidir. Yapısal arama, aramada bir aralık veya mutlak değere sahip olabilen tarih, saat veya sayı gibi verileri ifade eder ve eşleşmeler ya evet ya hayırdır, ancak kısmen eşleşme olamaz. Yapılandırılmamış arama ise kısmi eşleşmelerle ilgilidir ve eşleşmenin alaka düzeyini belirlemek için puanlama (scoring) devreye girer. Elasticsearch yapılandırılmamış aramalarda çok fazla seçeneğimiz var. Wildcard, Querystring vs. gibi geniş yelpazede aramalar yaptırabiliyorsunuz ve bunları alaka düzeylerine göre yani skorlarına göre sıralayabiliyorsunuz.
Aşağıdaki gibi bir ekosistemimiz olduğunu düşünelim. Kullanıcı girdisi olacak, arama servisi geliştirilecek ve doğru yapıda oluşturduğumuz aramaları ES indeksleri içinde aratacağız. Bulunan dökümanlardan skorlama yaparak sonuçlar almaya çalışacağız.

Bu konuyu 2 farklı temelde ele alacağız:
- Birincisi ElasticClient ile expression-based sorguları oluşturmak.
- İkincisi NEST’in getirdiği QueryContainer ile dinamik sorgu oluşturma işlemi.
ElasticClient ile Sorgular Oluşturmak

Şimdi ElasticClient kullanarak SearchAsync için expression bazlı arama sorguları oluşturacağız. SearchAsync kullanımına göre beklenilen şey Func<SearchDescriptor<Regulation>, ISearchRequest>.
1 – Sayfalama Yaptırmak
“from” ve “size” kullanılarak sayfalama yaptırılır.
int pageSkip = 0;
int pageSize = 100;
var response = await client.SearchAsync<Regulation>(s => s.From(pageSkip).Size(pageSize));2 – Integer Değerleri Filtrelemek
Term Query kullanarak arattırma yaptırmak:
int typeId = 6;
var response = await client.SearchAsync<Regulation>(s => s.Size(pageSize).Query(q => q.Term(c => c.Field(p => p.TypeId).Value(typeId))));Terms Query kullanarak arattırma yaptırmak: Bunu SQL’de “IN” yerine sayabilirsiniz. Bir dizi içerisinde arattırma yapılıyor. İlişkili kayıtlarımda arattırma yapmak için böyle kullanabiliyoruz:
int[] sampleArray = new int[] { 11, 24, 31, 45 };
var response = await client.SearchAsync<Regulation>(s => s.Size(pageSize).Query(q => q.Terms(c => c.Field(p => p.RelatedItems).Terms(sampleArray)))3. Tarihe göre arattırma
Burada “DateRange Query” kullanılıyor.
var d1 = await client.SearchAsync<Regulation>(sr => sr
.Query(q => q.DateRange(r => r
.Field(f => f.AddedDate)
.GreaterThanOrEquals(new DateTime(2024, 02, 03))
.LessThan(new DateTime(2025, 01, 01))
))
);Daha fazlasını burada açıkladılar: https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-range-query
4 – String değerleri arattırma
String ifadeler yapılandırılmamış sorgular konusudur. Kimin daha iyi eşleştiğini belirlemek için puana dahil edilen kısmi eşleşmelere olanak tanır. Match(), Prefix() ve MatchPhrasePrefix() sorgularının hepsi yapılandırılmamış sorgulardır. Bunun konusunu biraz ayıralım.
— Match query: Bir veya birden fazla kelime geçen kayıtları bulmak için kullanılır.
var result = await client.SearchAsync<Regulation>(s => s
.Query(q => q.Match(m => m
.Field(f => f.Title)
.Query("data protection regulation")))
);Match araması, kelimeleri analiz edip “data”, “protection” veya “regulation” geçen kayıtları bulur. Eğer “title” alanın text tipindeyse (default analyzer varsa) bu şekilde çalışır.
Burada analyzer önemli bir husus fakat bu ileride işlenecek ayrı bir konu olacak.
— Prefix query: Alan belirli bir kelimeyle başlıyorsa bu tür kullanılabilir.
var result = await client.SearchAsync<Regulation>(s => s
.Query(q => q.Prefix(p => p
.Field(f => f.RegulationNo)
.Value("REG-2025")
))
);Sadece tek bir kelime destekler. “REG-2025” ile başlayan ifadeyi getirir. Örneğin “REG-2025-ABC” eşleşir ama “NEW-REG-2025” eşleşmez. C# üzerinde “StartsWith” gibi düşünebilirsiniz.
— Exact match: keyword bir suffix ifadedir ve tam eşleşme için gereklidir. Analyzer kullanılmadan birebir aynı kelime aranır.
var result = await client.SearchAsync<Regulation>(s => s
.Query(q => q.Term(t => t
.Field(f => f.RegulationNo.Suffix("keyword"))
.Value("REG-2025-001")
))
);— MatchPhrase query: Cümle içinde sırayla geçen kelimeleri bulur. Örnekte “title” alanında arattırma yapılıyor.
var result = await client.SearchAsync<Regulation>(s => s
.Query(q => q.MatchPhrase(m => m
.Field(f => f.Title)
.Query("data protection act")
))
);— MatchPhrasePrefix query: Cümle başına göre önerme yapar. Aşağıda “title” alanında bir aratma yapıyoruz.
var result = await client.SearchAsync<Regulation>(s => s
.Query(q => q.MatchPhrasePrefix(m => m
.Field(f => f.Title)
.Query("data protecti")
.MaxExpansions(10)
))
);“data protecti”, “data protection”, “data protective” vb. gibi tamamlamaları da yakalar. MaxExpansions ile kaç varyasyon genişletileceğini kontrol edebilirsiniz.
— Bool query: Birden fazla koşulu birleştirmeyi sağlar. Bunu dinamik sorgu üretiminde sıkça kullanacağız.
var result = await client.SearchAsync<Regulation>(s => s
.Query(q => q.Bool(b => b
.Must(
m => m.Match(mm => mm.Field(f => f.Title).Query("environment")),
m => m.Term(t => t.Field(f => f.IsActive).Value(true))
)
.Filter(
f => f.Range(r => r.Field(f => f.ReleaseDate).Gte("2024-01-01"))
)
))
);Yukarıdaki sorguda 2 sorgu birleştirilmiş ama dikkatinizi bu ifade mutlaka çekmeli: “Must”. Bu bir zorunluluk. Yani sorguların hepsi kesin ifadeyle sağlanmalı.
“Filter” ifadesi ise skoru etkilemeden filtreleme yapar ve performanslıdır.
Boolean Sorgular
Boolean sorgular, birden fazla koşulun bulunduğu bileşik (composed) sorgulardır ve bu koşulların toplamı AND, OR ve NOT operatörleriyle birleştirilir.
Boolean sorgular oluştururken bu sorgulara filter (filtre) ekleyebiliriz. Bir filtre temelde Must() sorgusuna benzer ancak filtrelenen sonuçlar skor hesaplamasına dahil edilmez.
Bu da skor hesaplamasını hızlandırır ve aramanın daha az kaynak tüketmesini sağlar.
Bu sebeple yapısal (structured) koşulları mümkünse filter içine, yapısal olmayan (unstructured) koşulları ise skoru etkileyebilecek şekilde Must() içine koymak daha doğrudur.
Operatörler şöyledir: AND (&&), OR (||), NOT (!), + (Filter olarak geçer)
AND ve OR operatörleriyle tek satırda boolean query yazabiliriz. Aşağıda bir örneğini tasarladım. Bold yaptığım alanlarda bool query hazırlayıp operatörler kullandım.
var result = await client.SearchAsync<Regulation>(sr => sr
.Query(q => +q.Terms(c => c.Field(p => p.TypeId).Terms(typeIds)) &&
q.Match(m => m.Field(f => f.Title).Query(query)) ||
q.Match(m => m.Field(f => f.Content).Query(query))
) &&
!q.Term(t => t.Field(f => f.IsDeleted).Value(true))
)
);
“+” operatörünü kullandığımız için skorlamayı etkilemeyecek. “Title” veya “Content” bölümlerimizde eşleşecek şekilde arattırma yaptık. Sonunda ise “silinmemiş” kayıtlarımız için koşul belirttik.
Arama Filtrelerinin Genişletilmesi
Tek satırda boolean query yaptırmak işi biraz karmaşık hale getirebilir. Biz bu filtreleri farklı bir şekilde de yazabiliriz. Karışık geliyorsa bu sorguları dışarıda da tanımlayabiliriz. Örneğin aşağıda 2 farklı “match query” hazırlayıp “Must()” olarak getirilmesini istedik:
var matchTitleFilter = new QueryContainerDescriptor<Regulation>().Match(m => m.Field(f => f.Title).Query(query));
var matchContentFilter = new QueryContainerDescriptor<Regulation>().Match(m => m.Field(f => f.Content).Query(query));
var result = await client.SearchAsync<Regulation>(sr => sr
.Query(q => q.Bool(b => b.Must(matchTitleFilter, matchContentFilter))));“QueryContainerDescriptor” bize bu filtrenin dışarıda kullanımını sağlıyor. En altta ise “Bool” query hazırlayıp “Must” diyerek arama filtremizi katı hale getirdik.
Bunun “should” örneği ise böyle yazılabilir:
var result = await client.SearchAsync<Regulation>(sr => sr
.Query(q => q.Bool(b => b
.Should(matchTitleFilter, matchContentFilter)
.MinimumShouldMatch(1)
))
);“MinimumShouldMatch” belirterek en az 1 tane eşleşmesi gerektiğini vurguladık. Bu sorgu daha esnek. “Must” daha katı bir arama yapar.
Bu özellikler sayesinde birçok sorguyu yaparak istediğinizi bulabilirsiniz. Burada direkt “client” kullanılarak sorgu oluşturuluyor. Oysa bunu “ISearchRequest” formatında da vererek daha esnek sorgular oluşturabiliriz.
Sorguları Dinamik Olarak Oluşturmak
Varsayalım arama kriterleriniz fazla veya koşul sayısı belirsiz. Bir listede ürün adını aratmak gibi basit bir şeyden bahsetmiyorum. Bir tabloda veya arama sonuçlarında sizden çok daha fazlasını beklediklerini düşünün. Dinamik olarak birden fazla koşula bağlı çalışması gerekiyor.

Görsel: Elastic.co
Arama filtrelerinin genişletilmesi bölümünde gösterdiğim gibi, önceden filtreleri orada olduğu gibi tanımlamak işinizi görecektir ancak AND, OR, NOT gibi sembolleri dinamik olarak birleştirmek zor olur. Çünkü kaç tane filtre ekleneceğini bilmiyorsunuz. Yeni geliştirmeler talep ettiklerinde de işler daha karışık olacaktır.
Bu durumda, bool operatörünü ve filter array kullanmak en doğru yaklaşımdır. Neticede boolean sorgular must, should ve hatta iç içe (nested) farklı bool query hazırlamanıza olanak sağlıyor.
1. QueryContainerDescriptor ile filtre tanımlama
Aşağıdaki örnekte, istediğiniz sayıda filtreyi dinamik olarak ekleyebileceğiniz bir yapı kuruluyor. Açıklamaları da içerisinde belirteceğim.
// TMK hakkında bir arattırma yapacağız
string query = "TMK 4721";
// Filtreleri dışarıda oluşturuyorum
var matchTitleFilter = new QueryContainerDescriptor<Regulation>().Match(m => m.Field(f => f.Title).Query(query));
var matchNoFilter = new QueryContainerDescriptor<Regulation>().Match(m => m.Field(f => f.RegulationNo).Query(query));
var filterQueries = new QueryContainer[]
{
matchTitleFilter,
matchNoFilter
};
// Should kullandığımızda "OR" gibi davranacak
var boolQuery = new BoolQuery
{
Name = "boolQuery",
Should = filterQueries,
MinimumShouldMatch = 1
};
var result = await client.SearchAsync<Regulation>(sr => sr.Query(q => boolQuery));Dilerseniz bu query içerisine “filter” da yazabilirsiniz.
2. Koşullandırarak bool query hazırlamak
İşler karmaşık hale geldiğinde dinamik olarak daha okunaklı ve sade arama yaptırmak istediğiniz birçok alan olabilir. Modelimizde “Title”, “DecisionType” gibi alanlarımız var. Arama sonuçlarında bunları koşullandırarak filtreleyebiliriz.
Örneğimizde sayfalama yaptırarak, sıralama yaptırarak ve isteğini koşullandırarak dinamik olarak boolean query hazırlanıyor:
int pageNumber = 5;
int requestOffset = (pageNumber - 1) * pageSize;
string searchValue = "Türk Ceza Kanunu";
string decisionType = "Kanun";
// AND sorgusu hazırlıyoruz
var mustQueries = new List<QueryContainer>();
if (!string.IsNullOrEmpty(searchValue))
{
mustQueries.Add(new MatchQuery
{
Field = "title",
Query = searchValue
});
}
if (!string.IsNullOrEmpty(decisionType))
{
mustQueries.Add(new TermQuery
{
Field = "decisionType.keyword",
Value = decisionType
});
}
// Sıralama yaptırıyoruz. Skora göre ve tarihe göre tersten
var sortList = new List<ISort>
{
new FieldSort { Field = "_score", Order = SortOrder.Descending },
new FieldSort { Field = "publishedDate", Order = SortOrder.Descending }
};
// Bool query oluşturuyoruz ve "must" listesini veriyoruz
var boolQuery = new BoolQuery
{
Must = mustQueries
};
// Sorgu konteyneri oluşturuyoruz
IQueryContainer queryContainer = new QueryContainer { Bool = boolQuery };
// Elastic'e bu isteği gönderiyoruz
string indexName = "regulations";
var response = await _elasticContext.SearchAsync<Regulation>(new SearchRequest(indexName)
{
TrackScores = true,
TrackTotalHits = true,
Size = pageSize,
From = requestOffset,
Query = queryContainer,
Sort = sortList,
RequestCache = true
});Bu yapının avantajlarını şöyle sıralayabiliriz:
- Basit: BoolQuery ile doğrudan “Must” içinde istediğiniz kadar filtre ekleyebilirsiniz.
- Okunabilir: Func veya karmaşık filters yapısına gerek yok.
- Genişletilebilir: Daha sonra should veya filter ekleyerek genişletebilirsiniz.
Burada performans da kazandırdık. Çünkü RequestCache aktif, TrackScores sadece gerekli durumlarda açık.
Boosting (Öncelik)
Yapısal olmayan (unstructured) aramalarda, bazı alanların diğerlerinden daha önemli (öncelikli) olmasını isteyebiliriz. Bu durumda, Boost() metodunu kullanarak ilgili eşleşmenin skorunu çarpanla artırabiliriz. Yani Boost() değeri ne kadar büyükse, o alanın arama sonucuna etkisi o kadar fazla olur.
Örnek:
// Title alanını 2.5 kat daha önemli yapıyorum
var boostedQuery = new QueryContainerDescriptor<Regulation>()
.MatchPhrasePrefix(m => m
.Boost(2.5)
.Field(f => f.Title)
.Slop(3)
.Query(searchText)
);Burada kullandığımız “Slop(3)” ifadesi, aranan kelimeler arasında en fazla 3 kelimelik boşluk olabilir.
Sorting (Sıralama)
Sayısal (numeric) veya tarih (date) tipindeki alanlara göre sıralama oldukça basittir:
var result = await client.SearchAsync<Regulation>(qry.Sort(s => s.Ascending(x => x.CreatedDate)));Ama metin (text) alanlarda sıralama yapmak yasaktır Text alanları Elasticsearch içinde full-text olarak saklanır ve doğrudan sıralanamaz. Bu nedenle sıralama yapmak için keyword tipi alan kullanılır. Keyword aynı alanın kopyasıdır, sıralama veya agregasyon için saklanır.
Genel anlamda sorguların anlaşıldığı kanaatindeyim. İlerleyen dönemlerde sorgu türlerine göre örnekler yapacağız. Hangi query türü nerede ve nasıl kullanılacağı hususunda daha ayrıntılı içerikler hazırlamaya çalışacağım. Umarım faydalı olmuştur.



Yorum bırakın