Elasticsearch インデックスを構築し、そこにいくつかのドキュメントをロードしたので、フルテキスト検索を実装する準備が整いました。

検索の仕組み

チュートリアル アプリケーションで検索ソリューションがどのように機能するかを簡単に確認してみましょう。Flask アプリケーションを実行している状態で、 http://localhost:5001にアクセスして次のようなメイン ページにアクセスできます。

このページをレンダリングするコードは、 app.pyファイルに実装されています。

これは、HTML テンプレートをレンダリングする非常にシンプルなエンドポイントです。Flask アプリケーションでは、テンプレートはtemplatesサブディレクトリに配置されているため、このテンプレートとアプリケーションに含まれる他のテンプレートがそこにあります。

ファイルtemplates/index.htmlの検索フィールドの実装を見てみましょう。このテンプレートの関連部分は次のとおりです。

ここでは、これがqueryという名前のtextタイプの単一のフィールドを持つ HTML フォームであることがわかります。フォームのmethod属性はPOSTに設定されており、ブラウザにこのフォームを POST リクエストで送信するように指示します。action属性は、Flask アプリケーションのhandle_searchエンドポイントに対応する URL に設定されます。フォームが送信されると、 handle_search()関数が実行されます。

handle_search()の現在の実装を以下に示します。

この関数は、ユーザーがテキスト フィールドに入力したテキストを Flask のrequest.form辞書から取得し、それをqueryローカル変数に格納します。次に、関数はindex.htmlテンプレートをレンダリングしますが、このタイプでは、ページに検索結果を表示できるようにいくつかの追加の引数を渡します。テンプレートが受け取る 4 つの引数は次のとおりです。

  • query: ユーザーがフォームに入力したクエリ テキスト。
  • results: 検索結果のリスト
  • from_: 最初の結果のゼロベースのインデックス
  • total: 結果の合計数

検索機能が実装されていないため、現時点ではrender_template()関数に渡される引数は結果が見つからなかったことを示します。

ここでのタスクは、フルテキスト クエリを実装し、実際の結果を渡してindex.htmlページに表示できるようにすることです。

Elasticsearch サービスは、JSON 形式に基づくクエリ DSL (ドメイン固有言語) を使用してクエリを定義します。

Python のElasticsearchクライアントには、検索クエリを送信するために使用されるsearch()メソッドがあります。このメソッドを使用するsearch()ヘルパー メソッドをsearch.py に追加しましょう。

このメソッドは、インデックス名を使用して Elasticsearch クライアントのsearch()メソッドを呼び出します。query_args引数は、メソッドに提供されたすべてのキーワード引数をキャプチャし、それらをes.search()メソッドに渡します。これらの引数は、呼び出し元が検索対象を指定する方法になります。

一致クエリ

Elasticsearchクエリ DSL は、インデックスをクエリするためのさまざまな方法を提供します。ドキュメントのサブセクションを確認すると、実行可能なさまざまな種類のクエリについて理解できるようになります。テキストを検索するという非常に一般的なタスクについては、フルテキストクエリのセクションで説明します。

最初の検索実装では、 Match クエリを使用しましょう。以下にこのクエリを使用する例を示します。

上記の例は、生の HTTP リクエストに似た形式で示されています。この形式は Elasticsearch のドキュメントや Elasticsearch API コンソールで広く使用されているため、理解しておくと便利です。幸いなことに、この形式は、Python クライアント ライブラリを使用して呼び出しに変換するのが非常に簡単です。以下に、上記の例と同等の Python コードを示します。

API コンソールの例を Python に変換するときは、クエリ本体の最上位キーを Python 呼び出しのキーワード引数に変換する必要があることに注意してください。また、例では、Python 呼び出しを行うときに必要となるインデックスも指定されていません。

クエリ構造を見ると、どのような種類の検索が要求されているのかを推測できるでしょう。この呼び出しでは、 nameというフィールドに対してmatchクエリが要求され、検索するテキストはsearch text hereです。

このスタイルのクエリは、チュートリアル アプリケーションに組み込むのが比較的簡単です。app.pyを開き、 handle_search()メソッドを見つけます。現在のバージョンをこの新しいバージョンに置き換えます。

この新しいバージョンのエンドポイントの2行目のes.search()の呼び出しは、上記のsearch.pyに追加されたsearch()メソッドを呼び出します。これにより、Elasticsearch クライアントのsearch()メソッドが呼び出されます。

クエリが何を実行するのかわかりますか?これは上記の例に似たmatchクエリです。検索されるフィールドはnameで、前のセクションで作成したmy_documentsインデックス内のドキュメントのタイトルが含まれています。検索するテキストは、ユーザーが Web ページの検索フィールドに入力したものであり、 queryローカル変数に保存されます。

