AlgoliaとNext.jsを利用して、高度な全文検索を実現する

このチュートリアルでは、AlgoliaNext.js 利用して、高度な全文検索を実現する手順を紹介します。

前提条件

  1. Algoliaにサインアップしていること
  2. Next.jsの API Routes について理解していること
  3. Newt CDN APIと newt-client-js を利用して、公開済みのコンテンツを取得する方法について理解していること

概要

以下の4ステップにわけて、チュートリアルを進めていきます。

  1. 全文検索を実現する
  2. 全文検索をカスタマイズする
  3. ソートを追加する
  4. ファセット検索を追加する

最終的に、以下の検索ページを作成します。

各コンポーネントの機能は以下の通りです。

algolia1.jpg

作成したページは公開しています。実際に機能を触ってご確認いただくことも可能です。
※データについては、デモ用のデータなので、正確でないものもあります。
https://newt-algolia-nextjs.vercel.app/

完成時のコード

また、完成時のコードを以下に公開しています。実装時の参考として、ご覧いただけます。
Newt-Inc/newt-algolia-nextjs

検索するデータのモデル

このチュートリアルでは、静的サイトジェネレータの検索ページを作成します。静的サイトジェネレータは、以下のような情報を持つモデルとします。

モデル: ジェネレーター

フィールド名 フィールドID フィールドタイプ オプション
タイトル title テキスト 必須
ロゴ logo 画像 必須
説明 description マークダウン 必須
URL url テキスト 必須
タグ tags 選択(子要素: テキスト) 必須・複数値
スター star 数字 必須

1. 全文検索を実現する

まず、Algoliaを利用して、全文検索が可能な検索ページを作成します。
ここでは、以下のことを行います。

Newtからのデータ取得とAlgoliaへのデータ連携を行うために、Next.jsのAPIルートを利用します。

1-1. Newtからのデータ取得

はじめに、Newtで定義した ジェネレーター のデータを取得する getGenerators を実装します。description フィールドはマークダウンタイプであるため、デフォルトではHTMLの値が返却されますが、ここではテキスト形式でデータを受け取るものとします。

// lib/api.ts

import { createClient } from 'newt-client-js'
import { Generator } from '../types/generator'

const client = createClient({
  spaceUid: process.env.NEXT_PUBLIC_NEWT_SPACE_UID + '',
  token: process.env.NEXT_PUBLIC_NEWT_CDN_TOKEN + '',
  apiType: 'cdn',
})

export const getGenerators = async (): Promise<Generator[]> => {
  const { items } = await client.getContents<Generator>({
    appUid: process.env.NEXT_PUBLIC_NEWT_APP_UID + '',
    modelUid: process.env.NEXT_PUBLIC_NEWT_MODEL_UID + '',
    query: {
      description: { fmt: 'text' },
    },
  })
  return items
}

getContents の返り値の型として Promise<Generator[]> を指定しています。この Generator の型は、Newtの管理画面で設定したモデル情報をもとに定義します。
コードの詳細は types/generator.ts をご確認下さい。

環境変数として、以下の値を定義します。Newtの管理画面での値をもとに定義して下さい。

NEXT_PUBLIC_NEWT_SPACE_UID=スペースUID
NEXT_PUBLIC_NEWT_APP_UID=AppUID
NEXT_PUBLIC_NEWT_MODEL_UID=モデルUID
NEXT_PUBLIC_NEWT_CDN_TOKEN=Newt CDN APIトークン

1-2. Algoliaへのデータ連携

以下の手順でデータを連携します。

  1. APIキーの確認
  2. データのフォーマット
  3. データの連携

1-2-1. APIキーの確認

はじめに、AlgoliaのAPIキーを確認しておきます。
Algoliaの管理画面に入り、「API Keys」のページから確認できます。

algolia5.jpg

上記で確認した、AlgoliaのApplication IDとAdmin API Keyの値を環境変数として追加します。
Algolia Primary Indexについては、お好きな名前で定義して下さい(このチュートリアルでは generator_relevance としています)。
ALGOLIA_ADMIN_API_KEY については、後述するクライアントサイドでの処理から参照しないため、NEXT_PUBLIC_ を外します。

