GatsbyJS 検索機能を実装する(JsSearchを利用)

2019-11-10

公式ドキュメントの実装方法を参考にマークダウンファイルを検索できるようにする。
検索の仕組みは

  1. 検索したいキーをJSONに出力
  2. 画面読み込み時にJSONのロード
  3. 入力された検索ワードでフィルター

という感じです。

まずはサンプルページを作成

こちらを参考に一覧ページを詳細ページを作成。

マークダウンファイルを少し変更

dataディレクトリを新しく作成し、検索キー用にsearchを追加します。

data/file1.md
---
path: "/file1"
date: "2019-06-16"
title: "マークダウンファイル その1"
search: "検索その1"
---

## マークダウンファイルの内容
data/file2.md
---
path: "/file2"
date: "2019-06-19"
title: "マークダウンファイル その2"
search: "検索その2箇条書き"
---

+ 箇条書き
+ 箇条書き
+ 箇条書き
data/file3.md
---
path: "/file2"
date: "2019-06-19"
title: "マークダウンファイル その2"
search: "検索その3テーブル"
---

|A  |B  |C  |
|:--|:-:|--:|
|1  |2  |3  |
|4  |5  |6s  |

一覧ページ(index.js)を修正

検索結果用に日付とタイトルのみ表示します。

src/pages/index.js
import React from "react"

import { Link,graphql } from "gatsby"

export default ({data}) => {
    return(
        <div>
        {data.allMarkdownRemark.edges.map(({ node }, index) => (
        <div key={index}>
            <span>{node.frontmatter.date}</span>
            <Link to={node.fields.slug} >{node.frontmatter.title}</Link>
        </div>
        ))}
        </div>
    )
}

export const query = graphql`
query MyQuery {
    allMarkdownRemark(sort: {fields: frontmatter___date, order: ASC}) {
      totalCount
      edges {
        node {
          frontmatter {
            title
            path
            date
          }
          excerpt
          fields {
            slug
          }
        }
      }
    }
  }  
`

とりあえずサンプルページが完成

サーバをきどうして確認すると
一覧画面と gatsby_search_1.png

リンク先の詳細画面が表示できる。 gatsby_search_2.png

検索したいキーをJSONに出力

サーバー起動時にJSONを出力するようにgatsby-node.jsを変更

  1. ファイル出力用にfsを読み込む
  2. graphqlでfrontmatterのtitile、search、dateも取得する
  3. 取得したtitile、search、slugをJSONに出力する(表示用にdateも出力する)
gatsby-node.js
exports.onCreateNode = ({ node,actions }) => {
    if (node.internal.type === `MarkdownRemark`) {
        const { createNodeField } = actions
        createNodeField({
            node,//このノードに
            name: `slug`,//slugという名前で
            value: node.frontmatter.path//pathを設定
        })
    }
}

//ページ生成
const path = require(`path`)
//1. ファイル出力用にfsを読み込む
var fs=require("fs");
exports.createPages = ({ graphql, actions }) => {
    const blogTemplate = path.resolve(`src/templates/blog.js`)
    const { createPage } = actions

    // マークダウンファイルのslugを取得
    return graphql(`    
        {
            allMarkdownRemark {
                edges {
                    node {
                        fields {
                            slug
                        }
#                     2.検索用に追加
                        frontmatter {
                            title
                            search
                            date
                        }
                    }
                }
            }
        }
    `).then(
        result => {
            // 検索用JSONの内容を格納する
            const search = [];
            result.data.allMarkdownRemark.edges.forEach(edge  => {

                createPage({
                    path: edge.node.fields.slug,
                    component: blogTemplate,
                    context: {
                        slug: edge.node.fields.slug,
                    },
                })

                // 3.slug、タイトル、検索キー、日付を配列に入れていく
                search.push({
                    slug: edge.node.fields.slug,
                    title: edge.node.frontmatter.title,
                    search: edge.node.frontmatter.search,
                    date: edge.node.frontmatter.date
                })
            })

            // JSONファイルに出力
            fs.writeFileSync('./static/search.json', JSON.stringify(search, null, 4))

        }
    )
}

サーバを起動するとstatic配下にsearch.jsonが作成されます。

static/search.json
[
    {
        "slug": "/file2",
        "title": "マークダウンファイル その2",
        "search": "検索その2箇条書き",
        "date": "2019-10-28"
    },
    {
        "slug": "/file1",
        "title": "マークダウンファイル その1",
        "search": "検索その1",
        "date": "2019-10-12"
    },
    {
        "slug": "/file3",
        "title": "マークダウンファイル その3",
        "search": "検索その3テーブル",
        "date": "2019-11-01"
    }
]

とりあえず検索機能を実装してみる

の前にindex.jsでSearchContainerを呼び出して、出力するように修正します。

index.js
import React from "react"
import Search from "../components/SearchContainer"

// 表示はすべてSearchで行う
export default () => {
    return(
        <div>
            <Search />
        </div>
    )
}

検索を行うSearchContainer.jsをsrc/components/に作成します。
※公式ドキュメントの内容を一部消しています(エラー処理など)。

