k近傍法(kNN)探索
k 最近傍 (kNN) アルゴリズムは、 dense_vectorタイプのフィールドに対して類似性検索を実行します。このタイプの検索は、より適切には「近似 kNN」と呼ばれ、ベクトルまたは埋め込みを検索用語として受け入れ、インデックス内の近いエントリを検索します。
このセクションでは、前のセクションで作成したドキュメント埋め込みを使用して kNN 検索を実行する方法を学習します。
knnクエリ
チュートリアルの全文検索セクションでは、Elasticsearch クライアントのsearch()メソッドに渡されるクエリオプションについて学習しました。ベクトルを検索する場合は、代わりにknnオプションが使用されます。
以下に、ユーザーが検索フォームに入力したクエリに対して kNN 検索を実行する、 app.pyのhandle_search()関数の新しいバージョンを示します。
@app.post('/')
def handle_search():
query = request.form.get('query', '')
filters, parsed_query = extract_filters(query)
from_ = request.form.get('from_', type=int, default=0)
results = es.search(
knn={
'field': 'embedding',
'query_vector': es.get_embedding(parsed_query),
'num_candidates': 50,
'k': 10,
},
size=5,
from_=from_
)
return render_template('index.html', results=results['hits']['hits'],
query=query, from_=from_,
total=results['hits']['total']['value'])このバージョンの関数では、 queryオプションがknnに置き換えられました。ページ区切りのsizeおよびfrom_オプションは同じままで、関数およびindex.htmlテンプレート内のその他のすべても以前と同じです。
knn検索オプションは、検索を構成するいくつかのパラメータを受け入れます。
field: 検索するインデックス内のフィールド。フィールドはdense_vectorタイプである必要があります。query_vector: 検索する埋め込み。これは検索テキストから生成された埋め込みである必要があります。num_candidates: 各シャードから検討する候補ドキュメントの数。Elasticsearch は各シャードからこの数の候補を取得し、それらを 1 つのリストに結合して、最も近い「k」を見つけて結果として返します。k: 返される結果の数。この数値はパフォーマンスに直接影響するため、できるだけ小さくする必要があります。このオプションで渡される値はnum_candidates未満である必要があります。
上記のコードで使用されている設定では、最も一致する 10 件の結果が返されます。
この新しいバージョンのアプリケーションをぜひお試しください。このタイプの検索がいかに便利かを理解するための良い例を 2 つ示します。
アメリカ英語の「vacation」に相当するイギリス英語の「holiday」を検索すると、holiday という単語自体は文書内に表示されていないにもかかわらず、kNN 検索では「Vacation Policy」という文書が最上位の結果として返されます。
「猫と犬」またはペットに関連するその他の用語を検索すると、ドキュメントの概要に特定のペットについて言及されていないにもかかわらず、「Office Pet Policy」ドキュメントが最上位の結果として表示されます。
kNNクエリでのフィルターの使用
このチュートリアルの全文セクションで定義されている検索クエリでは、ユーザーは検索テキストの任意の場所で構文category:<category-name>を使用して、特定のカテゴリの使用を要求できます。app.pyのextract_filters()関数は、これらのフィルター式を検索クエリから見つけて分離する役割を担っています。前のセクションのhandle_search()関数のバージョンでは、 filters変数は使用されない為、カテゴリ フィルターは無視されます。
幸いなことに、 knnオプションはフィルタリングもサポートしています。フィルターオプションは実際には同じタイプのフィルターを受け入れるため、フィルターはextract_filters()関数によって返されるものとまったく同じようにknnクエリに直接挿入できます。
@app.post('/')
def handle_search():
query = request.form.get('query', '')
filters, parsed_query = extract_filters(query)
from_ = request.form.get('from_', type=int, default=0)
results = es.search(
knn={
'field': 'embedding',
'query_vector': es.get_embedding(parsed_query),
'k': 10,
'num_candidates': 50,
**filters,
},
size=5,
from_=from_
)
return render_template('index.html', results=results['hits']['hits'],
query=query, from_=from_,
total=results['hits']['total']['value'])集計は kNN クエリでも適切に機能するため、集計を再度追加することもできます。
@app.post('/')
def handle_search():
query = request.form.get('query', '')
filters, parsed_query = extract_filters(query)
from_ = request.form.get('from_', type=int, default=0)
results = es.search(
knn={
'field': 'embedding',
'query_vector': es.get_embedding(parsed_query),
'k': 10,
'num_candidates': 50,
**filters,
},
aggs={
'category-agg': {
'terms': {
'field': 'category.keyword',
}
},
'year-agg': {
'date_histogram': {
'field': 'updated_at',
'calendar_interval': 'year',
'format': 'yyyy',
},
},
},
size=5,
from_=from_
)
aggs = {
'Category': {
bucket['key']: bucket['doc_count']
for bucket in results['aggregations']['category-agg']['buckets']
},
'Year': {
bucket['key_as_string']: bucket['doc_count']
for bucket in results['aggregations']['year-agg']['buckets']
if bucket['doc_count'] > 0
},
}
return render_template('index.html', results=results['hits']['hits'],
query=query, from_=from_,
total=results['hits']['total']['value'], aggs=aggs)このバージョンのhandle_search()関数は、キーワードベースの検索ではなくベクトル検索を使用して実装された、フルテキスト検索バージョンと同じ機能を備えています。
次のセクションでは、これら 2 つの異なる検索方法の結果を結合する方法を学習します。