InstantSearchとAutocompleteを組み合わせてイケてる検索体験を構築しよう🔎

こちらは Algolia Advent Calendar 2021 の1日目の記事になります。


Autocompleteは検索体験をより良くするものとして活用できる一方で、検索以外のところにも活用することができます。例えば、アットマークでメンションをする時に、そのユーザーを補完する So, You Want to Build an @mention Autocomplete Feature? こちらの記事に出てくるようなユースケースにも利用可能です。とはいえ、Algoliaがオープンソースとして公開しているAutocompleteライブラリは、InstantSearchと組み合わせて使うことで特に日本語のモバイル検索においてはTransliterationが力を発揮するのではないかと思います。

ということで、私が普段、個人的に使っているAlgoliaの日本語検索デモサイトの https://ranqueen.ninja/ のデータを元に InstantSearch と Autocomplete の組み合わせの解説をしていきます。

Autocomplete with InstantSearch.js example

動くものを題材にしてやっていきたいと思いますので、まずはAutocompleteとInstantSearchのexampleをコピーしてきます。

git clone git@github.com:algolia/autocomplete.git

そして、autocomplateディレクトリに移動してからyarnで諸々インストールして、

$ cd autocomplete
$ yarn
$ yarn
yarn install v1.22.15
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...

〜略〜
lerna success run Ran npm script 'prepare' in 9 packages in 25.3s:
lerna success - @algolia/autocomplete-core
lerna success - @algolia/autocomplete-js
lerna success - @algolia/autocomplete-plugin-algolia-insights
lerna success - @algolia/autocomplete-plugin-query-suggestions
lerna success - @algolia/autocomplete-plugin-recent-searches
lerna success - @algolia/autocomplete-plugin-tags
lerna success - @algolia/autocomplete-preset-algolia
lerna success - @algolia/autocomplete-shared
lerna success - @algolia/autocomplete-theme-classic
✨  Done in 72.85s.

ローカルにサーバーを起動していきます。

$ yarn workspace @algolia/autocomplete-example-instantsearch start
yarn workspace v1.22.15
yarn run v1.22.15
$ parcel index.html
ℹ️ Server running at http://localhost:1234

localhost:1234 で👇のような画面が出たらOKです。

Algolia Indexを日本語のものに変更

対象のディレクトリに移動してコードに変更を加えていきます。

$ cd examples/instantsearch/
$ code ./

後ほど詳細をご説明しますが、searchClient.tsの中でアプリケーションIDと検索のみのAPI Keyを元にインスタンスの生成を行っているので、ここを ranqueen.ninja で使っているものにします。

Indexおよびカテゴリのアトリビュートの指定はinstantsearch.tsで行っているので👇を変更します。

すると、以下のような形になります。iPhone Xの画像や NaN となっているコメント数などは後ほど消していきたいと思いますが、一旦先に進みます。

autocomplete.tsxの中でQuery Suggestions用のインデックスを指定しているところがあるので、そこを ranqueen.ninja のQuery Suggestionsに置き換えます。

Query Suggestionsの設定でカテゴリが追加されている必要がありますが👇こんな感じでAutoCompleteが出てきたら動作設定完了です!

index.html

それでは HTML から見ていきたいと思います。

bodyタグの中身は以下のようになっています。

header。ここに検索バーがあるわけですが、autocomplateというidのdivタグを定義していて、この裏側でautocompleteが動くという形になります。

    <header class="header">
      <div class="header-wrapper wrapper">
        <nav class="header-nav">
          <a href="/">Home</a>
        </nav>
        <div id="autocomplete"></div>
      </div>
    </header>

検索結果を表示するdiv。categoriesが左側にカテゴリを表示、hitsが検索結果を整形したもの、そしてpaginationでページネート。こちらはInstantSearchのwidgetでやりくりされる部分になります。

    <div class="container wrapper">
      <div>
        <div id="categories"></div>
      </div>
      <div>
        <div id="hits"></div>
        <div id="pagination"></div>
      </div>
    </div>

最後にtsファイルを読み込んで終わり。今回特にenvの方は触れませんが、app.tsで諸々実装していく形になります。

    <script src="env.ts"></script>
    <script src="src/app.ts"></script>

ということで40行弱の非常にシンプルなHTMLとなっています。

app.ts

HTMLからロードされるファイル。ここではautocompleteとinstantsearchのそれぞれを同じディレクトリにあるファイルからimportして、startするだけにしています。また、スタイルシートをインポートしてそれっぽく。

import '@algolia/autocomplete-theme-classic';
import '../style.css';

import { startAutocomplete } from './autocomplete';
import { search } from './instantsearch';

search.start();
startAutocomplete();

instantsearch.ts

InstantSearchのwidgetを使った hierarchicalMenuWithHeader, hits, paginationみたいなところは、昨年のAdvent Calendarで書いた InstantSearch.js で Algolia をはじめよう などをみていただくとして、ちょっと手を入れたところだけを少しご紹介します。

階層型メニューで、自分のIndexは categories.lvl0、categories.lvl1 という形になっていて、レベル0の方は INSTANT_SEARCH_HIERARCHICAL_ATTRIBUTE という定数になっているものの、レベル1階層はベタ書きになっていたので、そこを👇のように。

検索結果を表示する hits のテンプレートの部分はレーティングやコメント等は無くしてシンプルな感じにしました。

ということで、実際に表示されるarticleタグ内の商品情報は👇のような感じに。

autocomplete.tsx

検索履歴

いよいよAutocomplete周りの実装になります。まずはRecent Searches(検索履歴)のプラグインを使っていきます。

