LoginSignup
8
7

More than 1 year has passed since last update.

Gatsby+microCMSサイトにAlgolia全文検索機能を実装

Last updated at Posted at 2020-06-06

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を作成する⁉
えっえっ:heart_eyes_cat:

ということでAlgoliaできまり.

まずはバックエンド側

Algoliaダッシュボードでの作業

ログインできる状態にしておくだけ。
(下画面↓はすでにある別サイトのインデックススタティクス)
image.png

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のページにある。
image.png

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 ↓を見ながら試行錯誤。

image.png

Algoliaにインデックス

あとはgatsby buildするだけで……
おやエラー(゚Д゚)

image.png

サイズが大きすぎる、というエラー。
無料枠だと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記事分が自動でほとんど時間かからずインデックス作成される。
image.png

Algoliaダッシュボード上でテストできる。リアルタイムに検索結果が反映される。
image.png

Algoliaインデックスのコンフィグを編集

できあがったインデックスのコンフィグを編集する。
Searchable Attributesに検索ワードに引っ掛けたいフィールドを追加する。

image.png

あとは言語設定だろうか。Japaneseにした。
image.png

他にもいろいろなコンフィグがあるが基本的にこれだけでよいと思う。不思議だがこれでしばらく(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 />
・・・・・・

これだけで下図の通りいちおう機能としては使えるようになる。

image.png

あとは検索結果表示を調整したりスタイルを整える。

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

image.png
image.png

ちなみに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
}

参考:API reference.

さらに必要な実装

検索結果画面の表示非表示スイッチ機能

このままでは常に検索結果が表示されている状態なので(全レコードが表示されつづける)、検索ワードがカラのときは<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>    
);
・・・・・・

しかし表示されない。。。

image.png

これはAlgoliaダッシュボードで次のようにSnippetingにbody属性を追加して解決。
image.png

100文字になった。
image.png

####検索ヒットワードを強調

ヒットワードを強調表示にしたい。これは<Snippet />tagName=""オプションをつけるだけ。

<Snippet tagName="mark" hit={hit} attribute="body" />

markならこうなる。
image.png

strongにすればこうなる
image.png

ビルドにあたっての注意点

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にアップすれば良かろうと思ったが。
image.png
image.png

しかしいまのところgatsby-plugin-algoliaでは複数Indiceに対応していないようだ。どうしたものか…。

同じIndiceに追加した場合gatsby buildすると手動追加したレコードは消えてしまう。

ワークアラウンド

アップするときの作業手順として、
gatsby build
firebase deploy
同じIndiceに静的ページインデックス(JSONファイル)を手動追加

③の作業をすることで回避。一時しのぎなのでスクリプト化はあえてしないこととする。

補足:パフォーマンス

普通Twitterシェアボタンなどを埋め込むとパフォーマンスは悪くなる。
以下はDisqusを埋め込んだページのパフォーマンス。
image.png

Algoliaの埋め込みはまったく影響ないように見える。
image.png
image.png
どうやらロードにかかる時間はほんの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

8
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7