Gatsby+microCMSサイトにAlgolia全文検索機能を実装
経緯と調査
Bing Custom Searchの設置
サイト内検索機能として自社サイトにBing Custom Searchを設置(サイト内検索)のとおりBing Custom Searchを埋め込む予定だった。しかし、<Helmet />
やhtml.js、Iflamely、様々なことを試したが駄目だった。表示されない。
Markdownファイルを検索用データにする
以下のコマンドでマークダウン用のプラグインを追加、
$ yarn add gatsby-transformer-remark
そしてマークダウンファイルを作りローカルに保存。プラグインでそれを読み込んで検索するやり方がある。悪くない。しかし全文検索でなくキーワードに登録した単語での検索になる。マークダウンファイルの準備も必要。
参考:
Gatsby製ブログでサイト内検索を実装しました
Gatsby と Netlify で Jamstack 構成のブログサイトを作ろう
JS Searchで実装
たぶん、同じくキーワードでの検索でありマークダウンファイルを準備しなければならない。
参考:
GatsbyJS 検索機能を実装する(JsSearchを利用)
Algoliaで実装
Algoliaの実装は「Angular+Firebase+Algoliaで全文検索機能搭載方法 」でいちどやった。それなりに大変だったので忌避していたが、ちょっと調べたらGatsby用のプラグインあり。しかも「…The index will be synchronised with the provided index name on Algolia on the build step in Gatsby. 」
Gatsbyビルド時に自動的にAlgoliaのIndexを作成する⁉
えっえっ
ということでAlgoliaできまり.
まずはバックエンド側
Algoliaダッシュボードでの作業
ログインできる状態にしておくだけ。
(下画面↓はすでにある別サイトのインデックススタティクス)
Gatsby側の作業
プラグインをインストール
$ yarn add gatsby-plugin-algolia react-instantsearch-dom dotenv
.envファイルを作成。
プロジェクトルートに.env.productionファイルと.env.developmentファイルを作成。
それぞれに以下のように記述。(以下のキー名は例)。
GATSBY_ALGOLIA_APP_ID = KA4OJA9KAS
GATSBY_ALGOLIA_SEARCH_KEY=lkjas987ef923ohli9asj213k12n59a
ALGOLIA_ADMIN_KEY = lksa09sadkj1230asd09dfvj12309aj
各キーはAlgoliaダッシュボードのAPI Keysのページにある。
gatsby-config.jsを編集
以下のようにプラグインのコンフィグとクエリを記述。
// gatsby-config.js
require('dotenv').config({
path: `.env.${process.env.NODE_ENV}`,
});
module.exports = {
siteMetadata: {
title: `ほげほげサイト`,
titleTemplate: `%s · ほげほげサイト`,
・・・略・・・
},
plugins: [
・・・略・・・
{
resolve: `gatsby-plugin-algolia`,
options: {
appId: process.env.GATSBY_ALGOLIA_APP_ID,
apiKey: process.env.ALGOLIA_ADMIN_KEY,
indexName: "hoge-site",
queries: [
{
query: `{
allMicrocmsArticles {
edges {
node {
num
title
body
category {
name
}
}
}
}
}`,
transformer: ({ data }) => data.allMicrocmsArticles.edges.map(({ node }) => {
return {
id: node.category[0].name + '-article/' + node.num,
body: sumarrize(node.body),
title: node.title,
category: node.category[0].name
}
})
},
],
chunkSize: 100000,
settings: {
queryLanguages: ['ja']
},
},
},
],
}
・indexNameは好きな名前。
・IDはAlgoliaのレコードにつけられる一意ID。一意になればなんでもいい。microCMSが自動アサインした記事IDでいい。わたしは好みの問題でスラグと同一になるようにした。またそうすればそのIDがLink先としてそのまま利用できる。
・記事ページの本文(長~い)をインデックスしたいのでチャンクサイズを100000にした。デフォルトは10000
・GitのマニュアルにはallSitePageで全ページ取得できるみたいに書いてあるがタイトルやコンテンツ(本文)のデータがそこにはない。allMarkdownRemarkにもない。わからない。静的ページもすべてインデックスしたかったがとりあえずmicroCMSでアップした動的ページだけでも、ということでallMicrocmsArticlesをインデックスするようにした。このへんはhttp://localhost:8000/___graphql ↓を見ながら試行錯誤。
Algoliaにインデックス
あとはgatsby buildするだけで……
おやエラー(゚Д゚)
サイズが大きすぎる、というエラー。
無料枠だと10kbが上限らしい。
これは仕方ないので、
// gatsby-config.js
・・・略・・・
transformer: ({ data }) => data.allMicrocmsArticles.edges.map(({ node }) => {
return {
id: node.category[0].name + '-article/' + node.num,
body: sumarrize(node.body), // ← 3000字以内にトリミング
title: node.title,
}
})
},
],
chunkSize: 100000,
},
},
],
}
// トリミングする関数
let striptags = require('striptags');
function sumarrize(html) {
const metaDescription = striptags(html).replace(/\r?\n/g, '').trim();
return metaDescription.length <= 3000
? metaDescription
: metaDescription.slice(0, 3000) + '...';
}
こんなふうにsumarrize()関数作って記事本文(body部分)が3000字以内に収まるようにトリミング。GraphQLの操作でもっとエレガントなやり方があると思うがわからない。
これでgatsby buildする。
40記事分が自動でほとんど時間かからずインデックス作成される。
Algoliaダッシュボード上でテストできる。リアルタイムに検索結果が反映される。
Algoliaインデックスのコンフィグを編集
できあがったインデックスのコンフィグを編集する。
Searchable Attributesに検索ワードに引っ掛けたいフィールドを追加する。
他にもいろいろなコンフィグがあるが基本的にこれだけでよいと思う。不思議だがこれでしばらく(1時間とか)放っておくと検索精度が最適化されていく。なので焦っていろいろなコンフィグをいじらないほうが良い。
これでバックエンド側は終了
フロント側の作業
検索画面を設置
Algolia InstantsearchというモジュールでUIを作っていく。
インストール
不要。
react-instantsearch-domとgatsby-plugin-algoliaを最初にインストールしたので。
gatsby-config.jsの編集
不要
設置方法
componentsフォルダの下にSearchフォルダを作成し、そこにindex.jsファイルを作成。
そして以下のコード。
// components/Search/index.js
import React from 'react'
import algoliasearch from 'algoliasearch/lite';
import {
InstantSearch,
SearchBox,
Hits,
} from 'react-instantsearch-dom';
const searchClient = algoliasearch(
'KA4OJA9KAS', 'lkjas987ef923ohli9asj213k12n59a'
);
// InstantSearchのコンフィグとレンダリング
const Search = () => {
return (
<>
<InstantSearch
searchClient={searchClient}
indexName="hoge-site"
>
<SearchBox />
<Hits />
</InstantSearch>
</>
)
}
export default Search
<InstantSearch />
タグにAlgoliaのApplication IDとSearch-Only API Key、それにindex名を設定し、<InstantSearch />
タグで挟む形でサーチボックス<SearchBox />
や検索結果を表示する<Hits />
などを記述する。
そしたらSearchコンポーネントをLayout.jsの適当な場所に埋め込んでみる。
// components/layout.js
・・・略・・・
import Search from './Search'
・・・略・・・
<Search />
・・・略・・・
これだけで下図の通りいちおう機能としては使えるようになる。
あとは検索結果表示を調整したりスタイルを整える。
// components/Search/index.js
import React from 'react'
import { Link } from "gatsby"
import algoliasearch from 'algoliasearch/lite';
import {
InstantSearch,
SearchBox,
Hits,
Highlight,
Stats,
Pagination,
SortBy
} from 'react-instantsearch-dom';
import './Search.css'
const searchClient = algoliasearch(
'KA4OJA9KAS', 'lkjas987ef923ohli9asj213k12n59a'
);
const Header = () => (
<header className="header">
<SearchBox
className="search-bar"
translations={{ placeholder: '検索ワード' }}
/>
</header>
);
const Hit = ({ hit }) => (
<div className="card">
<div className="card-contents">
<Highlight attribute="title" hit={hit} className="card-title" />
<Link to={hit.id} >
<span className="link-to">記事本文へ</span>
</Link>
<Highlight attribute="body" hit={hit} className="card-year"/>
<div className="card-rating">Rating: {hit.rating}</div>
<div className="card-genre"> <span>{hit.category}</span></div>
</div>
</div>
);
const Content = () => (
<main>
<div className="information">
<Stats/>
</div>
<Hits hitComponent={Hit} />
<div> <Pagination/> </div>
</main>
);
// InstantSearchのコンフィグとレンダリング
const Search = () => {
return (
<>
<InstantSearch
searchClient={searchClient}
indexName="hoge-site"
>
<Header />
<div className="body-content">
<Content/>
</div>
</InstantSearch>
</>
)
}
export default Search
ちなみにCSSは以下の通り。
// components/Search/Search.css
.header{
padding-top: 1em;
width: 100%;
display: flex;
background-color: #dce2e9;
height: 11vh
}
.search-bar{
display: flex;
justify-content: center;
width: 100%
}
input{
max-width: 500px;
border: none;
border-radius: .5em;
padding: 10px
}
.ais-SearchBox-submit{
width: 50px;
border: none;
padding: 10px;
color: #c4c4c4
}
.ais-SearchBox-reset{
width: 50px;
border: solid;
padding: 10px;
color: #c4c4c4
}
main{
width: 100%;
}
ul{
width: 100%;
display: flex;
flex-wrap: wrap
}
li{
list-style-type: none;
}
.ais-Hits-item{
width: 100%;
}
.card{
background-color: #f9f9f9;
display: flex;
border-radius: 10px;
margin:20px;
padding: 15px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
.card-contents{
margin-left: 20px;
display: flex;
align-content: center;
flex-direction: column;
justify-content: space-between
}
.link-to{
color: blue;
}
.card-title{
font-weight: bold
}
.card-genre > span{
font-size: 15px;
width: 20px;
padding: 4px;
background-color: #c4c4c4
}
.information{
padding-top: 10px;
display: flex;
justify-content: space-around;
font-size: 11px
}
a{
text-decoration: none
}
a:visited{
color: black;
}
.ais-Pagination-list{
display: flex;
justify-content: center
}
.ais-Pagination-item{
margin: 5px
}
さらに必要な実装
検索結果画面の表示非表示スイッチ機能
このままでは常に検索結果が表示されている状態なので(全レコードが表示されつづける)、検索ワードがカラのときは<Hits />
を非表示にする機能を実装する。
//components/Search/index.js
import React, { useState, useEffect } from 'react' // 変更
import { Link } from "gatsby"
import algoliasearch from 'algoliasearch/lite';
import {
InstantSearch,
SearchBox,
Hits,
Highlight,
Stats,
Pagination,
SortBy
} from 'react-instantsearch-dom';
import './Search.css'
const searchClient = algoliasearch(
'KA4OJA9KAS', 'lkjas987ef923ohli9asj213k12n59a'
);
// Headerコンポーネントは削除して<SearchBox />はSearchコンポーネントに移動
const Hit = ({ hit }) => (
<div className="card">
<div className="card-contents">
<Highlight attribute="title" hit={hit} className="card-title" />
<Link to={hit.id} >
<span className="link-to">記事本文へ</span>
</Link>
<Highlight attribute="body" hit={hit} className="card-year"/>
<div className="card-rating">Rating: {hit.rating}</div>
<div className="card-genre"> <span>{hit.category}</span></div>
</div>
</div>
);
const Content = () => (
<main>
<div className="information">
<Stats/>
</div>
<Hits hitComponent={Hit} />
<div> <Pagination/> </div>
</main>
);
// InstantSearchのコンフィグとレンダリング
const Search = () => {
const [flag, setFlag] = React.useState(false) //追加
return (
<>
<InstantSearch
searchClient={searchClient}
indexName="hoge-site"
>
{/* <header>...</header>を追加 */}
<header className="header">
<SearchBox
className="search-bar"
translations={{ placeholder: '検索ワード' }}
onSubmit={(event) => {
event.preventDefault();
setFlag(true);
}}
onReset={(event) => {
event.preventDefault();
setFlag(false);
}}
onKeyUp={(event) => {
event.preventDefault();
setFlag(false);
}}
/>
</header>
<div className={!flag ? 'input-empty' : 'input-value'}>
<div className="body-content">
<Content/>
</div>
</div>
</InstantSearch>
</>
)
}
export default Search
SearchBoxのイベントによりオフオンするbooleanステート変数flagを追加。flagがfalseであれば検索結果画面を表示しないようにする。
ステート変数をコンポーネント間でやりとりするのが面倒なので<SearchBox />
はSearchコンポーネントに移動し、Headerコンポーネントは削除。
本文表示をトリミング
検索結果に表示する記事本文が長すぎるので適当な分量にトリミングする。これは<Highlight />
を<Snippet />
に変えるだけでよい。
//components/Search/index.js
import React, { useState, useEffect } from 'react'
import { Link } from "gatsby"
import algoliasearch from 'algoliasearch/lite';
import {
InstantSearch,
SearchBox,
Hits,
Highlight,
Stats,
Pagination,
Snippet // 追加
} from 'react-instantsearch-dom';
import './Search.css'
const searchClient = algoliasearch(
'KA4OJA9KAS', 'lkjas987ef923ohli9asj213k12n59a'
);
const Hit = ({ hit }) => (
<div className="card">
<div className="card-contents">
<Highlight attribute="title" hit={hit} className="card-title" />
<Link to={hit.id} >
<span className="link-to">記事本文へ</span>
</Link>
<Snippet attribute="body" hit={hit} /> // 変更
<div className="card-rating">Rating: {hit.rating}</div>
<div className="card-genre"> <span>{hit.category}</span></div>
</div>
</div>
);
・・・略・・・
しかし表示されない。。。
これはAlgoliaダッシュボードで次のようにSnippetingにbody属性を追加して解決。
####検索ヒットワードを強調
ヒットワードを強調表示にしたい。これは<Snippet />
にtagName=""
オプションをつけるだけ。
<Snippet tagName="mark" hit={hit} attribute="body" />
ビルドにあたっての注意点
gatsby build
により自動でAlgoliaにインデックスアップロードすると、ダッシュボードで設定したコンフィグSnippeting, Searchable attributes,およびIndex Languagesがリセットされてしまう。
これはgatsby-config.js
でgatsby-plugin-algoliaプラグインのsettingsを追加してあげて解決。API Parameters
//gatsby-config.js
・・・略・・・
{
resolve: `gatsby-plugin-algolia`,
settings: {
searchableAttributes: ['body','title'],
indexLanguages: ['ja'],
queryLanguages: ['ja'],
attributesToSnippet: ['body:100']
},
・・・略・・・
静的ページのインデックス
静的ページは手作業でインデックスを別のIndiceにアップすれば良かろうと思ったが。
しかしいまのところgatsby-plugin-algoliaでは複数Indiceに対応していないようだ。どうしたものか…。
※ 同じIndiceに追加した場合gatsby build
すると手動追加したレコードは消えてしまう。
ワークアラウンド
アップするときの作業手順として、
① gatsby build
② firebase deploy
③ 同じIndiceに静的ページインデックス(JSONファイル)を手動追加
③の作業をすることで回避。一時しのぎなのでスクリプト化はあえてしないこととする。
補足:パフォーマンス
普通Twitterシェアボタンなどを埋め込むとパフォーマンスは悪くなる。
以下はDisqusを埋め込んだページのパフォーマンス。
Algoliaの埋め込みはまったく影響ないように見える。
どうやらロードにかかる時間はほんの5ミリセカンドのようだ。
本の宣伝
Gatsbyバージョン5>>>>改訂2版
前編の『Gatsby5前編ー最新Gatsbyでつくるコーポレートサイト』と後編の『Gatsby5後編ー最新GatsbyとmicroCMSでつくるコーポレートサイト《サイト内検索機能付き》』を合わせ、次のようなデモサイトを構築します。
→ https://yah-space.work
静的サイトジェネレーターGatsby最新バージョン5の基本とFile System Route APIを使用して動的にページを生成する方法を解説。またバージョン5の新機能《Slicy API》《Script API》《Head API》を紹介、実装方法も。《Gatsby Functions》での問い合わせフォーム実装やGatsby Cloudへのアップロード方法も!
Gatsby5前編ー最新Gatsbyでつくるコーポレートサイト ~基礎の基礎から応用、新機能の導入まで(書籍2,980円)
最新Gatsby5とmicroCMSを組み合わせてのコーポレートサイト作成手順を解説。《サイト内検索機能》をGatsbyバージョン4からの新機能《Gatsby Functions》と《microCMSのqパラメータ》で実装。また、SEOコンポーネントをカスタマイズしてmicroCMS APIをツイッターカードに表示させるOGPタグ実装方法も解説。
Gatsby5後編ー最新GatsbyとmicroCMSでつくるコーポレートサイト《サイト内検索機能付き》(書籍 2,790円)
##参考:
API reference.
How to use Algolia for instant search
Tutorial: Algolia React InstantSearch implementation for a React Gatsby App
InstantSearch
Gatsby製サイトにAlgoliaのサイト内検索を実装する
algolia/gatsby-plugin-algolia
Adding Search with Algolia