EElasticsearch Handbook

ORTA

.NET Entegrasyonu (Production Patterns)

Production .NET uygulamalarında Elasticsearch client'ı doğru yapılandırmak, DI, resilience, ve error handling kritiktir.

.NET Client (Self-Managed)
// === DI Registration (Program.cs) ===
builder.Services.AddSingleton<ElasticsearchClient>(sp =>
{
    var config = sp.GetRequiredService<IOptions<ElasticConfig>>().Value;

    var pool = config.Nodes.Length > 1
        ? new StaticNodePool(config.Nodes.Select(n => new Uri(n)))
        : new SingleNodePool(new Uri(config.Nodes[0]));

    var settings = new ElasticsearchClientSettings(pool)
        .Authentication(new ApiKey(config.ApiKey))
        .DefaultIndex(config.DefaultIndex)
        .RequestTimeout(TimeSpan.FromSeconds(config.TimeoutSeconds))
        .MaxRetries(3)
        .MaxRetryTimeout(TimeSpan.FromSeconds(30))
        .DisableDirectStreaming(config.EnableDebug)
        .OnRequestCompleted(details =>
        {
            if (details.HttpStatusCode >= 400)
            {
                var logger = sp.GetRequiredService<ILogger<ElasticsearchClient>>();
                logger.LogWarning("ES request failed: {Method} {Path} -> {Status}",
                    details.HttpMethod, details.Uri?.PathAndQuery, details.HttpStatusCode);
            }
        });

    return new ElasticsearchClient(settings);
});

// === Configuration ===
public class ElasticConfig
{
    public string[] Nodes { get; set; } = ["http://localhost:9200"];
    public string ApiKey { get; set; } = "";
    public string DefaultIndex { get; set; } = "products";
    public int TimeoutSeconds { get; set; } = 10;
    public bool EnableDebug { get; set; } = false;
}

// === Generic Repository Pattern ===
public class ElasticRepository<T> where T : class
{
    private readonly ElasticsearchClient _client;
    private readonly string _indexName;
    private readonly ILogger _logger;

    public ElasticRepository(ElasticsearchClient client, string indexName, ILogger logger)
    {
        _client = client;
        _indexName = indexName;
        _logger = logger;
    }

    public async Task<T?> GetByIdAsync(string id)
    {
        var response = await _client.GetAsync<T>(id, g => g.Index(_indexName));
        if (!response.IsValidResponse)
        {
            if (response.ApiCallDetails.HttpStatusCode == 404) return null;
            _logger.LogError("ES Get failed: {Debug}", response.DebugInformation);
            throw new ElasticException("Get failed", response.ElasticsearchServerError);
        }
        return response.Source;
    }

    public async Task<PagedResult<T>> SearchAsync(
        Action<SearchRequestDescriptor<T>> configure,
        int page = 0, int size = 20)
    {
        var response = await _client.SearchAsync<T>(s =>
        {
            s.Index(_indexName).From(page * size).Size(size);
            configure(s);
        });

        if (!response.IsValidResponse)
            throw new ElasticException("Search failed", response.ElasticsearchServerError);

        return new PagedResult<T>(
            response.Documents.ToList(),
            response.Total,
            page, size);
    }

    public async Task IndexAsync(T document, string id)
    {
        var response = await _client.IndexAsync(document, i => i
            .Index(_indexName).Id(id));
        if (!response.IsValidResponse)
            throw new ElasticException("Index failed", response.ElasticsearchServerError);
    }
}

public record PagedResult<T>(List<T> Items, long Total, int Page, int Size);

Örnek: Bir SaaS platformunda ElasticRepository generic pattern ile Product, Order, Customer index'leri yönetilir. SingleNodePool dev'de, StaticNodePool prod'da kullanılır. MaxRetries=3 ile geçici network hatalarına dayanıklılık sağlanır.

ElasticsearchClient SINGLETON olmalıdır! Her request'te yeni client oluşturmak connection pool'u bozar ve socket exhaustion'a yol açar. DI'da AddSingleton kullanın.

Elastic Cloud Bağlantısı

.NET Client (Elastic Cloud)
// === Elastic Cloud connection (cloud-id + API key) ===
// Elastic Cloud console'dan cloud-id ve API key alın
builder.Services.AddSingleton<ElasticsearchClient>(sp =>
{
    var config = sp.GetRequiredService<IOptions<ElasticCloudConfig>>().Value;

    // Cloud ID: "deployment-name:base64-encoded-data"
    var settings = new ElasticsearchClientSettings(config.CloudId,
            new ApiKey(config.ApiKey))
        .DefaultIndex(config.DefaultIndex)
        .RequestTimeout(TimeSpan.FromSeconds(10))
        .MaxRetries(3);

    return new ElasticsearchClient(settings);
});

