.NET 개발자를 위한 Elasticsearch 튜토리얼

게시 됨: 2022-03-11

.NET 개발자는 프로젝트에서 Elasticsearch를 사용해야 합니까? Elasticsearch는 Java를 기반으로 구축되었지만 Elasticsearch가 모든 프로젝트에 대한 전체 텍스트 검색에 가치가 있는 많은 이유를 제공한다고 생각합니다.

기술로서의 Elasticsearch는 지난 몇 년 동안 많은 발전을 이루었습니다. 전체 텍스트 검색을 마술처럼 느끼게 할 뿐만 아니라 텍스트 자동 완성, 집계 파이프라인 등과 같은 기타 정교한 기능을 제공합니다.

깔끔한 .NET 에코시스템에 Java 기반 서비스를 도입하는 것이 불편하더라도 Elasticsearch를 설치하고 구성한 후에는 대부분의 시간을 가장 멋진 .NET 패키지 중 하나와 함께 보내게 되므로 걱정하지 마십시오. 거기: 네스트.

이 기사에서는 .NET 프로젝트에서 놀라운 검색 엔진 솔루션인 Elasticsearch를 사용하는 방법을 배우게 됩니다.

설치 및 구성

Elasticsearch 자체를 개발 환경에 설치하는 것은 Elasticsearch와 선택적으로 Kibana를 다운로드하는 것입니다.

압축을 풀면 다음과 같은 bat 파일이 유용합니다.

 cd "D:\elastic\elasticsearch-5.2.2\bin" start elasticsearch.bat cd "D:\elastic\kibana-5.0.0-windows-x86\bin" start kibana.bat exit

