Mert Özen Her Satırda Daha İleri
Veritabanı Elasticsearch ile Arama — #02

Elasticsearch Lab'ı: start-local, Kibana Dev Tools, Cluster Sağlığı ve .NET'ten İlk Bağlantı

Mert Özen 14 Kas 2026 14 dk 10 görüntülenme
Elasticsearch Lab'ı: start-local, Kibana Dev Tools, Cluster Sağlığı ve .NET'ten İlk Bağlantı

Yerel bir Elasticsearch lab'ı için Elastic'in start-local script'i ya da security açık, sürümü sabit küçük bir Docker Compose dosyası yeterli; uygulama elastic kullanıcısıyla değil, yalnız kendi index'lerine yetkili bir API key ve singleton bir ElasticsearchClient ile bağlanır. Gerçek çıktılarla start-local, Kibana için service account token, yellow health ve allocation explain, scoped API key'in 403'leri, Elastic.Clients.Elasticsearch 9.5 ile ilk IndexAsync/GetAsync ve hata durumları.

Sürüm notu: Bu yazıdaki örnekler Elasticsearch 9.5.6 ve Kibana 9.5.6 (resmî Docker image'ları), .NET 10 ve Elastic.Clients.Elasticsearch 9.5.3 ile çalıştırılmıştır; start-local script'inin anlatımı 0.14.0 sürümünün kaynağına dayanıyor.

Önce Tahmin Et

İlk yazıda security'si kapalı, tek seferlik bir container kullanmıştık. Bu sefer sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) için kalıcı bir lab kuruyoruz: security açık bir Elasticsearch, yanında Kibana ve .NET'ten bağlanan küçük bir API. Container ayağa kalktıktan sonra üç şey deneyeceğiz:

curl localhost:9200

PUT /books
GET /_cluster/health

# yalnız books* index'lerine yetkisi olan bir API key ile:
GET /

İlki parolasız bir istek; ne döner? İkincisinde tek node'lu cluster'da yeni bir index açıyoruz; health green mi kalır? Üçüncüsünde uygulamaya verdiğimiz API key ile Elasticsearch'ün kök adresine bir istek atıyoruz; .NET client'ın InfoAsync()'i de tam olarak bunu yapıyor. Çalışır mı?

start-local Ne Yapıyor?

Elastic'in yerel geliştirme için önerdiği yol tek satırlık bir script: curl -fsSL https://elastic.co/start-local | sh. Script'in 0.14.0 sürümünü indirip okudum; yaptıkları şunlar:

Çalıştığı klasörde elastic-start-local adında bir klasör açıyor ve içine .env, docker-compose.yml ile start.sh, stop.sh, uninstall.sh dosyalarını yazıyor. .env'de rastgele üretilmiş elastic parolası (ES_LOCAL_PASSWORD), Kibana'nın iç kullanıcısının parolası ve şifreleme anahtarı, heap ayarı (-Xms128m -Xmx2g) ve lisansın bitiş tarihi duruyor. Compose dosyası Elasticsearch'ü xpack.security.enabled=true ama HTTP üzerinde TLS (Transport Layer Security; ağ trafiğini şifreleyen protokol) kapalı (xpack.security.http.ssl.enabled=false) ve xpack.license.self_generated.type=trial ile başlatıyor, portları yalnız 127.0.0.1'e açıyor. Elasticsearch hazır olunca script elastic-start-local adında bir API key üretip ES_LOCAL_API_KEY olarak .env'e ekliyor. Trial lisans 30 gün; süre dolduktan sonra start.sh cluster'ı ücretsiz Basic lisansa geçiriyor.

Seçenekler az: -v 9.5.6 sürümü sabitliyor (verilmezse script Elastic'in yayın listesinden en son sürümü seçiyor), --esonly Kibana'sız yalnız Elasticsearch kuruyor, --edot bir OpenTelemetry collector ekliyor. Elastic'in dokümanı bunun yalnız yerel geliştirme için olduğunu, production'da kullanılmaması gerektiğini açıkça yazıyor. Windows'ta ise script doğrudan çalışmıyor; doküman WSL (Windows Subsystem for Linux; Windows içinde Linux çalıştıran katman) kurmanı istiyor.

Bu seride start-local'ı kullanmıyoruz, yerine aynı ayarları açıkça yazdığımız küçük bir compose dosyası kullanıyoruz. Sebepler: Windows'ta WSL olmadan çalışsın, sürüm ve heap repo'da sabit dursun, trial yerine baştan Basic lisansla çalışalım ki 30 gün sonra davranış değişmesin ve kurulumun her adımı yazının içinde görünsün. Hızlıca bir şey denemek istersen start-local'ın kendisi de iyi bir seçenek; aşağıdaki Dev Tools ve .NET adımları onunla da aynı.

Kendi Lab'ımız: compose.yaml

Dosya repo'da lab/compose.yaml:

# Bookstore arama lab'ı: Elasticsearch (her zaman) ve Kibana ("kibana" profile'ı).
# İlk "docker compose up"tan önce .env.example'ı .env olarak kopyala ve ELASTIC_PASSWORD'ü ayarla.
name: bookstore-search-lab

services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:${ES_VERSION:-9.5.6}
    environment:
      - node.name=bookstore-es
      - discovery.type=single-node
      - ELASTIC_PASSWORD=${ELASTIC_PASSWORD:?Set ELASTIC_PASSWORD in lab/.env}
      - xpack.security.enabled=true
      - xpack.security.http.ssl.enabled=false
      - ES_JAVA_OPTS=-Xms512m -Xmx512m
    ports:
      - "127.0.0.1:9200:9200"
    volumes:
      - es-data:/usr/share/elasticsearch/data
    healthcheck:
      test: ["CMD-SHELL", "curl -s -o /dev/null -w '%{http_code}' -u elastic:$$ELASTIC_PASSWORD http://localhost:9200 | grep -q 200"]
      interval: 5s
      timeout: 5s
      retries: 40

  kibana:
    image: docker.elastic.co/kibana/kibana:${ES_VERSION:-9.5.6}
    profiles: ["kibana"]
    depends_on:
      elasticsearch:
        condition: service_healthy
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
      - ELASTICSEARCH_SERVICEACCOUNTTOKEN=${KIBANA_SERVICE_TOKEN:-}
    ports:
      - "127.0.0.1:5601:5601"

volumes:
  es-data:

Birkaç satırın sebebi var. discovery.type=single-node node'un başka node aramadan kendi başına cluster kurmasını sağlıyor. ELASTIC_PASSWORD yerleşik superuser elastic'in parolası; :? sayesinde boşsa compose hiç başlamıyor. Lisans ayarı yok, yani varsayılan Basic lisans geçerli. Heap 512 MB; lab verisi için yeterli, gerçek veride heap'i ve container belleğini birlikte büyütmek gerekiyor (12. yazı). Kibana'yı bir profile'a koydum, çünkü önce Elasticsearch'te Kibana için bir token üretmemiz gerekiyor. Repo kökünden:

# Linux/macOS'ta Copy-Item yerine: cp lab/.env.example lab/.env
Copy-Item lab/.env.example lab/.env
# lab/.env içinde ELASTIC_PASSWORD'e uzun, rastgele bir değer yaz
docker compose -f lab/compose.yaml up -d --wait

lab/.env .gitignore'da; repo'ya yalnız .env.example giriyor. Şimdi tahmindeki ilk istek:

curl localhost:9200

{"error":{"root_cause":[{"type":"security_exception","reason":"missing authentication credentials for REST request [/]","header":{"WWW-Authenticate":["Basic realm=\"security\", charset=\"UTF-8\"","ApiKey"]}}], ... ,"status":401}

Security açık bir cluster kimliği belirsiz isteğe 401 dönüyor ve WWW-Authenticate header'ında iki yöntemi söylüyor: kullanıcı adı/parola (Basic) ve API key. curl -u elastic:<parola> localhost:9200 ile ilk yazıdaki sürüm bilgisini görürsün.

Kibana ve Service Account Token

Kibana, Elasticsearch'e kendi adına bağlanmak için bir kimlik istiyor. Burada iki yol var: yerleşik kibana_system kullanıcısına parola atamak (start-local bunu yapıyor) ya da elastic/kibana service account'u için bir token üretmek. Ben ikincisini seçtim: tek bir HTTP isteği yeterli ve token, yetkileri Elasticsearch tarafından sabit tanımlanmış elastic/kibana service account'una ait; bir kullanıcının parolasını paylaşmıyoruz. İsteği Elasticsearch container'ının içinden atıyoruz; parola container'daki ortam değişkeninden geliyor, komutta görünmüyor:

docker compose -f lab/compose.yaml exec elasticsearch sh -c 'curl -s -u elastic:$ELASTIC_PASSWORD -X POST http://localhost:9200/_security/service/elastic/kibana/credential/token/bookstore-lab'

{
  "created": true,
  "token": {
    "name": "bookstore-lab",
    "value": "<token>"
  }
}

Tek tırnak önemli: PowerShell de bash de içindeki $ELASTIC_PASSWORD'e dokunmuyor, değişkeni container'daki sh açıyor. value'yu lab/.env'deki KIBANA_SERVICE_TOKEN'a yaz ve Kibana'yı başlat:

docker compose -f lab/compose.yaml --profile kibana up -d

Kibana'nın açılması bir dakika kadar sürüyor. http://localhost:5601'de elastic kullanıcısıyla giriş yap ve menüden Management altındaki Dev Tools'u aç (doğrudan adres: http://localhost:5601/app/dev_tools#/console). Kibana loglarında TLS ve şifreleme anahtarıyla ilgili uyarılar görürsün (xpack.encryptedSavedObjects.encryptionKey gibi); Dev Tools için gerekmiyor, alerting gibi özellikleri kullanacaksan anahtarları ayarlaman gerekiyor. Bu lab, Elasticsearch ve Kibana birlikte, bende yaklaşık 2 GB bellek kullandı.

Dev Tools'ta İlk İstekler

Dev Tools'un console'u isteği method, path ve JSON gövde olarak yazmanı istiyor; host ve kimlik bilgisi Kibana'dan geliyor. Önce cluster'ın durumu:

GET /_cluster/health
{
  "cluster_name" : "docker-cluster",
  "status" : "green",
  "timed_out" : false,
  "number_of_nodes" : 1,
  "number_of_data_nodes" : 1,
  "active_primary_shards" : 51,
  "active_shards" : 51,
  "unassigned_shards" : 0,
  ...
}

51 shard'ın neredeyse hepsi Kibana'nın kendi sistem index'leri; Kibana açılmadan önce bu sayı 2'ydi. Şimdi Bookstore'un index'ini, hiçbir ayar vermeden açalım:

PUT /books

GET /_cluster/health?filter_path=status,unassigned_shards
{
  "status" : "yellow",
  "unassigned_shards" : 1
}

GET /_cat/indices/books?v&h=health,status,index,pri,rep,docs.count
health status index pri rep docs.count
yellow open   books   1   1          0

İlk yazıda gördüğümüz durum: index'in bir primary ve bir replica shard'ı var, replica atanamıyor. Elasticsearch'e nedenini sorabiliriz:

GET /_cluster/allocation/explain?filter_path=can_allocate,node_allocation_decisions.node_name,node_allocation_decisions.deciders
{ "index": "books", "shard": 0, "primary": false }

{
  "can_allocate" : "no",
  "node_allocation_decisions" : [
    {
      "node_name" : "bookstore-es",
      "deciders" : [
        {
          "decider" : "same_shard",
          "decision" : "NO",
          "explanation" : "a copy of this shard is already allocated to this node [[books][0], node[<node-id>], [P], s[STARTED], ...]"
        }
      ]
    }
  ]
}

same_shard kuralı aynı shard'ın iki kopyasının aynı node'da durmasını engelliyor. Health renklerinin anlamı net: green her shard atanmış, yellow bütün primary'ler atanmış ama en az bir replica atanmamış (veri tam, yedek eksik), red en az bir primary atanmamış (bir kısım veriye erişilemiyor). Tek node'lu lab'da yellow beklenen durum; düzeltmek için replica'yı sıfıra çekiyoruz:

PUT /books/_settings
{ "index": { "number_of_replicas": 0 } }

GET /_cluster/health?filter_path=status,unassigned_shards
{
  "status" : "green",
  "unassigned_shards" : 0
}

Production'da replica'yı sıfıra çekmek yellow'u green yapar ama node kaybında veri kaybı demektir; orada doğru cevap ikinci bir node. Şimdi bir document yazıp okuyalım. _id olarak ISBN'i (International Standard Book Number; kitabın uluslararası standart numarası) kullanıyoruz:

PUT /books/_doc/9781000000016
{ "isbn": "9781000000016", "title": "İnce Memed", "authors": [ { "name": "Yaşar Kemal", "role": "author" } ], "language": "tr", "price": 385.00 }

{
  "_index" : "books",
  "_id" : "9781000000016",
  "_version" : 1,
  "result" : "created",
  "_shards" : { "total" : 1, "successful" : 1, "failed" : 0 },
  "_seq_no" : 0,
  "_primary_term" : 1
}

GET /books/_doc/9781000000016

{
  "_index" : "books",
  "_id" : "9781000000016",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "isbn" : "9781000000016",
    "title" : "İnce Memed",
    ...
    "price" : 385.0
  }
}

Aynı PUT'u "price": 370.00 ile tekrar gönderince "result" : "updated", "_version" : 2 ve "_seq_no" : 1 dönüyor: aynı _id'ye yazmak document'ı bütünüyle değiştiriyor. _seq_no ve _primary_term optimistic concurrency için kullanılıyor (5. yazı). Bir de fark: GET /books/_doc/{id} real-time çalışıyor, refresh beklemeden son yazılanı döndürüyor; _search ise ilk yazıda gördüğümüz gibi refresh'i bekliyor.

API Key: Uygulamaya Yalnız İhtiyacı Olan Yetki

elastic superuser; cluster'da her şeyi yapabiliyor. Uygulamanın bu parolayla bağlanması, uygulamada bir açık olduğunda saldırganın cluster'ı silebilmesi demek. Bookstore API'si için yalnız books ile başlayan index'lere erişebilen, süresi dolan bir API key üretiyoruz:

POST /_security/api_key
{
  "name": "bookstore-api-dev",
  "expiration": "30d",
  "role_descriptors": {
    "bookstore-books": {
      "cluster": [],
      "indices": [
        { "names": ["books*"], "privileges": ["read", "write", "create_index", "view_index_metadata"] }
      ]
    }
  }
}

{
  "id" : "<id>",
  "name" : "bookstore-api-dev",
  "expiration" : <epoch-ms>,
  "api_key" : "<api_key>",
  "encoded" : "<encoded>"
}

expiration 30 gün sonrasını epoch milisaniye olarak gösteriyor. encoded, id:api_key'in Base64 hâli; istemciler Authorization: ApiKey <encoded> header'ı ile bunu gönderiyor. Değer yalnız bu cevapta görünüyor, sonra Elasticsearch'ten geri alınamıyor; hemen güvenli bir yere koy. API key'in yetkisi, onu oluşturan kullanıcının yetkisiyle role_descriptors'ın kesişimi; elastic ile oluşturduk ama yalnız books* üzerinde işlem yapabiliyor:

curl -H "Authorization: ApiKey <encoded>" "localhost:9200/books/_count?pretty"
{
  "count" : 1,
  ...
}

curl -H "Authorization: ApiKey <encoded>" "localhost:9200/_cluster/health?pretty"
"reason" : "action [cluster:monitor/health] is unauthorized for API key id [<id>] of user [elastic], this action is granted by the cluster privileges [monitor,manage,all]"
"status" : 403

curl -H "Authorization: ApiKey <encoded>" -X DELETE "localhost:9200/books?pretty"
"reason" : "action [indices:admin/delete] is unauthorized for API key id [<id>] of user [elastic] on indices [books], this action is granted by the index privileges [delete_index,manage,all]"

Hata mesajları hangi yetkinin eksik olduğunu da söylüyor; yetkiyi genişletmeden önce bu satırı oku, çoğu zaman tek bir privilege yeterli. Tahmindeki üçüncü sorunun cevabı da burada: GET / cluster seviyesinde bir işlem (cluster:monitor/main) ve bu key'in cluster yetkisi yok.

.NET'ten İlk Bağlantı

Resmî client Elastic.Clients.Elasticsearch; NuGet'teki son kararlı sürümü 9.5.3 (28 Eylül 2026). Sürüm numarası sunucuyu takip ediyor: 9.x client 9.x ve 10.x sunucularla çalışacak şekilde tasarlanıyor, minor sürümlerde breaking change olabiliyor; ayrıntılar 10. yazıda. Paylaşılan Bookstore.Search projesine ekliyoruz:

dotnet add src/Bookstore.Search package Elastic.Clients.Elasticsearch --version 9.5.3
public sealed class ElasticsearchOptions
{
    public const string SectionName = "Elasticsearch";

    public string Url { get; set; } = "http://localhost:9200";

    public string ApiKey { get; set; } = "";
}

public static class SearchClientFactory
{
    public const string BooksIndex = "books";

    // Uygulama başına tek client: connection pool'u o tutuyor ve thread-safe
    public static ElasticsearchClient Create(ElasticsearchOptions options)
    {
        if (string.IsNullOrWhiteSpace(options.ApiKey))
        {
            throw new InvalidOperationException("Elasticsearch:ApiKey is empty. Create an API key (post 02) and set it.");
        }

        ElasticsearchClientSettings settings = new ElasticsearchClientSettings(new Uri(options.Url))
            .Authentication(new ApiKey(options.ApiKey))
            .DefaultMappingFor<Book>(m => m.IndexName(BooksIndex).IdProperty(b => b.Isbn));

        return new ElasticsearchClient(settings);
    }
}

DefaultMappingFor<Book> iki şeyi tek yerde söylüyor: Book tipindeki document'lar books index'ine gidiyor ve _id olarak Isbn property'si kullanılıyor. Böylece her çağrıda index adı yazmak gerekmiyor. Lab konsolundaki first-connection komutu bağlanıyor, Dune'u index'liyor ve geri okuyor:

InfoResponse info = await client.InfoAsync();
Console.WriteLine(info.IsValidResponse
    ? $"Connected: {info.Name}, Elasticsearch {info.Version.Number}"
    : $"Info failed: HTTP {info.ApiCallDetails.HttpStatusCode}, {info.ElasticsearchServerError?.Error?.Type}");

IReadOnlyList<Book> books = await BookCatalog.LoadAsync(Path.Combine("data", "books.json"));
Book dune = books.First(b => b.Title == "Dune" && b.Language == "en");

IndexResponse indexed = await client.IndexAsync(dune);
Console.WriteLine($"Index: {indexed.Result}, _id {indexed.Id}, _version {indexed.Version}, _seq_no {indexed.SeqNo}");

GetResponse<Book> got = await client.GetAsync<Book>(dune.Isbn);
Console.WriteLine(got.Found
    ? $"Get: {got.Source!.Title} ({got.Source.Language}), {got.Source.Authors[0].Name}, {got.Source.Price} {got.Source.Currency}, published {got.Source.PublishedOn:yyyy-MM-dd}"
    : "Get: not found");

GetResponse<Book> missing = await client.GetAsync<Book>("9780000000000");
Console.WriteLine($"Missing: IsValidResponse {missing.IsValidResponse}, Found {missing.Found}, HTTP {missing.ApiCallDetails.HttpStatusCode}");
$env:BOOKSTORE_ES_API_KEY = "<encoded>"     # bash: export BOOKSTORE_ES_API_KEY=...
dotnet run --project src/Bookstore.SearchLab -- first-connection

Info failed: HTTP 403, security_exception
Index: Created, _id 9781000000221, _version 1, _seq_no 2
Get: Dune (en), Frank Herbert, 520.0 TRY, published 2019-08-01
Missing: IsValidResponse True, Found False, HTTP 404

Dört satırın dördü de bir şey öğretiyor. InfoAsync 403 aldı ama exception fırlamadı: client varsayılan olarak hata durumunu response'a koyuyor, kontrol etmek senin işin (IsValidResponse). Bağlantıyı test etmek için GET / kullanacaksan key'e monitor yetkisi ekle; vermek istemiyorsan bağlantıyı yetkili olduğun bir index üzerinden (_count gibi) dene. IndexAsync index adını ve _id'yi DefaultMappingFor'dan aldı. Source serializer System.Text.Json kullanıyor ve property'leri camelCase yazıyor; DateOnly "2019-08-01" olarak gitti. Son satır ince bir nokta: olmayan bir document için IsValidResponse true, çünkü 404 bu API için geçerli bir cevap; "bulundu mu" sorusunu Found cevaplıyor.

API tarafında client DI'a (Dependency Injection; bağımlılıkları framework'ün oluşturup verdiği yapı) singleton olarak giriyor. Her istekte yeni client oluşturmak, her seferinde yeni bir connection pool ve yeni TCP bağlantıları demek:

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

ElasticsearchOptions elasticsearch = builder.Configuration.GetSection(ElasticsearchOptions.SectionName).Get<ElasticsearchOptions>()
    ?? new ElasticsearchOptions();

// Singleton: client connection pool'u ve node durumunu tutuyor; istek başına oluşturma
builder.Services.AddSingleton<ElasticsearchClient>(_ => SearchClientFactory.Create(elasticsearch));

WebApplication app = builder.Build();

app.MapBookEndpoints();

app.Run();
app.MapGet("/books/{isbn}", async (string isbn, ElasticsearchClient client, ILoggerFactory loggerFactory, CancellationToken cancellationToken) =>
{
    GetResponse<Book> response = await client.GetAsync<Book>(isbn, cancellationToken);

    if (response.IsValidResponse)
    {
        return response.Found ? Results.Ok(response.Source) : Results.NotFound();
    }

    // Transport hataları da sunucu hataları da buraya düşüyor; header'ları değil sebebi logla
    ILogger logger = loggerFactory.CreateLogger("Bookstore.Api.BookEndpoints");
    logger.LogError("Elasticsearch get failed: HTTP {StatusCode} {ErrorType} {Reason}",
        response.ApiCallDetails.HttpStatusCode,
        response.ElasticsearchServerError?.Error?.Type,
        response.ElasticsearchServerError?.Error?.Reason ?? response.ApiCallDetails.OriginalException?.Message);
    return Results.Problem("Search is unavailable.", statusCode: StatusCodes.Status503ServiceUnavailable);
});

appsettings.json'da yalnız Elasticsearch:Url var, ApiKey boş. Geliştirme makinesinde key'i user-secrets'a koyuyoruz; repo'ya hiç girmiyor:

dotnet user-secrets set "Elasticsearch:ApiKey" "<encoded>" --project src/Bookstore.Api
dotnet run --project src/Bookstore.Api

curl -i localhost:5080/books/9781000000221
HTTP/1.1 200 OK
{"isbn":"9781000000221","title":"Dune","authors":[{"name":"Frank Herbert","role":"author"}],"categories":["Fiction","Science Fiction"],"language":"en","price":520.0,"currency":"TRY","stockQuantity":30,"publishedOn":"2019-08-01","averageRating":4.7,"description":"A desert planet, a spice everyone wants and a young heir who must survive among the Fremen."}

curl -i localhost:5080/books/9780000000000
HTTP/1.1 404 Not Found

Cluster'a ulaşılamazsa ne oluyor? API'yi Elasticsearch__Url=http://localhost:9299 ile, boş bir porta yönlendirerek başlattım:

HTTP/1.1 503 Service Unavailable

fail: Bookstore.Api.BookEndpoints[0]
      Elasticsearch get failed: HTTP (null) (null) Connection refused (localhost:9299)

Transport hatasında HTTP durum kodu ve sunucu hatası yok, sebep OriginalException'da. Arama kutusu çalışmasa da sipariş ve sepet çalışmaya devam etmeli; endpoint bu yüzden exception fırlatmıyor, 503 dönüyor. ASP.NET Core Web API serisinin "ASP.NET Core'da Global Hata Yönetimi: Exception Middleware ve ProblemDetails" yazısındaki yaklaşım burada da geçerli.

Eski projelerde NEST (Elasticsearch'ün eski .NET client'ı) paketini görürsen: NEST 7.x hattının client'ıydı, NuGet'teki son sürümü 7.17.5. Elastic 8.13 ile birlikte NEST'i deprecated ilan etti; 8.x sunucularla compatibility modunda çalışabiliyordu ama yeni projeler için doğru paket Elastic.Clients.Elasticsearch. Geçiş notları 10. yazıda.

OpenSearch'ü Aynı Lab'da Çalıştırmak

OpenSearch'ü denemek istersen aynı compose yapısına opensearchproject/opensearch image'ı ile ayrı bir servis ekleyebilirsin (Docker Hub'daki son sürüm 3.9.0), Kibana'nın karşılığı OpenSearch Dashboards, .NET client'ı ise ayrı bir paket: OpenSearch.Client. Bu yazıda OpenSearch'ü çalıştırmadım; image'ın ilk açılışta bir admin parolası istemesi gibi kurulum farkları ve API'lerin ayrıştığı noktalar 13. yazıda.

Üretimde Dikkat

Bu lab'daki iki kısayol production'da kabul edilmez. Birincisi HTTP üzerinde TLS kapalı: API key ve parola ağda düz metin gidiyor. Lab'da port yalnız 127.0.0.1'e açık olduğu için risk düşük, ama sunucuda TLS şart. İkincisi tek node: node'un diski giderse index de gider. Production kurulumunda en az üç master-eligible node, TLS, rol bazlı yetki ve snapshot yedeği 12. yazının konusu.

API key'leri bir envanter gibi yönet. Her uygulama ve ortam için ayrı key üret, adına uygulamayı ve ortamı yaz (bookstore-api-dev gibi), expiration ver ve yenilemeyi planla. Key sızarsa DELETE /_security/api_key ile id'sinden geçersiz kılabilirsin; bunu yapabilmek için id'yi kaydetmiş olman gerekiyor.

Elastic Cloud kullanıyorsan bağlantı bilgisi URL yerine bir Cloud ID ile gelebiliyor ve client'ın bunun için ayrı bir constructor'ı var. Bu seride bulut hesabı kullanmadığım için o yolu çalıştırmadım; client tarafındaki ayarlar 10. yazıda.

Sık Düşülen Tuzaklar

Parolayı ya da API key'i repo'ya yazmak. appsettings.Development.json'a "nasılsa local" diye konan key, repo public olduğunda herkesin key'i olur. Geliştirmede user-secrets, sunucuda ortam değişkeni ya da bir secret store kullan.

Uygulamayı elastic kullanıcısıyla bağlamak. Çalışır, ama uygulamadaki tek bir açık bütün cluster'ı tehlikeye atar. Uygulamaya yalnız kendi index'lerinde yetkisi olan bir API key ver.

Tek node'da yellow health'i hata sanıp alarm kurmak ya da tersine, production'da yellow'u görmezden gelmek. Lab'da beklenen durum; production'da yedeksiz çalıştığını söylüyor.

Response'u kontrol etmemek. Client hata durumunda varsayılan olarak exception fırlatmıyor; IsValidResponse'a bakmayan kod 403'ü de 404'ü de "sonuç yok" diye kullanıcıya gösterir.

Her istekte yeni ElasticsearchClient oluşturmak. Client'ın kendisi thread-safe ve uygulama boyunca tek instance olarak yaşaması için tasarlanmış.

Küçük Bir Deneme

★☆☆ books index'ini sil, bu sefer PUT /books gövdesinde "settings": { "number_of_replicas": 0 } vererek oluştur. Health hiç yellow'a düşüyor mu? Sonra "number_of_shards": 3 ile bir books-test index'i aç ve GET /_cat/shards/books-test?v çıktısına bak.

★★☆ Yalnız read yetkisi olan, books-reader adında ikinci bir API key üret. Bu key ile first-connection komutunu çalıştır ve Index: satırına dikkat et: document yazılmadığı hâlde satır neden Created diyor? Kodu IsValidResponse'a bakacak şekilde düzelt ve hata mesajındaki privilege listesinden key'e hangi yetkinin eksik olduğunu bul.

★★★ API'yi çalışır durumdayken DELETE /_security/api_key ile bookstore-api-dev key'ini "ids" listesinden geçersiz kıl ve /books/9781000000221'i tekrar çağır. Log satırında HTTP durum kodu ve hata tipi ne? Uygulama yeniden başlatılmadan yeni key'e nasıl geçebilir; IOptionsMonitor ile client'ı yeniden oluşturmanın bedeli ne olur?

Bir sonraki yazıda books index'ini bu sefer bilerek kuruyoruz: dynamic mapping'in ISBN ve tarih alanlarında yaptığı tahminler, text ile keyword farkı, multi-field, fiyat için scaled_float, yazarlar listesinde object ile nested farkı ve bir mapping'in neden sonradan değiştirilemediği.