범위가 지정된 검색 제안 및 검색 쿼리 수정을 작성하는 방법

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

전자상거래 웹사이트에서 적절한 검색 결과로 쇼핑객들을 유지시키는 데는 결국 한 번의 기회밖에 없습니다. Harris Poll에 따르면, 온라인 쇼핑객의 76%가 성공적이지 못한 검색 후 소매 웹사이트를 다시 찾지 않습니다.

따라서, 구매자가 원하는 것을 빠르게 찾을 수 있도록 검색 경험을 최적화하는 것이 중요합니다. 이것이 최신 검색 경험의 이론입니다. 즉, 요즘에는 단순히 일치하는 제품을 반환하는 검색창을 제공하는 것만으로는 충분하지 않습니다. 전자상거래 사이트에는 사용자가 원하는 제품으로 직접 안내하는 완전한 검색 도구 세트가 포함되어 있어야 합니다.

이 블로그에서는, 범위가 지정된 제안 및 “다음 항목을 찾으려고 했습니까?”라고도 알려진 쿼리 수정이라는 일반적인 두 가지 기능을 추가하여 전자상거래 검색을 개선하는 방법에 대해 알아보겠습니다.

다음 예제는 Elastic 8.5를 사용하여 테스트되었습니다. 

범위가 지정된 제안 

전자상거래 소매업체들 사이에서 공통적으로 발생하는 한 가지 어려움은 다양한 범주로 구성된 대규모 제품 카탈로그입니다. 이 시나리오에서는, 사용자가 관심을 가질 수 있는 도메인이나 범주를 이해하기 위해 간단한 쿼리를 구문 분석하는 것이 어렵습니다. 예를 들어, 사용자가 전자제품 소매점에서 "평면 스크린"을 검색하면 TV, TV 스탠드, 컴퓨터 모니터 등에서 결과를 볼 수 있습니다. 사용자가 TV를 찾는데 평면 스크린 TV 스탠드를 제시하는 것은 구매자 여정의 현 단계에서는 관련이 없을 수 있습니다.

범위가 지정된 제안은 특정 범주나 브랜드 내에서 쿼리를 제안하여 사용자가 가장 관심 있는 주제에 대한 검색 범위를 좁히는 데 도움이 됩니다.

이것은 사용자가 정제된 결과 집합을 신속하게 볼 수 있도록 도와주는 강력한 기능입니다. Search UI 및 Elastic을 사용하여 범위가 지정된 검색 제안을 구현하는 것도 간단합니다. 다음 섹션에서는 빌드 방법에 대해 설명해 드리겠습니다. 

제안을 위한 데이터 수집

위의 예에서 설명한 대로, 사용자가 입력하면 제안된 쿼리와 관련 범위가 표시됩니다. 이러한 제안을 구축하는 가장 일반적인 방법은 분석 데이터를 사용하여 고객이 흔히 관련시키는 가장 일반적인 쿼리와 범위를 수집하는 것입니다.

예를 들어, 전자상거래 웹사이트의 분석 데이터를 검토한 결과, 상위 쿼리가 "lcd tv"이고 사용자가 가장 자주 클릭하는 제품이 "TV & 홈시어터" 범주에 포함되어 있다고 가정해 보겠습니다.

이 데이터를 사용하여 다음과 같은 해당 범위의 제안을 나타내는 문서를 작성할 수 있습니다. 

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

이 문서에는 쿼리 및 관련 범주가 포함되어 있습니다. 필요한 경우 이를 더욱 복잡하게 만들 수 있습니다. 예를 들어, 카테고리 목록을 만들거나 "브랜드"와 같은 다른 범위를 추가할 수 있습니다.

그런 다음 Elasticsearch에 전용 인덱스를 생성하고, 이 인덱스의 이름을 "suggest"로 지정하여 분석 데이터를 사용해 구축한 범위가 지정된 제안 목록을 저장합니다. 이 작업은 데이터 변환을 사용하거나 Python 스크립트와 같은 사용자 정의 코드를 사용하여 수행할 수 있습니다. 인덱스 "suggest"에 대한 인덱스 매핑 예제는  여기에서 찾아보실 수 있습니다. 

