検索候補の絞り込みと検索クエリの修正を実装する方法

blog-search-results-dark-720x420.png

関連性が高い検索結果で、単発の購入者をeコマースWebサイトのリピート客にします。Harris Pollによると、「検索がうまく動作せず、そのショッピングWebサイトを利用しなかったことがある」と回答したオンライン購入者の割合は76%になります。

このため、検索エクスペリエンスを最適化し、購入者が必要な商品をすばやく見つけられるようにすることが非常に重要です。これは最新の検索エクスペリエンスの背景にある理論です。現在では、一致する商品を表示する検索バーがあるだけでは不十分です。eコマースサイトには、目的の商品までユーザーを案内するパーソナライズされた包括的な検索ツール群が実装されている必要があります。

このブログでは、検索候補の絞り込みとクエリの修正(「Did you mean?」)という2つの一般的な機能を追加して、eコマース検索の改善を始める方法について説明します。

次の例はElastic 8.5を使用してテストされました。 

候補の絞り込み 

eコマース小売業者で共通する1つの課題は、多数の異なるカテゴリから構成される大規模な製品カタログです。このシナリオでは、シンプルなクエリを解析して、ユーザーが関心を持っているかもしれない分野やカテゴリを把握することは困難です。たとえば、ユーザーが電気小売店で「フラット画面」を検索した場合、テレビ、テレビスタンド、コンピューターモニターなどの下に結果が表示されるかもしれません。ユーザーがテレビを探しているのにフラット画面のテレビスタンドを表示するのは、この段階では購入者にとって関連性がない可能性があります。

検索候補の絞り込みでは、特定のカテゴリやブランドに限定したクエリを候補として示すことで、ユーザーが検索を最も関心のあるトピックに絞り込むことができます。

これは、絞り込まれた結果セットがすばやくユーザーに表示されるため、強力な機能です。Search UIとElasticを使用した検索候補の絞り込みを実装する方法もシンプルです。次のセクションでは、その実装方法について説明します。 

候補のデータを収集する

上記の例で説明したように、ユーザーが入力すると、クエリの候補と関連付けられた範囲が表示されます。このような候補を生成するには、分析データを使用して、顧客によって共通して関連付けられている最も一般的なクエリと範囲を収集する方法が一般的です。

たとえば、当社のeコマースWebサイトの分析データをレビューしているときに、上位のクエリが「lcd tv」であることがわかり、ユーザーが最も頻繁にクリックする商品が「テレビ・ホームシアター」カテゴリで見つかったとします。

このデータを使用して、次のような対応する範囲が絞り込まれた候補を表すドキュメントを作成できます。 

{
   "name": "lcd tv",
   "weight": 112,
   "category": {
       "name": "TV & Home Theater",
       "value": 111
   }
}

このドキュメントにはクエリと関連付けられたカテゴリが含まれます。必要に応じて、より複雑なドキュメントを作成できます。たとえば、カテゴリのリストを作成したり、「ブランド」などの別の範囲を追加したりできます。 

それから、Elasticsearchで専用のインデックスを作成します。この場合は、「suggest」という名前を付けて、分析データを使用して作成する範囲が絞り込まれた候補のリストを格納します。このためには、データ変換を使用するか、Pythonスクリプトなどのカスタムコードを使用できます。このインデックス「suggest」のインデックスマッピング例 はこちらをご覧ください。 

候補のオートコンプリート

使用される候補のリストを準備したので、それを検索エクスペリエンスに追加する必要があります。

Search UIを使用すると、わずか2、3分でElasticで検索エクスペリエンスを構築できます。この新しく構築されたインターフェースは、検索候補の絞り込みの基盤として使用できます。

Search UIは構成オブジェクトを使用して、ニーズに合わせて検索をカスタマイズします。次に、候補構成に対応する構成オブジェクトのスニペットを示します。この場合、インデックス、クエリの対象のフィールド(結果の一部として返されるフィールド)を指定します。

suggestions: {
     types: {
       popularQueries: {
         search_fields: {
           "name.suggest": {} // fields used to query
         },
         result_fields: {
           name: {
             raw: {}
           },
           "category.name": {
             raw: {}
           }
         },
         index: "suggest",
         queryType: "results"
       }
     },
     size: 5
   }

その後、検索結果に移動するときにクエリの一部としてカテゴリを渡すSearchBoxコンポーネントを構成します。

