Elasticsearch NEST Client¶
NEST, .NET için resmi Elasticsearch istemcisidir; yanlış kullanımlar arama performansını düşürür ve index yönetimini zorlaştırır.
1. ElasticClient’ı Her Çağrıda Oluşturmak¶
❌ Yanlış Kullanım: Her istekte yeni ElasticClient oluşturmak.
public async Task<List<Product>> SearchAsync(string query)
{
var settings = new ConnectionSettings(new Uri("http://localhost:9200"))
.DefaultIndex("products");
var client = new ElasticClient(settings); // Her çağrıda yeni bağlantı
var response = await client.SearchAsync<Product>(s => s
.Query(q => q.Match(m => m.Field(f => f.Name).Query(query))));
return response.Documents.ToList();
}
✅ İdeal Kullanım: DI ile singleton olarak kaydedin.
builder.Services.AddSingleton<IElasticClient>(sp =>
{
var settings = new ConnectionSettings(new Uri("http://localhost:9200"))
.DefaultIndex("products")
.DefaultMappingFor<Product>(m => m.IndexName("products"))
.DefaultMappingFor<Order>(m => m.IndexName("orders"))
.EnableDebugMode()
.RequestTimeout(TimeSpan.FromSeconds(30));
return new ElasticClient(settings);
});
public class ProductSearchService
{
private readonly IElasticClient _client;
public ProductSearchService(IElasticClient client) => _client = client;
public async Task<List<Product>> SearchAsync(string query)
{
var response = await _client.SearchAsync<Product>(s => s
.Query(q => q.Match(m => m.Field(f => f.Name).Query(query))));
return response.Documents.ToList();
}
}
2. Mapping Tanımlamamak¶
❌ Yanlış Kullanım: Elasticsearch’ün otomatik mapping yapmasına güvenmek.
await _client.IndexDocumentAsync(new Product
{
Name = "Laptop",
Description = "Güçlü dizüstü bilgisayar",
Price = 15000
});
// Elasticsearch tüm string'leri text+keyword olarak map'ler, gereksiz alan
✅ İdeal Kullanım: Explicit mapping ile index oluşturun.
public static class ElasticsearchIndexSetup
{
public static async Task CreateProductIndexAsync(IElasticClient client)
{
var existsResponse = await client.Indices.ExistsAsync("products");
if (existsResponse.Exists) return;
await client.Indices.CreateAsync("products", c => c
.Settings(s => s
.NumberOfShards(1)
.NumberOfReplicas(1)
.Analysis(a => a
.Analyzers(an => an
.Custom("turkish_analyzer", ca => ca
.Tokenizer("standard")
.Filters("lowercase", "turkish_stop")))))
.Map<Product>(m => m
.Properties(p => p
.Keyword(k => k.Name(n => n.Id))
.Text(t => t.Name(n => n.Name)
.Analyzer("turkish_analyzer")
.Fields(f => f.Keyword(kw => kw.Name("keyword"))))
.Text(t => t.Name(n => n.Description).Analyzer("turkish_analyzer"))
.Number(n => n.Name(nn => nn.Price).Type(NumberType.Double))
.Date(d => d.Name(n => n.CreatedAt))
.Keyword(k => k.Name(n => n.CategoryId)))));
}
}
3. Arama Sorgusunu Tek Alana Sınırlamak¶
❌ Yanlış Kullanım: Sadece bir alanda arama yapmak.
var response = await _client.SearchAsync<Product>(s => s
.Query(q => q.Match(m => m.Field(f => f.Name).Query(searchTerm))));
// Sadece Name alanında arar, Description'da eşleşme kaçırılır
✅ İdeal Kullanım: Multi-match ve boosting ile kapsamlı arama yapın.
public async Task<SearchResult<Product>> SearchAsync(string searchTerm, int page = 1, int pageSize = 20)
{
var response = await _client.SearchAsync<Product>(s => s
.From((page - 1) * pageSize)
.Size(pageSize)
.Query(q => q
.Bool(b => b
.Should(
sh => sh.MultiMatch(mm => mm
.Fields(f => f
.Field(p => p.Name, boost: 3)
.Field(p => p.Description, boost: 1))
.Query(searchTerm)
.Type(TextQueryType.BestFields)
.Fuzziness(Fuzziness.Auto)),
sh => sh.Prefix(p => p
.Field(f => f.Name)
.Value(searchTerm.ToLower())))))
.Highlight(h => h
.Fields(f => f
.Field(p => p.Name)
.Field(p => p.Description))
.PreTags("<mark>")
.PostTags("</mark>")));
return new SearchResult<Product>
{
Items = response.Documents.ToList(),
TotalCount = response.Total,
Highlights = response.Hits.ToDictionary(
h => h.Id, h => h.Highlight)
};
}
4. Bulk İşlem Kullanmamak¶
❌ Yanlış Kullanım: Tek tek belge index’lemek.
foreach (var product in products)
{
await _client.IndexDocumentAsync(product); // Her belge için ayrı HTTP isteği
}
// 10.000 belge = 10.000 HTTP isteği
✅ İdeal Kullanım: Bulk API ile toplu işlem yapın.
public async Task BulkIndexAsync(IEnumerable<Product> products)
{
var bulkResponse = await _client.BulkAsync(b => b
.Index("products")
.IndexMany(products)
.Refresh(Refresh.WaitFor));
if (bulkResponse.Errors)
{
foreach (var item in bulkResponse.ItemsWithErrors)
{
_logger.LogError("Bulk index hatası: {Id} - {Error}",
item.Id, item.Error.Reason);
}
}
}
// Çok büyük veri setleri için scroll ile okuma
public async IAsyncEnumerable<Product> ScrollAllAsync()
{
var response = await _client.SearchAsync<Product>(s => s
.Size(1000)
.Scroll("2m")
.Query(q => q.MatchAll()));
while (response.Documents.Any())
{
foreach (var doc in response.Documents)
yield return doc;
response = await _client.ScrollAsync<Product>("2m", response.ScrollId);
}
await _client.ClearScrollAsync(c => c.ScrollId(response.ScrollId));
}
5. Aggregation Kullanmamak¶
❌ Yanlış Kullanım: İstatistikleri uygulama tarafında hesaplamak.
var allProducts = await _client.SearchAsync<Product>(s => s.Size(10000));
var avgPrice = allProducts.Documents.Average(p => p.Price);
var categories = allProducts.Documents.GroupBy(p => p.CategoryId).Count();
// Tüm belgeleri çekip bellekte hesaplamak verimsiz
✅ İdeal Kullanım: Elasticsearch aggregation ile sunucu tarafında hesaplayın.
public async Task<ProductStats> GetStatsAsync()
{
var response = await _client.SearchAsync<Product>(s => s
.Size(0) // Belge döndürme, sadece aggregation
.Aggregations(a => a
.Average("avg_price", avg => avg.Field(f => f.Price))
.Terms("by_category", t => t
.Field(f => f.CategoryId)
.Size(20))
.DateHistogram("by_month", dh => dh
.Field(f => f.CreatedAt)
.CalendarInterval(DateInterval.Month))));
return new ProductStats
{
AveragePrice = response.Aggregations.Average("avg_price").Value ?? 0,
CategoryCounts = response.Aggregations.Terms("by_category").Buckets
.ToDictionary(b => b.Key, b => b.DocCount ?? 0),
MonthlyTrend = response.Aggregations.DateHistogram("by_month").Buckets
.Select(b => new MonthlyCount(b.Date, b.DocCount))
.ToList()
};
}