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

More advanced features of the high-level .NET client

원문 보기 위키 갱신

More advanced features of the high-level .NET client (OpenSearch.Client)

출처: 문서

본문

고수준 .NET 클라이언트(OpenSearch.Client)의 고급 기능

다음 예제는 OpenSearch.Client의 더 고급 기능을 보여줘요. 간단한 예제는 Getting started guide를 참고하세요. 이 예제는 Getting started guide에서 사용한 것과 동일한 다음 Student 클래스를 사용해요. OpenSearch.Client는 이 클래스의 프로퍼티 이름을 firstName, lastName, gpa, gradDate 필드 이름으로 변환해요. 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는 동적 매핑을 사용해서 인덱싱되는 문서의 필드 타입을 추론해요. 하지만 문서의 스키마를 더 세밀하게 제어하고 싶다면 명시적 매핑을 OpenSearch에 전달할 수 있어요. 이 매핑에서 문서의 일부 또는 전체 필드에 대한 데이터 타입을 정의할 수 있어요.

마찬가지로 OpenSearch.Client는 클래스 프로퍼티의 타입을 기반으로 필드 데이터 타입을 추론하는 자동 매핑(auto mapping)을 사용해요. 자동 매핑을 사용하려면 AutoMap의 기본 생성자로 students 인덱스를 생성하세요:

var createResponse = await osClient.Indices.CreateAsync("students",
    c => c.Map(m => m.AutoMap<Student>()));

{% include copy.html %}

자동 매핑을 사용하면 Gpa는 double로 매핑되고, FirstName, LastName, GradDate는 문자열 프로퍼티라서 keyword 서브필드를 가진 text로 매핑돼요. GradDate로 날짜 범위를 검색하려면 yyyy-MM-dd 형식의 date로 매핑하세요. FirstName과 LastName을 검색하면서 대소문자를 구분하는 완전 일치만 허용하고 싶다면, 이 필드들을 keyword로만 매핑해서 분석 과정을 생략할 수 있어요. Query DSL에서는 다음 쿼리로 이를 수행할 수 있어요:

PUT students
{
  "mappings" : {
    "properties" : {
      "firstName" : {
        "type" : "keyword"
      },
      "lastName" : {
        "type" : "keyword"
      },
      "gradDate" : {
        "type" : "date",
        "format" : "yyyy-MM-dd"
      }
    }
  }
}

OpenSearch.Client에서는 fluid lambda 문법을 사용해 이 필드들을 매핑할 수 있어요:

var createResponse = await osClient.Indices.CreateAsync(index,
                c => c.Map(m => m.AutoMap<Student>()
                .Properties<Student>(p => p
                .Keyword(k => k.Name(f => f.FirstName))
                .Keyword(k => k.Name(f => f.LastName))
                .Date(d => d.Name(f => f.GradDate).Format("yyyy-MM-dd")))));

{% include copy.html %}

설정

매핑 외에도 인덱스를 생성할 때 프라이머리 샤드와 레플리카 샤드 개수 같은 설정을 지정할 수 있어요. 다음 쿼리는 프라이머리 샤드 개수를 1로, 레플리카 샤드 개수를 2로 설정해요:

PUT students
{
  "mappings" : {
    "properties" : {
      "firstName" : {
        "type" : "keyword"
      },
      "lastName" : {
        "type" : "keyword"
      },
      "gradDate" : {
        "type" : "date",
        "format" : "yyyy-MM-dd"
      }
    }
  }, 
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 2
  }
}

OpenSearch.Client에서 앞선 쿼리와 동일한 동작은 다음과 같아요:

var createResponse = await osClient.Indices.CreateAsync(index,
                            c => c.Map(m => m.AutoMap<Student>()
                            .Properties<Student>(p => p
                            .Keyword(k => k.Name(f => f.FirstName))
                            .Keyword(k => k.Name(f => f.LastName))
                            .Date(d => d.Name(f => f.GradDate).Format("yyyy-MM-dd"))))
                            .Settings(s => s.NumberOfShards(1).NumberOfReplicas(2)));

{% include copy.html %}

Bulk API를 사용해 여러 문서 인덱싱하기

Index와 IndexDocument로 문서 하나를 인덱싱하고 IndexMany로 여러 문서를 인덱싱하는 것 외에도, Bulk나 BulkAll을 사용하면 문서 인덱싱을 더 세밀하게 제어할 수 있어요. 문서를 하나씩 인덱싱하면 보내는 문서마다 HTTP 요청이 하나씩 만들어져서 비효율적이에요. BulkAll 헬퍼를 사용하면 재시도, 청킹(chunking), 백오프(back off) 요청 기능을 직접 처리하지 않아도 돼요. 요청이 실패하면 자동으로 재시도하고, 서버가 내려가 있으면 백오프하며, 한 번의 HTTP 요청으로 몇 개의 문서를 보낼지 제어해요.