👇のように検索履歴用のプラグインを使います。

import { createLocalStorageRecentSearchesPlugin } from '@algolia/autocomplete-plugin-recent-searches';

以下のようにすることで、instantsearchというキーでローカルストレージに保持された検索履歴から3件取得というような形になります。

const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({
  key: 'instantsearch',
  limit: 3,
  transformSource({ source }) {
    return {
      ...source,
      getItemUrl({ item }) {
        return getItemUrl({
          query: item.label,
          category: item.category,
        });
      },
      onSelect({ setIsOpen, setQuery, item, event }) {
        onSelect({
          setQuery,
          setIsOpen,
          event,
          query: item.label,
          category: item.category,
        });
      },
      templates: {
        ...source.templates,
        // Update the default `item` template to wrap it with a link
        // and plug it to the InstantSearch router.
        item(params) {
          const { children } = (source.templates.item(params) as any).props;

          return (
            <ItemWrapper
              query={params.item.label}
              category={params.item.category}
            >
              {children}
            </ItemWrapper>
          );
        },
      },
    };
  },
});

ブラウザのDev ToolsでみてみるとLocalStorageは👇のようになりますが、

Autocompleteに表示される検索履歴は3件です。

もちろん、limit: 4にすると👇のように4件の表示になります。

クエリサジェスト

続いてQuery Suggestionsになりますが、こちらも👇のようにプラグインを使うことで簡単に実装できます。

import { createQuerySuggestionsPlugin } from '@algolia/autocomplete-plugin-query-suggestions';

いわゆるプレディクションというか、Autocompleteといって一番イメージしやすい部分かなと思います。とくにモバイルでフリック入力をする場合など、ひらがなを入力するだけで補完が効くのは便利かなと思います。

ここではAlgoliaのQuery Suggestion用のIndexを指定して👇のように実装しています。

const querySuggestionsPlugin = createQuerySuggestionsPlugin({
  searchClient,
  indexName: 'ranqueen.ninja_query_suggestions',
  getSearchParams() {
    const currentCategory = getInstantSearchCurrentCategory();

    if (!currentCategory) {
      return recentSearchesPlugin.data.getAlgoliaSearchParams({
        hitsPerPage: 6,
      });
    }

    return recentSearchesPlugin.data.getAlgoliaSearchParams({
      hitsPerPage: 3,
      facetFilters: [
        `${INSTANT_SEARCH_INDEX_NAME}.facets.exact_matches.${INSTANT_SEARCH_HIERARCHICAL_ATTRIBUTE}.value:-${currentCategory}`,
      ],
    });
  },
  categoryAttribute: [
    INSTANT_SEARCH_INDEX_NAME,
    'facets',
    'exact_matches',
    INSTANT_SEARCH_HIERARCHICAL_ATTRIBUTE,
  ],
  transformSource({ source }) {
    const currentCategory = getInstantSearchCurrentCategory();

    return {
      ...source,
      sourceId: 'querySuggestionsPlugin',
      getItemUrl({ item }) {
        return getItemUrl({
          query: item.query,
          category: item.__autocomplete_qsCategory,
        });
      },
      onSelect({ setIsOpen, setQuery, event, item }) {
        onSelect({
          setQuery,
          setIsOpen,
          event,
          query: item.query,
          category: item.__autocomplete_qsCategory,
        });
      },
      getItems(params) {
        if (!params.state.query) {
          return [];
        }

        return source.getItems(params);
      },
      templates: {
        ...source.templates,
        header() {
          if (!currentCategory) {
            return null;
          }

          return (
            <Fragment>
              <span className="aa-SourceHeaderTitle">In other categories</span>
              <div className="aa-SourceHeaderLine" />
            </Fragment>
          );
        },
        item(params) {
          const { children } = (source.templates.item(params) as any).props;

          return (
            <ItemWrapper
              query={params.item.query}
              category={params.item.__autocomplete_qsCategory}
            >
              {children}
            </ItemWrapper>
          );
        },
      },
    };
  },
});

これによって、『あ』と入力するだけで『アナ雪』がサジェストされる形となります。Query SuggestionsのIndexは一日に一回、過去の検索履歴およびヒットした状況を元にAlgolia側で自動的に生成するものになります。(ただクエリされた数が多いものが出てくるわけではないので、悪質なボットによる、よろしくない言葉等は出てきませんのでご安心ください。)

debounce.ts

AutocompleteとInstantSearchの検索結果が両方同時にマウスやキーボードの操作と一緒に動くと見ている方は少し混乱してしまうので、debounceとうtsファイルを作って調整できるようにしています。

こちらを instantsearch.ts の中で👇のようにexportして(便宜上分かりやすいように2500ミリ秒に変更)

export const debouncedSetInstantSearchUiState = debounce(
  setInstantSearchUiState,
  2500
);

状態が変わった際にこちらを適用することで、検索結果の表示は2.5秒遅れて表示されるような形になります。こちらの適切な設定時間に関しましては、テストや検証を通じて最適解を見出していただければと思います。

    onStateChange({ prevState, state }) {
      if (prevState.query !== state.query) {
        debouncedSetInstantSearchUiState({ query: state.query });
      }
    },

👇ご覧の通り、さすがに2.5秒だと長過ぎるかな、と。。


ということで、今回はInstantSearchとAutocompleteを組み合わせて、より良いSearch and Discovery体験をエンドユーザーにお届けしましょうという内容でした。

2021年の Algolia Advent Calendar は興味深い話がたくさん続いていきますので、よろしければ是非チェックしてみてください!

コメント

タイトルとURLをコピーしました