NEXT_PUBLIC_ALGOLIA_APPLICATION_ID=Algolia Application ID
NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX=Algolia Primary Index
ALGOLIA_ADMIN_API_KEY=Algolia Admin API Key

1-2-2. データのフォーマット

続いて、取得したデータをAlgoliaの求める形式にフォーマットします。
Algoliaでは、各オブジェクトを一意の objectID で識別するため、Newtから取得したコンテンツの _id 情報をもとに、objectID を設定します。

データ形式の詳細については、Algoliaの What Is in a Record のドキュメントをご確認ください。
const generators = await getGenerators()
const formattedGenerators = generators.map((generator) => {
  return {
    objectID: generator._id,
    ...generator,
  }
})

1-2-3. データの連携

Algoliaの JavaScript APIクライアント を利用します。

JavaScript APIクライアント以外にも、ダッシュボードからの操作など、いくつかのデータ送信方法が用意されています。
詳細については、Algoliaの Send and Update Your Data のドキュメントをご確認ください。

以下のようなコードとなります。

import algoliasearch from 'algoliasearch'

// Algoliaとの接続と認証
const algolia = algoliasearch(
  process.env.NEXT_PUBLIC_ALGOLIA_APPLICATION_ID + '',
  process.env.ALGOLIA_ADMIN_API_KEY + ''
)

// インデックスの作成
const index = algolia.initIndex(
  process.env.NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX + ''
)

// (中略)formattedGeneratorsの取得

// レコードの保存
await index.saveObjects(formattedGenerators)

これらのデータ連携の処理を実行するため、Next.jsのAPIルートを利用します。
まとめると、以下のようになります。

// api/algolia/save.ts

import algoliasearch from 'algoliasearch'
import { NextApiRequest, NextApiResponse } from 'next'
import { getGenerators } from '../../../lib/api'

const algolia = algoliasearch(
  process.env.NEXT_PUBLIC_ALGOLIA_APPLICATION_ID + '',
  process.env.ALGOLIA_ADMIN_API_KEY + ''
)

const index = algolia.initIndex(
  process.env.NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX + ''
)

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    const generators = await getGenerators()
    const formattedGenerators = generators.map((generator) => {
      return {
        objectID: generator._id,
        ...generator,
      }
    })

    await index.saveObjects(formattedGenerators)
    res.status(200).json({ message: 'Success' })
  } catch (err: any) {
    res.status(400).json({ message: err?.message })
  }
}

ローカル環境で、このメソッドを実行する場合、以下にリクエストを送ります。

http://localhost:3000/api/algolia/save

データが連携されると、Algoliaの管理画面で、以下のようにインデックスが表示されます。

algolia4.png

1-2-4. 本番環境からWebhookを利用してデータを連携する場合

本番環境からWebhookを利用する場合、以下の検証を追加します。

  • Newtの Webhook から実行することを想定して、POSTメソッドのみを受け付ける
  • リクエストが有効なものか、クエリパラメータのsecretの値で検証する(ここでは ALGOLIA_SECRET_TOKEN という環境変数を利用する)
if (req.method !== 'POST') {
  return res.status(405).json({ message: `Method not allowed` })
}
if (req.query.secret !== process.env.ALGOLIA_SECRET_TOKEN) {
  return res.status(401).json({ message: 'Invalid token' })
}

1-2-5. データ連携の方針

Algoliaでは、データの変更に合わせてインデックスを最新に保つ必要があります。インデックスを更新する方法としては、以下の3つの方法が考えられます。

  1. Full reindexing
  2. Full record updates
  3. Partial record updates

このチュートリアルでは saveObjects のメソッドを利用して、2のFull record updatesの形式でデータを同期していますが、ユースケースに応じて、適切なデータ連携方針を選択するようご注意ください。

各方針の詳細については、Algoliaの Different Synchronization Strategies のドキュメントをご確認ください。

1-3. React InstantSearch Hooksを利用したUIの作成

