.NET Core에서 Elasticsearch 연동하기

.NET Core 환경에서 Elasticsearch를 활용하는 방법을 Nest 클라이언트를 통해 살펴보겠습니다.

Nest 클라이언트 설치

.NET Core 프로젝트에서 Elasticsearch를 사용하기 위해 먼저 Nest NuGet 패키지를 설치합니다.

Elasticsearch 연결

단일 노드 연결

로컬 환경에 Elasticsearch가 실행 중이라면, 간단하게 ElasticClient 인스턴스를 생성하여 연결할 수 있습니다. 기본적으로 http://localhost:9200으로 연결을 시도합니다.


var client = new ElasticClient();

Elasticsearch가 다른 주소나 포트에서 실행 중이거나, 기본 인덱스를 지정하고 싶다면 ConnectionSettings를 사용하여 클라이언트 설정을 구성할 수 있습니다.


var connectionSettings = new ConnectionSettings(new Uri("http://example.com:9200"))
    .DefaultIndex("my_default_index"); // 기본 인덱스 설정

var client = new ElasticClient(connectionSettings);

클러스터 연결

여러 노드로 구성된 Elasticsearch 클러스터에 연결하려면, ConnectionSettings에 여러 노드의 주소를 포함하는 연결 풀(Connection Pool)을 설정합니다.

SniffingConnectionPool은 클러스터의 노드 정보를 주기적으로 탐지하여 최적의 노드로 요청을 라우팅합니다. 다음은 세 개의 노드로 구성된 클러스터에 연결하는 예제입니다.


var uris = new[]
{
    new Uri("http://localhost:9200"),
    new Uri("http://localhost:9201"),
    new Uri("http://localhost:9202"),
};

var connectionPool = new SniffingConnectionPool(uris);
var connectionSettings = new ConnectionSettings(connectionPool)
    .DefaultIndex("my_default_index");

var client = new ElasticClient(connectionSettings);

문서 색인 (Indexing)

단일 문서 색인

개별 문서를 Elasticsearch에 색인하려면 IndexDocument 또는 비동기 버전인 IndexDocumentAsync 메서드를 사용합니다.


public class UserProfile
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

// ...

var newUser = new UserProfile
{
    Id = 1,
    Name = "John Doe",
    Email = "john.doe@example.com"
};

// 동기 방식
var indexResponse = client.IndexDocument(newUser);
if (!indexResponse.IsValid)
{
    // 오류 처리 로직
    Console.WriteLine($"Error indexing document: {indexResponse.DebugInformation}");
}

// 비동기 방식
var indexResponseAsync = await client.IndexDocumentAsync(newUser);
if (!indexResponseAsync.IsValid)
{
    // 오류 처리 로직
    Console.WriteLine($"Error indexing document asynchronously: {indexResponseAsync.DebugInformation}");
}

색인 시 파라미터 설정

문서를 색인할 때 특정 인덱스에 저장하거나, 문서 ID를 지정하는 등 추가적인 옵션을 설정하려면 Index 메서드를 사용합니다.


var newUser = new UserProfile
{
    Id = 2,
    Name = "Jane Smith",
    Email = "jane.smith@example.com"
};

// 옵션 1: Index 메서드와 람다식 사용
var indexResponse1 = client.Index(newUser, idx => idx.Index("users"));

// 옵션 2: IndexRequest 객체 사용
var indexRequest = new IndexRequest<UserProfile>(newUser, "users");
var indexResponse2 = client.Index(indexRequest);

대량의 문서를 한 번에 색인하려면 IndexMany 또는 IndexManyAsync 메서드를 활용할 수 있습니다. 자세한 내용은 Elasticsearch .NET 클라이언트 문서를 참고하세요.

문서 재색인 (Reindexing)

서버 측 재색인

기존 인덱스의 문서를 다른 인덱스로 복사하거나 변환해야 할 때, Elasticsearch의 Reindex API를 사용할 수 있습니다. Nest 클라이언트에서는 ReindexOnServerReindexOnServerAsync 메서드를 제공합니다.

재색인을 시작하기 전에 대상 인덱스가 미리 생성되어 있어야 합니다.


var reindexResponse = client.ReindexOnServer(r => r
    .Source(s => s.Index("source_index_name")) // 원본 인덱스
    .Destination(d => d.Index("destination_index_name")) // 대상 인덱스
    .WaitForCompletion() // 작업 완료 대기
);

if (!reindexResponse.IsValid)
{
    // 오류 처리
    Console.WriteLine($"Reindex error: {reindexResponse.DebugInformation}");
}

자세한 재색인 옵션은 Elasticsearch .NET 클라이언트 문서를 참고하십시오.

Elasticsearch 서비스 래퍼 (Wrapper)

Elasticsearch 연동 로직을 캡슐화하여 재사용성을 높이기 위해 서비스 인터페이스와 구현체를 정의할 수 있습니다.

설정 옵션 클래스


public class ElasticsearchOptions
{
    /// <summary>
    /// Elasticsearch 접속 URL
    /// </summary>
    public string ServiceUrl { get; set; }

    /// <summary>
    /// 기본 인덱스 이름
    /// </summary>
    public string DefaultIndexName { get; set; }
}

서비스 인터페이스


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

public interface IElasticsearchService
{
    /// <summary>
    /// Elasticsearch 클라이언트 인스턴스를 반환합니다.
    /// </summary>
    ElasticClient GetClient();

