Elasticsearch .NET для C# (.NET): полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Elasticsearch .NET для C# (.NET): полное руководство с примерами

1. Что это и зачем нужна

Elasticsearch .NET (NEST) — это официальный высокоуровневый клиент для работы с поисковым движком Elasticsearch из приложений на C# и других языках платформы .NET. Библиотека предоставляет строго типизированный API, который позволяет выполнять все операции с Elasticsearch: индексацию документов, поиск, агрегации, управление индексами и маппингами, используя привычные для .NET-разработчика паттерны (LINQ, асинхронные методы, сериализация через System.Text.Json или Newtonsoft.Json).

Зачем нужна эта библиотека? Elasticsearch — это мощная распределённая система полнотекстового поиска и аналитики в реальном времени. Если ваше приложение на C# должно выполнять сложный поиск по большим объёмам данных (логи, товары, документы), обрабатывать аналитические запросы (агрегации) или управлять кластером Elasticsearch программно, то NEST — это стандартный выбор. Она скрывает низкоуровневые детали HTTP-запросов и JSON-сериализации, предоставляя fluent-интерфейс и строгую типизацию.

Библиотека поддерживает все версии Elasticsearch начиная с 6.x, а также совместима с .NET Standard 2.0, что позволяет использовать её в проектах на .NET Framework 4.6.1+, .NET Core 2.0+ и .NET 5/6/7/8/9. NEST активно развивается компанией Elastic и сообществом, имеет подробную документацию и большое количество примеров.

2. Установка

Установка производится через NuGet Package Manager. Используйте одну из следующих команд:

// Через .NET CLI
dotnet add package NEST

// Через Package Manager Console
Install-Package NEST

// Или через NuGet UI: ищем "NEST" и устанавливаем последнюю стабильную версию

Также можно установить низкоуровневый клиент Elasticsearch.Net, но NEST включает его в себя как зависимость.

3. Быстрый старт — минимальный рабочий пример

Создайте консольное приложение .NET и добавьте using-директивы:

using System;
using System.Threading.Tasks;
using Nest;
using static System.Console;

public class Program
{
    public static async Task Main(string[] args)
    {
        // 1. Настройка подключения
        var settings = new ConnectionSettings(new Uri("http://localhost:9200"))
            .DefaultIndex("my_index");
        
        var client = new ElasticClient(settings);

        // 2. Определение модели данных
        var document = new Product
        {
            Id = 1,
            Name = "Ноутбук",
            Description = "Мощный игровой ноутбук",
            Price = 1500.99
        };

        // 3. Индексация документа
        var indexResponse = await client.IndexAsync(document, idx => idx.Index("products"));
        if (indexResponse.IsValid)
            WriteLine($"Документ индексирован: {indexResponse.Id}");

        // 4. Поиск документа
        var searchResponse = await client.SearchAsync(s => s
            .Index("products")
            .Query(q => q
                .Match(m => m
                    .Field(f => f.Name)
                    .Query("ноутбук")
                )
            )
        );

        foreach (var hit in searchResponse.Hits)
        {
            WriteLine($"Найден: {hit.Source.Name} - {hit.Source.Price:C}");
        }
    }
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public double Price { get; set; }
}

4. Основные методы/функции/классы

4.1. ElasticClient.IndexAsync

Сигнатура: Task<IndexResponse> IndexAsync<T>(T document, Func<IndexDescriptor<T>, IIndexRequest<T>> selector = null, CancellationToken ct = default)

Что делает: Индексирует (добавляет или обновляет) документ в указанном индексе. Если документ с таким ID уже существует, он будет перезаписан.

var response = await client.IndexAsync(new Product { Id = 2, Name = "Мышь" }, 
    idx => idx.Index("products"));

4.2. ElasticClient.GetAsync

Сигнатура: Task<GetResponse<T>> GetAsync<T>(DocumentPath<T> id, Func<GetDescriptor<T>, IGetRequest> selector = null, CancellationToken ct = default)

Что делает: Получает документ по его ID из указанного индекса.

var getResponse = await client.GetAsync(1, g => g.Index("products"));
if (getResponse.Found)
    WriteLine(getResponse.Source.Name);