다음 예제에서 BulkAll은 인덱스 이름, 백오프 재시도 횟수, 백오프 시간으로 구성돼요. 또한 최대 병렬 처리 수(maximum degrees of parallelism) 설정이 데이터를 담은 병렬 HTTP 요청의 개수를 제어해요. 마지막으로 size 파라미터는 한 번의 HTTP 요청으로 몇 개의 문서를 보낼지 지정해요.

프로덕션 환경에서는 size를 100–1000개 문서로 설정하는 것을 권장해요. {: .tip}

BulkAll은 데이터 스트림을 받아서 백그라운드 작업을 관찰할 때 사용할 수 있는 Observable을 반환해요.

var bulkAll = osClient.BulkAll(ReadData(), r => r
            .Index(index)
            .BackOffRetries(2)
            .BackOffTime("30s")
            .MaxDegreeOfParallelism(4)
            .Size(100));

{% include copy.html %}

Boolean 쿼리로 검색하기

OpenSearch.Client는 OpenSearch의 전체 쿼리 기능을 제공해요. match 쿼리를 사용하는 단순한 검색 외에도, gradDate 범위로 필터링하는 더 복잡한 Boolean 쿼리를 만들어서 2022년에 졸업한 학생을 검색하고 성(last name) 기준으로 정렬할 수 있어요. 다음 예제에서는 검색을 10개 문서로 제한하고, scroll API를 사용해 결과의 페이지네이션을 제어해요.

var gradResponse = await osClient.SearchAsync<Student>(s => s
                        .Index(index)
                        .From(0)
                        .Size(10)
                        .Scroll("1m")
                        .Query(q => q
                        .Bool(b => b
                        .Filter(f => f
                        .DateRange(r => r
                            .Field(fld => fld.GradDate)
                            .GreaterThanOrEquals("2022-01-01")
                            .LessThanOrEquals("2022-12-31")))))
                        .Sort(srt => srt.Ascending(f => f.LastName)));

{% include copy.html %}

응답에는 OpenSearch에서 일치하는 문서를 담고 있는 Documents 프로퍼티가 들어 있어요. 데이터는 Student 타입으로 역직렬화된 JSON 객체 형태라서 강력한 타입(strongly typed) 방식으로 프로퍼티에 접근할 수 있어요. 모든 직렬화와 역직렬화는 OpenSearch.Client가 처리해요.

애그리게이션

OpenSearch.Client는 애그리게이션을 포함해 OpenSearch의 전체 쿼리 기능을 갖추고 있어요. 검색 결과를 버킷으로 그룹화하는 것(예: GPA 범위별로 학생 그룹화) 외에도 합계나 평균 같은 메트릭을 계산할 수 있어요. 다음 쿼리는 인덱스에 있는 모든 학생의 평균 GPA를 계산해요.

Size를 0으로 설정하면 OpenSearch가 실제 문서가 아니라 애그리게이션 결과만 반환해요. {: .tip}

var aggResponse = await osClient.SearchAsync<Student>(s => s
                                .Index(index)
                                .Size(0)
                                .Aggregations(a => a
                                .Average("average gpa", 
                                            avg => avg.Field(fld => fld.Gpa))));

{% include copy.html %}

인덱스 생성 및 데이터 인덱싱 샘플 프로그램

이 절의 샘플 프로그램은 프로그램을 실행하는 디렉터리에 있는 students.csv 파일에서 학생 레코드를 읽어요. 파일을 한 줄에 학생 레코드 하나씩 FirstName,LastName,Gpa,GradDate 형식으로 작성하고, 졸업 날짜는 yyyy-MM-dd 형식으로 지정하세요. 헤더 행은 포함하지 마세요. 예를 들면 다음과 같아요:

John,Doe,3.89,2022-05-15
Wei,Zhang,3.65,2022-05-15
Zhang,Li,3.72,2022-06-10

{% include copy.html %}

다음 프로그램은 students 인덱스가 존재하면 삭제하고, 인덱스를 생성한 다음, 파일에서 학생 레코드를 읽어서 OpenSearch에 인덱싱해요:

using System.Globalization;
using OpenSearch.Client;

namespace NetClientProgram;

internal class Program
{
    private const string index = "students";

    public static IOpenSearchClient osClient = new OpenSearchClient();