두 서비스를 모두 시작한 후 여기에서 광범위하게 설명된 대로 항상 로컬 Kibana 서버(일반적으로 http://localhost:5601에서 사용 가능)를 확인하고, 인덱스와 유형을 사용하고, 순수한 JSON을 사용하여 검색할 수 있습니다.

첫 번째 단계

철저하고 훌륭한 개발자가 되면 경영진의 완전한 지원과 이해를 바탕으로 단위 테스트 프로젝트를 추가하고 최소 90%의 코드 적용 범위로 SearchService를 작성하는 것으로 시작합니다.

첫 번째 단계는 Elasticsearch 서버에 일종의 연결 문자열을 제공하도록 app.config 파일을 명확하게 구성하는 것입니다.

Elasticsearch의 멋진 점은 완전히 무료라는 것입니다. 하지만 여전히 Elastic.co에서 제공하는 Elastic Cloud 서비스를 사용하는 것이 좋습니다. 호스팅 서비스를 사용하면 모든 유지 관리 및 구성이 상당히 쉬워집니다. 더군다나 2주 동안 무료 평가판을 사용할 수 있습니다. 여기에서 모든 예제를 시험해 볼 수 있을 만큼 충분합니다!

여기에서 로컬로 실행 중이므로 다음과 같은 구성 키는 다음을 수행해야 합니다.

 <add key="Search-Uri" value="http://localhost:9200" />

Elasticsearch 설치는 기본적으로 포트 9200에서 실행되지만 원하는 경우 변경할 수 있습니다.

ElasticClient 및 NEST 패키지

ElasticClient는 우리를 위해 대부분의 작업을 수행할 멋진 작은 동료이며 NEST 패키지와 함께 제공됩니다.

먼저 패키지를 설치합시다.

클라이언트를 구성하려면 다음과 같이 사용할 수 있습니다.

 var node = new Uri(ConfigurationManager.AppSettings["Search-Uri"]); var settings = new ConnectionSettings(node); settings.ThrowExceptions(alwaysThrow: true); // I like exceptions settings.PrettyJson(); // Good for DEBUG var client = new ElasticClient(settings);

인덱싱 및 매핑

무언가를 검색할 수 있으려면 일부 데이터를 ES에 저장해야 합니다. 사용된 용어는 "인덱싱"입니다.

"매핑"이라는 용어는 데이터베이스의 데이터를 Elasticsearch에 직렬화되고 저장될 객체에 매핑하는 데 사용됩니다. 이 자습서에서는 EF(Entity Framework)를 사용합니다.

일반적으로 Elasticsearch를 사용할 때 사이트 전체 검색 엔진 솔루션을 찾고 있을 것입니다. 일종의 피드 또는 다이제스트를 사용하거나 사용자, 블로그 항목, 제품, 카테고리, 이벤트 등과 같은 다양한 엔터티의 모든 결과를 반환하는 Google과 유사한 검색을 사용합니다.

이들은 아마도 데이터베이스의 하나의 테이블이나 엔터티가 아니라 다양한 데이터를 집계하고 제목, 설명, 날짜, 작성자/소유자, 사진 등과 같은 몇 가지 공통 속성을 추출하거나 파생하려고 할 것입니다. 또 다른 점은 하나의 쿼리에서 수행하지 않을 수 있지만 ORM을 사용하는 경우 해당 블로그 항목, 사용자, 제품, 범주, 이벤트 또는 기타 항목 각각에 대해 별도의 쿼리를 작성해야 한다는 것입니다.

블로그 게시물이나 제품과 같은 각 "큰" 유형에 대한 색인을 만들어 프로젝트를 구성했습니다. 그런 다음 동일한 인덱스에 속하는 보다 구체적인 유형에 대해 일부 Elasticsearch 유형을 추가할 수 있습니다. 예를 들어 기사가 기사, 비디오 기사 또는 팟캐스트가 될 수 있는 경우 해당 기사는 여전히 "기사" 색인에 있지만 해당 색인에는 네 가지 유형이 있습니다. 그러나 여전히 데이터베이스에서 동일한 쿼리일 가능성이 높습니다.

각 인덱스에 대해 적어도 하나의 유형이 필요하다는 것을 명심하십시오. 아마도 인덱스와 이름이 같은 유형일 것입니다.

엔터티를 매핑하려면 몇 가지 추가 클래스를 만들어야 합니다. 저는 보통 DocumentSearchItemBase 클래스를 사용합니다. 여기서 각 특수 클래스는 BlogPostSearchItem , ProductSearchItem 등을 상속합니다.

저는 해당 클래스 내에 매퍼 표현식을 사용하는 것을 좋아합니다. 필요한 경우 언제든지 표현식을 수정할 수 있습니다.

Elasticsearch를 사용한 초기 프로젝트 중 하나에서 저는 훌륭하고 긴 switch-case 문으로 매핑 및 인덱싱을 수행하는 상당히 큰 SearchService 클래스를 작성했습니다. Elasticsearch에 던지고 싶은 각 엔터티 유형에 대해 매핑이 포함된 스위치와 쿼리가 있었습니다. 그거 했어.

그러나 그 과정을 통해 적어도 나에게는 그것이 최선의 방법이 아니라는 것을 배웠습니다.

보다 우아한 솔루션은 일종의 스마트 IndexDefinition 클래스와 각 인덱스에 대한 특정 인덱스 정의 클래스를 갖는 것입니다. 이런 식으로 내 기본 IndexDefinition 클래스는 사용 가능한 모든 인덱스 목록과 필수 분석기 및 상태 보고서와 같은 일부 도우미 메서드를 저장할 수 있으며 파생된 인덱스별 클래스는 데이터베이스 쿼리 및 각 인덱스에 대한 데이터 매핑을 구체적으로 처리합니다. 이것은 나중에 ES에 추가 엔터티를 추가해야 할 때 특히 유용합니다. IndexDefinition 에서 상속받은 또 다른 SomeIndexDefinition 클래스를 추가하는 것으로 귀결되며 인덱스에서 원하는 데이터를 쿼리하는 몇 가지 메서드만 구현하면 됩니다.

Elasticsearch가 말하다

Elasticsearch로 할 수 있는 모든 것의 핵심은 쿼리 언어입니다. 이상적으로는 Elasticsearch와 통신할 수 있는 데 필요한 것은 쿼리 객체를 구성하는 방법을 아는 것뿐입니다.

무대 뒤에서 Elasticsearch는 HTTP를 통해 JSON 기반 API로 기능을 노출합니다.

API 자체와 쿼리 개체의 구조는 상당히 직관적이지만 많은 실제 시나리오를 처리하는 것은 여전히 ​​번거로울 수 있습니다.

일반적으로 Elasticsearch에 대한 검색 요청에는 다음 정보가 필요합니다.

  • 어떤 인덱스와 어떤 유형이 검색되는지

  • 페이지 매김 정보(건너뛸 항목 수와 반환할 항목 수)

  • 구체적인 유형 선택(여기서 하려는 것처럼 집계를 수행할 때)

  • 쿼리 자체

  • 하이라이트 정의(Elasticsearch는 원하는 경우 히트를 자동으로 하이라이트할 수 있음)

예를 들어, 일부 사용자만 사이트의 프리미엄 콘텐츠를 볼 수 있는 검색 기능을 구현하거나 일부 콘텐츠는 작성자의 "친구"에게만 표시되도록 할 수 있습니다.

쿼리 개체를 구성할 수 있다는 것은 이러한 문제에 대한 솔루션의 핵심이며 많은 시나리오를 다루려고 할 때 실제로 문제가 될 수 있습니다.

위의 모든 것 중에서 가장 중요하고 설정하기 가장 어려운 것은 당연히 쿼리 세그먼트이며 여기서는 주로 이에 초점을 맞출 것입니다.

쿼리는 BoolQuery와 MatchPhraseQuery , TermsQuery , DateRangeQueryBoolQuery 와 같은 기타 쿼리가 결합된 재귀 ExistsQuery 입니다. 이것들은 기본적인 요구 사항을 충족시키기에 충분했으며 시작하기에 좋습니다.

MultiMatch 쿼리는 검색을 수행하려는 필드를 지정하고 결과를 조금 더 조정할 수 있기 때문에 매우 중요합니다. 이에 대해서는 나중에 다시 설명하겠습니다.

MatchPhraseQuery 는 예를 들어 특정 작성자( AuthorId )별 결과 일치 또는 모든 공개 기사 일치( ContentPrivacy=Public )와 같이 기존 SQL 데이터베이스의 외래 키 또는 열거형과 같은 정적 값으로 결과를 필터링할 수 있습니다.

TermsQuery 는 기존 SQL 언어로 "in"으로 번역됩니다. 예를 들어 사용자의 친구 중 한 명이 작성한 모든 기사를 반환하거나 고정된 판매자 집합에서만 제품을 가져올 수 있습니다. SQL과 마찬가지로 성능에 영향을 미치므로 이를 남용해서는 안 되며 이 배열에 10,000개의 구성원을 배치해야 하지만 일반적으로 합리적인 양을 상당히 잘 처리합니다.

DateRangeQuery 는 자체 문서화됩니다.

ExistsQuery 는 흥미로운 기능입니다. 특정 필드가 없는 문서를 무시하거나 반환할 수 있습니다.

이것들을 BoolQuery 와 결합하면 복잡한 필터링 로직을 정의할 수 있습니다.

예를 들어 블로그 게시물이 표시되어야 하는 시점을 나타내는 AvailableFrom 필드가 있을 수 있는 블로그 사이트를 생각해 보십시오.

AvailableFrom <= Now 와 같은 필터를 적용하면 해당 특정 필드가 전혀 없는 문서를 얻을 수 없습니다(데이터를 집계하고 일부 문서에는 해당 필드가 정의되어 있지 않을 수 있음). 이 문제를 해결하려면 ExistsQueryDateRangeQuery 를 결합하고 BoolQuery 에서 최소한 하나의 요소가 충족된다는 조건으로 BoolQuery 내에서 래핑합니다. 이 같은:

 BoolQuery Should (at least one of the following conditions should be fulfilled) DateRangeQuery with AvailableFrom condition Negated ExistsQuery for field AvailableFrom

쿼리를 부정하는 것은 그렇게 간단하고 바로 사용할 수 있는 작업이 아닙니다. 그러나 BoolQuery 의 도움으로 이것이 가능합니다.

 BoolQuery MustNot ExistsQuery

자동화 및 테스트

일을 더 쉽게 하기 위해 권장되는 방법은 확실히 테스트를 작성하는 것입니다.

이렇게 하면 더 효율적으로 실험할 수 있으며 더 중요한 것은 더 복잡한 필터와 같은 새로운 변경 사항이 기존 기능을 손상시키지 않도록 할 수 있다는 것입니다. 나는 Elasticsearch의 엔진과 같은 것을 조롱하는 팬이 아니기 때문에 명시적으로 "단위 테스트"라고 말하고 싶지 않았습니다. 모의는 ES가 실제로 어떻게 작동하는지에 대한 현실적인 근사치가 될 수 없습니다. 따라서 다음과 같은 경우 통합 테스트가 될 수 있습니다. 당신은 용어 팬입니다.

실제 사례

인덱싱, 매핑 및 필터링으로 모든 기초 작업이 완료되면 이제 가장 흥미로운 부분인 검색 매개변수를 조정하여 더 나은 결과를 얻을 준비가 되었습니다.

지난 프로젝트에서 Elasticsearch를 사용하여 사용자 피드를 제공했습니다. 모든 콘텐츠는 생성 날짜별로 정렬된 한 곳으로 집계되고 일부 옵션이 포함된 전체 텍스트 검색이 제공됩니다. 피드 자체는 매우 간단합니다. 데이터 어딘가에 날짜 필드가 있는지 확인하고 해당 필드를 기준으로 정렬하십시오.

반면에 검색은 기본적으로 놀랍도록 잘 작동하지 않습니다. 당연히 Elasticsearch는 데이터에서 중요한 것이 무엇인지 알 수 없기 때문입니다. Title , Tags (array) 및 Body 필드가 있는 데이터가 있다고 가정해 보겠습니다. body 필드는 HTML 콘텐츠가 될 수 있습니다(좀 더 사실적으로 만들기 위해).

철자 오류

요구 사항: 철자 오류가 발생하거나 단어 끝이 다른 경우에도 검색 결과를 반환해야 합니다. 예를 들어 "나무로 된 숟가락으로 할 수 있는 멋진 일"이라는 제목의 기사가 있는 경우 "물건" 또는 "나무"를 검색하면 여전히 일치하는 것을 찾고 싶습니다.

이를 처리하려면 분석기, 토크나이저, 문자 필터 및 토큰 필터에 대해 잘 알고 있어야 합니다. 이는 인덱싱 시 적용되는 변환입니다.

  • 분석기를 정의해야 합니다. 인덱스별로 정의할 수 있습니다.

  • 분석기는 문서의 일부 필드에 적용할 수 있습니다. 이는 속성 또는 유창한 API를 사용하여 수행할 수 있습니다. 이 예에서는 속성을 사용하고 있습니다.

  • 분석기는 필터, 문자 필터 및 토크나이저의 조합입니다.

요구 사항(부분 단어 일치)을 충족하기 위해 다음으로 구성된 "자동 완성" 분석기를 만듭니다.

  • 영어 불용어 필터: "and" 또는 "the"와 같은 영어의 모든 일반적인 단어를 제거하는 필터입니다.

  • 트림 필터: 각 토큰 주변의 공백을 제거합니다.

  • 소문자 필터: 모든 문자를 소문자로 변환합니다. 이것은 우리가 데이터를 가져올 때 소문자로 변환된다는 의미가 아니라 대소문자 불변 검색을 활성화한다는 의미입니다.

  • Edge-n-gram 토크나이저: 이 토크나이저는 부분 일치를 가능하게 합니다. 예를 들어, "My granny has wood chair"라는 문장이 있는 경우 "wood"라는 용어를 검색할 때 우리는 여전히 그 문장에서 히트를 치고 싶습니다. edge-n-gram이 하는 일은 "woo", "wood", "wooden" 및 "wooden"을 저장하여 적어도 세 글자로 된 부분 단어 일치를 찾도록 합니다. 매개변수 MinGram 및 MaxGram은 저장할 최소 및 최대 문자 수를 정의합니다. 우리의 경우 최소 3글자에서 최대 15글자까지 사용할 수 있습니다.

다음 섹션에서는 모든 항목이 함께 바인딩됩니다.

 analysis.Analyzers(a => a .Custom("autocomplete", cc => cc .Filters("eng_stopwords", "trim", "lowercase") .Tokenizer("autocomplete") ) .Tokenizers(tdesc => tdesc .EdgeNGram("autocomplete", e => e .MinGram(3) .MaxGram(15) .TokenChars(TokenChar.Letter, TokenChar.Digit) ) ) .TokenFilters(f => f .Stop("eng_stopwords", lang => lang .StopWords("_english_") ) );

그리고 이 분석기를 사용하려면 원하는 필드에 다음과 같이 주석을 달아야 합니다.

 public class SearchItemDocumentBase { ... [Text(Analyzer = "autocomplete", Name = nameof(Title))] public string Title { get; set; } ... }

이제 콘텐츠가 많은 거의 모든 응용 프로그램에서 매우 일반적인 요구 사항을 보여주는 몇 가지 예를 살펴보겠습니다.

HTML 청소

요구 사항: 일부 필드에는 내부에 HTML 텍스트가 있을 수 있습니다.

당연히 "섹션"을 검색하여 "<section>...</section>"과 같은 것을 반환하거나 "body"를 검색하여 HTML 요소 "<body>"를 반환하는 것을 원하지 않을 것입니다. 이를 방지하기 위해 인덱싱하는 동안 HTML을 제거하고 내부에 내용만 남깁니다.

운 좋게도 당신은 그 문제를 가진 첫 번째 사람이 아닙니다. Elasticsearch는 다음과 같은 유용한 문자 필터와 함께 제공됩니다.

 analysis.Analyzers(a => a .Custom("html_stripper", cc => cc .Filters("eng_stopwords", "trim", "lowercase") .CharFilters("html_strip") .Tokenizer("autocomplete") )

그리고 그것을 적용하려면:

 [Text(Analyzer = "html_stripper", Name = nameof(HtmlText))] public string HtmlText { get; set; }

중요 필드

요구 사항: 제목의 일치 항목이 콘텐츠 내 일치 항목보다 더 중요해야 합니다.

운 좋게도 Elasticsearch는 일치가 한 필드 또는 다른 필드에서 발생하는 경우 결과를 높이는 전략을 제공합니다. 이것은 boost ​​옵션을 사용하여 검색 쿼리 구성 내에서 수행됩니다.

 const int titleBoost = 15; .Query(qx => qx.MultiMatch(m => m .Query(searchRequest.Query.ToLower()) .Fields(ff => ff .Field(f => f.Title, boost: titleBoost) .Field(f => f.Summary) ... ) .Type(TextQueryType.BestFields) ) && filteringQuery)

보시다시피 MultiMatch 쿼리는 이와 같은 상황에서 매우 유용하며 이와 같은 상황은 전혀 드물지 않습니다! 종종 일부 필드는 더 중요하고 일부 필드는 그렇지 않습니다. 이 메커니즘을 통해 이를 고려할 수 있습니다.

부스트 값을 즉시 설정하는 것이 항상 쉬운 것은 아닙니다. 원하는 결과를 얻으려면 이것으로 약간의 플레이가 필요합니다.

기사 우선 순위 지정

요구 사항: 일부 기사는 다른 기사보다 더 중요합니다. 작성자가 더 중요하거나 기사 자체에 더 많은 좋아요/공유/찬성 등이 있습니다. 더 중요한 기사는 더 높은 순위를 차지해야 합니다.

Elasticsearch를 사용하면 스코어링 기능을 구현할 수 있으며 "Importance" 필드를 정의하는 방식으로 단순화합니다. 이 필드는 두 배 값(이 경우에는 1보다 큼)입니다. 고유한 중요도 함수/요소를 정의하고 적용할 수 있습니다. 비슷하게. 가장 적합한 여러 부스트 및 스코어링 모드를 정의할 수 있습니다. 이것은 우리에게 잘 작동했습니다.

 .Query(q => q .FunctionScore(fsc => fsc .BoostMode(FunctionBoostMode.Multiply) .ScoreMode(FunctionScoreMode.Sum) .Functions(f => f .FieldValueFactor(b => b .Field(nameof(SearchItemDocumentBase.Rating)) .Missing(0.7) .Modifier(FieldValueFactorModifier.None) ) ) .Query(qx => qx.MultiMatch(m => m .Query(searchRequest.Query.ToLower()) .Fields(ff => ff ... ) .Type(TextQueryType.BestFields) ) && filteringQuery) ) )

각 영화에는 등급이 있으며 우리는 출연한 영화의 평균 등급으로 배우 등급을 추론했습니다(매우 과학적인 방법은 아님). [0,1] 구간에서 해당 등급을 두 배 값으로 조정했습니다.

전체 단어 일치

요구 사항: 전체 단어 일치의 순위가 더 높아야 합니다.

지금까지는 검색에 대해 상당히 좋은 결과를 얻고 있지만 부분적으로 일치하는 결과가 정확히 일치하는 것보다 순위가 더 높을 수 있습니다. 이를 처리하기 위해 자동 완성 분석기를 사용하지 않고 대신 키워드 토크나이저를 사용하고 정확한 일치 결과를 더 높게 푸시하는 부스트 요소를 제공하는 "키워드"라는 이름의 추가 필드를 문서에 추가했습니다.

이 필드는 정확한 단어가 일치하는 경우에만 일치합니다. 자동 완성 분석기처럼 "나무"에 대해 "나무"와 일치하지 않습니다.

마무리

이 기사는 .NET 프로젝트에서 Elasticsearch를 설정하는 방법에 대한 개요를 제공하고 약간의 노력으로 모든 곳에서 멋진 검색 기능을 제공해야 합니다.

학습 곡선은 다소 가파를 수 있지만 특히 올바르게 조정하고 훌륭한 검색 결과를 얻기 시작할 때 그만한 가치가 있습니다.

변경 사항을 도입하고 놀 때 매개 변수를 너무 많이 엉망으로 만들지 않도록 예상 결과와 함께 철저한 테스트 사례를 추가하는 것을 항상 기억하십시오.

이 기사의 전체 코드는 GitHub에서 사용할 수 있으며 TMDB 데이터베이스에서 가져온 데이터를 사용하여 각 단계에서 검색 결과가 어떻게 개선되고 있는지 보여줍니다.