Algoliaでは、検索インターフェースを素早く構築するために、いくつかのライブラリが用意されています。このチュートリアルでは、Next.jsを利用するため、React InstantSearch Hooks を利用します。
他にも Vue InstantSearchAngular InstantSearch などがあります。

ここでは、React InstantSearch Hooksで定義済みのUIコンポーネントを利用して、検索画面を作成します。
InstantSearch, SearchBox, Hits, PoweredBy の4つを利用します。順に説明します。

ここで紹介していないウィジェットについても、Algoliaの Showcase for React InstantSearch Hooks Widgets のページを見れば、どのようなウィジェットが用意されているか、簡単に確認できます。興味のある方はご確認下さい。

1-3-1. InstantSearch

InstantSearch はReact InstantSearch Hooksを使い始めるためのルートコンポーネントです。
引数として、indexNamesearchClient を渡します。

searchClientにはAlgoliaの Application IDSearch-Only API Key を渡します。
Application ID は、1-2-2で設定した環境変数を利用して指定します。
Search-Only API Key は、Algoliaの管理画面に入り、「API Keys」のページから確認できます。

algolia6.jpg

確認した、Search-Only API Key の値を環境変数として追加します。

NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_API_KEY=Algolia Search-Only API Key

以下のようなコードとなります。

// pages/index.tsx

import algoliasearch from 'algoliasearch/lite'
import { InstantSearch } from 'react-instantsearch-hooks-web'

const searchClient = algoliasearch(
  process.env.NEXT_PUBLIC_ALGOLIA_APPLICATION_ID + '',
  process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_API_KEY + ''
)

const Home = () => {
  return (
    <InstantSearch
      indexName={process.env.NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX + ''}
      searchClient={searchClient}
    >
      {/* Widgets */}
    </InstantSearch>
  )
}

1-3-2. SearchBox

SearchBox は、ユーザーがテキストベースのクエリを実行するためのウィジェットです。
InstantSearchの下層に配置します。

pages/index.tsx を以下のように修正します。

import algoliasearch from 'algoliasearch/lite'
- import { InstantSearch } from 'react-instantsearch-hooks-web'
+ import { InstantSearch, SearchBox } from 'react-instantsearch-hooks-web'

// (中略)

const Home = () => {
  return (
    <InstantSearch
      indexName={process.env.NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX + ''}
      searchClient={searchClient}
    >
-     {/* Widgets */}
+     <SearchBox />
    </InstantSearch>
  )
}

1-3-3. Hits

Hits は、検索結果の一覧を表示するためのウィジェットです。hitComponent のpropsを利用することで、各検索結果の表示をカスタマイズできます。

import algoliasearch from 'algoliasearch/lite'
- import { InstantSearch, SearchBox } from 'react-instantsearch-hooks-web'
+ import { InstantSearch, SearchBox, Hits } from 'react-instantsearch-hooks-web'
+ import { Hit } from '../components/Hit'

// (中略)

const Home = () => {
  return (
    <InstantSearch
      indexName={process.env.NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX + ''}
      searchClient={searchClient}
    >
      <SearchBox />
+     <Hits hitComponent={Hit} />
    </InstantSearch>
  )
}

各検索結果をハイライトしたい場合、Highlight を使います。
例えば title 属性と description 属性をハイライトしたい場合、以下のように記載します。

// components/Hit.tsx

import type { Hit as HitType } from 'instantsearch.js'
import { Highlight } from 'react-instantsearch-hooks-web'
import { Generator } from '../types/generator'

export const Hit = ({ hit }: { hit: HitType & Generator }) => {
  return (
    <div>
      <Highlight attribute="title" hit={hit} />
      <Highlight attribute="description" hit={hit} />
    </div>
  )
}

また、検索結果が0件の場合に表示をカスタマイズすることも可能です。
useInstantSearch() フックを使用します。

詳細は、Algoliaの Conditional Display in React InstantSearch Hooks のドキュメントをご確認ください。
// components/NoResults.tsx

import { useInstantSearch } from 'react-instantsearch-hooks-web'

