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

最終更新日:

Table of contents

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

記事内で使用している主なソフトウェアのバージョン

  • Next.js(next): 13.4.19
  • newt-client-js(newt-client-js): 3.2.6
  • algoliasearch(algoliasearch): 4.20.0
  • React InstantSearch(react-instantsearch): 7.0.3

前提条件

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

概要

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

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

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

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

algolia1.jpg

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

完成時のコード

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


1. Algoliaのセットアップ

1-1. アプリケーションの作成

Algoliaにサインアップしたら、このチュートリアルで利用するアプリケーションを1つ用意しておきましょう。
Settings > Applications から「Create Application」で新しくアプリケーションを作成します。
algolia12.jpg

内容はお好きなように設定いただいて構いませんが、ここでは以下の内容で作成します。

  • NAME YOUR APPLICATION(アプリケーション名): Static Site Generators
  • CHOOSE YOUR SUBSCRIPTION(サブスクリプションプラン): Build(FREE)
  • Data Center(データセンターの場所): US West

1-2. インデックスの作成

アプリケーションを作成すると、インデックスの作成に進みます。
ここでは、generator_relevance という名前でインデックスを作成しておきます。

algolia13.png

インデックスを作成後「Import your records」の表示がありますが、後述のステップで実行するので、ここではまだ行いません。


2. Newtのセットアップ

検索対象となるモデル・コンテンツを用意しておきましょう。

2-1. ジェネレーターモデルの作成

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

モデル: ジェネレーター

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

algolia14.png

2-2. ジェネレーターコンテンツの入稿

作成したジェネレーターモデルにコンテンツを入稿します。
ここでは、内容の正確性は問題ではないので、適当に入力いただいて構いません。
※ 内容にこだわりたい方は、Site Generators の内容などを参考に入力してみましょう。
algolia15.png


3. 検索対象のデータをAlgoliaに連携する

1でAlgoliaにインデックスを作成しましたが、まだ レコード を登録できていません。
2で登録したジェネレーターコンテンツを、Algoliaにインポートしましょう。

ここではAPIを利用して、データをインポートします。
詳細はAlgoliaの Importing with the API のドキュメントをご確認ください。

また、最終的にはNewtでコンテンツを更新した時にAlgoliaへのデータ連携を行えるよう、Next.jsの Route Handlers を利用します。
Route Handlersを利用することで、本番環境からWebhookを利用してデータを更新することができます。

以下のステップで進めていきます。

  • AlgoliaのAPIクライアントのセットアップ
  • Newtからのデータ取得
  • Algoliaへのデータ送信

3-1. AlgoliaのAPIクライアントのセットアップ

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

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

algolia5.jpg

上記で確認した、AlgoliaのApplication IDとAdmin API Keyの値を環境変数として .env.local に追加します。
また、1-2で作成したインデックスの名前を NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX として定義します。
ALGOLIA_ADMIN_API_KEY については、後述するクライアントサイドでの処理から参照しないため、NEXT_PUBLIC_ を外します。

.env.local
NEXT_PUBLIC_ALGOLIA_APPLICATION_ID=Algolia Application ID
ALGOLIA_ADMIN_API_KEY=Algolia Admin API Key
NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX=generator_relevance

3-1-2. ルートハンドラの作成と、AlgoliaのAPIクライアントの作成

Algoliaにデータを連携するためのルートハンドラを作成します。
エンドポイントが /api/algolia/sync となるように、app/api/algolia/sync/route.ts を作成します。
このエンドポイントにPOSTリクエストを送った時に、Algoliaにデータを連携させるものとします。

また、AlgoliaのAPIクライアントを作成するために algoliasearch をインストールします。
レコードを追加するために、initIndex を実行し、インデックスオブジェクトを作成しておきましょう。