// Search bar component
<SearchBox
   onSelectAutocomplete={(suggestion, config, defaultHandler) => {
       // User selects a scoped suggestion - Category
       if (suggestion.name && suggestion.category) {
           const params = { q: suggestion.name.raw, category: suggestion.category.raw.name };
           // Navigate to search result page with category passed as parameters
           navigate({
               pathname: '/search',
               search: `?${createSearchParams(params)}`,
           });
       // User selects normal suggestion
       } else if (suggestion) {
           // Navigate to search result page
           window.location.href = "/search?q=" + suggestion.suggestion;
       }
       defaultHandler(suggestion);
   }}
   autocompleteSuggestions={{
       popularQueries: {
           sectionTitle: "Popular queries",
           queryType: "results",
           displayField: "name",
           categoryField: "category"
       }
   }}
   autocompleteView={AutocompleteView}
/>

ここでは、AutocompleteView関数を渡して、オートコンプリートビューをカスタマイズし、候補のクエリと一緒にカテゴリが表示されるようにします。 

次に、クエリと関連付けられた範囲を表示する方法を示す、AutocompleteView関数のコードスニペットを示します。 

{suggestions.slice(0,1).map((suggestion) => {
   index++;
   const suggestionValue = getDisplayField(suggestion)
   const suggestionScope = getCategoryField(suggestion)
   return (
     <li
       {...getItemProps({
         key: suggestionValue,
         index: index - 1,
         item: {
           suggestion: suggestionValue,
           ...suggestion.result
         }
       })}
     >
       <span>{suggestionValue}</span>
       <ul><span style={{marginLeft: "20px"}}>in {suggestionScope}</span></ul>
     </li>
   );
})}

そして、検索結果ページで、クエリパラメーターを処理し、結果セットをフィルタリングするだけです。 

 const [searchParams] = useSearchParams();
   useEffect(() => {
       if (searchParams.get('category')) addFilter("department", [searchParams.get('category')], "all")
   }, [searchParams]);

すべてを合わせると、次のような結果になります。

Video thumbnail

クエリ修正(「Did you mean?」)

購入者は、クエリ用語の選択肢が少なかったり、入力ミスがあったりすることが原因で関連する検索結果が表示されないと、ストレスを感じます。検索結果が返されなかったり、結果件数が非常に少なくなったりすることを避けるには、一般的に「Did you mean?」と呼ばれる改善されたクエリを提案するのが最適です。

この機能はさまざまな方法で実装できますが、Search UIとElasticを使用する方法が最も簡単です。 

データの分析

Did you mean?」機能を作成するために使用するデータセットは、検索候補の絞り込みで使用されるデータセットと似ているため、クエリの結果が返されなかった場合には、頻繁に使用されるクエリをユーザーに提案することができます。  

「Did you mean?」では類似したドキュメント構造を使用する

{
   "name": "lcd tv",
   "weight": 112,
   "category": {
       "name": "TV & Home Theater",
       "value": 111
   }
}

このステップの重要な点は、Elasticsearchでどのようにデータにインデックスが作成されるかということです。関連性の高い候補を表示するには、カスタムアナライザーなどの特定のElasticsearch機能を使用する必要があります。

次に、この例で使用するインデックス設定とマッピングを示します。 

{
    "settings":
    {
        "index":
        {
            "number_of_shards": 1,
            "analysis":
            {
                "analyzer":
                {
                    "trigram":
                    {
                        "type": "custom",
                        "tokenizer": "standard",
                        "filter":
                        [
                            "lowercase",
                            "shingle"
                        ]
                    },
                    "reverse":
                    {
                        "type": "custom",
                        "tokenizer": "standard",
                        "filter":
                        [
                            "lowercase",
                            "reverse"
                        ]
                    }
                },
                "filter":
                {
                    "shingle":
                    {
                        "type": "shingle",
                        "min_shingle_size": 2,
                        "max_shingle_size": 3
                    }
                }
            }
        }
    },
    "mappings":
    {
        "properties":
        {
            "category":
            {
                "properties":
                {
                    "name":
                    {
                        "type": "text",
                        "fields":
                        {
                            "keyword":
                            {
                                "type": "keyword",
                                "ignore_above": 256
                            }
                        }
                    },
                    "value":
                    {
                        "type": "long"
                    }
                }
            },
            "name":
            {
                "type": "search_as_you_type",
                "doc_values": "false",
                "max_shingle_size": 3,
                "fields":
                {
                    "reverse":
                    {
                        "type": "text",
                        "analyzer": "reverse"
                    },
                    "suggest":
                    {
                        "type": "text",
                        "analyzer": "trigram"
                    }
                }
            },
            "weight":
            {
                "type": "rank_feature",
                "fields":
                {
                    "numeric":
                    {
                        "type": "integer"
                    }
                }
            }
        }
    }
}

関連性が高い候補を取得するために特定の設定とマッピングを使用する理由に関する詳細については、候補ドキュメントを参照してください。 

検索の準備

