본문 바로가기
WIKI 기술 지식 베이스

Getting started with the high-level .NET client

원문 보기 위키 갱신

Getting started with the high-level .NET client (OpenSearch.Client)

출처: 문서

본문

고수준 .NET 클라이언트 (OpenSearch.Client) 시작하기

OpenSearch.Client는 고수준 .NET 클라이언트예요. 강력한 형식의 요청과 응답은 물론 Query DSL도 제공해요. 요청과 응답을 자동으로 파싱하고 직렬화/역직렬화하는 모델을 제공하기 때문에, raw JSON 요청을 직접 만들거나 raw JSON 응답을 직접 파싱할 필요가 없어요. 필요하다면 OpenSearch.Client에서 OpenSearch.Net 저수준 클라이언트도 사용할 수 있어요. 클라이언트의 전체 API 문서는 OpenSearch.Client API 문서를 참고하세요.

이 시작 가이드에서는 OpenSearch에 연결하는 방법, 문서를 인덱싱하는 방법, 쿼리를 실행하는 방법을 살펴봐요. 클라이언트 소스 코드는 opensearch-net 저장소에서 확인할 수 있어요.

OpenSearch.Client 설치하기

OpenSearch.Client를 설치하려면 OpenSearch.Client NuGet 패키지를 내려받아 원하는 IDE에서 프로젝트에 추가하세요. Microsoft Visual Studio에서는 다음 단계를 따르면 돼요:

  • Solution Explorer 패널에서 솔루션이나 프로젝트를 마우스 오른쪽 버튼으로 클릭하고 Manage NuGet Packages for Solution을 선택하세요.
  • OpenSearch.Client NuGet 패키지를 검색하고 Install을 선택하세요.

또는 .NET CLI를 사용해 프로젝트에 OpenSearch.Client를 추가할 수도 있어요:

dotnet add package OpenSearch.Client --version 2.2.0

{% include copy.html %}

.csproj 파일에 OpenSearch.Client를 추가할 수도 있어요:

<Project>
  ...
  <ItemGroup>
    <PackageReference Include="OpenSearch.Client" Version="2.2.0" />
  </ItemGroup>
</Project>

{% include copy.html %}

OpenSearch.Client는 OpenSearch.Net에 의존하기 때문에, OpenSearch.Client를 설치하면 저수준 클라이언트도 함께 설치돼요. 지원되는 OpenSearch 버전과 대상 프레임워크에 대한 정보는 호환성을 참고하세요.

샘플 데이터

이 페이지의 예제는 학생 한 명, 즉 인덱스의 문서 하나에 해당하는 객체를 나타내기 위해 다음 Student 클래스를 사용해요. ToString 메서드는 콘솔 출력을 위해 Student를 포맷팅해요:

using System.Globalization;

public class Student
{
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
    public double Gpa { get; set; }
    public string GradDate { get; set; } = string.Empty;

    public override string ToString() =>
        string.Format(CultureInfo.InvariantCulture,
            "{% raw %}Student{{firstName='{0}', lastName='{1}', gpa={2}, gradDate={3}}}{% endraw %}",
            FirstName, LastName, Gpa, GradDate);
}

{% include copy.html %}

기본적으로 OpenSearch.Client는 카멜 케이스를 사용해 프로퍼티 이름을 필드 이름으로 변환하기 때문에, Student는 firstName, lastName, gpa, gradDate 필드를 담은 문서로 인덱싱돼요. {: .note}

OpenSearch에 연결하기