少しわかりにくいですが、Algoliaの Creating indices に記載のある通り、initIndexを実行しても新しくインデックスが作成されるわけではありません。
レコードの追加に必要となる、インデックスオブジェクトが作成されます。
app/api/algolia/sync/route.ts
1import type { NextRequest } from 'next/server'
2import algoliasearch from 'algoliasearch'
3
4const algolia = algoliasearch(
5  process.env.NEXT_PUBLIC_ALGOLIA_APPLICATION_ID + '',
6  process.env.ALGOLIA_ADMIN_API_KEY + '',
7)
8
9const index = algolia.initIndex(
10  process.env.NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX + '',
11)
12
13export async function POST(request: NextRequest) {}

3-2. Newtからのデータ取得

Newtで定義した ジェネレーター のコンテンツを取得する getGenerators を実装します。

3-2-1. ジェネレーターの型を定義する

newt-client-js をインストールし、投稿の型 Generator を定義します。

types/generator.ts
1import type { Content, Media } from 'newt-client-js'
2
3export interface Generator extends Content {
4  title: string
5  logo: Media
6  description: string
7  url: string
8  tags: string[]
9  star: number
10}

3-2-2. 環境変数を定義する

環境変数として、以下の値を .env.local に定義します。
検索対象となるモデル・コンテンツが含まれる、スペースUID・AppUID・モデルUID・Newt CDN APIトークンの値を定義して下さい。

.env.local
NEWT_SPACE_UID=スペースUID
NEWT_APP_UID=AppUID
NEWT_MODEL_UID=モデルUID
NEWT_CDN_TOKEN=Newt CDN APIトークン

3-2-3. ジェネレーターのコンテンツを取得する

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

lib/newt.ts
1import 'server-only'
2import { createClient } from 'newt-client-js'
3import { cache } from 'react'
4import type { Generator } from '@/types/generator'
5
6const client = createClient({
7  spaceUid: process.env.NEWT_SPACE_UID + '',
8  token: process.env.NEWT_CDN_TOKEN + '',
9  apiType: 'cdn',
10})
11
12export const getGenerators = cache(async () => {
13  const { items } = await client.getContents<Generator>({
14    appUid: process.env.NEWT_APP_UID + '',
15    modelUid: process.env.NEWT_MODEL_UID + '',
16    query: {
17      description: { fmt: 'text' },
18    },
19  })
20  return items
21})

3-3. Algoliaへのデータ送信

3-1で設定した、AlgoliaのAPIクライアントを利用して送信します。

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

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

app/api/algolia/sync/route.ts
1import type { NextRequest } from 'next/server'
2import algoliasearch from 'algoliasearch'
3import { getGenerators } from '@/lib/newt'
4
5const algolia = algoliasearch(
6  process.env.NEXT_PUBLIC_ALGOLIA_APPLICATION_ID + '',
7  process.env.ALGOLIA_ADMIN_API_KEY + '',
8)
9
10const index = algolia.initIndex(
11  process.env.NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX + '',
12)
13
14export async function POST(request: NextRequest) {
15  try {
16    const generators = await getGenerators()
17    const formattedGenerators = generators.map((generator) => {
18      return {
19        objectID: generator._id,
20        ...generator,
21      }
22    })
23
24    await index.saveObjects(formattedGenerators)
25    return new Response('Success', { status: 200 })
26  } catch (err: any) {
27    return new Response(err?.message, { status: 400 })
28  }
29}

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

データ形式の詳細については、Algoliaの What Is in a Record のドキュメントをご確認ください。

また、saveObjects メソッドを利用して、Algoliaにデータを送信しています。

ローカルサーバーを立ち上げた後、curlコマンドを利用して、以下のようにリクエストを送ってみましょう。

curl -X POST http://localhost:3000/api/algolia/sync

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

また、「Display Preferences」より、画像の読み込み先を設定できます。
logo.src などのように、画像のURLを指定すると、管理画面に表示されるようになります。
algolia17.png

これでAlgoliaにデータを送信することができました。
algolia4.png

「Browse」タブの「Search」に検索ワードを入力すると、リアルタイムで検索結果が変わるのが確認できます。


4. React InstantSearch Hooksを利用したUIの作成

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

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

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

4-1. InstantSearch

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

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

algolia6.jpg

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

.env.local
NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_API_KEY=Algolia Search-Only API Key

