notebook

都内でWEB系エンジニアやってます。

AlgoliaのInstantsearchで検索文字入力が終わったと判断してからリクエストを送るようにする

AlgoliaのInstantsearchを用いて検索UIを作っていたが、検索文字列に変化があるたびにリクエストを送る仕様になっていた

なのである程度入力が終わったと判断できるタイミングでリクエストを送るようにしたいということで調べて対応してみた

Algoliaの料金体系とInstantsearchの挙動

自分はFreeプランしか使っていないが

10000req/月もしくは10000Record/月までは無料で使える

基本的にAlgoliaを使うのであればUI側はAlgoliaが提供するライブラリInstantsearchを使って開発することになるはず

そして、Instantsearchを使って実装する場合、デフォルトの挙動としては検索文字列に変化があるとそのぶんだけリクエストを送るようになっている

たとえば「Ruby」で検索しようとした場合

  • Rで検索リクエスト
  • Ruで検索リクエスト
  • Rubで検索リクエスト
  • Rubyで検索リクエスト

というようになる

このような方式をas you typeと呼ぶらしい(ドキュメントなどにそう書いてあることが多い)

この場合に次の2点でちょっと微妙だなと感じていた

  • 無料プランだと最終的に検索したいのは「Ruby」なのでその入力が終わるまでリクエスト送信を待ってほしい
  • 結果が返ってくるまでに時間がかかる場合、検索結果のレンダリングがカクつく
    • 実装が悪い可能性もある…

上記を解決するために「検索文字列の変化が一定時間以上なかったら」検索リクエストを送るようにしてみる

調べたら、ドキュメントに対応方法が載っていたので対応していく

Improve performance for React InstantSearch Hooks | Algolia

https://www.algolia.com/doc/guides/building-search-ui/going-further/improve-performance/react-hooks/#disabling-as-you-typewww.algolia.com

Reactで使うInstantsearchについて

InstantsearchのReact用ライブリは2022-08-03現在2種類あり、旧版のreact-instantsearch-dom、新版のreact-instantsearch-hooks-webがある

react-instantsearch-hooks-webの方は、名前の通りReact Hooksが使えるようになっている

React Instantsearch(旧)のドキュメントを見に行くと「新たに使う場合はhooks版を使ってください」と書いてあったので基本的にはhooksの方を使えば良さそう

今回はアナウンスに従いReact Instantsearch Hooksを使った

検索文字列に1秒変化がない場合に検索リクエストを送信する

ドキュメントの実装例がJavaScriptなのでコピーするだけだと型周りでエラーが出てしまうためそのあたりをドキュメントの実装例からは変更している

  • tsx(一部抜粋)
import type { NextPage } from "next";
import algoliasearch from "algoliasearch/lite";
import { useRef } from "react";
import {
  InstantSearch,
  SearchBox,
  Index,
  InfiniteHits,
  PoweredBy,
} from "react-instantsearch-hooks-web";
import type { UseSearchBoxProps } from "react-instantsearch-hooks-web";
import type { SearchClient } from "instantsearch.js";

const Search: NextPage = () => {
  const indices = (process.env.NEXT_PUBLIC_ALGOLIA_INDICES || "").split(",");
  const timerId = useRef<ReturnType<typeof setTimeout>>();

  const searchClient = algoliasearch(
    process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || "",
    process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY || ""
  );

  // NOTE: https://www.algolia.com/doc/guides/building-search-ui/going-further/improve-performance/react-hooks/#disabling-as-you-type
  // 入力確定判断まで1秒待つ
  const queryHook: UseSearchBoxProps["queryHook"] = (query, search) => {
    if (timerId.current) {
      clearTimeout(timerId.current);
    }

    timerId.current = setTimeout(() => search(query), 1000);
  };

  return (
    <InstantSearch searchClient={searchClient} indexName={indices[1]}>
      <SearchBox placeholder="Search" queryHook={queryHook}></SearchBox>
      <div>
        {indices.map((index) => {
          return (
            <div key={index}>
              <Index key={index} indexName={index}>
                <h2>{index}</h2>
                <InfiniteHits
                  hitComponent={PageHit}
                ></InfiniteHits>
              </Index>
            </div>
          );
        })}
      </div>
      <br />
      <PoweredBy />
    </InstantSearch>
  );
};

InfiniteHitsコンポーネント以下は独自で実装しているが今回の話の範囲ではないため除外する

ポイントはSearchBoxqueryHookプロパティ

このqueryHookはクエリの文字列に変化があるたびに呼ばれるコールバック関数を渡す

用途としては

  • クエリの内容を入力された文字列から少し変えたい
  • クエリによって処理を変えたい

など、検索文字列の変化から検索リクエストの間に何かしらの処理を挟みたい場合に使う

ドキュメントではsetTimeoutと組み合わせることで「一定期間入力がない場合のみ検索のリクエストを送信する」ということを行っている

今回やりたいことはまさにこのパターンなのでドキュメントにしたがってやっていく

queryHook関数の引数でqueryは検索文字列、searchは検索の実行(リクエストをAlgoliaに送信する処理)

setTimeoutで1000ms後に検索を実行する、それまでに入力の変化によりコールバックが呼ばれた場合はclearTimeoutで設定されたTimerを解除して新たにTimerをセットするというような実装になっている

型に関しては下記のようにReturnTypesetTimeoutの戻り値の型を取得して型注釈としている

const timerId = useRef<ReturnType<typeof setTimeout>>();

単純にuseStateでtimerIdを管理しようとするとうまくいかなかったのでuseRefを使って管理するようにした

こういう使い方をできるのを知らなかったので勉強になった

まとめ

  • AlgoliaのInstantsearchで、SearchBoxのqueryHookコールバックを用いて検索文字列の入力が1秒間ない場合に初めてリクエストを送るようにした
  • useRefを用いてsetTimeoutの戻り値を管理するようにした

また、感想として、AlgoliaのInstantsearchのドキュメントを眺めてみるとさまざまなユースケースに対応できるようにかなりいろいろ機能があるなーと感じた

とりあえず一通りさっと読んで把握するだけでも面白いと思う