export const NoResultsBoundary = ({ children, fallback }: any) => {
  const { results } = useInstantSearch()

  if (!results.__isArtificial && results.nbHits === 0) {
    return (
      <>
        {fallback}
        <div hidden>{children}</div>
      </>
    )
  }

  return children
}

export const NoResults = () => {
  const { indexUiState } = useInstantSearch()

  return (
    <div className="ais-Hits_Empty">
      <p>
        No results for <q>{indexUiState.query}</q>.
      </p>
    </div>
  )
}

pages/index.tsx は以下のようになります。

// pages/index.tsx

import algoliasearch from 'algoliasearch/lite'
import { InstantSearch, SearchBox, Hits } from 'react-instantsearch-hooks-web'
import { Hit } from '../components/Hit'
+ import { NoResultsBoundary, NoResults } from '../components/NoResults'

// (中略)

const Home = () => {
  return (
    <InstantSearch
      indexName={process.env.NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX + ''}
      searchClient={searchClient}
    >
      <SearchBox />
-     <Hits hitComponent={Hit} />
+     <NoResultsBoundary fallback={<NoResults />}>
+       <Hits hitComponent={Hit} />
+     </NoResultsBoundary>
    </InstantSearch>
  )
}

1-3-4. PoweredBy

Algoliaの無料プランを利用する場合、Search by Algolia のロゴを入れる必要があります。
PoweredBy のウィジェットを利用します。

pages/index.tsx を以下のように修正します。

// pages/index.tsx

import algoliasearch from 'algoliasearch/lite'
- import { InstantSearch, SearchBox, Hits } from 'react-instantsearch-hooks-web'
+ import {
+   InstantSearch,
+   SearchBox,
+   Hits,
+   PoweredBy,
+ } from 'react-instantsearch-hooks-web'
import { Hit } from '../components/Hit'
import { NoResultsBoundary, NoResults } from '../components/NoResults'

// (中略)

const Home = () => {
  return (
    <InstantSearch
      indexName={process.env.NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX + ''}
      searchClient={searchClient}
    >
      <SearchBox />
+     <PoweredBy />
      <NoResultsBoundary fallback={<NoResults />}>
        <Hits hitComponent={Hit} />
      </NoResultsBoundary>
    </InstantSearch>
  )
}

1-3-5. スタイル

ウィジェットのスタイルをカスタマイズするには「既存のクラスに従ってスタイルを作成する」「InstantSearchのテーマを利用する」など、いくつかの方法があります。
ここでは、既存のクラスに従って独自のスタイルを作成しています。
定義の詳細は styles/globals.css のファイルをご確認下さい。

スタイルのカスタマイズの詳細については、Algoliaの Customize a React InstantSearch Hooks Widget のドキュメントをご確認ください。

まとめ

ヘッダーやフッターの追加、スタイルも追加して、各ファイルは以下のように定義します。

// pages/index.tsx

import type { NextPage } from 'next'
import Head from 'next/head'
import algoliasearch from 'algoliasearch/lite'
import {
  InstantSearch,
  SearchBox,
  Hits,
  PoweredBy,
} from 'react-instantsearch-hooks-web'
import styles from '../styles/Home.module.css'
import { Hit } from '../components/Hit'
import { NoResultsBoundary, NoResults } from '../components/NoResults'

const searchClient = algoliasearch(
  process.env.NEXT_PUBLIC_ALGOLIA_APPLICATION_ID + '',
  process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_API_KEY + ''
)

const Home: NextPage = () => {
  return (
    <div className={styles.Wrapper}>

      // (中略)

      <InstantSearch
        indexName={process.env.NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX + ''}
        searchClient={searchClient}
      >
        <header className={styles.Header}>

          // (中略)

          <h1>Static Site Generators 😉</h1>
          <div className="ais-Search_Wrapper">
            <SearchBox />
            <span className="ais-Search_Icon">
              <img src="/search.svg" alt="" width="19" height="19" />
            </span>
            <PoweredBy className="ais-Search_Logo" />
          </div>
        </header>
        <div className={styles.Container}>
          <main className={styles.Main}>
            <NoResultsBoundary fallback={<NoResults />}>
              <Hits hitComponent={Hit} />
            </NoResultsBoundary>
          </main>
        </div>

        // (中略)

      </InstantSearch>

      // (中略)

    </div>
  )
}

