.NET開発者向けのElasticsearchチュートリアル

公開: 2022-03-11

.NET開発者はプロジェクトでElasticsearchを使用する必要がありますか? ElasticsearchはJavaに基づいて構築されていますが、Elasticsearchがプロジェクトの全文検索に一撃の価値がある理由はたくさんあると思います。

Elasticsearchは、テクノロジーとして、過去数年間で長い道のりを歩んできました。 全文検索を魔法のように感じさせるだけでなく、テキストのオートコンプリート、集計パイプラインなどの他の高度な機能を提供します。

きちんとした.NETエコシステムにJavaベースのサービスを導入することを考えて不快感を覚えても心配しないでください。Elasticsearchをインストールして構成すると、ほとんどの時間を最もクールな.NETパッケージの1つに費やすことになります。そこに:NEST。

この記事では、.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が提供するElasticCloudサービスを使用することをお勧めします。 ホスト型サービスにより、すべての保守と構成がかなり簡単になります。 さらに、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にシリアル化されて保存されるオブジェクトにマッピングするために使用されます。 このチュートリアルでは、Entity Framework(EF)を使用します。

一般に、Elasticsearchを使用する場合、おそらくサイト全体の検索エンジンソリューションを探しています。 ある種のフィードまたはダイジェスト、またはユーザー、ブログエントリ、製品、カテゴリ、イベントなどのさまざまなエンティティからのすべての結果を返すGoogleのような検索を使用します。

これらはおそらくデータベース内の1つのテーブルまたはエンティティであるだけでなく、さまざまなデータを集約し、タイトル、説明、日付、作成者/所有者、写真などの一般的なプロパティを抽出または導出する必要があります。 もう1つのことは、おそらく1つのクエリでは実行しないでしょうが、ORMを使用している場合は、それらのブログエントリ、ユーザー、製品、カテゴリ、イベントなどごとに個別のクエリを作成する必要があります。

ブログ投稿や製品など、「大きな」タイプごとにインデックスを作成してプロジェクトを構成しました。 次に、同じインデックスに分類されるより具体的なタイプに、いくつかのElasticsearchタイプを追加できます。 たとえば、記事がストーリー、ビデオ記事、またはポッドキャストである場合でも、それは「記事」インデックスに含まれますが、そのインデックスにはこれら4つのタイプが含まれます。 ただし、それでもデータベース内の同じクエリである可能性があります。

インデックスごとに少なくとも1つのタイプが必要であることに注意してください。おそらく、インデックスと同じ名前のタイプです。

エンティティをマップするには、いくつかの追加のクラスを作成する必要があります。 私は通常、 DocumentSearchItemBaseクラスを使用します。このクラスから、各特殊クラスはBlogPostSearchItemProductSearchItemなどを継承します。

私はそれらのクラス内にマッパー式を含めるのが好きです。 必要に応じて、いつでも式を変更できます。

Elasticsearchを使用した初期のプロジェクトの1つで、マッピングとインデックス作成を備えたかなり大きなSearchServiceクラスを作成し、適切で長いswitch-caseステートメントを使用しました。Elasticsearchにスローするエンティティタイプごとに、マッピングを備えたスイッチとクエリがありました。それをしました。

しかし、プロセス全体を通して、少なくとも私にとっては、それが最善の方法ではないことを学びました。

より洗練された解決策は、ある種のスマートなIndexDefinitionクラスと、インデックスごとに特定のインデックス定義クラスを用意することです。 このように、私の基本IndexDefinitionクラスは、使用可能なすべてのインデックスのリストと、必要なアナライザーやステータスレポートなどのいくつかのヘルパーメソッドを格納できます。一方、派生インデックス固有のクラスは、データベースのクエリと各インデックスのデータのマッピングを処理します。 これは、後でESにエンティティを追加する必要がある場合に特に便利です。 それは、 IndexDefinitionから継承する別のSomeIndexDefinitionクラスを追加することであり、インデックスに必要なデータをクエリするいくつかのメソッドを実装する必要があります。

Elasticsearch Speak

Elasticsearchでできることすべての中核は、クエリ言語です。 理想的には、Elasticsearchと通信できるようにするために必要なのは、クエリオブジェクトの作成方法を知っていることだけです。

舞台裏では、Elasticsearchはその機能をHTTPを介したJSONベースのAPIとして公開しています。

API自体とクエリオブジェクトの構造はかなり直感的ですが、実際の多くのシナリオを処理するのは依然として面倒な場合があります。

通常、Elasticsearchへの検索リクエストには、次の情報が必要です。

  • どのインデックスとどのタイプが検索されますか

  • ページネーション情報(スキップするアイテムの数、および返すアイテムの数)

  • 具体的な型の選択(これから行うように、集計を行う場合)

  • クエリ自体

  • ハイライト定義(Elasticsearchは必要に応じてヒットを自動的にハイライトできます)

たとえば、一部のユーザーだけがサイトのプレミアムコンテンツを表示できる検索機能を実装したり、一部のコンテンツをその作成者の「友達」だけに表示したりすることができます。

クエリオブジェクトを構築できることは、これらの問題の解決策の中核であり、多くのシナリオをカバーしようとすると、実際に問題になる可能性があります。

上記のすべてから、設定するのに最も重要で最も難しいのは、当然、クエリセグメントです。ここでは、主にそれに焦点を当てます。

クエリは、BoolQueryと、 DateRangeQueryTermsQueryMatchPhraseQueryBoolQueryなどの他のクエリを組み合わせた再帰的な構造ExistsQuery 。 これらは基本的な要件を満たすのに十分であり、最初は良いはずです。

