Next.js + Algoliaで全文検索UIを実装する
昨日の記事でContentfulの記事をAlgoliaのインデックスに登録したので、今回はアプリケーション側での実装方法についてまとめる。
このブログはNext.jsで構築しているので、検索UIの実装にはreact-instantsearchを利用した。
Algolia管理画面での設定
前回のおさらいになるが、インデックスは↓として登録している。
{
url: "記事URL",
title: "タイトル",
description: "記事概要",
content: "MarkdownをPlainTextに変換した記事本文",
objectID: "Contentfulの記事ID",
}
Indices => Configuration => Searchable attributes で検索対象とする属性を設定する。
今回は title
と description
を検索対象とした。
また、Indices => Configuration => Language で言語をJapaneseに設定しておいた。
- Index Languages: Japanese
- Query Languages: Japanese
検索UIの実装
next.js/examples/with-algolia-react-instantsearch を参考にしつつ実装を行った。
- 必要ライブラリのインストール
yarn add algoliasearch react-instantsearch-dom instantsearch.css
yarn add -D @types/react-instantsearch-dom
lib/algolia.ts
import { MultipleQueriesQuery } from '@algolia/client-search'
import algoliasearch from 'algoliasearch/lite'
const algoliaSearchApiKey = process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY || ''
const appId = process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_APP_ID || ''
const algoliaClient = algoliasearch(appId, algoliaSearchApiKey)
export const indexName = process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_INDEX_NAME || ''
export const minQueryLength = 3
export const searchClient = {
search(requests: MultipleQueriesQuery[]) {
// コンポーネント表示時に検索を実行しないように
// 検索文字がminQueryLength未満の場合は実行しないように
// https://www.algolia.com/doc/guides/building-search-ui/going-further/conditional-requests/react/
if (
requests.every(({ params }: MultipleQueriesQuery) => {
return !params?.query || params.query.length < minQueryLength
})
) {
return Promise.resolve({
results: requests.map(() => ({
hits: [],
nbHits: 0,
nbPages: 0,
page: 0,
processingTimeMS: 0,
})),
})
}
return algoliaClient.search(requests)
},
}
デフォルトのSearchClientだとコンポーネント表示時に空文字列で検索を実行してしまうため、Conditional Requestsを実装した。
ついでに検索文字がminQueryLength未満の場合は実行しないようにしておいた。
components/Search.tsx
import { useRouter } from 'next/router'
import React, { useCallback, useEffect, useState } from 'react'
import { Hit } from 'react-instantsearch-core'
import {
Configure,
connectSearchBox,
Highlight,
Hits,
InstantSearch,
PoweredBy,
SearchBox,
} from 'react-instantsearch-dom'
import { indexName, minQueryLength, searchClient } from '@/lib/algolia'
type HitDoc = {
url: string
title: string
content: string
description?: string
objectID: string
}
const HitComponent = ({ hit, onClick }: { hit: Hit<HitDoc>; onClick: () => void }) => {
const router = useRouter()
const clickHandler = (url: string) => {
onClick()
router.push(url)
}
return (
<a
href={hit.url}
className="w-full cursor-pointer"
onClick={(e) => {
e.preventDefault()
clickHandler(hit.url)
}}
>
<div>
<Highlight className="text-gray-800" attribute="title" hit={hit} />
</div>
<div>
<Highlight className="mt-2 text-gray-500" attribute="description" hit={hit} />
</div>
</a>
)
}
const SearchResult = connectSearchBox(({ refine, currentRefinement }) => {
const [isShow, shouldShow] = useState(false)
useEffect(() => {
shouldShow(!!currentRefinement)
}, [currentRefinement, shouldShow])
const handleResetSearchWords = useCallback(() => {
refine('')
}, [refine])
if (!isShow) return null
return (
<div className="bg-white rounded-sm">
<Hits
hitComponent={({ hit }: { hit: Hit<HitDoc> }) => (
<HitComponent hit={hit} onClick={handleResetSearchWords} />
)}
/>
{currentRefinement.length >= minQueryLength && (
<div className="flex justify-end p-2 dark:text-gray-800">
<PoweredBy />
</div>
)}
</div>
)
})
export const Search = () => {
return (
<>
<InstantSearch indexName={indexName} searchClient={searchClient}>
<Configure hitsPerPage={50} />
<div className="flex justify-center md:justify-end">
<div className="w-full md:w-1/4">
<SearchBox translations={{ placeholder: 'Search' }} />
</div>
</div>
<div className="mt-1">
<SearchResult />
</div>
</InstantSearch>
</>
)
}
Linkでページ遷移を行った場合はページ遷移後も検索結果が表示されたままになってしまうため、下記記事を参考にさせていただきクリック時に検索文字列をリセットすることにした。
また、Algoliaを無料プランで利用する場合はPoweredByの表示が必須とのことだったので、検索結果画面に表示するようにしている。
pages/_app.tsx
satellite-min.css
をimportして検索機能のcssを設定する。
import 'instantsearch.css/themes/satellite-min.css'
import '@/styles/tailwind.css'
import '@/styles/algolia.css'
Tailwind CSSと組み合わせた際に、↓のようにiOS実機で入力フォームの表示が角丸になる現象が発生した。
そこで、一部のcssを上書きして調整した。
/* styles/algolia.css */
.ais-SearchBox-form {
background-color: transparent;
}
.ais-SearchBox-input {
-webkit-appearance: none;
box-shadow: none;
}
これでNext.js + Algoliaでの全文検索UIが実装できた。