export default Home
// components/Hit.tsx

import type { Hit as HitType } from 'instantsearch.js'
import { Highlight } from 'react-instantsearch-hooks-web'
import { Generator } from '../types/generator'

export const Hit = ({ hit }: { hit: HitType & Generator }) => {
  return (
    <>
      <div className="ais-Hits-item_Logo">
        <img
          src={hit.logo.src}
          alt={hit.logo.fileName}
          width="40"
          height="40"
        />
      </div>
      <div className="ais-Hits-item_Data">
        <div className="ais-Hits-item_Header">
          <h2 className="ais-Hits-item_Name">
            <a href={hit.url} rel="noreferrer noopener" target="_blank">
              <Highlight attribute="title" hit={hit} />
            </a>
          </h2>
          <p className="ais-Hits-item_URL">{hit.url}</p>
        </div>
        <p className="ais-Hits-item_Description">
          <Highlight attribute="description" hit={hit} />
        </p>
        <div className="ais-Hits-item_Footer">
          <div className="ais-Hits-item_Tags">
            {hit.tags.map((tag: string) => {
              return <span key={tag}>{tag}</span>
            })}
          </div>
          <div className="ais-Hits-item_Star">
            <img src="/star.svg" alt="" width="16" height="15" />
            <span>{hit.star}</span>
          </div>
        </div>
      </div>
    </>
  )
}

以上でシンプルな全文検索を行えるようになりました。


2. 全文検索をカスタマイズする

次に、全文検索のカスタマイズを行います。具体的には以下のことを行います。

  • 検索に利用するフィールドの指定と優先順位付け
  • デフォルトの並び順の指定

2-1. 検索に利用するフィールドの指定と優先順位付け

1では、すべてのフィールドを検索対象としていましたが、指定したフィールドのみが検索対象となるように設定を行います。

Algoliaでは、どのフィールドを検索対象に含めるか設定できるため、URLやロゴなど、表示のみに使用するフィールドは検索対象から除外することができます。
また、どのフィールドとマッチした場合に、関連性が高いと判断するか、明示的に指定できます。

検索対象フィールドの詳細については、Algoliaの Searchable Attributes のドキュメントをご確認ください。

ここでは、タイトル・タグ・説明のどれかと一致する場合、検索結果として表示することとし、優先順位は「タイトル > タグ > 説明」の順番とします。

検索対象の属性の指定は、ダッシュボード経由でもAPI経由でもできますが、ここではAPI経由で指定します。Next.jsのAPIルートを利用します。

API経由で指定する場合、インデックスに searchableAttributes という属性を設定します。

// api/algolia/setup.ts

import algoliasearch from 'algoliasearch'
import { NextApiRequest, NextApiResponse } from 'next'

const algolia = algoliasearch(
  process.env.NEXT_PUBLIC_ALGOLIA_APPLICATION_ID + '',
  process.env.ALGOLIA_ADMIN_API_KEY + ''
)

const primaryIndex = algolia.initIndex(
  process.env.NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX + ''
)

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    // 検索対象の指定
    await primaryIndex.setSettings({
      searchableAttributes: ['title', 'tags', 'description'],
    })

    res.status(200).json({ message: 'Success' })
  } catch (err: any) {
    res.status(400).json({ message: err?.message })
  }
}

ローカル環境で、このメソッドを実行する場合、以下にリクエストを送ります。

http://localhost:3000/api/algolia/setup

データが連携されると、Algoliaの管理画面で Searchable attributes が以下のように表示されます。

algolia7.jpg

これで、検索対象フィールドの指定と、優先順位付けができました。

2-2. デフォルトの並び順の指定

続いて、デフォルトの並び順を指定します。これを指定することで、検索文字列が入力されていなかった場合や、同じ関連度の場合の並び順が決定します。

検索結果のランク付けについて、詳細はAlgoliaの Custom Ranking のドキュメントをご確認ください。

ここでは、スターの降順で表示することとします。