結果を含む検索応答の部分はresponse['hits']です。これはいくつかのキーを持つオブジェクトであり、この実装ではそのうちの 2 つが重要です。

  • response['hits']['hits']: 検索結果のリスト。
  • response['hits']['total']: 利用可能な結果の合計数。結果の数はvalueサブキーで指定されるため、実際には結果の合計数を取得する式はresults['hits']['total']['value']になります。結果の数が多い場合、結果の合計数は概算になることに注意してください。詳細については、レスポンス本文のドキュメントを参照してください。

この新しいバージョンのエンドポイントでのrender_template()の呼び出しでは、結果のリストがresultsテンプレート引数に渡され、結果の合計数がtotalに渡されます。query引数は以前と同様にクエリ文字列を受け取り、 from_ページ区切りが追加されたときに後で実装されるため、0 にハードコードされたままです。

これにより、アプリケーションに全文検索が初めて実装されました。Web ブラウザに戻り、 http://localhost:5001に移動してアプリケーションを開きます。何らかの理由で Flask アプリケーションが実行されていない場合は、これを行う前にアプリケーションを再起動してください。policywork from homeなどの検索テキストを入力すると、関連する結果が表示されます。以下は、 work from homeを検索したときの結果です。

スターター アプリケーションと共にダウンロードしたindex.htmlテンプレートには、検索結果をレンダリングするためのすべてのロジックが含まれています。これについて興味がある方は、このテンプレートの結果リストをレンダリングするセクションを次に示します。

このコードから興味深いのは、返された結果に関連付けられたデータが_sourceキーの下で利用できることです。結果に割り当てられた一意の識別子を含む_idフィールドもあります。

各結果に関連付けられたスコアは_scoreから取得できます。スコアは関連性の尺度を提供し、スコアが高いほどクエリテキストとの一致度が高いことを示します。デフォルトでは、結果は最高スコアから最低スコアの順に返されます。Elasticsearch のスコアは、 Okapi BM25アルゴリズムを使用して計算されます。

このセクションで説明されているトピックをさらに詳しく調べたい場合は、次のリンクを使用してください。

個々の結果の取得

index.htmlテンプレートが各検索結果のタイトルをリンクとしてレンダリングしていることに気付いたかもしれません。リンクは、スターター Flask アプリケーションに実装された 3 番目で最後のエンドポイントget_documentを指します。提供されている実装では、「ドキュメントが見つかりません」というハードコードされたテキストが返されるため、アプリケーションを操作中に結果のいずれかをクリックすると、このテキストが表示されます。

個々のドキュメントを正しくレンダリングするには、 search.pyretrieve_document()ヘルパーメソッドを追加しましょう。Elasticsearch クライアントのget()メソッドを使用します。

ここでは、各ドキュメントに割り当てられた一意の識別子がどのように役立つかがわかります。これは、アプリケーションが個々のドキュメントを参照するために使用できる識別子です。

get_document()エンドポイントの現在の実装は次のとおりです。

このエンドポイントに関連付けられた URL にはドキュメントidが含まれており、各検索結果に対してレンダリングされるリンクにもそれぞれの URL に id が組み込まれていることがわかります。そのため、この単純な実装を、ドキュメントを取得してレンダリングする実装に置き換えるだけで済みます。エンドポイントを次の更新バージョンに置き換えます。

ここでは、 search.pyretrieve_document()メソッドを使用して、要求されたドキュメントを取得します。次に、 nameフィールドからのタイトルと、 contentからの段落のリストを使用して、 document.htmlがレンダリングされます。

さらにいくつかのクエリを実行し、結果をクリックすると、完全なコンテンツが表示されるはずです。

複数のフィールドの検索

アプリケーションをしばらく操作してみると、多くのクエリが結果を返さないことに気付いたかもしれません。ご存知のとおり、検索は現在、各ドキュメントのnameフィールド (ドキュメントのタイトルが保存されている場所) に実装されています。ドキュメントにはsummarycontentフィールドもあり、これらにも検索される可能性のある長いテキストが含まれていますが、現時点ではこれらは無視されます。

このセクションでは、インデックスの複数のフィールドにわたって検索を実行するように要求する、もう 1 つの一般的な全文検索クエリであるMulti-matchについて学習します。

以下はドキュメントの複数一致クエリの例です。

この例をベースにして、 handle_search()エンドポイントを拡張し、 namesummarycontentフィールドを組み合わせて複数一致クエリを実行してみましょう。更新されたエンドポイント コードは次のとおりです。

この変更により、検索するテキストが大幅に増え、一部のクエリではデフォルトで返される最大数である 10 件を超える結果が返される可能性があります。次の章では、ページ区切りを使用して長い結果リストを処理する方法について学習します。

最先端の検索体験を構築する準備はできましたか?

十分に高度な検索は 1 人の努力だけでは実現できません。Elasticsearch は、データ サイエンティスト、ML オペレーター、エンジニアなど、あなたと同じように検索に情熱を傾ける多くの人々によって支えられています。ぜひつながり、協力して、希望する結果が得られる魔法の検索エクスペリエンスを構築しましょう。

はじめましょう