Подключение полнотекстового поиска из Elasticsearch в ASP.NET Core

Иван Гурин Технологии интеграции информационных систем 8 мин

Elasticsearch - это распределённый поисковый и аналитический движок, построенный поверх Apache Lucene. В контексте backend-разработки он чаще всего используется не как база данных общего назначения, а как специализированный сервис для:

  • полнотекстового поиска по большим объёмам данных;
  • быстрого поиска с релевантным ранжированием;
  • аналитики и агрегаций по текстовым и структурированным данным;
  • поиска по логам, событиям и time-series данным.

Типичные сценарии использования в ASP.NET Core:

  • поиск по статьям, товарам, документам;
  • автодополнение и fuzzy-поиск;
  • поиск с фильтрами и сортировками;
  • вынесение «тяжёлого» поиска из основной БД (SQL).

В этой статье мы рассмотрим минимальное, но рабочее подключение Elasticsearch для полнотекстового поиска в ASP.NET Core без усложнения архитектуры.

Данный пример написан для официального .NET клиента - библиотеки Elastic.Clients.Elasticsearch версии 9.x.

Архитектура примера

Архитектура намеренно упрощена:

  1. ASP.NET Core приложение
  2. Один сервис для работы с Elasticsearch
  3. Один индекс с текстовыми документами
  4. REST-контроллер

Без брокеров и без фоновых сервисов.

Практический пример

Установка пакета

Добавьте NuGet-пакет (через консоль диспетчера пакетов)

Install-Package Elastic.Clients.Elasticsearch

Модель документа

public class ArticleDocument
{
    public int Id { get; set; }

    // Основное поле для полнотекстового поиска
    public string Content { get; set; } = string.Empty;
}

Конфигурация клиента Elasticsearch

using Elastic.Clients.Elasticsearch;

var settings = new ElasticsearchClientSettings(new Uri("http://localhost:9200"))
    .Authentication(new BasicAuthentication("elastic", "elastic_password"))
    .DefaultIndex("articles");

// Клиент регистрируется как Singleton
builder.Services.AddSingleton(new ElasticsearchClient(settings));

Ключевые моменты:

  • DefaultIndex упрощает вызовы клиента
  • Клиент потокобезопасен и должен жить как Singleton
  • Здесь нет проверок доступности Elasticsearch - это упрощение

Сервис работы с Elasticsearch

using Elastic.Clients.Elasticsearch;

public class ArticleSearchService
{
    private readonly ElasticsearchClient _client;

    public ArticleSearchService(ElasticsearchClient client)
    {
        _client = client;
    }

    public async Task IndexAsync(IEnumerable<ArticleDocument> documents)
    {
        // Bulk-индексация - предпочтительный способ записи
        var response = await _client.BulkAsync(b => b
            .Index("articles")
            .IndexMany(documents)
        );

        if (response.Errors)
        {
            throw new InvalidOperationException("Ошибка при индексации документов");
        }
    }

    public async Task<IReadOnlyCollection<ArticleDocument>> SearchAsync(string query)
    {
        var response = await _client.SearchAsync<ArticleDocument>(s => s
            .Indices("articles")
            .Query(q => q
                .Match(m => m
                    .Field(f => f.Content)
                    .Query(query)
                )
            )
        );

        return response.Documents;
    }
}

Ключевые моменты:

  • Используется BulkAsync, а не одиночная индексация
  • Match-запрос - базовый вариант полнотекстового поиска
  • Ошибки bulk-запроса нужно проверять явно

Регистрация сервиса:

builder.Services.AddScoped<ArticleSearchService>();

Контроллер

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/search")]
public class SearchController : ControllerBase
{
    private readonly ArticleSearchService _service;

    public SearchController(ArticleSearchService service)
    {
        _service = service;
    }

    [HttpGet("index")]
    public async Task<IActionResult> Index()
    {
        var documents = new[]
        {
            new ArticleDocument { Id = 1, Content = "Моя первая статья по ASP.NET Core и Elasticsearch" },
            new ArticleDocument { Id = 2, Content = "Полнотекстовый поиск в .NET" },
            new ArticleDocument { Id = 3, Content = "Работа с Elasticsearch 9 - быстрый старт" }
        };

        await _service.IndexAsync(documents);
        return Ok();
    }

    [HttpGet]
    public async Task<IActionResult> Search([FromQuery] string q)
    {
        var result = await _service.SearchAsync(q);
        return Ok(result);
    }
}

URL-ы:

  • GET /api/search/index - загрузка данных
  • GET /api/search?q=Elasticsearch - поиск

Инфраструктура

docker-compose.yml

services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:9.2.3
    container_name: elasticsearch
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=true
      - ELASTIC_PASSWORD=elastic_password
      - xpack.security.http.ssl.enabled=false
      - xpack.security.transport.ssl.enabled=false
      - ES_JAVA_OPTS=-Xms1g -Xmx1g
    ports:
      - "9200:9200"
    volumes:
      - esdata:/usr/share/elasticsearch/data
    ulimits:
      memlock:
        soft: -1
        hard: -1
    networks:
      - elastic

  kibana:
    image: docker.elastic.co/kibana/kibana:9.2.3
    container_name: kibana
    depends_on:
      - elasticsearch
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
      - ELASTICSEARCH_USERNAME=kibana_system
      - ELASTICSEARCH_PASSWORD=kibana_password
    ports:
      - "5601:5601"
    networks:
      - elastic

volumes:
  esdata:

networks:
  elastic:
    driver: bridge

Для корректного запуска kibana при первом запуске Elasticsearch требуется указать пароль системного пользователя kibana_system с помощью команды:

docker exec -it elasticsearch bin/elasticsearch-reset-password -u kibana_system -i

Проверка работы

  1. Запустите Elasticsearch (локально или через Docker)
  2. Запустите ASP.NET Core приложение
  3. Выполните GET /api/search/index
  4. Дождитесь успешного ответа
  5. Выполните GET /api/search?q=elasticsearch
  6. Убедитесь, что документы найдены

Для запроса elasticsearch должен быть такой результат:

[
    { "id": 3, "content": "Работа с Elasticsearch 9 - быстрый старт" },
    { "id": 1, "content": "Моя первая статья по ASP.NET Core и Elasticsearch" }
]

Если поиск ничего не возвращает:

  • проверьте, что индекс создан
  • учтите NRT-задержку (refresh)

Методические ремарки

Упрощения примера:

  • нет схемы индекса (mappings)
  • нет обработки refresh
  • нет retries и логирования

Для production:

  • явные mappings и analyzers
  • управление lifecycle индексов
  • timeout и cancellation token
  • health-check Elasticsearch
  • DTO для API

Типичные ошибки новичков:

  • использование Elasticsearch как primary DB
  • индексация «по одному документу"
  • отсутствие контроля версий API

Заключение

В этой статье мы:

  • подключили Elasticsearch 9 к ASP.NET Core
  • реализовали индексацию и полнотекстовый поиск
  • разобрали ключевые архитектурные решения

Дальнейшие шаги:

  • analyzers и stemming
  • фильтры и сортировки
  • autocomplete и suggesters
  • интеграция с реальной БД

Пример минимален, но отражает реальную точку входа в Elasticsearch для .NET-разработчика.