public class ElasticCloudConfig
{
    // Cloud ID format: "my-deployment:dXMtY2VudHJhbC0xLmd..."
    public string CloudId { get; set; } = "";
    public string ApiKey { get; set; } = "";
    public string DefaultIndex { get; set; } = "products";
}

// appsettings.json:
// {
//   "ElasticCloud": {
//     "CloudId": "my-app:dXMtY2VudHJhbC0xLmdjcC5jbG91ZC5lcy5pbzo0NDMkZTBl...",
//     "ApiKey": "VnVhQ2ZIY0JDZGJrUW0...",
//     "DefaultIndex": "products"
//   }
// }

Self-managed vs Cloud: Self-managed'da StaticNodePool + new Uri() kullanırsınız. Elastic Cloud'da ise CloudId + ApiKey yeterli — TLS, load balancing, node discovery otomatik. Aynı ElasticsearchClient API'si her iki ortamda da çalışır.

Elastic Serverless (Preview): Elastic Cloud Serverless, cluster yönetimi gerektirmeyen fully-managed seçenektir. Aynı .NET client, Serverless endpoint'ine de bağlanır — sadece endpoint URL + API key yeterli. Şu an preview aşamasında; production workload'ları için Elastic Cloud (hosted) tercih edin.

Polly Resilience Patterns

.NET Client (Polly + HttpClientFactory)
// === Polly Resilience Pipeline (Microsoft.Extensions.Resilience) ===
// NuGet: Microsoft.Extensions.Http.Resilience 9.x

// Program.cs — Resilient HTTP yapılandırması
builder.Services.AddHttpClient("elasticsearch")
    .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://es-cluster:9200"))
    .AddResilienceHandler("es-pipeline", builder =>
    {
        // Circuit Breaker: 5 hata → 30s devre dışı
        builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
        {
            FailureRatio = 0.5,
            SamplingDuration = TimeSpan.FromSeconds(10),
            MinimumThroughput = 5,
            BreakDuration = TimeSpan.FromSeconds(30),
            ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
                .HandleResult(r => r.StatusCode == System.Net.HttpStatusCode.TooManyRequests
                    || r.StatusCode >= System.Net.HttpStatusCode.InternalServerError)
        });

        // Retry: exponential backoff + jitter
        builder.AddRetry(new HttpRetryStrategyOptions
        {
            MaxRetryAttempts = 3,
            Delay = TimeSpan.FromMilliseconds(500),
            BackoffType = DelayBackoffType.Exponential,
            UseJitter = true,
            ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
                .HandleResult(r => r.StatusCode == System.Net.HttpStatusCode.TooManyRequests
                    || (int)r.StatusCode >= 500)
        });

        // Timeout: per-request
        builder.AddTimeout(TimeSpan.FromSeconds(10));
    });

// === ElasticsearchClient ile Polly (transport-level) ===
builder.Services.AddSingleton<ElasticsearchClient>(sp =>
{
    var httpFactory = sp.GetRequiredService<IHttpClientFactory>();
    var config = sp.GetRequiredService<IOptions<ElasticConfig>>().Value;

    var settings = new ElasticsearchClientSettings(
            new StaticNodePool(config.Nodes.Select(n => new Uri(n))))
        .Authentication(new ApiKey(config.ApiKey))
        .RequestTimeout(TimeSpan.FromSeconds(10))
        .MaxRetries(3)
        .MaxRetryTimeout(TimeSpan.FromSeconds(30))
        .DeadTimeout(TimeSpan.FromSeconds(60))   // Dead node re-check interval
        .MaxDeadTimeout(TimeSpan.FromMinutes(5)); // Max backoff for dead nodes

    return new ElasticsearchClient(settings);
});

// === Health check with circuit state awareness ===
public class ResilientElasticHealthCheck : IHealthCheck
{
    private readonly ElasticsearchClient _client;
    private readonly ResiliencePipelineProvider<string> _pipelines;

    public ResilientElasticHealthCheck(
        ElasticsearchClient client,
        ResiliencePipelineProvider<string> pipelines)
    {
        _client = client;
        _pipelines = pipelines;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, CancellationToken ct = default)
    {
        try
        {
            var response = await _client.PingAsync(ct);
            return response.IsValidResponse
                ? HealthCheckResult.Healthy()
                : HealthCheckResult.Unhealthy("ES ping failed");
        }
        catch (Exception ex) when (ex.Message.Contains("circuit"))
        {
            return HealthCheckResult.Unhealthy("Circuit breaker OPEN — ES overloaded");
        }
    }
}

ES client'ın kendi retry mekanizması varMaxRetries(3) ayarı transport layer'da çalışır. Polly ek bir katman olarak HTTP seviyesinde circuit breaker + jittered retry ekler. İkisini birlikte kullanın: ES client retry geçici DNS/network hatalarını, Polly ise 429/5xx pattern'lerini yakalar.