4.3. ElasticClient.SearchAsync

Сигнатура: Task<ISearchResponse<T>> SearchAsync<T>(Func<SearchDescriptor<T>, ISearchRequest> selector = null, CancellationToken ct = default)

Что делает: Выполняет поиск по индексу с использованием различных типов запросов (match, term, bool, range и т.д.).

var result = await client.SearchAsync(s => s
    .Index("products")
    .Query(q => q
        .Bool(b => b
            .Must(m => m.Match(t => t.Field(f => f.Name).Query("ноутбук")),
                   m => m.Range(r => r.Field(f => f.Price).GreaterThan(1000)))
        )
    )
    .Size(10)
    .Sort(so => so.Ascending(f => f.Price))
);

4.4. ElasticClient.UpdateAsync

Сигнатура: Task<UpdateResponse<T>> UpdateAsync<T>(DocumentPath<T> id, Func<UpdateDescriptor<T, T>, IUpdateRequest<T, T>> selector, CancellationToken ct = default)

Что делает: Частично обновляет существующий документ (без полной переиндексации).

var updateResponse = await client.UpdateAsync(1, u => u
    .Index("products")
    .Doc(new Product { Price = 1600.00 })
);

4.5. ElasticClient.DeleteAsync

Сигнатура: Task<DeleteResponse> DeleteAsync<T>(DocumentPath<T> id, Func<DeleteDescriptor<T>, IDeleteRequest> selector = null, CancellationToken ct = default)

Что делает: Удаляет документ по его ID из указанного индекса.

var deleteResponse = await client.DeleteAsync(1, d => d.Index("products"));

4.6. ElasticClient.BulkAsync

Сигнатура: Task<BulkResponse> BulkAsync(Func<BulkDescriptor, IBulkRequest> selector, CancellationToken ct = default)

Что делает: Выполняет массовые операции (индексация, обновление, удаление) за один HTTP-запрос. Значительно повышает производительность при работе с большим количеством документов.

var bulkResponse = await client.BulkAsync(b => b
    .Index("products")
    .IndexMany(new List
    {
        new Product { Id = 3, Name = "Клавиатура", Price = 50 },
        new Product { Id = 4, Name = "Монитор", Price = 300 }
    })
    .Delete(d => d.Id(1))
);

4.7. ElasticClient.Indices.CreateAsync

Сигнатура: Task<CreateIndexResponse> Indices.CreateAsync(IndexName index, Func<CreateIndexDescriptor, ICreateIndexRequest> selector = null, CancellationToken ct = default)

Что делает: Создаёт новый индекс с заданными настройками (количество шардов, реплик, маппинг полей).

var createResponse = await client.Indices.CreateAsync("products", c => c
    .Settings(s => s
        .NumberOfShards(2)
        .NumberOfReplicas(1)
    )
    .Map(m => m
        .Properties(p => p
            .Text(t => t.Name(n => n.Name))
            .Number(n => n.Name(nn => nn.Price).Type(NumberType.Double))
        )
    )
);

4.8. ElasticClient.Indices.DeleteAsync

Сигнатура: Task<DeleteIndexResponse> Indices.DeleteAsync(Indices indices, Func<DeleteIndexDescriptor, IDeleteIndexRequest> selector = null, CancellationToken ct = default)

Что делает: Удаляет один или несколько индексов.

var deleteIndexResponse = await client.Indices.DeleteAsync("products");

4.9. ElasticClient.AggregationsAsync (через SearchAsync)

Сигнатура: Встроено в SearchAsync через параметр Aggregations.

Что делает: Выполняет агрегационные запросы (группировка, статистика, гистограммы и т.д.).

var aggResponse = await client.SearchAsync(s => s
    .Index("products")
    .Size(0)
    .Aggregations(a => a
        .Average("avg_price", avg => avg.Field(f => f.Price))
        .Terms("by_name", t => t.Field(f => f.Name.Suffix("keyword")))
    )
);

var avgPrice = aggResponse.Aggregations.Average("avg_price").Value;
var buckets = aggResponse.Aggregations.Terms("by_name").Buckets;

5. Реальный практический пример (100-200 строк кода)