src/components/SearchContainer.js
import React, { Component } from "react"
import Axios from "axios"
import * as JsSearch from "js-search"
import { Link } from "gatsby"

class Search extends Component {
  state = {
    bookList: [],//JSONの内容を格納
    search: [],//JsSearchインスタンスを格納
    searchResults: [],//検索結果を格納
    searchQuery: "",//検索ワード
  }
  /**
   * 初期化 search.jsonを読み込む
   */
  async componentDidMount() {
    Axios.get("search.json")
      .then(result => {
        const bookData = result
        this.setState({ bookList: bookData.data })
        this.rebuildIndex()
      })
      .catch(err => {

      })
  }

  /**
   * 検索方法の設定
   */
  rebuildIndex = () => {
    const { bookList } = this.state;
    // JsSearchインスタンス作成(検索対象のリストでユニークとなるキーを指定する)
    const dataToSearch = new JsSearch.Search("slug");

    // 検索ワードをいい感じに変換する(とりあえずスペースで分割し、複数文字列で検索)
    dataToSearch.tokenizer = {
      tokenize( text ) {
        return text.split(/\s+/i);      
      }
    };
    
    // 部分一致で検索する    
    dataToSearch.indexStrategy = new JsSearch.AllSubstringsIndexStrategy();
    
    // 検索ワードを小文字変換、trimする(記述しなくてもデフォルトで設定されている)
    dataToSearch.sanitizer = new JsSearch.LowerCaseSanitizer();
    
    //検索方法の設定
    dataToSearch.searchIndex = new JsSearch.TfIdfSearchIndex("slug");
    
    // 検索を行うキー
    dataToSearch.addIndex("title");
    dataToSearch.addIndex("search");
    
    // 検索対象となるリストを設定
    dataToSearch.addDocuments(bookList);
    this.setState({ search: dataToSearch });
  }

  /**
   * 検索ワード変更時に検索を行う
   */
  searchData = e => {
    const { search } = this.state
    const queryResult = search.search(e.target.value);
    this.setState({ searchQuery: e.target.value, searchResults: queryResult })
  }

  render() {
    const { bookList, searchResults, searchQuery } = this.state
    const queryResults = searchQuery === "" ? bookList : searchResults
    return (
      <div>
        <div>
            <div>
            検索ワード:<input
                id="Search"
                value={searchQuery}
                onChange={this.searchData}
              />
            </div>
          <div>
            検索結果:{queryResults.length}
                {queryResults.map(item => {
                  return (
                      <div key={item.slug}>
                        <span>{item.date}</span>
                        <Link to={item.slug} >{item.title}</Link>
                      </div>
                  )
                })}
          </div>
        </div>
      </div>
    )
  }
}
export default Search

画面の表示すると、最初は3件表示されていますが、検索ワードで絞り込みができます。
gatsby_search_3.png 「その2」で検索。
gatsby_search_4.png 画面には表示されていないけど、検索キーsearchに指定した「テーブル」で検索。
gatsby_search_5.png 「2019」は画面に表示されているが、検索キーに指定していないので該当なし。
gatsby_search_6.png

SearchContainerコンポーネントは何してるのか

コメントもいっぱい書いたけど、せっかくなのでSearchContainerのまとめ。

componentDidMount内の処理

コンポーネントの配置後に1回だけ実行される。
search.jsonの内容を取得して、stateのbookListに格納した後、rebuildIndexを実行。

rebuildIndex

JsSearchインスタンスのオプションや検索キーを設定し、stateのsearchに格納。
各オプションは自前で実装することも可能です。

  • tokenizer
    入力された検索ワードの分割方法を指定オプション。
    デフォルトではSimpleTokenizerが設定されているが、マルチバイトに対応していなかったので自前でスペース分割するように実装。
    文書を検索したときに「a」や「the」などを除くStopWordsTokenizerも用意されているが英語のみ対応。
  • indexStrategy
    検索部分を指定する(デフォルトは前方一致)。
    前方一致:PrefixIndexStrategy
    部分一致:AllSubstringsIndexStrategy
    完全一致:ExactWordIndexStrategy
  • sanitizer
    検索ワードを変換する(デフォルトは小文字変換とtrim)。
    小文字変換後、trim:LowerCaseSanitizer
    trimのみ:CaseSensitiveSanitizer
  • searchIndex
    検索インデックスの指定(デフォルトはTfIdfSearchIndex)。
    検索対象の登場回数などを考慮:TfIdfSearchIndex
    順番に表示:UnorderedSearchIndex
  • addIndex
    検索対象のJSON内で検索を行うキーを指定。
  • addDocuments
    検索対象となるJSONを指定する。

searchData

stateからJsSearchインスタンスを取得し、入力された検索ワードで検索を行う。

参考

JsSearchの公式ドキュメント

■同じタグの記事(最新5件)
GatsbyJS FaunaDBからデータを取得する
GatsbyJS PostgreSQLの内容を取得する
GatsbyJS rehypeReactでマークダウンの内容を変更する
GatsbyJS マークダウンにコンポーネントを表示する
GatsbyJS トランスフォーマープラグインを使用してマークダウン...
■同じタグの記事を見る