    /// <summary>
    /// 여러 문서를 비동기적으로 색인합니다.
    /// </summary>
    /// <typeparam name="TDocument">문서 타입</typeparam>
    /// <param name="documents">색인할 문서 리스트</param>
    Task IndexManyAsync<TDocument>(List<TDocument> documents) where TDocument : class;

    /// <summary>
    /// 단일 문서를 비동기적으로 삽입 또는 업데이트합니다.
    /// </summary>
    /// <typeparam name="TDocument">문서 타입</typeparam>
    /// <param name="document">삽입/업데이트할 문서</param>
    /// <param name="indexName">대상 인덱스 이름 (선택 사항)</param>
    Task InsertOrUpdateAsync<TDocument>(TDocument document, string indexName = null) where TDocument : class;

    /// <summary>
    /// 지정된 ID를 가진 문서를 비동기적으로 삭제합니다.
    /// </summary>
    /// <typeparam name="TDocument">문서 타입</typeparam>
    /// <param name="documentId">삭제할 문서의 ID</param>
    /// <param name="indexName">대상 인덱스 이름 (선택 사항)</param>
    /// <returns>삭제 성공 여부</returns>
    Task<bool> DeleteAsync<TDocument>(string documentId, string indexName = null) where TDocument : class;

    /// <summary>
    /// 지정된 이름의 인덱스를 비동기적으로 삭제합니다.
    /// </summary>
    /// <param name="indexName">삭제할 인덱스 이름</param>
    /// <returns>삭제 성공 여부</returns>
    Task<bool> DropIndexAsync(string indexName);

    /// <summary>
    /// 지정된 이름의 인덱스를 비동기적으로 생성합니다.
    /// </summary>
    /// <param name="indexName">생성할 인덱스 이름</param>
    Task CreateIndexAsync(string indexName);
}

서비스 구현 클래스


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

public class ElasticsearchService : IElasticsearchService
{
    private readonly ElasticsearchOptions _options;
    private ElasticClient _client;

    public ElasticsearchService(ElasticsearchOptions options)
    {
        _options = options ?? throw new ArgumentNullException(nameof(options));
        InitializeClient();
    }

    private void InitializeClient()
    {
        var uri = new Uri(_options.ServiceUrl);
        var connectionSettings = new ConnectionSettings(uri)
            .DefaultIndex(_options.DefaultIndexName); // 기본 인덱스 설정

        _client = new ElasticClient(connectionSettings);

        // 연결 상태 확인 (선택 사항)
        var healthResponse = _client.Cluster.Health();
        if (!healthResponse.IsValid)
        {
            // 로깅 또는 예외 처리
            Console.WriteLine($"Elasticsearch connection failed: {healthResponse.DebugInformation}");
        }
    }

    public ElasticClient GetClient()
    {
        return _client;
    }

    public async Task IndexManyAsync<TDocument>(List<TDocument> documents) where TDocument : class
    {
        if (documents == null || !documents.Any())
        {
            return;
        }

        var bulkResponse = await _client.IndexManyAsync(documents);
        if (!bulkResponse.IsValid)
        {
            // 오류 처리
            Console.WriteLine($"Bulk indexing error: {bulkResponse.DebugInformation}");
        }
    }

    public async Task InsertOrUpdateAsync<TDocument>(TDocument document, string indexName = null) where TDocument : class
    {
        var indexDescriptor = new IndexDescriptor<TDocument>()
            .Index(string.IsNullOrEmpty(indexName) ? _options.DefaultIndexName : indexName);

        var response = await _client.IndexAsync(document, indexDescriptor);
        if (!response.IsValid)
        {
            // 오류 처리
            Console.WriteLine($"Insert or update error: {response.DebugInformation}");
        }
    }

    public async Task<bool> DeleteAsync<TDocument>(string documentId, string indexName = null) where TDocument : class
    {
        var deleteDescriptor = new DeleteDescriptor<TDocument>()
            .Id(documentId)
            .Index(string.IsNullOrEmpty(indexName) ? _options.DefaultIndexName : indexName);

        var response = await _client.DeleteAsync(deleteDescriptor);
        return response.IsValid;
    }

    public async Task<bool> DropIndexAsync(string indexName)
    {
        if (string.IsNullOrEmpty(indexName))
        {
            throw new ArgumentException("Index name cannot be null or empty.", nameof(indexName));
        }

        var response = await _client.Indices.DeleteAsync(indexName);
        return response.IsValid;
    }

    public async Task CreateIndexAsync(string indexName)
    {
        if (string.IsNullOrEmpty(indexName))
        {
            throw new ArgumentException("Index name cannot be null or empty.", nameof(indexName));
        }

        // 인덱스 생성 전 이미 존재하는지 확인 (선택 사항)
        var existsResponse = await _client.Indices.ExistsAsync(indexName);
        if (!existsResponse.Exists)
        {
            var createIndexResponse = await _client.Indices.CreateAsync(indexName);
            if (!createIndexResponse.IsValid)
            {
                // 오류 처리
                Console.WriteLine($"Index creation failed: {createIndexResponse.DebugInformation}");
            }
        }
    }
}

이 서비스 구현체는 ASP.NET Core의 의존성 주입(Dependency Injection)을 통해 애플리케이션 전반에 걸쳐 사용될 수 있습니다.

태그: dotnet-core elasticsearch nest C# NoSQL

7월 16일 17:00에 게시됨