    public static async Task Main(string[] args)
    {
        // Delete the "students" index if it exists so that the index is created with the following mappings
        var existResponse = await osClient.Indices.ExistsAsync(index);

        if (existResponse.Exists)
        {
            await osClient.Indices.DeleteAsync(index);
        }

        // Create an index "students"
        // Map FirstName and LastName as keyword and GradDate as date
        var createResponse = await osClient.Indices.CreateAsync(index,
            c => c.Map(m => m.AutoMap<Student>()
            .Properties<Student>(p => p
            .Keyword(k => k.Name(f => f.FirstName))
            .Keyword(k => k.Name(f => f.LastName))
            .Date(d => d.Name(f => f.GradDate).Format("yyyy-MM-dd"))))
            .Settings(s => s.NumberOfShards(1).NumberOfReplicas(1)));

        if (!createResponse.IsValid || !createResponse.Acknowledged)
        {
            throw new Exception("Create response is invalid.");
        }

        // Take a stream of data and send it to OpenSearch
        var bulkAll = osClient.BulkAll(ReadData(), r => r
        .Index(index)
        .BackOffRetries(2)
        .BackOffTime("20s")
        .MaxDegreeOfParallelism(4)
        .Size(10)
        .RefreshOnCompleted());

        // Wait until the data upload is complete.
        // FromMinutes specifies a timeout.
        // r is a response object that is returned as the data is indexed.
        bulkAll.Wait(TimeSpan.FromMinutes(10), r =>
            Console.WriteLine("Data chunk indexed"));
    }

    // Reads student data in the form "FirstName,LastName,Gpa,GradDate"
    public static IEnumerable<Student> ReadData()
    {
        foreach (var line in File.ReadLines("students.csv"))
        {
            var fields = line.Split(',');
            yield return new Student
            {
                FirstName = fields[0],
                LastName = fields[1],
                Gpa = double.Parse(fields[2], CultureInfo.InvariantCulture),
                GradDate = fields[3]
            };
        }
    }
}

{% include copy.html %}

검색 샘플 프로그램

다음 프로그램은 이름과 졸업 날짜로 학생을 검색하고, 평균 GPA를 계산한 다음 인덱스를 삭제해요.

using OpenSearch.Client;

namespace NetClientProgram;

internal class Program
{
    private const string index = "students";

    public static IOpenSearchClient osClient = new OpenSearchClient();

    public static async Task Main(string[] args)
    {
        await SearchByName();

        await SearchByGradDate();

        await CalculateAverageGpa();

        // Delete the index
        Console.WriteLine("Deleting the index......");
        var deleteIndexResponse = await osClient.Indices.DeleteAsync(index);
        Console.WriteLine("Acknowledged: " + deleteIndexResponse.Acknowledged.ToString().ToLowerInvariant());
    }

    private static async Task SearchByName()
    {
        Console.WriteLine("Searching for name......");

        var nameResponse = await osClient.SearchAsync<Student>(s => s
                                .Index(index)
                                .Query(q => q
                                .Match(m => m
                                .Field(fld => fld.FirstName)
                                .Query("Zhang"))));

        if (!nameResponse.IsValid)
        {
            throw new Exception("Name query response is not valid.");
        }

        foreach (var s in nameResponse.Documents)
        {
            Console.WriteLine("  " + s);
        }
    }

    private static async Task SearchByGradDate()
    {
        Console.WriteLine("Searching for grad date......");

        // Search for all students who graduated in 2022
        var gradResponse = await osClient.SearchAsync<Student>(s => s
                                .Index(index)
                                .From(0)
                                .Size(10)
                                .Scroll("1m")
                                .Query(q => q
                                .Bool(b => b
                                .Filter(f => f
                                .DateRange(r => r
                                    .Field(fld => fld.GradDate)
                                    .GreaterThanOrEquals("2022-01-01")
                                    .LessThanOrEquals("2022-12-31")))))
                                .Sort(srt => srt.Ascending(f => f.LastName)));


        if (!gradResponse.IsValid)
        {
            throw new Exception("Grad date query response is not valid.");
        }

        while (gradResponse.Documents.Any())
        {
            foreach (var data in gradResponse.Documents)
            {
                Console.WriteLine("  " + data);
            }
            gradResponse = await osClient.ScrollAsync<Student>("1m", gradResponse.ScrollId);
        }

        // Release the resources held by the scroll context
        await osClient.ClearScrollAsync(c => c.ScrollId(gradResponse.ScrollId));
    }

    public static async Task CalculateAverageGpa()
    {
        Console.WriteLine("Calculating average GPA......");

        // Search and aggregate
        // Size 0 means documents are not returned, only aggregation is returned
        var aggResponse = await osClient.SearchAsync<Student>(s => s
                                .Index(index)
                                .Size(0)
                                .Aggregations(a => a
                                .Average("average gpa",
                                            avg => avg.Field(fld => fld.Gpa))));

        if (!aggResponse.IsValid) throw new Exception("Aggregation response not valid");

        var avg = aggResponse.Aggregations.Average("average gpa").Value;
        Console.WriteLine($"Average GPA is {avg}");
    }
}

{% include copy.html %}

관련 문서

더 알아보기 (Learn more)