자동 완성 제안

이제 제안 목록을 사용할 준비가 되었으므로, 이를 검색 경험에 추가해야 합니다.

Search UI를 사용하면, 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

쿼리 수정(“다음 항목을 찾으려고 했습니까?”)

쇼핑객들은 쿼리 용어를 잘못 선택하거나 오타로 인해 관련 검색 결과를 보지 못할 때 답답해할 수 있습니다. 검색 결과를 전혀 또는 거의 반환하지 않는 상황을 피하려면, 일반적으로 "다음 항목을 찾으려고 했습니까?"라고 하는 방법으로 더 나은 쿼리를 제안하는 것이 좋습니다

이 기능을 구현하는 여러 가지 방법이 있지만, 이것이 Search UI 및 Elastic을 사용하여 이를 구축하기 위한 가장 빠른 방법입니다. 

데이터 분석

다음 항목을 찾으려고 했습니까?” 기능을 구축하는 데 사용할 데이터 세트는 범위가 지정된 제안에 사용한 데이터 세트와 유사하므로 쿼리에서 결과가 반환되지 않는 경우 사용자에게 인기 있는 쿼리를 제안할 수 있습니다.  

"다음 항목을 찾으려고 했습니까?"에 대해 유사한 문서 구조를 사용하세요.

{
   "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"
                           }
                       ]
                   }
               }
           }
       }
   }
}

애플리케이션으로부터 제안 받기

Elasticsearch로부터 제안을 받기 위해, 클라이언트가 /api/suggest에서 사용할 수 있는 백엔드 API를 추가하겠습니다. 이 새로운 API는 Elasticsearch를 호출하고, 검색 템플릿을 실행한 후 제안할 쿼리를 전달한 다음, 쿼리 제안을 반환합니다. 백엔드 API를 사용하면 프런트 엔드 부분의 복잡성을 줄일 수 있습니다.

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

예를 들어, 쿼리가 "스패커"인 경우 API는 "스피커" 제안을 반환합니다. 이는 가장 일반적인 쿼리가 포함되는 이전에 수집된 데이터를 기반으로 합니다. suggest API는 쿼리에 가장 가까운 용어를 반환하게 됩니다. 

결과 페이지에 “다음 항목을 찾으려고 했습니까?” 추가하기

이제 프런트 엔드 애플리케이션에 "다음 항목을 찾으려고 했습니까?" 기능을 추가할 수 있습니다. 이를 위해 우리는 범위가 지정된 제안 부분에 사용했던 것과 동일한 React 애플리케이션에서 계속 작업하려고 합니다. 

우리가 추가한 새로운 API는 React 애플리케이션에서 사용할 수 있으며, 현재 쿼리에 대한 결과가 없을 경우 제안을 표시할 수 있습니다. 

여기서 중요한 점은 사용자가 입력한 각 쿼리에 대한 제안을 받는 것입니다. 결과가 없으면, 사용자는 제안을 보게 됩니다. 다음과 같은 다른 구현도 가능합니다. 결과가 거의 없는 경우 제안을 표시하거나 사용자의 쿼리 대신 제안을 자동으로 실행할 수 있습니다.

애플리케이션에는, 검색 결과를 표시하는 SearchResults라는 React 구성 요소가 있습니다. 여기에 백엔드 API /kr/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 Enterprise Search를 사용하여 "다음 항목을 찾으려고 했습니까?"와 범위가 지정된 제안을 쉽게 구축하는 방법을 알아보았습니다. 이러한 기능은 기존 검색 경험에 쉽게 통합되어 여러분의 사용자가 원하는 것을 더 빨리 찾을 수 있습니다. 이러한 예제를 만드는 데 사용된 코드를 검토하려면, GitHub 리포지토리에서 이 코드를 확인하세요.

현대적인 검색 환경을 제공하는 것은 전자상거래 웹사이트와 고객 지원, 웹사이트 검색, 내부 워크플레이스 검색사용자 정의 검색 애플리케이션과 같은 많은 다른 사용 사례에서 매우 중요합니다. 이 블로그 게시물의 단계를 따라 모든 최종 사용자를 위한 더 나은 검색 경험을 구축할 수 있습니다.