公式ドキュメントの実装方法を参考にマークダウンファイルを検索できるようにする。 検索の仕組みは
- 検索したいキーを JSON に出力
- 画面読み込み時に JSON のロード
- 入力された検索ワードでフィルター
という感じです。
まずはサンプルページを作成
こちらを参考に一覧ページを詳細ページを作成。
マークダウンファイルを少し変更
data ディレクトリを新しく作成し、検索キー用に search を追加します。
---
path: "/file1"
date: "2019-06-16"
title: "マークダウンファイル その1"
search: "検索その1"
---
## マークダウンファイルの内容
---
path: "/file2"
date: "2019-06-19"
title: "マークダウンファイル その2"
search: "検索その2箇条書き"
---
+ 箇条書き
+ 箇条書き
+ 箇条書き
---
path: "/file2"
date: "2019-06-19"
title: "マークダウンファイル その2"
search: "検索その3テーブル"
---
|A |B |C |
|:--|:-:|--:|
|1 |2 |3 |
|4 |5 |6s |
一覧ページ(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
}
}
}
}
}
`
とりあえずサンプルページが完成
検索したいキーを JSON に出力
サーバー起動時に JSON を出力するように gatsby-node.js を変更
- ファイル出力用に fs を読み込む
- graphql で frontmatter の titile、search、date も取得する
- 取得した titile、search、slug を JSON に出力する(表示用に date も出力する)
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 が作成されます。
[
{
"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 を呼び出して、出力するように修正します。
import React from "react"
import Search from "../components/SearchContainer"
// 表示はすべてSearchで行う
export default () => {
return(
<div>
<Search />
</div>
)
}
検索を行う SearchContainer.js を src/components/に作成します。 ※公式ドキュメントの内容を一部消しています(エラー処理など)。
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 件表示されていますが、検索ワードで絞り込みができます。 「その 2」で検索。 画面には表示されていないけど、検索キー search に指定した「テーブル」で検索。 「2019」は画面に表示されているが、検索キーに指定していないので該当なし。
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 インスタンスを取得し、入力された検索ワードで検索を行う。