API経由で指定する場合、インデックスに customRanking という属性を設定します。
api/algolia/setup.ts を以下のように修正します。

    await primaryIndex.setSettings({
      searchableAttributes: ['title', 'tags', 'description'],
+     customRanking: ['desc(star)'],
    })

このメソッドを実行し、データが連携されると、Algoliaの管理画面の Ranking and Sorting にカスタムランキングが追加されます。

algolia8.jpg

これで、デフォルトの並び順が指定されました。


3. ソートを追加する

次に、ソートの機能を追加します。具体的には以下のことを行います。

  • インデックスの追加(レプリカの作成と設定)
  • ソートUIの追加

Algoliaでは検索結果を明示的にソートすることが可能です。
大きく2タイプのソートが提供されていて、Exhaustive sorting(指定された属性に基づく厳密なソート)Relevant sorting(関連性によるソート) があります。

ソートの詳細については、Algoliaの Sorting Results のドキュメントをご確認ください。

ここでは、Exhaustive sortingを利用して、指定された属性の値をもとに、ソートできるようにします。スターの降順ソートと、タイトルの昇順ソートを追加してみましょう。

最終的には、2で設定したソートに加え、以下の3つの選択肢からソート順を選べるようにします。

  • Relevance(関連性によるソート。2で設定したもの)
  • GitHub Stars(スターの降順)
  • Title(タイトルの昇順)

algolia2.jpg

3-1. インデックスの追加(レプリカの作成と設定)

Algoliaでは同じデータに対して、異なるランキングを提供する場合、それぞれ異なるインデックスを使用する必要があります。
追加されたインデックスは、レプリカと呼ばれます。

レプリカの詳細については、Algoliaの Understanding Replicas のドキュメントをご確認ください。

レプリカの作成・設定は、ダッシュボード経由でもAPI経由でもできますが、ここではAPI経由で指定します。Next.jsのAPIルートを利用します。

3-1-1. レプリカの作成

レプリカを作成するには、プライマリインデックスに setSettings メソッドを使用します。
レプリカにはstandard replicaとvirtual replicaの2種類がありますが、exhaustive sortingで利用するため、standard replicaとして作成します。

ここでは、スターの降順に並べるためのレプリカと、タイトルの昇順に並べるためのレプリカを定義します。
インデックスに replicas という属性を設定します。

api/algolia/setup.ts を以下のように修正します。

    await primaryIndex.setSettings({
      searchableAttributes: ['title', 'tags', 'description'],
      customRanking: ['desc(star)'],
+     replicas: [
+       process.env.NEXT_PUBLIC_ALGOLIA_REPLICA_INDEX_STAR + '',
+       process.env.NEXT_PUBLIC_ALGOLIA_REPLICA_INDEX_TITLE + '',
+     ],
    })

また、環境変数にそれぞれのレプリカの名前を定義します。お好きな名前で定義して下さい(このチュートリアルでは generator_stargenerator_title としています)。

NEXT_PUBLIC_ALGOLIA_REPLICA_INDEX_STAR=Algolia Replica Index (スター)
NEXT_PUBLIC_ALGOLIA_REPLICA_INDEX_TITLE=Algolia Replica Index (タイトル)

このメソッドを実行し、Algoliaの管理画面を確認すると、以下のようにインデックス(レプリカ)が追加されていることがわかります。

algolia9.jpg

3-1-2. レプリカの設定

レプリカの設定を変更するには、以下の作業が必要です。

  • レプリカの初期化
  • setSettings を利用した設定変更

ここでは、スターの降順に並べるための replicaIndexStar と、タイトルの昇順に並べるための replicaIndexName を定義します。
それぞれ customRanking を利用して、カスタムランキングを設定します。
customRanking: ['desc(star)']customRanking: ['asc(title)'] のように指定します。

また ranking を利用して、ランキングの基準を設定します。
デフォルトでは、以下のようになっていますが、

  ranking: [
    'typo',
    'geo',
    'words',
    'filters',
    'proximity',
    'attribute',
    'exact',
    'custom',
  ]

