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("Логи проиндексированы");
Также в библиотеке
Генератор QR-кодов на C# (.NET) с помощью QRCoder
HTTP-клиент для C++: топ библиотек для работы с сетью
JSON обработка в JavaScript (Node.js): полное руководство
Реактивное программирование в JavaScript (Node.js): библиотека RxJS