以下のようなコードとなります。
Client Components を利用します。
react-instantsearch をインストールしておきましょう。

app/page.tsx
1'use client'
2import algoliasearch from 'algoliasearch/lite'
3import { InstantSearch } from 'react-instantsearch'
4
5const searchClient = algoliasearch(
6  process.env.NEXT_PUBLIC_ALGOLIA_APPLICATION_ID + '',
7  process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_API_KEY + ''
8)
9
10export default function Home() {
11  return (
12    <InstantSearch
13      indexName={process.env.NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX}
14      searchClient={searchClient}
15    >
16      {/* Widgets */}
17    </InstantSearch>
18  )
19}

4-2. SearchBox

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

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

app/page.tsx
import algoliasearch from 'algoliasearch/lite'
import { InstantSearch } from 'react-instantsearch'
import { InstantSearch, SearchBox } from 'react-instantsearch'

// (中略)

export default function Home() {
  return (
    <InstantSearch
      indexName={process.env.NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX}
      searchClient={searchClient}
    >
      {/* Widgets */}
      <SearchBox />
    </InstantSearch>
  )
}

4-3. Hits

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

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

app/page.tsx
import algoliasearch from 'algoliasearch/lite'
import { InstantSearch, SearchBox } from 'react-instantsearch'
import { InstantSearch, SearchBox, Hits } from 'react-instantsearch'
import { Hit } from '@/components/Hit'

// (中略)

export default function Home() {
  return (
    <InstantSearch
      indexName={process.env.NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX}
      searchClient={searchClient}
    >
      <SearchBox />
      <Hits hitComponent={Hit} />
    </InstantSearch>
  )
}

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

components/Hit.tsx
1import { Highlight } from 'react-instantsearch'
2import type { Hit as AlgoliaHit } from 'instantsearch.js'
3import type { Generator } from '@/types/generator'
4
5export function Hit({ hit }: { hit: AlgoliaHit & Generator }) {
6  return (
7    <div>
8      <Highlight attribute="title" hit={hit} />
9      <Highlight attribute="description" hit={hit} />
10    </div>
11  )
12}

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

詳細は、Algoliaの Conditional display in React InstantSearch のドキュメントをご確認ください。
components/NoResults.tsx
1import { useInstantSearch } from 'react-instantsearch'
2
3export const NoResultsBoundary = ({ children, fallback }: any) => {
4  const { results } = useInstantSearch()
5
6  if (!results.__isArtificial && results.nbHits === 0) {
7    return (
8      <>
9        {fallback}
10        <div hidden>{children}</div>
11      </>
12    )
13  }
14
15  return children
16}
17
18export const NoResults = () => {
19  const { indexUiState } = useInstantSearch()
20
21  return (
22    <div className="ais-Hits_Empty">
23      <p>
24        No results for <q>{indexUiState.query}</q>.
25      </p>
26    </div>
27  )
28}

app/page.tsx は以下のようになります。

app/page.tsx
import algoliasearch from 'algoliasearch/lite'
import { InstantSearch, SearchBox, Hits } from 'react-instantsearch'
import { Hit } from '../components/Hit'
import { NoResults, NoResultsBoundary } from '@/components/NoResults'

// (中略)

export default function 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>
  )
}

4-4. PoweredBy

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

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

app/page.tsx
import algoliasearch from 'algoliasearch/lite'
import { InstantSearch, SearchBox, Hits } from 'react-instantsearch'
import { InstantSearch, SearchBox, Hits, PoweredBy } from 'react-instantsearch'
import { Hit } from '@/components/Hit'
import { NoResults, NoResultsBoundary } from '@/components/NoResults'

// (中略)

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

4-5. スタイル

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

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

まとめ

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