ここではカスタムランキングを優先するため、custom を最上位に定義します。

  ranking: [
+   'custom',
    'typo',
    'geo',
    'words',
    'filters',
    'proximity',
    'attribute',
    'exact',
-   'custom',
  ]

api/algolia/setup.ts を以下のように修正します。

// api/algolia/setup.ts

import algoliasearch from 'algoliasearch'
import { NextApiRequest, NextApiResponse } from 'next'

const algolia = algoliasearch(
  process.env.NEXT_PUBLIC_ALGOLIA_APPLICATION_ID + '',
  process.env.ALGOLIA_ADMIN_API_KEY + ''
)

const primaryIndex = algolia.initIndex(
  process.env.NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX + ''
)
+ const replicaIndexStar = algolia.initIndex(
+   process.env.NEXT_PUBLIC_ALGOLIA_REPLICA_INDEX_STAR + ''
+ )
+ const replicaIndexName = algolia.initIndex(
+   process.env.NEXT_PUBLIC_ALGOLIA_REPLICA_INDEX_TITLE + ''
+ )

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    await primaryIndex.setSettings({
      searchableAttributes: ['title', 'tags', 'description'],
      customRanking: ['desc(star)'],
      replicas: [
        process.env.NEXT_PUBLIC_ALGOLIA_REPLICA_INDEX_STAR + '',
        process.env.NEXT_PUBLIC_ALGOLIA_REPLICA_INDEX_TITLE + '',
      ],
    })

+   await replicaIndexStar.setSettings({
+     searchableAttributes: ['title', 'tags', 'description'],
+     customRanking: ['desc(star)'],
+     ranking: [
+       'custom',
+       'typo',
+       'geo',
+       'words',
+       'filters',
+       'proximity',
+       'attribute',
+       'exact',
+     ],
+   })
+ 
+   await replicaIndexName.setSettings({
+     searchableAttributes: ['title', 'tags', 'description'],
+     customRanking: ['asc(title)'],
+     ranking: [
+       'custom',
+       'typo',
+       'geo',
+       'words',
+       'filters',
+       'proximity',
+       'attribute',
+       'exact',
+     ],
+   })
+
    res.status(200).json({ message: 'Success' })
  } catch (err: any) {
    res.status(400).json({ message: err?.message })
  }
}

このメソッドを実行し、Algoliaの管理画面を確認すると、レプリカインデックスの Ranking and Sorting にカスタムランキングのみが設定されていることがわかります。

algolia10.jpg

3-2. ソートUIの追加

SortBy を利用します。
3-1で追加したインデックスをSortByに渡します。

pages/index.tsx を以下のように修正します。

// pages/index.tsx

import {
  InstantSearch,
  SearchBox,
  Hits,
  PoweredBy,
+ SortBy,
} from 'react-instantsearch-hooks-web'

// (中略)

const Home: NextPage = () => {
  return (
      // (中略)

      <InstantSearch
        indexName={process.env.NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX + ''}
        searchClient={searchClient}
      >
        // (中略)

        <div className={styles.Container}>
+         <nav className={styles.Nav}>
+           <h2>Sort</h2>
+           <SortBy
+             items={[
+               {
+                 value: process.env.NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX + '',
+                 label: 'Relevance',
+               },
+               {
+                 value:
+                   process.env.NEXT_PUBLIC_ALGOLIA_REPLICA_INDEX_STAR + '',
+                 label: 'GitHub Stars',
+               },
+               {
+                 value:
+                   process.env.NEXT_PUBLIC_ALGOLIA_REPLICA_INDEX_TITLE + '',
+                 label: 'Title',
+               },
+             ]}
+           />
+         </nav>
          <main className={styles.Main}>
            <NoResultsBoundary fallback={<NoResults />}>
              <Hits hitComponent={Hit} />
            </NoResultsBoundary>
          </main>
        </div>

        // (中略)

      </InstantSearch>

      // (中略)

  )
}

export default Home

これで、3種類のソート方法を選択できるようになりました。


4. ファセット検索を追加する

最後に、ファセット検索を追加します。具体的には以下のことを行います。

  • ファセット検索で利用する属性の指定
  • ファセット検索UIの追加

