AlgoliaとNext.js (App Router) を利用して、高度な全文検索を実現する
Table of contents
- 記事内で使用している主なソフトウェアのバージョン
- 前提条件
- 概要
- 完成時のコード
- 1. Algoliaのセットアップ
- 1-1. アプリケーションの作成
- 1-2. インデックスの作成
- 2. Newtのセットアップ
- 2-1. ジェネレーターモデルの作成
- 2-2. ジェネレーターコンテンツの入稿
- 3. 検索対象のデータをAlgoliaに連携する
- 3-1. AlgoliaのAPIクライアントのセットアップ
- 3-2. Newtからのデータ取得
- 3-3. Algoliaへのデータ送信
- 4. React InstantSearch Hooksを利用したUIの作成
- 4-1. InstantSearch
- 4-2. SearchBox
- 4-3. Hits
- 4-4. PoweredBy
- 4-5. スタイル
- まとめ
- 5. Newtのデータ更新時に、Algoliaに自動連携する
- 5-1. Webhookを利用してデータを連携する
- 5-2. データ連携の方針
- 6. 全文検索をカスタマイズする
- 6-1. 検索に利用するフィールドの指定と優先順位付け
- 6-2. デフォルトの並び順の指定
- 7. ソートを追加する
- 7-1. インデックスの追加(レプリカの作成と設定)
- 7-2. ソートUIの追加
- 8. ファセット検索を追加する
- 8-1. ファセット検索で利用する属性の指定
- 8-2. ファセット検索UIの追加
このチュートリアルでは、Algolia と Next.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
前提条件
- Algoliaにサインアップしていること
- Next.jsの Route Handlers について理解していること
- Newt CDN APIと newt-client-js を利用して、公開済みのコンテンツを取得する方法について理解していること
概要
以下の4ステップにわけて、チュートリアルを進めていきます。
- 全文検索を実現する
- 全文検索をカスタマイズする
- ソートを追加する
- ファセット検索を追加する
最終的に、以下の検索ページを作成します。
各コンポーネントの機能は以下の通りです。
作成したページは公開しています。実際に機能を触ってご確認いただくことも可能です。
※データについては、デモ用のデータなので、正確でないものもあります。
https://newt-nextjs-algolia.vercel.app/
完成時のコード
また、完成時のコードを以下に公開しています。実装時の参考として、ご覧いただけます。
Newt-Inc/newt-nextjs-algolia
1. Algoliaのセットアップ
1-1. アプリケーションの作成
Algoliaにサインアップしたら、このチュートリアルで利用するアプリケーションを1つ用意しておきましょう。
Settings > Applications から「Create Application」で新しくアプリケーションを作成します。
内容はお好きなように設定いただいて構いませんが、ここでは以下の内容で作成します。
- NAME YOUR APPLICATION(アプリケーション名):
Static Site Generators
- CHOOSE YOUR SUBSCRIPTION(サブスクリプションプラン):
Build(FREE)
- Data Center(データセンターの場所):
US West
1-2. インデックスの作成
アプリケーションを作成すると、インデックスの作成に進みます。
ここでは、generator_relevance
という名前でインデックスを作成しておきます。
インデックスを作成後「Import your records」の表示がありますが、後述のステップで実行するので、ここではまだ行いません。
2. Newtのセットアップ
検索対象となるモデル・コンテンツを用意しておきましょう。
2-1. ジェネレーターモデルの作成
このチュートリアルでは、静的サイトジェネレータの検索ページを作成します。静的サイトジェネレータとして、以下の情報を持つモデルを作成しましょう。
モデル: ジェネレーター
フィールド名 | フィールドID | フィールドタイプ | オプション |
---|---|---|---|
タイトル | title | テキスト | 必須 |
ロゴ | logo | 画像 | 必須 |
説明 | description | マークダウン | 必須 |
URL | url | テキスト | 必須 |
タグ | tags | 選択(子要素: テキスト) | 必須・複数値 |
スター | star | 数字 | 必須 |
2-2. ジェネレーターコンテンツの入稿
作成したジェネレーターモデルにコンテンツを入稿します。
ここでは、内容の正確性は問題ではないので、適当に入力いただいて構いません。
※ 内容にこだわりたい方は、Site Generators の内容などを参考に入力してみましょう。
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」のページから確認できます。
上記で確認した、AlgoliaのApplication IDとAdmin API Keyの値を環境変数として .env.local
に追加します。
また、1-2で作成したインデックスの名前を NEXT_PUBLIC_ALGOLIA_PRIMARY_INDEX
として定義します。
※ ALGOLIA_ADMIN_API_KEY
については、後述するクライアントサイドでの処理から参照しないため、NEXT_PUBLIC_
を外します。
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
を実行し、インデックスオブジェクトを作成しておきましょう。
レコードの追加に必要となる、インデックスオブジェクトが作成されます。
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
を定義します。
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トークンの値を定義して下さい。
NEWT_SPACE_UID=スペースUID
NEWT_APP_UID=AppUID
NEWT_MODEL_UID=モデルUID
NEWT_CDN_TOKEN=Newt CDN APIトークン
3-2-3. ジェネレーターのコンテンツを取得する
ジェネレーターコンテンツを取得する getGenerators
を実装します。
description
フィールドはマークダウンタイプであるため、デフォルトではHTMLの値が返却されますが、ここではテキスト形式でデータを受け取るものとします。
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クライアントを利用して送信します。
詳細については、Algoliaの Send and update your data のドキュメントをご確認ください。
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
を設定しています。
また、saveObjects メソッドを利用して、Algoliaにデータを送信しています。
ローカルサーバーを立ち上げた後、curlコマンドを利用して、以下のようにリクエストを送ってみましょう。
curl -X POST http://localhost:3000/api/algolia/sync
データが連携されると、Algoliaの管理画面で、以下のようにインデックスが表示されます。
また、「Display Preferences」より、画像の読み込み先を設定できます。
logo.src
などのように、画像のURLを指定すると、管理画面に表示されるようになります。
これでAlgoliaにデータを送信することができました。
「Browse」タブの「Search」に検索ワードを入力すると、リアルタイムで検索結果が変わるのが確認できます。
4. React InstantSearch Hooksを利用したUIの作成
Algoliaでは、検索インターフェースを素早く構築するために、いくつかのライブラリが用意されています。
このチュートリアルでは、Next.jsを利用するため、React InstantSearch を利用します。他にも Vue InstantSearch や Angular InstantSearch などがあります。
ここでは、React InstantSearchで定義済みのUIコンポーネントを利用して、検索画面を作成します。
InstantSearch・SearchBox・Hits・PoweredBy の4つを利用します。順に説明します。
4-1. InstantSearch
InstantSearch はReact InstantSearchを使い始めるためのルートコンポーネントです。
引数として、indexName
と searchClient
を渡します。
searchClientにはAlgoliaの Application ID
と Search-Only API Key
を渡します。
Application ID
は、3-1-1で設定した環境変数を利用して指定します。
Search-Only API Key
は、Algoliaの管理画面に入り、「API Keys」のページから確認できます。
確認した、Search-Only API Key
の値を環境変数として追加します。
NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_API_KEY=Algolia Search-Only API Key
以下のようなコードとなります。
Client Components を利用します。
react-instantsearch をインストールしておきましょう。
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
を以下のように修正します。
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
を以下のように修正します。
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
属性をハイライトしたい場合、以下のように記載します。
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()
フックを使用します。
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
は以下のようになります。
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
を以下のように修正します。
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.css や page.module.css のファイルをご確認下さい。
まとめ
ヘッダーやフッター、スタイルも追加して、各ファイルは以下のように定義します。
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}
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などにデプロイすると、本番環境で検索機能を利用できることがわかります。
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
を以下のように修正します。
(省略)
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
となります。
5-2. データ連携の方針
Algoliaでは、データの変更に合わせてインデックスを最新に保つ必要があります。インデックスを更新する方法としては、以下の3つの方法が考えられます。
- Full reindexing
- Full record updates
- Partial record updates
このチュートリアルでは saveObjects のメソッドを利用して、2のFull record updatesの形式でデータを同期していますが、ユースケースに応じて、適切なデータ連携方針を選択するようご注意ください。
6. 全文検索をカスタマイズする
次に、全文検索のカスタマイズを行います。具体的には以下のことを行います。
- 検索に利用するフィールドの指定と優先順位付け
- デフォルトの並び順の指定
6-1. 検索に利用するフィールドの指定と優先順位付け
ここまでは、すべてのフィールドを検索対象としていましたが、指定したフィールドのみが検索対象となるように設定を行います。
Algoliaでは、どのフィールドを検索対象に含めるか設定できるため、URLやロゴなど、表示のみに使用するフィールドは検索対象から除外することができます。
また、どのフィールドとマッチした場合に、関連性が高いと判断するか、明示的に指定できます。
ここでは、タイトル・タグ・説明のどれかと一致する場合、検索結果として表示することとし、優先順位は「タイトル > タグ > 説明」の順番とします。
検索対象の属性の指定は、ダッシュボード経由でもAPI経由でもできますが、ここではAPI経由で指定します。Next.jsのRoute Handlersを利用します。
API経由で指定する場合、インデックスに searchableAttributes という属性を設定します。
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
が以下のように表示されます。
これで、検索対象フィールドの指定と、優先順位付けができました。
6-2. デフォルトの並び順の指定
続いて、デフォルトの並び順を指定します。これを指定することで、検索文字列が入力されていなかった場合や、同じ関連度の場合の並び順が決定します。
ここでは、スターの降順で表示することとします。
API経由で指定する場合、インデックスに customRanking という属性を設定します。
api/algolia/setup.ts
を以下のように修正します。
await primaryIndex.setSettings({
searchableAttributes: ['title', 'tags', 'description'],
customRanking: ['desc(star)'],
})
このメソッドを実行すると、Algoliaの管理画面の Ranking and Sorting
にカスタムランキングが追加されます。
これで、デフォルトの並び順が指定されました。
先ほどと並び順が変わったことがわかります。
7. ソートを追加する
次に、ソートの機能を追加します。具体的には以下のことを行います。
- インデックスの追加(レプリカの作成と設定)
- ソートUIの追加
Algoliaでは検索結果を明示的にソートすることが可能です。
大きく2タイプのソートが提供されていて、Exhaustive sorting(指定された属性に基づく厳密なソート) と Relevant sorting(関連性によるソート) があります。
ここでは、Exhaustive sortingを利用して、指定された属性の値をもとに、ソートできるようにします。スターの降順ソートと、タイトルの昇順ソートを追加してみましょう。
最終的には、2で設定したソートに加え、以下の3つの選択肢からソート順を選べるようにします。
- Relevance(関連性によるソート。6で設定したもの)
- GitHub Stars(スターの降順)
- Title(タイトルの昇順)
7-1. インデックスの追加(レプリカの作成と設定)
Algoliaでは同じデータに対して、異なるランキングを提供する場合、それぞれ異なるインデックスを使用する必要があります。
追加されたインデックスは、レプリカと呼ばれます。
レプリカの作成・設定は、ダッシュボード経由でもAPI経由でもできますが、ここではAPI経由で指定します。Next.jsのAPIルートを利用します。
7-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_star
・generator_title
としています)。
NEXT_PUBLIC_ALGOLIA_REPLICA_INDEX_STAR=generator_star
NEXT_PUBLIC_ALGOLIA_REPLICA_INDEX_TITLE=generator_title
このメソッドを実行し、Algoliaの管理画面を確認すると、以下のようにインデックス(レプリカ)が追加されていることがわかります。
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
を以下のように修正します。
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
にカスタムランキングのみが設定されていることがわかります。
7-2. ソートUIの追加
SortBy を利用します。
7-1で追加したインデックスをSortByに渡します。
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種類のソート方法を選択できるようになりました。
8. ファセット検索を追加する
最後に、ファセット検索を追加します。具体的には以下のことを行います。
- ファセット検索で利用する属性の指定
- ファセット検索UIの追加
Algoliaのファセットを利用すると、選択した属性のグループに対してカテゴリーを作成し、ユーザーが検索結果を絞り込めるようになります。
ここでは、タグの情報をもとに、ユーザーが結果をフィルタできるようにします。
8-1. ファセット検索で利用する属性の指定
ファセット検索を利用するためには、事前に利用する属性を指定しておく必要があります。これは、ダッシュボード経由でもAPI経由でもできますが、ここではAPI経由で指定します。
attributesForFaceting を利用して、タグの情報がファセット検索で利用できるように指定します。
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の管理画面を確認すると、各インデックスの Facets
で tags
の属性が設定されていることがわかります。
8-2. ファセット検索UIの追加
RefinementList を利用します。
表示するファセットは最大10個、ファセットの順番は ['count:desc', 'name:asc']
とします。
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>
// (中略)
)
}
これで、ファセット検索ができるようになりました。
以上で、すべてのステップが終了となります。
ここまでで説明したコードは、すべて Newt-Inc/newt-nextjs-algolia に公開しています。
もし、どこかわかりにくいところがあれば、こちらのコードも参考にしていただければと思います。
Algoliaはここで紹介した以外にも様々な機能が充実しているので、ぜひ様々な機能を試してみてください。