MultiMatchクエリは、検索を実行するフィールドを指定し、結果をもう少し微調整できるため、非常に重要です。これについては後で説明します。

MatchPhraseQueryは、従来のSQLデータベースの外部キーまたは列挙型などの静的な値で結果をフィルタリングできます。たとえば、特定の作成者による結果の照合( AuthorId )、またはすべての公開記事の照合( ContentPrivacy=Public )の場合です。

TermsQueryは、「in」として従来のSQL言語に変換されます。 たとえば、ユーザーの友人の1人が書いたすべての記事を返したり、固定された一連の販売者から独占的に商品を入手したりできます。 SQLと同様に、パフォーマンスに影響を与えるため、これを使いすぎて10,000のメンバーをこの配列に配置しないでください。ただし、通常、妥当な量をかなり適切に処理します。

DateRangeQueryは自己文書化されています。

ExistsQueryは興味深いものです。特定のフィールドを持たないドキュメントを無視または返すことができます。

これらをBoolQueryと組み合わせると、複雑なフィルタリングロジックを定義できます。

たとえば、ブログ投稿に、いつ表示するかを示すAvailableFromフィールドを含めることができるブログサイトについて考えてみます。

AvailableFrom <= Nowのようなフィルターを適用すると、その特定のフィールドをまったく持たないドキュメントは取得されません(データを集約し、一部のドキュメントではそのフィールドが定義されていない可能性があります)。 この問題を解決するには、 ExistsQueryDateRangeQueryと組み合わせて、 BoolQueryの少なくとも1つの要素が満たされるという条件で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を使用してユーザーフィードを提供しました。すべてのコンテンツが作成日順に1つの場所に集約され、一部のオプションを使用して全文検索が行われました。 フィード自体は非常に簡単です。 データのどこかに日付フィールドがあることを確認し、そのフィールドで並べ替えてください。

一方、検索は、箱から出してすぐにはうまく機能しません。 これは、当然、Elasticsearchはデータに重要なものが何であるかを知ることができないためです。 (他のフィールドの中でも) TitleTags (array)、 Bodyフィールドを持つデータがあるとしましょう。 本文フィールドはHTMLコンテンツにすることができます(物事をもう少し現実的にするため)。

スペルミス

要件:スペルミスが発生した場合、または単語の末尾が異なる場合でも、検索結果が返される必要があります。 たとえば、「木のスプーンでできる素晴らしいこと」というタイトルの記事がある場合、「もの」や「木」を検索すると、それでも一致したいと思います。

これに対処するには、アナライザー、トークナイザー、charフィルター、およびトークンフィルターに精通している必要があります。 これらは、インデックス作成時に適用される変換です。

  • アナライザーを定義する必要があります。 これは、インデックスごとに定義できます。

  • アナライザーは、ドキュメントの一部のフィールドに適用できます。 これは、属性または流暢なAPIを使用して実行できます。 この例では、属性を使用しています。

  • アナライザーは、フィルター、charフィルター、およびトークナイザーの組み合わせです。

要件(部分的な単語の一致)を満たすために、次の要素で構成される「オートコンプリート」アナライザーを作成します。

  • 英語のストップワードフィルター:「and」や「the」など、英語の一般的な単語をすべて削除するフィルター。

  • トリムフィルター:各トークンの周りの空白を削除します

  • 小文字フィルター:すべての文字を小文字に変換します。 これは、データをフェッチするときに小文字に変換されることを意味するのではなく、大文字と小文字を区別しない検索を有効にします。

  • Edge-n-gramトークナイザー:このトークナイザーを使用すると、部分的に一致させることができます。 たとえば、「私のおばあちゃんは木製の椅子を持っています」という文がある場合、「木」という用語を探すときに、その文にヒットしたいと思います。 edge-n-gramが行うことは、「woo」、「wood」、「woode」、および「wooden」を格納して、少なくとも3文字と一致する部分的な単語が見つかるようにすることです。 パラメータ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>…</ section>」や「body」のようなものを返し、HTML要素「<body>」を返すことは望ましくありません。 これを回避するために、インデックス作成中にHTMLを削除し、コンテンツのみを内部に残します。

幸いなことに、あなたはその問題を抱えている最初の人ではありません。 Elasticsearchには、そのための便利なcharフィルターが付属しています。

 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を使用すると、スコアリング関数を実装できます。また、フィールド「重要度」を定義する方法で簡略化しています。これは、2倍の値(この場合は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]の間隔で2倍の値にスケーリングしました。

フルワードマッチ

要件:完全な単語の一致は上位にランク付けする必要があります。

これまでのところ、検索でかなり良い結果が得られていますが、部分一致を含む一部の結果は、完全一致よりもランクが高くなる可能性があります。 これに対処するために、オートコンプリートアナライザーを使用せず、代わりにキーワードトークナイザーを使用し、完全一致の結果をより高くプッシュするブースト係数を提供する「キーワード」という名前のフィールドをドキュメントに追加しました。

このフィールドは、完全に一致する単語が一致する場合にのみ一致します。 オートコンプリートアナライザーのように、「木」の「木」とは一致しません。

要約

この記事では、.NETプロジェクトでElasticsearchをセットアップする方法の概要を説明し、少しの努力で、どこでも優れた検索機能を提供する必要があります。

学習曲線は少し急になる可能性がありますが、特にそれを適切に調整して優れた検索結果を取得し始める場合は、それだけの価値があります。

変更を導入して遊んでいるときにパラメータをあまり混乱させないように、期待される結果を含む徹底的なテストケースを常に追加することを忘れないでください。

この記事の完全なコードはGitHubで入手でき、TMDBデータベースから取得したデータを使用して、各ステップで検索結果がどのように向上しているかを示します。