Algoliaのファセットを利用すると、選択した属性のグループに対してカテゴリーを作成し、ユーザーが検索結果を絞り込めるようになります。

ファセットの詳細については、Algoliaの Faceting のドキュメントをご確認ください。

ここでは、タグの情報をもとに、ユーザーが結果をフィルタできるようにします。

algolia3.jpg

4-1. ファセット検索で利用する属性の指定

ファセット検索を利用するためには、事前に利用する属性を指定しておく必要があります。これは、ダッシュボード経由でもAPI経由でもできますが、ここではAPI経由で指定します。

attributesForFaceting を利用して、タグの情報がファセット検索で利用できるように指定します。

api/algolia/setup.ts を以下のように修正します。

    await primaryIndex.setSettings({
      searchableAttributes: ['title', 'tags', 'description'],
      customRanking: ['desc(star)'],
+     attributesForFaceting: ['tags'],
      replicas: [
        process.env.NEXT_PUBLIC_ALGOLIA_REPLICA_INDEX_STAR + '',
        process.env.NEXT_PUBLIC_ALGOLIA_REPLICA_INDEX_TITLE + '',
      ],
    })

    await replicaIndexStar.setSettings({
      searchableAttributes: ['title', 'tags', 'description'],
      customRanking: ['desc(star)'],
+     attributesForFaceting: ['tags'],
      ranking: [
        'custom',
        'typo',
        'geo',
        'words',
        'filters',
        'proximity',
        'attribute',
        'exact',
      ],
    })

    await replicaIndexName.setSettings({
      searchableAttributes: ['title', 'tags', 'description'],
      customRanking: ['asc(title)'],
+     attributesForFaceting: ['tags'],
      ranking: [
        'custom',
        'typo',
        'geo',
        'words',
        'filters',
        'proximity',
        'attribute',
        'exact',
      ],
    })

このメソッドを実行し、Algoliaの管理画面を確認すると、各インデックスの Facetstags の属性が設定されていることがわかります。

algolia11.jpg

4-2. ファセット検索UIの追加

RefinementList を利用します。
表示するファセットは最大10個、ファセットの順番は ['count:desc', 'name:asc'] とします。

pages/index.tsx を以下のように修正します。

// pages/index.tsx

import {
  InstantSearch,
  SearchBox,
  Hits,
  PoweredBy,
  SortBy,
+ RefinementList,
} from 'react-instantsearch-hooks-web'

// (中略)

const Home: NextPage = () => {
  return (
      // (中略)

      <InstantSearch
        indexName={process.env.NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX + ''}
        searchClient={searchClient}
      >
        // (中略)

        <div className={styles.Container}>
          <nav className={styles.Nav}>
            <h2>Sort</h2>
            <SortBy
              items={[
                {
                  value: process.env.NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX + '',
                  label: 'Relevance',
                },
                {
                  value:
                    process.env.NEXT_PUBLIC_ALGOLIA_REPLICA_INDEX_STAR + '',
                  label: 'GitHub Stars',
                },
                {
                  value:
                    process.env.NEXT_PUBLIC_ALGOLIA_REPLICA_INDEX_TITLE + '',
                  label: 'Title',
                },
              ]}
            />
+           <h2>Filter</h2>
+           <RefinementList
+             attribute={'tags'}
+             limit={10}
+             sortBy={['count:desc', 'name:asc']}
+           />
          </nav>
          <main className={styles.Main}>
            <NoResultsBoundary fallback={<NoResults />}>
              <Hits hitComponent={Hit} />
            </NoResultsBoundary>
          </main>
        </div>

        // (中略)

      </InstantSearch>

      // (中略)

  )
}

export default Home

これで、ファセット検索ができるようになりました。

以上で、すべてのステップが終了となります。
ここまでで説明したコードは、すべて Newt-Inc/newt-algolia-nextjs に公開しています。
もし、どこかわかりにくいところがあれば、こちらのコードも参考にしていただければと思います。

Algoliaはここで紹介した以外にも様々な機能が充実しているので、ぜひ様々な機能を試してみてください。

Newt Made in Newt