app/page.tsx
1'use client'
2import Image from 'next/image'
3import algoliasearch from 'algoliasearch/lite'
4import { InstantSearch, SearchBox, Hits, PoweredBy } from 'react-instantsearch'
5import { Hit } from '@/components/Hit'
6import { NoResults, NoResultsBoundary } from '@/components/NoResults'
7import styles from './page.module.css'
8
9const searchClient = algoliasearch(
10  process.env.NEXT_PUBLIC_ALGOLIA_APPLICATION_ID + '',
11  process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_API_KEY + ''
12)
13
14export default function Home() {
15  return (
16    <div className={styles.Wrapper}>
17      <InstantSearch
18        indexName={process.env.NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX}
19        searchClient={searchClient}
20      >
21        <header className={styles.Header}>
22          <dl>
23            <dt>Newt・Algolia・Next.js Example</dt>
24            <dd>
25              <a
26                href="https://github.com/Newt-Inc/newt-nextjs-algolia"
27                rel="noreferrer noopener"
28                target="_blank"
29              >
30                GitHub
31              </a>
32              <a
33                href="https://www.newt.so/docs/tutorials/search-by-algolia"
34                rel="noreferrer noopener"
35                target="_blank"
36              >
37                Tutorial
38              </a>
39            </dd>
40          </dl>
41          <h1>Static Site Generators 😉</h1>
42          <div className="ais-Search_Wrapper">
43            <SearchBox />
44            <span className="ais-Search_Icon">
45              <Image src="/search.svg" alt="" width="19" height="19" />
46            </span>
47            <PoweredBy className="ais-Search_Logo" />
48          </div>
49        </header>
50        <div className={styles.Container}>
51          <main className={styles.Main}>
52            <NoResultsBoundary fallback={<NoResults />}>
53              <Hits hitComponent={Hit} />
54            </NoResultsBoundary>
55          </main>
56        </div>
57        <footer className={styles.Footer}>
58          <dl>
59            <dt>Newt・Algolia・Next.js Example</dt>
60            <dd>
61              <a
62                href="https://github.com/Newt-Inc/newt-nextjs-algolia"
63                rel="noreferrer noopener"
64                target="_blank"
65              >
66                GitHub
67              </a>
68              <a
69                href="https://www.newt.so/docs/tutorials/search-by-algolia"
70                rel="noreferrer noopener"
71                target="_blank"
72              >
73                Tutorial
74              </a>
75            </dd>
76          </dl>
77        </footer>
78      </InstantSearch>
79      <a
80        href="https://newt.so/"
81        rel="noreferrer noopener"
82        target="_blank"
83        className={styles.Badge}
84      >
85        <Image src="/logo.svg" alt="Newt" width="16" height="13" />
86        <span className={styles.Badge_Text}>Made in Newt</span>
87      </a>
88    </div>
89  )
90}
components/Hit.tsx
1import Image from 'next/image'
2import { Highlight } from 'react-instantsearch'
3import type { Hit as AlgoliaHit } from 'instantsearch.js'
4import type { Generator } from '@/types/generator'
5
6export function Hit({ hit }: { hit: AlgoliaHit & Generator }) {
7  return (
8    <>
9      <div className="ais-Hits-item_Logo">
10        <Image
11          src={hit.logo.src}
12          alt={hit.logo.fileName}
13          width="40"
14          height="40"
15        />
16      </div>
17      <div className="ais-Hits-item_Data">
18        <div className="ais-Hits-item_Header">
19          <h2 className="ais-Hits-item_Name">
20            <a href={hit.url} rel="noreferrer noopener" target="_blank">
21              <Highlight attribute="title" hit={hit} />
22            </a>
23          </h2>
24          <p className="ais-Hits-item_URL">{hit.url}</p>
25        </div>
26        <p className="ais-Hits-item_Description">
27          <Highlight attribute="description" hit={hit} />
28        </p>
29        <div className="ais-Hits-item_Footer">
30          <div className="ais-Hits-item_Tags">
31            {hit.tags.map((tag: string) => {
32              return <span key={tag}>{tag}</span>
33            })}
34          </div>
35          <div className="ais-Hits-item_Star">
36            <Image src="/star.svg" alt="" width="16" height="15" />
37            <span>{hit.star}</span>
38          </div>
39        </div>
40      </div>
41    </>
42  )
43}

以上でシンプルな全文検索を行えるようになりました。
Vercelなどにデプロイすると、本番環境で検索機能を利用できることがわかります。