Пример: система логирования ошибок с поиском и агрегацией.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Nest;
using static System.Console;

public class LogEntry
{
    public Guid Id { get; set; }
    public DateTime Timestamp { get; set; }
    public string Level { get; set; } // ERROR, WARN, INFO
    public string Message { get; set; }
    public string Source { get; set; }
    public int ResponseTimeMs { get; set; }
}

public class LogAnalyzer
{
    private readonly ElasticClient _client;
    private const string IndexName = "app-logs";

    public LogAnalyzer(string elasticUrl)
    {
        var settings = new ConnectionSettings(new Uri(elasticUrl))
            .DefaultIndex(IndexName)
            .EnableDebugMode();
        _client = new ElasticClient(settings);
    }

    public async Task InitializeAsync()
    {
        var exists = await _client.Indices.ExistsAsync(IndexName);
        if (!exists.Exists)
        {
            await _client.Indices.CreateAsync(IndexName, c => c
                .Settings(s => s.NumberOfShards(1).NumberOfReplicas(0))
                .Map(m => m
                    .AutoMap()
                    .Properties(p => p
                        .Date(d => d.Name(n => n.Timestamp).Format("yyyy-MM-dd'T'HH:mm:ss"))
                        .Keyword(k => k.Name(n => n.Level))
                        .Text(t => t.Name(n => n.Message).Analyzer("standard"))
                    )
                )
            );
            WriteLine("Индекс создан");
        }
    }

    public async Task IndexLogAsync(LogEntry log)
    {
        var response = await _client.IndexAsync(log, idx => idx.Index(IndexName));
        if (!response.IsValid)
            WriteLine($"Ошибка индексации: {response.OriginalException?.Message}");
    }

    public async Task IndexBulkLogsAsync(IEnumerable logs)
    {
        var response = await _client.BulkAsync(b => b
            .Index(IndexName)
            .IndexMany(logs)
        );
        if (response.Errors)
        {
            foreach (var item in response.ItemsWithErrors)
                WriteLine($"Ошибка: {item.Error?.Reason}");
        }
    }

    public async Task> SearchErrorsAsync(DateTime from, DateTime to)
    {
        var response = await _client.SearchAsync(s => s
            .Index(IndexName)
            .Query(q => q
                .Bool(b => b
                    .Filter(f => f.Term(t => t.Field(fld => fld.Level).Value("ERROR")),
                             f => f.DateRange(r => r.Field(fld => fld.Timestamp)
                                 .GreaterThan(from).LessThan(to)))
                )
            )
            .Sort(so => so.Descending(f => f.Timestamp))
            .Size(100)
        );
        return new List(response.Documents);
    }

    public async Task> GetErrorStatsAsync()
    {
        var response = await _client.SearchAsync(s => s
            .Index(IndexName)
            .Size(0)
            .Aggregations(a => a
                .Terms("by_level", t => t.Field(f => f.Level.Suffix("keyword")))
            )
        );
        var stats = new Dictionary();
        var buckets = response.Aggregations.Terms("by_level");
        if (buckets != null)
        {
            foreach (var bucket in buckets.Buckets)
                stats[bucket.Key] = bucket.DocCount ?? 0;
        }
        return stats;
    }

    public static async Task Main()
    {
        var analyzer = new LogAnalyzer("http://localhost:9200");
        await analyzer.InitializeAsync();

        // Генерация тестовых логов
        var logs = new List();
        var random = new Random();
        for (int i = 0; i < 100; i++)
        {
            logs.Add(new LogEntry
            {
                Id = Guid.NewGuid(),
                Timestamp = DateTime.UtcNow.AddMinutes(-i),
                Level = i % 5 == 0 ? "ERROR" : (i % 3 == 0 ? "WARN" : "INFO"),
                Message = $"Событие #{i}: {(i % 5 == 0 ? "Критическая ошибка" : "Обычное событие")}",
                Source = $"Service-{i % 3}",
                ResponseTimeMs = random.Next(50, 2000)
            });
        }
        await analyzer.IndexBulkLogsAsync(logs);
        WriteLine("Логи проиндексированы");                    

Рекомендаций