各種フィルター

多くのアプリケーションでは、検索クエリのみで実行できる機能を補完する方法で、クエリをカスタマイズする権限をユーザーに提供する必要があります。この章では、フィルタリングについて学習します。フィルタリングとは、特定の条件を満たすインデックスに含まれるドキュメントのサブセットに対してのみ検索クエリが実行されるように指定できる手法です。

ブールクエリの紹介

フィルターを実装する前に、Elasticsearch で複合クエリがどのように実装されるかを理解する必要があります。

複合クエリを使用すると、アプリケーションは 2 つ以上の個別のクエリを組み合わせて一緒に実行し、適切な場合には結合された結果セットを返すことができます。Elasticsearch で複合クエリを作成する標準的な方法は、ブールクエリを使用することです。

ブールクエリは、2 つ以上の個別のクエリまたは句のラッパーとして機能します。クエリを組み合わせる方法は 4 つあります。

  • bool.must: 句が一致する必要があります。複数の句が指定されている場合は、すべてが一致する必要があります (AND 論理演算と同様)。

  • bool.should: mustなしで使用する場合、少なくとも 1 つの句が一致する必要があります (OR 論理演算と同様)。mustと組み合わせると、一致する各句によってドキュメントの関連性スコアが向上します。

  • bool.filter: 句に一致するドキュメントのみが検索結果の候補として考慮されます。

  • bool.must_not: 句に一致しない文書のみが検索結果の候補として考慮されます。

上記から推測できるように、ブールクエリはかなりの複雑さを伴い、さまざまな方法で使用できます。この章では、前の章で実装した複数一致の全文検索句と、結果を 1 つのカテゴリのドキュメントに制限するフィルターを組み合わせる方法を学習します。このチュートリアルで使用されるデータセットには、 sharepointteams 、またはgithubに設定できるcategoryフィールドが含まれていることに注意してください。

クエリにフィルターを追加する

チュートリアル アプリケーションに現在実装されている複数一致クエリは、次の構造を使用します。

{
    'multi_match': {
        'query': "query text here",
        'fields': ['name', 'summary', 'content'],
    }
}

この検索を特定のカテゴリに制限するフィルターを追加するには、クエリを次のように拡張する必要があります。

{
    'bool': {
        'must': [{
            'multi_match': {
                'query': "query text here",
                'fields': ['name', 'summary', 'content'],
            }
        }],
        'filter': [{
            'term': {
                'category.keyword': {
                    'value': "category to filter"
                }
            }
        }]
    }
}

このクエリの新しいコンポーネントを詳しく見てみましょう。

まず、 multi_matchクエリがbool.must句内に移動されました。bool.must句は通常、基本クエリが定義される場所です。must検索するクエリのリストを受け入れるため、必要に応じて複数の基本レベルクエリを組み合わせることができることに注意してください。

フィルタリングは、新しいクエリ タイプであるtermクエリを使用してbool.filterセクションで実装されます。matchまたはmulti_matchクエリはフルテキスト検索クエリであるため、フィルターに使用することはお勧めできません。フィルタリングの目的上、クエリは一致クエリのような関連性スコアではなく、ドキュメントごとに絶対的な真偽の回答を返す必要があります。

用語クエリは、指定されたフィールド内の値の正確な検索を実行します。このタイプのクエリは、識別子、ラベル、タグ、またはこの場合のようにカテゴリを検索する場合に便利です。

このクエリは、フルテキスト検索用にインデックスが付けられたフィールドでは適切に機能しません。文字列フィールドにはデフォルトのテキストタイプが割り当てられ、インデックスが作成される前にその内容が分析され、個々の単語に分割されます。Elasticsearch は文字列フィールドに 2 番目のタイプのキーワードを割り当てます。これにより、フィールドの内容全体がインデックス化され、 termクエリによるフィルタリングに適したものになります。クエリのフィルター部分でフィールド名category.keywordを使用すると、デフォルトのtext型ではなく、フィールドのkeyword型のバリアントが使用されます。

フィルターの指定

フィルターされたクエリを実装する前に、エンド ユーザーが希望するフィルターを入力できる方法を追加する必要があります。このチュートリアルで実装されているソリューションは、検索クエリのテキスト内でcategory:<category-name>パターンを検索します。フィルター式を探すために、 app.pyextract_filters()という関数を追加してみましょう。

def extract_filters(query):
    filters = []

    filter_regex = r'category:([^\s]+)\s*'
    m = re.search(filter_regex, query)
    if m:
        filters.append({
            'term': {
                'category.keyword': {
                    'value': m.group(1)
                }
            }
        })
        query = re.sub(filter_regex, '', query).strip()

    return {'filter': filters}, query

この関数は、ユーザーが入力したクエリを受け入れ、クエリ内で見つかったフィルターと、フィルターが削除された後の変更されたクエリを含むタプルを返します。フィルターパターンを検索するには、正規表現を使用します。この機能は追加のフィルターで拡張できるように設計されています。