インデックスが準備された後は、検索エクスペリエンスを構築できます。クエリに基づいて候補を取得するには、Elasticsearch APIを活用します。クライアント側であまり複雑にしたくない場合は、Elasticsearchでクエリが含まれる検索テンプレートを作成します。そうすると、クライアント側では、その検索テンプレートを実行するだけです。

PUT _scripts/did-you-mean-template
{
   "script": {
       "lang": "mustache",
       "source": {
           "suggest": {
               "text": "{{query_string}}",
               "simple_phrase": {
                   "phrase": {
                       "field": "name.suggest",
                       "size": 1,
                       "direct_generator": [
                           {
                               "field": "name.suggest",
                               "suggest_mode": "always"
                           },
                           {
                               "field": "name.reverse",
                               "suggest_mode": "always",
                               "pre_filter": "reverse",
                               "post_filter": "reverse"
                           }
                       ]
                   }
               }
           }
       }
   }
}

アプリケーションからの候補の取得

Elasticは、Elasticsearchから候補を取得するために、/api/suggestでクライアントが利用できるバックエンドAPIを追加しています。この新しいAPIは、Elasticsearchを呼び出し、検索テンプレートを実行し、候補を表示したいクエリを渡して、クエリの候補を返します。バックエンドAPIを使用すると、フロントエンド側の複雑さを低減できます。

client.searchTemplate({
       index: "suggest",
       id: 'did-you-mean-template',
       params: {
           query_string: req.body.query
       }
   })

たとえば、クエリが「spakers」の場合、候補の「speakers」がAPIによって返されます。 これは、以前にインジェストされた、最も頻繁に使用されるクエリを含むデータに基づきます。suggest APIは、構文的にクエリに最も近い用語を返します。 

「Did you mean?」を結果ページに追加する

ここで、「Did you mean?」機能をフロントエンドアプリケーションに追加できます。そのため、検索候補の絞り込みの部分で使用したのと同じReactアプリケーションで構築を続けます。 

追加した新しいAPIはReactアプリケーションから利用でき、現在のクエリに対する結果がない場合に候補を表示します。 

ここでの考え方は、ユーザーが入力した各クエリに対して候補を取得するということです。結果がない場合は、ユーザーに候補が表示されます。次のような別の実装方法も可能です。ほとんど結果が表示されない場合には候補を表示できます。あるいは、ユーザーのクエリの代わりに自動的に候補を実行できます。

このアプリケーションでは、SearchResultsという名前のReactコンポーネントがあり、検索結果を表示します。そして、バックエンドAPI /jp/api/suggestから候補を取得する関数を追加できます。

const fetchDidYouMeanSuggestion = async (query) => {
   const response = await fetch('/api/did_you_mean', {
       method: 'POST',
       headers: {
           Accept: 'application/json, text/plain, */*',
           'Content-Type': 'application/json',
       },
       body: JSON.stringify(query)
   })
   const body = await response.json();
   if (response.status !== 200) throw Error(body.message);
   return body;
}

その後、クエリが変更されたときには、ユーザーの絞り込みに応じて、APIを呼び出して候補を更新できます。

   // Get search params from dom router
   const [searchParams] = useSearchParams();
 
   useEffect(() => {
       // When the searchParams contains a query
       if (searchParams.get('q')) {
           // Set query for Search UI - Run the search
           setSearchTerm(searchParams.get('q'))
           // Fetch suggestion from backend API
           fetchDidYouMeanSuggestion({ query: searchParams.get('q') }).then(res => {
               setSuggestion(res.body?.suggest?.simple_phrase[0]?.options[0]?.text)
           })
               .catch(err => console.log(err));
       }
  }, [searchParams]);

そして、ユーザークエリの結果がない場合は、候補が表示されます。

{wasSearched && totalResults == 0 && <span>No results to show{suggestion ? <>, did you mean <span style={{ cursor: "pointer", color: "blue" }} onClick={() => navigateSuggest(suggestion)}>{suggestion}</span>?</> : "."}</span>}

最終的な結果は次のようになります。

Video thumbnail

まとめ

このブログ投稿では、Elasticエンタープライズ サーチを使用して「Did you mean?」と検索候補の絞り込みを簡単に実装する方法について説明しました。これらの機能は、既存の検索エクスペリエンスと肝がんに統合できるため、ユーザーは探している項目をよりすばやく検索できます。これらの例を作成するために使用されているコードをレビューしたい場合は、このGitHub repoをご覧ください。

最新の検索エクスペリエンスを提供することは、eコマースWebサイトや、カスタマーサポートWebサイト検索内部Workplace Searchカスタム検索アプリケーションなどの他の多くのユースケースにとって非常に重要です。このブログ投稿のステップに従うと、すべてのエンドユーザーにとって優れた検索エクスペリエンスを構築する時間を短縮できます。