algolia18.png


5. Newtのデータ更新時に、Algoliaに自動連携する

3のステップでは、エンドポイント api/algolia/sync にPOSTリクエストを送ると、NewtのデータがAlgoliaに連携されていました。
このままでは、データを更新するたびに都度手動でリクエストを送らなければなりません。
ここでは、Newtの管理画面からデータを更新したタイミングで、Webhookを利用して、自動でリクエストを送るよう設定してみましょう。

5-1. Webhookを利用してデータを連携する

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

  • リクエストが有効なものか、クエリパラメータのsecretの値で検証する(ここでは ALGOLIA_SECRET_TOKEN という環境変数を利用する)

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

app/api/algolia/sync/route.ts
(省略)

export async function POST(request: NextRequest) {
  const secret = request.nextUrl.searchParams.get('secret')
  if (secret !== process.env.ALGOLIA_SECRET_TOKEN) {
    return new Response('Invalid token', { status: 401 })
  }

  try {
    const generators = await getGenerators()
    (省略)

クエリパラメータにsecretの値を含め、また3-3で作成したデータ送信処理を呼び出すために、以下のようなURLを指定します。

https://<your-site.com>/api/algolia/sync?secret=<token>

例えば、ドメインが「newt-algolia-example.vercel.app」で、secretが「hogehoge」の場合、https://newt-algolia-example.vercel.app/api/algolia/sync?secret=hogehoge となります。

algolia19.png

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

app/api/algolia/setup/route.ts
1import algoliasearch from 'algoliasearch'
2
3const algolia = algoliasearch(
4  process.env.NEXT_PUBLIC_ALGOLIA_APPLICATION_ID + '',
5  process.env.ALGOLIA_ADMIN_API_KEY + '',
6)
7
8const primaryIndex = algolia.initIndex(
9  process.env.NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX + '',
10)
11
12export async function POST() {
13  try {
14    await primaryIndex.setSettings({
15      searchableAttributes: ['title', 'tags', 'description'],
16    })
17
18    return new Response('Success', { status: 200 })
19  } catch (err: any) {
20    return new Response(err?.message, { status: 400 })
21  }
22}

ローカルサーバーを立ち上げた後、curlコマンドを利用して、以下のようにリクエストを送ってみましょう。

curl -X POST http://localhost:3000/api/algolia/setup

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

algolia7.jpg

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

6-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

これで、デフォルトの並び順が指定されました。
先ほどと並び順が変わったことがわかります。
algolia20.png


7. ソートを追加する

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

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

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

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

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

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

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

algolia2.jpg

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

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

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

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

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

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

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

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

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 としています)。

.env.local
NEXT_PUBLIC_ALGOLIA_REPLICA_INDEX_STAR=generator_star
NEXT_PUBLIC_ALGOLIA_REPLICA_INDEX_TITLE=generator_title

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

algolia9.jpg

7-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',
  ]

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

app/api/algolia/setup/route.ts
import algoliasearch from 'algoliasearch'

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 async function POST() {
  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',
      ],
    })

    return new Response('Success', { status: 200 })
  } catch (err: any) {
    return new Response(err?.message, { status: 400 })
  }
}

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

algolia10.jpg

7-2. ソートUIの追加

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

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

app/page.tsx
import { InstantSearch, SearchBox, Hits, PoweredBy } from 'react-instantsearch'
import {
  InstantSearch,
  SearchBox,
  Hits,
  PoweredBy,
  SortBy,
} from 'react-instantsearch'

// (中略)

export default function Home() {
  return (
    <div className={styles.Wrapper}>
      <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>

      // (中略)

  )
}

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


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

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

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

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

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

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

algolia3.jpg

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

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

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

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

app/api/algolia/setup/route.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

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

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

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

app/page.tsx
import {
  InstantSearch,
  SearchBox,
  Hits,
  PoweredBy,
  SortBy,
  RefinementList,
} from 'react-instantsearch'

// (中略)

export default function Home() {
  return (
    <div className={styles.Wrapper}>
      <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>

      // (中略)

  )
}

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

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

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

NewtMade in Newt