フィルターが見つかった場合、 filtersリストは対応するフィルター式で拡張されます。この場合は、上で説明したように、 termクエリに基づいています。

この関数の動作をよりよく理解するには、Python セッションを開始し (最初に仮想環境がアクティブになっていることを確認してください)、次のコードを実行します。

from app import extract_filters
extract_filters('this is the search text category:sharepoint')

関数から返されるタプルは次のようになります。

{'filter': [{'term': 'category.keyword': {'value': 'sharepoint'}}]}, 'this is the search text'

フィルター検索の実装

残っているのは、 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(
        query={
            'bool': {
                'must': {
                    'multi_match': {
                        'query': parsed_query,
                        'fields': ['name', 'summary', 'content'],
                    }
                },
                **filters
            }
        },
        size=5,
        from_=from_
    )
    return render_template('index.html', results=results['hits']['hits'],
                           query=query, from_=from_,
                           total=results['hits']['total']['value'])

クエリはbool式を送信するように変更され、検索式はその下のmustセクション内に移動されました。extract_filters()関数は、クエリのフィルター部分を Elasticsearch に送信する必要がある形式で返すため、クエリ辞書の最上位レベルのboolキーの下にも挿入されます。

work from home category:sharepointなどの検索クエリを試して、指定されたカテゴリのドキュメントのみが返されるかどうかを確認してください。

範囲フィルター

Elasticsearch は、 termフィルター以外にもさまざまなフィルターをサポートしています。よく使用されるもう 1 つのフィルターは、数値と日付で機能するrangeフィルターです。updated_atフィールドに指定されている最終更新年に基づいて結果を制限できるyearフィルターを追加しましょう。

以下は、フィルターとしてcategory:<category>year:<yyyy>の両方を検索するextract_filters()関数の更新バージョンです。

def extract_filters(query):
    filters = []

    filter_regex = r'category:([^\s]+)\s*'
    m = re.search(filter_regex, query)
    if m:
        filters.append({
            'term': {
                'category.keyword': {
                    'value': m.group(1)
                }
            },
        })
        query = re.sub(filter_regex, '', query).strip()

    filter_regex = r'year:([^\s]+)\s*'
    m = re.search(filter_regex, query)
    if m:
        filters.append({
            'range': {
                'updated_at': {
                    'gte': f'{m.group(1)}||/y',
                    'lte': f'{m.group(1)}||/y',
                }
            },
        })
        query = re.sub(filter_regex, '', query).strip()

    return {'filter': filters}, query

このバージョンでは、クエリ文字列でyear:yyyy検索するための 2 番目の正規表現が追加されました。これは、 updated_atフィールドにrangeフィルターを作成し、範囲の下限と上限をコロンの後に指定された年に設定します。これは、正規表現の一致でm.group(1)としてキャプチャされます。

updated_atフィールドには完全な日付が含まれており、このフィルターでは年のみを確認すればよいため、少し複雑になります。幸いなことに、範囲フィルターを日付フィールドで使用すると、日付計算を使用して範囲の境界を拡張できます。範囲のgte (下限) およびlte (上限) パラメータに追加される||/yサフィックスは、指定された値が、フィールドと比較できる完全な日付を形成するために完了する必要がある年であることを示します。

この変更により、 year:2020 work from homeなどのクエリを含めて、要求された年のみの結果を表示できるようになります。クエリには 2 つのフィルターを含めることもできます (例: year:2020 category:teams work from home )。

すべて一致クエリ

新しいトピックに進む前に、検索クエリ テキスト フィールドにフィルターのみ (例: category:githubを入力してみてください。残念ながら、これによって結果は返されませんが、この場合に予想される動作は、要求されたカテゴリに一致するすべての結果を受け取ることです。

何が起こるかというと、 extract_filters()関数は、最初の要素にフィルターが含まれたタプルと、2 番目の要素に空のクエリ文字列が含まれたタプルを返します。multi_matchクエリは空の文字列を受け取り、空の文字列に一致するものがないため、空の結果リストを返します。

この特殊なケースに対処するには、検索テキストが空の場合にmulti_matchクエリをmatch_allに置き換えることができます。以下のhandle_search()関数のバージョンでは、これを実行するためのロジックが追加されています。app.py内の関数を更新します。

@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)

    if parsed_query:
        search_query = {
            'must': {
                'multi_match': {
                    'query': parsed_query,
                    'fields': ['name', 'summary', 'content'],
                }
            }
        }
    else:
        search_query = {
            'must': {
                'match_all': {}
            }
        }

    results = es.search(
        query={
            'bool': {
                **search_query,
                **filters
            }
        },
        size=5,
        from_=from_
    )
    return render_template('index.html', results=results['hits']['hits'],
                           query=query, from_=from_,
                           total=results['hits']['total']['value'])

このバージョンでは、カテゴリに一致するすべてのドキュメントを要求できます。スコアを計算するための検索用語がないため、返されるすべての結果が同じスコア 1.0 で返されることに注意してください。

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

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

はじめましょう