はじめに
Gatsby ブログに検索機能を付けたく、いくつかやりかたを調べてみた。
- Google カスタム検索を使う
- Algolia という有名な検索サービス(SaaS)を使う
- サーバを立てて、Elasticsearch などの有名な検索ソフトウェアを入れる(製品は色々と出ている…奥が深い)
このうち、今回は
Algolia という有名な検索サービス(SaaS)
を使って実装する。 本記事では実装した内容をメモする。できあがりイメージ
出来上がりイメージはこのような形となる。
- 調べたい語句を入れる。
- Enter キーを押す or 虫眼鏡マークをクリックすると、検索結果が表示される。
- 結果をクリックすると、該当ページに飛ぶ。
できること
記事のタイトル、descrpition
、タグの値を検索に用いて全記事からキーワード検索する。
できないこと
記事の本文を検索対象に入れることはできなかった。(やり方はあるのかもしれない)
モチベーション
このブログは個人的に勉強したことのメモの側面があるため、自分の過去の記憶を頼りに記事を探したい。検索機能が必要となる。
はてなブログの時には良い感じに全文検索機能を提供してくれていた。
Algolia とは
以下のような特徴がある検索サービスとのこと。
- 全文検索サービスを SaaS として提供
- GUI から検索ロジックを柔軟に設定可能
- レコード数/API アクセス数による従量課金
- 世界各地にデータセンターがあり、どのロケーションでも高速な検索が可能
- 様々なプログラミング言語向けに API を提供
- 様々なフロントエンドフレームワーク向けに便利なライブラリを提供
料金体系は以下のようになっていた。
Algolia Pricing | Pay As You Go | Site Search Pricing | Algolia
ひとまず月に 10,000 検索までは無料と思われる。
機能分担のイメージ
このブログは Gatsby Cloud 上でビルド、ホスティングしている。 検索を行うには、検索対象のキーワードや文章を取り込んで、検索用のインデックスデータを作成し、検索時にはそこを参照する。
インデックスデータはAlgolia側のサーバにあるため、ブログの内容に変更が入るたびに、Algolia側のサーバにデータを送信する仕組みが必要となる。
Gatsby.jsには便利なgatsby-plugin-algolia
というプラグインがあり、これを使えばビルドする際に Algolia 側にデータをプッシュしてくれる。
機能分担イメージ(間違っていたらすみません)
実施内容
- 基本的には、Gatsby 公式サイトの手順通りに実施した。
- ただし公式サイトのようにキーを打つごとに検索する仕様にするとクエリ発行量が必要以上に増えそうなので、エンターキー or 検索ボタンを押したときにのみ検索する仕様に変更した。
- 検索結果へのリンクがおかしかったので、修正した。
プラグインインストール
データを Algolia 側に渡すためのプラグインをインストールする。
npm install gatsby-plugin-algolia
gatsby-config.js
のplugin
配列の中に、以下の通り追加する。
{
resolve: `gatsby-plugin-algolia`,
options: {
appId: process.env.GATSBY_ALGOLIA_APP_ID,
apiKey: process.env.ALGOLIA_ADMIN_KEY,
queries: require("./src/utils/algolia-queries"),
},
},
ここで出てくる./src/utils/algolia-queries
は後ほど作成する。
また、gatsby-config.js
の先頭に以下の記載を追加しておく。環境変数を設定した.env
ファイルを読み込む設定。
require("dotenv").config()
Algolia サービスに登録・API キー取得
Algoliaのサイトでアカウントを作成する。GitHub のアカウントでログインできた。
「Settings」⇒「API Keys」と遷移し、API Key を取得する。
これを.env
ファイルに設定する。
GATSBY_ALGOLIA_APP_ID=<確認した値>
GATSBY_ALGOLIA_SEARCH_KEY=<確認した値>
ALGOLIA_ADMIN_KEY=<確認した値>
.env
ファイルが.gitignore
に入っていることを確認する。
.gitignore
に入っていないと、流出してはいけないAdmin API Keyを GitHub に上げてしまうので注意。
# dotenv environment variable files
.env*
データをAlgoliaに送信するためのソースコードの作成
algolia-queries.js
src/utils/algolia-queries.js
をほぼGatsby.jsの公式ページのとおり記載する。
※勉強しきれていないので、変にいじらない。
ただし、GraphQL のクエリ部分は各々変える必要がある。私の場合、マークダウンの変換にはgatsby-plugin-mdx
を使っているため、以下のようなクエリとなる。
また、検索に加えたいフィールドはここで取得しておく必要がある。id
も取得しておく必要がある。
記事作成時にマークダウン記事の情報を取得するクエリなどを参考にしながら作ればよい。
const escapeStringRegexp = require("escape-string-regexp")
const pagePath = `content`
const indexName = `Pages`
const pageQuery = `{ pages: allMdx( filter: { fileAbsolutePath: { regex: "/myblog/" } , } ) { edges { node { frontmatter { slug title description tags } id } } }}`function pageToAlgoliaRecord({ node: { id, frontmatter, fields, ...rest } }) {
return {
objectID: id,
...frontmatter,
...fields,
...rest,
}
}
const queries = [
{
query: pageQuery,
transformer: ({ data }) => data.pages.edges.map(pageToAlgoliaRecord),
indexName,
settings: { attributesToSnippet: [`excerpt:20`] },
},
]
module.exports = queries
ここまで作ったら試しにビルドしてみて、Algolia の「Index」ページで値が取得できていることが確認できればOK。
検索用コンポーネントの作成
検索用コンポーネントを作成していく。
プラグインインストール
ここは公式ページ通り。 プラグインのインストールをする。
npm install react-instantsearch-dom algoliasearch styled-components gatsby-plugin-styled-components @styled-icons/fa-solid
gatsby-config.js
のplugin
配列の中に、以下の通り追加する。
`gatsby-plugin-styled-components`,
SearchBox の作成
src/components/search-algolia/search-box.js
を作成する。
公式ページどおり作成してももちろん問題ないが、キーを打つごとに検索するとクエリ発行量が必要以上に増えそうなので、エンターキー or 検索ボタンを押したときにのみ検索する仕様に変更した。
import React from "react"
import { connectSearchBox } from "react-instantsearch-dom"
import { Search as SearchIcon } from "@styled-icons/fa-solid"
import { IconButton } from "@chakra-ui/react"
const SearchBox = ({ refine, currentRefinement, className, onFocus }) => {
const [value, setValue] = React.useState("")
return (
<form className={className}>
<IconButton
icon={<SearchIcon className="SearchIcon" />}
onClick={(e) => { e.preventDefault() refine(value) }} ></IconButton>
<input
className="SearchInput"
type="text"
placeholder="記事検索..."
aria-label="Search"
onFocus={onFocus}
name="search-input"
value={value} onChange={(e) => setValue(e.target.value)} onKeyDown={(e) => { if (e.code == "Enter") { e.preventDefault() refine(value) } }} />
</form>
)
}
export default connectSearchBox(SearchBox)
connectSearchBox
の解説
connectSearchBox
というのが、自分で UI を決めた検索ボックスを作りたいときに利用できる関数で、引数にはSearchBox
関数を取る。
引数に設定する関数SearchBox
は、refine
という props を持つ。
refine
は、「Algolia に投げるクエリを更新する」という役割を持っている。
ここでは、
<IconButton>
(Chakra ui で提供される、<button>
の拡張版のようなもの)に対してonClick
イベントとしてrefine(value)
を渡す。<input>
エリアのonKeyDown
イベントにも、refine(value)
を渡す。
とすることで、検索の意志があるときのみクエリを発行するようにした。
ちなみに、他の要素も忘れないようメモしておく。
- 検索と同時に読み込みなおされるのを防ぐために
e.preventDefault()
を設定しておく必要がある。 const [value, setValue] = React.useState("")
やonChange={(e) => setValue(e.target.value)}
は、一般的に React でフォームを作るときにやるやり方。e.code == "Enter"
とすることで Enter キーが押されたときの処理を記載できる。e.code
にどんな値が入るかは、https://developer.mozilla.org/ja/docs/Web/API/KeyboardEvent/code から調べられる。
※この部分は結構苦労した。。
SearchResults(検索結果)の作成
公式ページほぼそのままコピペだが、一点だけ、このブログに合わず不具合となった部分があるため修正をしている。
検索結果をクリックすると該当の記事に飛ぶようにするため、<Link>
の先にslug
を指定している部分があるが、ここは
<Link to={hit.slug}>
とすると誤りで、
<Link to={`/${hit.slug}`}>
とする必要がある。
このブログではすべてのページの上部に検索ボックスを表示するため、誤りのパターンで記載するとslug
が相対パスとなってしまう。
(例えば、https://bunsugi.com/about
ページで検索を行なって、slug
がgatsby-blog-algolia
の記事に飛ぼうとした場合に、https://bunsugi.com/about/gatsby-blog-algolia
に向かってしまう。)
import { Link } from "gatsby"
import { default as React } from "react"
import {
connectStateResults,
Highlight,
Hits,
Index,
Snippet,
PoweredBy,
} from "react-instantsearch-dom"
const HitCount = connectStateResults(({ searchResults }) => {
const hitCount = searchResults && searchResults.nbHits
return hitCount > 0 ? (
<div className="HitCount">
{hitCount} result{hitCount !== 1 ? `s` : ``}
</div>
) : null
})
const PageHit = ({ hit }) => (
<div>
<Link to={`/${hit.slug}`}> <h4>
<Highlight attribute="title" hit={hit} tagName="mark" />
</h4>
</Link>
<Snippet attribute="excerpt" hit={hit} tagName="mark" />
</div>
)
const HitsInIndex = ({ index }) => (
<Index indexName={index.name}>
<HitCount />
<Hits className="Hits" hitComponent={PageHit} />
</Index>
)
const SearchResult = ({ indices, className }) => (
<div className={className}>
{indices.map(index => (
<HitsInIndex index={index} key={index.name} />
))}
<PoweredBy />
</div>
)
export default SearchResult
大元の部品を作成(index.js)
ページコンポーネントから実際に呼び出すコンポーネントを作成する。
ここは公式ページそのままなので記載を省略する。
ファイル名:
src/components/search-algolia/index.js
Adding Search with Algolia | Gatsby
stlye 調整コンポーネントの作成
公式ページを参考に以下 4 つのファイルを作成する。コピペした部分は記載を省略する。
ファイル名:
src/components/search/use-click-outside.js
- 役割:検索ボックスの外をクリックしたらウインドウを閉じる
ファイル名:
src/components/search/styled-search-root.js
- 役割:検索ボックスの全体的なスタイリング。
ファイル名:
src/components/search/styled-search-result.js
- 役割:検索ボックス(結果部分)のスタイリング。
ファイル名:
src/components/search/styled-search-box.js
- 役割:検索ボックス(検索部分)のスタイリング。
styled-search-box.js
については1行だけ自分の好みで変えている。
元々は虫眼鏡マークを押すとインプットエリアが開く仕様だったが、わかりにくいので常に開いておくようにした。
import styled, { css } from "styled-components"
import SearchBox from "./search-box"
const open = css`
width: 10em;
background: ${({ theme }) => theme.background};
cursor: text;
margin-left: -1.6em;
padding-left: 1.6em;
`
const closed = css`
width: 0;
background: transparent;
cursor: pointer;
margin-left: -1em;
padding-left: 1em;
`
export default styled(SearchBox)`
display: flex;
flex-direction: row-reverse;
align-items: center;
margin-bottom: 0;
.SearchInput {
outline: none;
border: ${({ hasFocus }) => (hasFocus ? "auto" : "none")};
font-size: 1em;
transition: 100ms;
border-radius: 2px;
color: ${({ theme }) => theme.foreground};
::placeholder {
color: ${({ theme }) => theme.faded};
}
${({ hasFocus }) => (hasFocus ? open : open)} }
.SearchIcon {
width: 1em;
margin: 0.3em;
color: ${({ theme }) => theme.foreground};
pointer-events: none;
}
`
// ${({ hasFocus }) => (hasFocus ? open : closed)}
コンポーネントから呼び出し
<Search>
コンポーネントを作成したので、そこから呼び出す。
import * as React from "react"
import SearchAlgolia from "../search-algolia"
import { Box } from "@chakra-ui/react"
const searchIndices = [{ name: `Pages`, title: `Pages` }]
const Search = () => {
return (
<Box mx={5}>
<SearchAlgolia indices={searchIndices} /> </Box>
)
}
export default Search
本番環境への環境変数設定
.env
ファイルはGitHubにプッシュしないので、GatsbyCloud側に環境変数を設定する。
「Site Settings」から設定できる。
まとめ
- Gatsby ブログに Algolia による検索を導入した。
残課題
- 本文まで含めた検索ができていないので、運用上困るようであれば考える。
frontmatter
にkeywords
を作っても良いかもしれない。