기본 OpenSearch 호스트(http://localhost:9200)에 연결하려면 OpenSearchClient 객체를 생성할 때 기본 생성자를 사용하세요.

var client  = new OpenSearchClient();

{% include copy.html %}

주소를 알고 있는 단일 노드를 통해 OpenSearch 클러스터에 연결하려면, OpenSearch.Client 인스턴스를 생성할 때 해당 주소를 지정하세요:

var nodeAddress = new Uri("http://myserver:9200");
var client = new OpenSearchClient(nodeAddress);

{% include copy.html %}

여러 노드를 통해 OpenSearch에 연결할 수도 있어요. 노드 풀로 OpenSearch 클러스터에 연결하면 로드 밸런싱, 클러스터 페일오버 지원 같은 장점이 있어요. 여러 노드를 사용해 OpenSearch 클러스터에 연결하려면 각 노드의 주소를 지정하고 OpenSearch.Client 인스턴스를 위한 ConnectionSettings 객체를 생성하세요:

var nodes = new Uri[]
{
    new Uri("http://myserver1:9200"),
    new Uri("http://myserver2:9200"),
    new Uri("http://myserver3:9200")
};

var pool = new StaticConnectionPool(nodes);
var settings = new ConnectionSettings(pool);
var client = new OpenSearchClient(settings);

{% include copy.html %}

ConnectionSettings 사용하기

ConnectionConfiguration은 저수준 OpenSearch.Net 클라이언트에 구성 옵션을 전달할 때 사용해요. ConnectionSettings는 ConnectionConfiguration을 상속하면서, 요청에 사용할 기본 인덱스 이름이나 프로퍼티 이름과 필드 이름 간의 매핑 같은 고수준 클라이언트용 추가 구성 옵션을 제공해요. ConnectionSettings는 OpenSearch.Client 패키지의 일부예요.

노드 주소와, 인덱스 이름을 지정하지 않는 요청에서 사용할 기본 인덱스 이름을 설정하려면 ConnectionSettings 객체를 생성하세요:

var node = new Uri("http://myserver:9200");
var config = new ConnectionSettings(node).DefaultIndex("students");
var client = new OpenSearchClient(config);

{% include copy.html %}

인덱스 생성하기

다음 예제는 프라이머리 샤드 하나와 레플리카 하나를 가진 인덱스를 생성해요. 그리고 gradDate 필드를 yyyy-MM-dd 형식의 date로 명시적으로 매핑해요. 나머지 문서 필드는 문서를 인덱싱할 때 OpenSearch가 동적으로 매핑해요:

var index = "students";
var createIndexResponse = client.Indices.Create(index, c => c
    .Settings(s => s
        .NumberOfShards(1)
        .NumberOfReplicas(1))
    .Map<Student>(m => m
        .Properties(p => p
            .Date(d => d.Name(f => f.GradDate).Format("yyyy-MM-dd")))));

{% include copy.html %}

문서 하나 인덱싱하기

Student 인스턴스를 하나 만들어 보세요:

var student = new Student { FirstName = "John", LastName = "Doe", Gpa = 3.89, GradDate = "2022-05-15" };

{% include copy.html %}

문서 하나를 인덱싱할 때는 fluent lambda 문법과 object initializer 문법 중 하나를 사용할 수 있어요. 다음 예제들은 문서를 검색에서 바로 사용할 수 있도록 Refresh를 Refresh.True로 설정해요.

fluent lambda 문법을 사용해 이 Student를 ID 1로 students 인덱스에 인덱싱해 보세요:

var indexResponse = client.Index(student, i => i
    .Index(index)
    .Id("1")
    .Refresh(Refresh.True));

{% include copy.html %}

object initializer 문법을 사용해 이 Student를 ID 1로 students 인덱스에 인덱싱해 보세요:

var indexResponse = client.Index(new IndexRequest<Student>(student, index, "1")
{
    Refresh = Refresh.True
});

{% include copy.html %}

여러 문서 인덱싱하기

Bulk API를 사용하면 여러 문서를 한 번의 요청으로 인덱싱할 수 있어요:

var bulkResponse = client.Bulk(b => b
    .Index(index)
    .Refresh(Refresh.True)
    .Index<Student>(op => op
        .Id("2")
        .Document(new Student { FirstName = "Paulo", LastName = "Santos", Gpa = 3.93, GradDate = "2021-05-20" }))
    .Index<Student>(op => op
        .Id("3")
        .Document(new Student { FirstName = "Shirley", LastName = "Rodriguez", Gpa = 3.91, GradDate = "2019-05-10" })));

{% include copy.html %}

문서 검색하기

다음 코드를 사용하면 인덱스에 있는 모든 문서를 검색할 수 있어요:

var searchResponse = client.Search<Student>(s => s
    .Index(index)
    .Query(q => q.MatchAll()));
foreach (var doc in searchResponse.Documents)
{
    Console.WriteLine(doc);
}

{% include copy.html %}

searchResponse.Documents의 각 항목은 Student 객체이고, 각 필드는 프로퍼티로 접근할 수 있어요. 각 문서의 ID까지 가져오려면 searchResponse.Hits를 순회하세요. 각 hit는 Id 프로퍼티에 문서 ID를, Source 프로퍼티에 Student 객체를 담고 있어요:

foreach (var hit in searchResponse.Hits)
{
    Console.WriteLine($"ID: {hit.Id}, name: {hit.Source.FirstName} {hit.Source.LastName}, GPA: {hit.Source.Gpa}, graduation date: {hit.Source.GradDate}");
}

{% include copy.html %}

2019년에 졸업한 학생을 검색하려면 range 쿼리를 사용하세요. 다음 Query DSL range 쿼리는 gradDate가 2019년 범위에 속하는 문서를 검색해요:

GET students/_search
{
  "query": {
    "range": {
      "gradDate": {
        "gte": "2019-01-01",
        "lte": "2019-12-31"
      }
    }
  }
}

이 쿼리를 OpenSearch.Client로 작성하면 다음과 같아요:

var searchResponse = client.Search<Student>(s => s
    .Index(index)
    .Query(q => q
        .DateRange(r => r
            .Field(f => f.GradDate)
            .GreaterThanOrEquals("2019-01-01")
            .LessThanOrEquals("2019-12-31"))));

{% include copy.html %}

응답에는 문서가 하나 포함되어 있으며, 이 문서가 올바른 학생에 해당해요:

Student{firstName='Shirley', lastName='Rodriguez', gpa=3.91, gradDate=2019-05-10}

결과 페이징하기

결과를 페이징하려면 from과 size 파라미터를 사용하세요. 다음 예제는 학생들을 졸업 날짜 기준으로 정렬한 뒤 결과를 한 번에 두 개씩 가져와요. 첫 번째 요청은 결과의 첫 페이지를 반환하고, 두 번째 요청은 그다음 페이지를 반환해요:

var firstPageResponse = client.Search<Student>(s => s
    .Index(index)
    .Sort(so => so.Ascending(f => f.GradDate))
    .From(0)
    .Size(2));
foreach (var doc in firstPageResponse.Documents)
{
    Console.WriteLine(doc);
}

var nextPageResponse = client.Search<Student>(s => s
    .Index(index)
    .Sort(so => so.Ascending(f => f.GradDate))
    .From(2)
    .Size(2));
foreach (var doc in nextPageResponse.Documents)
{
    Console.WriteLine(doc);
}

{% include copy.html %}

from과 size 파라미터는 결과의 앞쪽 페이지를 다룰 때 잘 동작해요. 아주 많은 결과를 페이징하려면 point in time과 search_after를 사용하세요. 자세한 내용은 결과 페이징하기를 참고하세요.

문서 업데이트하기

부분 문서(partial document)를 사용해 문서를 업데이트할 수 있어요. 부분 문서에 포함된 필드만 업데이트돼요:

var updateResponse = client.Update<Student, object>("1", u => u
    .Index(index)
    .Doc(new { gpa = 3.92 }));

{% include copy.html %}

문서 삭제하기

다음 코드를 사용해 문서를 삭제할 수 있어요:

var deleteResponse = client.Delete<Student>("3", d => d
    .Index(index)
    .Refresh(Refresh.True));

{% include copy.html %}

인덱스 삭제하기

다음 코드를 사용해 인덱스를 삭제할 수 있어요:

var deleteIndexResponse = client.Indices.Delete(index);

{% include copy.html %}

OpenSearch.Client 메서드를 비동기로 사용하기

비동기 코드가 필요한 애플리케이션을 위해 OpenSearch.Client의 모든 메서드 호출에는 비동기 버전이 준비되어 있어요:

// synchronous method
var response = client.Index(student, i => i.Index(index).Id("1"));

// asynchronous method
var asyncResponse = await client.IndexAsync(student, i => i.Index(index).Id("1"));

{% include copy.html %}

저수준 OpenSearch.Net 클라이언트 사용하기

OpenSearch.Client는 LowLevel 프로퍼티를 통해 저수준 OpenSearch.Net 클라이언트를 노출해요. OpenSearch.Client가 메서드를 제공하지 않는 API를 호출하거나, OpenSearch.Client 쿼리 메서드를 사용하는 대신 요청 본문을 직접 구성하고 싶을 때 이 저수준 클라이언트를 사용하세요. 다음 예제는 range 쿼리를 익명 객체로 전송하고 응답을 SearchResponse<Student>로 역직렬화해요:

var lowLevelClient = client.LowLevel;

var searchResponseLow = lowLevelClient.Search<SearchResponse<Student>>(index,
    PostData.Serializable(
        new
        {
            query = new
            {
                range = new
                {
                    gradDate = new
                    {
                        gte = "2019-01-01",
                        lte = "2019-12-31"
                    }
                }
            }
        }));

if (searchResponseLow.IsValid)
{
    foreach (var doc in searchResponseLow.Documents)
    {
        Console.WriteLine(doc);
    }
}

{% include copy.html %}

샘플 프로그램

이 샘플 프로그램은 앞선 섹션의 코드들을 하나로 모은 거예요. Security plugin이 활성화된 클러스터에 연결해요. Security plugin이 없는 클러스터에 연결하려면 // Without security 주석이 붙은 줄을 변경하세요. 샘플 프로그램을 실행하기 전에 프로젝트에 Student 클래스가 정의되어 있는지 확인하세요.

이 샘플 프로그램은 테스트 전용이에요. 자체 서명 인증서를 사용하는 클러스터에 연결할 수 있도록 코드에 자격 증명을 직접 지정하고 인증서 검증을 비활성화해요. 프로덕션 환경에서는 자격 증명을 안전한 위치에서 불러오고 클러스터의 인증서를 검증하세요. {: .warning}

다음 샘플 프로그램은 클라이언트를 생성하고, 인덱스를 만들고, 문서를 개별적으로 그리고 벌크로 인덱싱하고, 문서를 검색하고, 문서를 업데이트하고, 문서를 삭제한 뒤 인덱스를 삭제해요:

using OpenSearch.Client;
using OpenSearch.Net;

namespace NetClientProgram;

internal class Program
{
    public static void Main(string[] args)
    {
        var settings = new ConnectionSettings(new Uri("https://localhost:9200")); // Without security, use http://localhost:9200
        settings.BasicAuthentication("admin", "<custom-admin-password>"); // Without security, remove this line
        settings.ServerCertificateValidationCallback(CertificateValidations.AllowAll); // Without security, remove this line
        var client = new OpenSearchClient(settings);

        // Create the index
        var index = "students";
        Console.WriteLine("Creating index......");
        var createIndexResponse = client.Indices.Create(index, c => c
            .Settings(s => s
                .NumberOfShards(1)
                .NumberOfReplicas(1))
            .Map<Student>(m => m
                .Properties(p => p
                    .Date(d => d.Name(f => f.GradDate).Format("yyyy-MM-dd")))));
        Console.WriteLine("Index created: " + createIndexResponse.Index);

        // Index a document
        Console.WriteLine("\nIndexing one student......");
        var student = new Student { FirstName = "John", LastName = "Doe", Gpa = 3.89, GradDate = "2022-05-15" };
        var indexResponse = client.Index(student, i => i
            .Index(index)
            .Id("1")
            .Refresh(Refresh.True));
        Console.WriteLine($"Result: {indexResponse.Result.ToString().ToLowerInvariant()}, id: {indexResponse.Id}, version: {indexResponse.Version}");

        // Bulk index documents
        Console.WriteLine("\nIndexing many students......");
        var bulkResponse = client.Bulk(b => b
            .Index(index)
            .Refresh(Refresh.True)
            .Index<Student>(op => op
                .Id("2")
                .Document(new Student { FirstName = "Paulo", LastName = "Santos", Gpa = 3.93, GradDate = "2021-05-20" }))
            .Index<Student>(op => op
                .Id("3")
                .Document(new Student { FirstName = "Shirley", LastName = "Rodriguez", Gpa = 3.91, GradDate = "2019-05-10" })));
        Console.WriteLine("Errors: " + bulkResponse.Errors.ToString().ToLowerInvariant());
        foreach (var item in bulkResponse.Items)
        {
            Console.WriteLine($"  {item.Result} id: {item.Id}");
        }

        // Search for all students
        Console.WriteLine("\nSearching for all students......");
        var searchResponse = client.Search<Student>(s => s
            .Index(index)
            .Sort(so => so.Ascending(f => f.GradDate))
            .From(0)
            .Size(2));
        Console.WriteLine("Total hits: " + searchResponse.Total);
        Console.WriteLine("Page 1:");
        foreach (var doc in searchResponse.Documents)
        {
            Console.WriteLine("  " + doc);
        }

        var nextPageResponse = client.Search<Student>(s => s
            .Index(index)
            .Sort(so => so.Ascending(f => f.GradDate))
            .From(2)
            .Size(2));
        Console.WriteLine("Page 2:");
        foreach (var doc in nextPageResponse.Documents)
        {
            Console.WriteLine("  " + doc);
        }

        // Search for students who graduated in 2019
        Console.WriteLine("\nSearching for students who graduated in 2019......");
        var searchResponse2 = client.Search<Student>(s => s
            .Index(index)
            .Query(q => q
                .DateRange(r => r
                    .Field(f => f.GradDate)
                    .GreaterThanOrEquals("2019-01-01")
                    .LessThanOrEquals("2019-12-31"))));
        Console.WriteLine("Total hits: " + searchResponse2.Total);
        foreach (var doc in searchResponse2.Documents)
        {
            Console.WriteLine("  " + doc);
        }

        // Update a document
        Console.WriteLine("\nUpdating a student's GPA......");
        var updateResponse = client.Update<Student, object>("1", u => u
            .Index(index)
            .Doc(new { gpa = 3.92 }));
        Console.WriteLine($"Result: {updateResponse.Result.ToString().ToLowerInvariant()}, version: {updateResponse.Version}");

        // Get the updated document
        var getResponse = client.Get<Student>("1", g => g.Index(index));
        Console.WriteLine("Updated document: " + getResponse.Source);

        // Delete a document
        Console.WriteLine("\nDeleting a student......");
        var deleteResponse = client.Delete<Student>("3", d => d
            .Index(index)
            .Refresh(Refresh.True));
        Console.WriteLine("Result: " + deleteResponse.Result.ToString().ToLowerInvariant());

        // Delete the index
        Console.WriteLine("\nDeleting the index......");
        var deleteIndexResponse = client.Indices.Delete(index);
        Console.WriteLine("Acknowledged: " + deleteIndexResponse.Acknowledged.ToString().ToLowerInvariant());
    }
}

{% include copy.html %}

샘플 프로그램의 출력은 다음과 같아요:

Creating index......
Index created: students

Indexing one student......
Result: created, id: 1, version: 1

Indexing many students......
Errors: false
  created id: 2
  created id: 3

Searching for all students......
Total hits: 3
Page 1:
  Student{firstName='Shirley', lastName='Rodriguez', gpa=3.91, gradDate=2019-05-10}
  Student{firstName='Paulo', lastName='Santos', gpa=3.93, gradDate=2021-05-20}
Page 2:
  Student{firstName='John', lastName='Doe', gpa=3.89, gradDate=2022-05-15}

Searching for students who graduated in 2019......
Total hits: 1
  Student{firstName='Shirley', lastName='Rodriguez', gpa=3.91, gradDate=2019-05-10}

Updating a student's GPA......
Result: updated, version: 2
Updated document: Student{firstName='John', lastName='Doe', gpa=3.92, gradDate=2022-05-15}

Deleting a student......
Result: deleted

Deleting the index......
Acknowledged: true

관련 문서

더 알아보기 (Learn more)