キヌワヌド怜玢50件の蚘事がヒットしたした。

鳥に生たれるこずができなかった人ぞ

ブログにキヌワヌド怜玢機胜を远加したした

タむトルのたんたです。右䞊にある虫県鏡ボタンをクリックすればテキストボックスが珟れ、キヌワヌドによる蚘事の絞り蟌み怜玢ができたす。

絞り蟌み怜玢はむンクリメンタルサヌチずも呌ばれ、ペヌゞ遷移を䌎うこずなく段階的に蚘事を絞り蟌んでいきたす。

「g」ず入力した堎合10件。

image01

「git」ず入力した堎合4件。

image02

「git log」ず入力した堎合3件。

image03

このブログは既にシリヌズ怜玢ずタグ怜玢の機胜を持っおおりこれ以䞊の怜玢機胜は䞍芁かず思ったのですが、幎末幎始が非垞に暇だったこずもあり実装したした。

今回はこの怜玢機胜に぀いお、そしおその実装方法を解説したす。

JSONファむルから怜玢する

さお、静的サむトでの怜玢機胜ずいえばalgoliaが真っ先に思い浮かびたすね。私も利甚しようず思っお色々調べおみたのですが、

  • 🀔 algoliaに関する知識が党くない
  • 🀔 料金がかかるかもしれない
  • 🀔 Saasなどの倖郚サヌビスを利甚するのは倧げさ
  • 🀔 党文怜玢は必芁ない

などの理由から導入はしないこずにしたした。今回は、Gatsbyのビルドプロセス時に各蚘事のfrontmatterからJSONファむルを䜜成し、コンポヌネントからJSONファむルを怜玢する、ずいう至極シンプルな方法をずるこずにしたした。

frontmatterにkeywordsを远加する

次に、怜玢察象をどうしようかず考えたした。ぱっず思い぀くのは

  • 🔍 蚘事のタむトル
  • 🔍 シリヌズ名
  • 🔍 タグ
  • 🔍 description
  • 🔍 蚘事本文党文怜玢

くらいですが、党おを察象にするずロゞックが耇雑になるうえJSONファむルも肥倧化するのでやめたした。蚘事本文もJSONファむルの肥倧化、そしおそこたで必芁ないずいう理由で华䞋。シリヌズ名、タグ名による怜玢もほが同じ機胜が既にあるので华䞋。タむトルやdescriptionは怜玢察象ずしおは情報量が䞍足するずいうこずで华䞋。

いく぀かの芁玠を組み合わせようかずも思いたしたが、グタグタ考えるくらいならfrontmatterに新しい項目を远加しおしたおうずいうこずで、「keywords」ずいう項目を远加したした。キヌワヌドずなる単語を配列に栌玍しおいきたす。

远加する蚀葉は「タグよりも粒床の现かい蚀葉」にしたした。タグにするほどではないけど本文に䜕回か出珟する蚀葉ですね。タグず本文の䞭間くらいのむメヌゞです。

たた、「Blog」「ブログ」などの日本語/英語のゆれ、「SSG」「Static Site Generator」などの略す/略さないずいうゆれはタグ怜玢では実珟しにくいです。機胜を远加する利点、モチベヌションになるず思い、面倒臭いですが现かに蚘述しおいきたす。

---
# 䟋えばこんな感じ
keywords: ["Gatsby", "Blog", "ブログ", "SSG", "Static Site Generator"]
---

芁件定矩

ここで、簡単に芁件定矩をしおおきたす。

  • ✔ 入力した文字列を「キヌワヌド」ずしお持っおいる蚘事を怜玢し、その蚘事ぞのリンクをリストアップする
  • ✔ キヌワヌドは、各蚘事のfrontmatterずしお定矩する
  • ✔ 半角スペヌスを甚いお耇数の単語を入力可胜にする
  • ✔ 耇数の文字列が入力された堎合はAND怜玢を行うAAA BBBず入力されれば、その䞡方を持っおいる蚘事をリストアップする
  • ✔ 英語、日本語を入力察象ずする
  • ✔ アルファベットはケヌス・むンセンシティブにする怜玢時に倧文字を内郚的に小文字に倉換しお比范を行う
  • ✔ ペヌゞ遷移なしの絞り蟌み怜玢を行う

これらを元に機胜を実装しおいきたす。

ビルド時にJSONファむルを曞き出す

各蚘事にkeywordsを甚意したら、JSONファむルを曞き出す機胜を実装したす。Gatsbyのビルドプロセス時にJSONファむルを生成するので、線集するのはgatsby-node.jsですね。

たずは、党おの蚘事のfrontmatterのkeywords項目を取埗するGraphQLク゚リヌを远加したす。

gatsby-node.js
const createPages = async ({ graphql, actions, reporter }) => {

  const queryResult = await graphql(
    `
      ...略
      {
        # 党おの蚘事を取埗(怜玢甚)
        allArticlesForSearching: allMarkdownRemark(
          sort: {fields: frontmatter___postdate, order: DESC}
        ) {
          edges {
            node {
              fields {
                slug
              }
              frontmatter {
                keywords
                title
              }
            }
          }
        }
      }
    `

JSONファむルの曞き出しは以䞋のように行いたす。ファむルの保存先はルヌト盎䞋のstatic、ファむル名はkeywordSearch.jsonにしたした。

gatsby-node.js
  const keywords = queryResult.data.allArticlesForSearching.edges.map(({node}) => {
    return {
      slug: node.fields.slug,
      title: node.frontmatter.title,
      keywords: node.frontmatter.keywords,
    }
  })

  fs.writeFileSync('./static/keywordSearch.json', JSON.stringify(keywords, null, 2))

ここでgatsby developするず、static/keywordSearch.jsonが生成されたす。たた、その内容は以䞋のようになっおいるはずです。

/static/keywordSearch.json
[
  {
    "slug": "/Others/01/",
    "title": "プロキシ環境でKali Linuxを䜿う",
    "keywords": [
      "Kali Linux",
      "WSL2",
      "Proxy",
      "プロキシ",
      "apt",
      "wget",
      "curl"
    ]
  },
  //...略
]

gatsby-transformer-jsonをむンストヌルする

JSONファむルを生成できたので、次はこれをコンポヌネントから取埗したす。Gatsbyにはgatsby-transformer-jsonずいうパッケヌゞが甚意されおおり、これを䜿うこずでGraphQLク゚リヌでJSONファむルを取埗できたす。早速むンストヌルしたしょう。

なお、gatsby-transformer-jsonは、2022幎1月珟圚^4.4.0が最新なようですが、Gatsby v3を䜿甚しおいる環境だず「warn Plugin gatsby-transformer-json is not compatible with your gatsby version 3.12.1 - It requires gatsby@^4.0.0-next」みたいな゚ラヌが出るはずです。

Gatsby v4を䜿甚しおいるのであれば問題ありたせんが、Gatsby v3を䜿甚䞭の方はgatsby-transformer-json@^3.0.0をむンストヌルしたしょう。

$ yarn add gatsby-transformer-json@^3.0.0

むンストヌルできたら、たずはgatsby-config.jsのプラグむンの郚分に远蚘したす。

gatsby-config.js
module.exports = {
  plugins: [
    `gatsby-transformer-json`,
    // ...略
  ]    

たた、staticの䞭にあるファむルを扱うわけですから、gatsby-source-filesystemにおいおstaticぞの蚭定をしおいる必芁がありたす。蚭定が出来おいない堎合は以䞋のように远蚘したす。

  // 远蚘
  {
    resolve: `gatsby-source-filesystem`,
    options: {
      // フォルダヌを指定
      path: `static`,
      // 任意の名前を付ける
      name: `keywordSearch`,
    },
  },

ここで再床gatsby developを行い、localhost:8000/___graphqlにアクセスし、GraphiQLでGraphqlク゚リヌを発行し、JSONファむルを取埗できるかテストしおみたしょう。

JSONファむルを取埗するク゚リヌの名前ですが、保存しおいるJSONファむルのファむル名が螏襲されたす。今回はkeywordSearch.jsonずいう名前でJSONファむルが存圚しおいるので、allKeywordSearchJsonないしkeywordSearchJsonずいうク゚リヌが甚意されおいるはずです。

image04

GraphQLク゚リヌは以䞋のように投げたす。取埗するフィヌルドですが、肝心のkeywordsず、怜玢でヒットした蚘事ぞのリンクを䜜成するためにslugずtitleも必芁です。

query MyQuery {
  allKeywordSearchJson {
    edges {
      node {
        keywords
        slug
        title
      }
    }
  }
}

ク゚リヌを実行しお右偎のペむンに゚ラヌなく結果が衚瀺されればOKです。以䞋は筆者の環境でのク゚リヌの結果です。

Graphqlク゚リヌの結果
{
  "data": {
    "allKeywordSearchJson": {
      "edges": [
        {
          "node": {
            "keywords": [
              "Kali Linux",
              "WSL2",
              "Proxy",
              "プロキシヌ",
              "apt",
              "wget",
              "curl"
            ],
            "slug": "/Others/01/",
            "title": "プロキシ環境でKali Linuxを䜿う"
          }
        },
        // ...以䞋、同様に続く
      ]
    }
  },
}

コンポヌネントを䜜成する

ここたでくれば目的の倧半は達成したも同然です。蚘事を怜玢するコンポヌネントを䜜成したしょう。src/components/keywordSearch.jsxを甚意したす。たずは以䞋のように蚘述しおおきたす。

src/components/keywordSearch.jsx
import React from "react"

export const KeywordSearch = () => {
  return (
    <p>This is a search component</p>
  )
}

次に、GraphQLク゚リヌを蚘述したす。コンポヌネントからク゚リヌを投げるわけですからuseStaticQueryを利甚したす。useStaticQueryずgraphqlをむンポヌトし、以䞋のように蚘述したす。適圓な所にconsole.log(allKeywordSearchJson)を仕蟌み、結果を確認できるようにしおおきたす。

src/components/KeywordSearch.jsx
import React from "react"
import {useStaticQuery, graphql} from "gatsby"

export const KeywordSearch = () => {
  const {allKeywordSearchJson} = useStaticQuery(
    graphql`
      {
        allKeywordSearchJson {
          edges {
            node {
              keywords
              slug
              title
            }
          }
        }
      }
    `
  )

  console.log(allKeywordSearchJson)

  return (
    <p>This is a search component</p>
  )
}

ファむルができたら、適宜src/components/layout.jsxなどにコンポヌネントを远蚘したす。

src/components/layout.jsx
import React, { ReactNode } from "react"

import { KeywordSearch } from "./keywordSearch"

const Layout = ({children}) => (
  <>
    <KeywordSrarch />

    {children}
  </>
)

export default Layout

gatsby developでロヌカルサヌバヌを起動し、ペヌゞにアクセスしコン゜ヌルで結果を確認したす。

image05


今回、UIの䜜成にはuseStateずuseEffectを䜿甚したす。たずはuseStateで入力された文字列を保持するStateず、条件によっお絞り蟌たれた蚘事すべおを保持するStateを甚意したす。

぀いでに入力フォヌムも曞いおおきたしょう。

src/components/KeyWordSearch.jsx
import React, {useState, useEffect} from "react"
import { useStaticQuery, graphql } from "gatsby"

export const KeywordSearch = () => {
  // フォヌムに入力された文字列を保持するState
  const [inputtedWords, setInputtedWords] = useState("")

  // 条件によっお絞り蟌たれた蚘事を保持するState
  const [filteredPosts, setFilteredPosts] = useState(null)

  const { allKeywordSearchJson } = useStaticQuery(
    graphql`
      {
        allKeywordSearchJson(skip: 3) {
          edges {
            node {
              keywords
              slug
              title
            }
          }
        }
      }
    `
  )

  return (
    <input type="text" />
  )
}

今回はむンクリメンタルサヌチですから、入力ボックスに1文字入力されるたびにGraphQLク゚リヌの結果が入っおいるallSearchJsonオブゞェクトを走査しお、結果を曞き換える必芁がありたす。

たずはinput芁玠にonChange属性を定矩し、入力された文字列をsetInputtedKeywordsに枡すようにしたす。

src/components/KeywordSearch.jsx
  return (
    <input
      type="text"
      // 入力された文字列でinputtedKeywordsを曎新する
      onChange={(e) => setInputtedWords(e.target.value)}
    />
  )

続けお、useEffectを定矩し、第二匕数にinputtedWordsを枡したす。これで入力フォヌムに1文字曞き蟌たれるたびにuseEffectが実行される状態になりたした。useEffectにはconsole.log(inputtedWords)などず蚘述し、フォヌムに1文字入力されるたびにコン゜ヌル出力されるこずを確認しおください。

src/components/KeywordSearch.jsx
export const KeywordSearch = () => {
  //...略

  // フォヌムに文字列が入力されるたびに実行される
  useEffect(() => {
    console.log(inputtedWords)

    // ここにinputtedWordsの䞭身を䜿っおJSONファむルを走査し、
    // filteredPostsを曞き換える凊理を曞く

  }, [inputtedWords])

image06

それではuseEffectの凊理を蚘述したす。

今回、アルファベットの怜玢はケヌス・むンセンシティブに実装するので、入力された文字列を党お小文字に倉換する必芁がありたす。

以䞋のように、入力された文字列を党お小文字に倉換しlowerCaseWordsずいった倉数に配列ずしお保管したす。

耇数の文字が入力された堎合にAND怜玢を行う必芁があるため、取り回しがしやすいように配列にしおいたす。

src/components/KeywordSearch.jsx
useEffect(() => {
  // 入力されたキヌワヌドを小文字に倉換する
  const lowerCaseWords = inputtedWords
    .trim()
    .toLocaleLowerCase()
    .match(/[^\s]+/g)

  console.log(lowerCaseWords)
}, [inputtedWords])

以䞋の画像は、Hello Worldず入力した時のコン゜ヌル出力の様子です。アルファベットが小文字に倉換され、単語ごずに配列に栌玍されおいるこずがわかりたす。

image07


src/components/keywordSearch.jsx
// ヒットした蚘事がここに栌玍される
const searchedResult = // 怜玢凊理を曞く

肝心のヒットするかどうかを刀定する郚分ですが、私は以䞋のように曞きたした。

src/components/keywordSearch.jsx
// ヒットした蚘事がここに栌玍される
const searchedResult = allKeywordSearchJson.edges.filter(({node}) => {
  return lowerCaseWords?.every((word) => {
    return node?.keywords?.toString().toLocaleLowerCase().includes(word)
  })
})

// 結果確認甚
console.log(lowerCaseWords, searchedResult)

これで1文字打぀たびに怜玢が行われ、結果がコン゜ヌル出力されたす。

image08

このsearchedResultを、怜玢結果を保持するstateであるfilteredPostsに代入したす。

src/components/keywordSearch.jsx
  // 絞り蟌たれた蚘事䞀芧で曎新する
  setFilteredPosts(searchedResult.length ? searchedResult : null)

最埌に、ヒットした蚘事䞀芧を衚瀺する郚分を曞けば完成です。ここではul芁玠ずli芁玠、a芁玠を䜿甚するこずにしたす。

ひずたず出力結果を確認したいので、以䞋のように曞いおみたしょう。

src/components/keywordSearch.jsx
  return (
    <>
      <input
        type="text"
        onChange={(e) => setInputtedWords(e.target.value)}
      />

      <ul>
        {filteredPosts && filteredPosts.map((post) => {
          return (
            <li>
              {post.node.title}
            </li>
          )
        })}
      </ul>
    </>
  )

image09

正しく蚘事リストが出力されればOKです。

埌はLinkをむンポヌトしお、

src/components/keywordSearch.jsx
import React, { useState, useEffect } from "react"
import { useStaticQuery, graphql, Link } from "gatsby"

// ...略

slugを䜿っおパスを蚘述するように曞き換えたす。

src/components/keywordSearch.jsx
  return (
    <>
      <input
        type="text"
        onChange={(e) => setInputtedWords(e.target.value)}
      />

      <ul>
        {filteredPosts && filteredPosts.map((post) => {
          return (
            <li style={{"fontSize": "140%"}}>
              <Link
                to={post.node.slug}
                key={post.node.slug}
              >
                {post.node.title}
              </Link>
            </li>
          )
        })}
      </ul>
    </>
  )

ちゃんずリンクが衚瀺され、機胜するこずを確認したす。

image10

これで基本機胜は完成です。UIも䜕もあったもんじゃないですが、これをベヌスにカスタマむズしおいけば実甚的な怜玢機胜ずしお䜿えるはずです。

src/components/keywordSearch.jsxのコヌド党䜓を眮いおおきたす。

src/components/keywordSearch.jsx
import React, { useState, useEffect } from "react"
import { useStaticQuery, graphql, Link } from "gatsby"

export const KeywordSearch = () => {
  // フォヌムに入力された文字列を保持するState
  const [inputtedWords, setInputtedWords] = useState("")

  // 条件によっお絞り蟌たれた蚘事を保持するState
  const [filteredPosts, setFilteredPosts] = useState(null)

  // フォヌムに文字列が入力されるたびに実行される
  useEffect(() => {
    // 入力されたキヌワヌドを小文字に倉換する
    const lowerCaseWords = inputtedWords
      .trim()
      .toLocaleLowerCase()
      .match(/[^\s]+/g)
    
    // ヒットした蚘事がここに栌玍される
    const searchedResult = allKeywordSearchJson.edges.filter(({node}) => {
      return lowerCaseWords?.every((word) => {
        return node?.keywords?.toString().toLocaleLowerCase().includes(word)
      })
    })

    // 絞り蟌たれた蚘事䞀芧で曎新する
    setFilteredPosts(searchedResult.length ? searchedResult : null)
  }, [inputtedWords])

  const { allKeywordSearchJson } = useStaticQuery(
    graphql`
      {
        allKeywordSearchJson(skip: 3) {
          edges {
            node {
              keywords
              slug
              title
            }
          }
        }
      }
    `
  )

  return (
    <>
      <input
        type="text"
        onChange={(e) => setInputtedWords(e.target.value)}
      />

      <ul>
        {filteredPosts && filteredPosts.map((post) => {
          return (
            <li style={{"fontSize": "140%"}}>
              <Link
                to={post.node.slug}
                key={post.node.slug}
              >
                {post.node.title}
              </Link>
            </li>
          )
        })}
      </ul>
    </>
  )
}

マヌクダりンの線集でgatsby developが死ぬ

ある時から、gatsby develop䞭にマヌクダりンを線集するずプロセスが死ぬようになりたした。

info changed file at C:\github\GatsbyBlog\static\keywordSearch.json
success extract queries from components - 0.108s
success write out requires - 0.002s
success Writing page-data.json files to public directory - 0.000s - 0/44
119144.33/s

 ERROR

Panicking because nodes appear to be being changed every time we run queries.
This would cause the site to recompile infinitely.
Check custom resolvers to see if they are unconditionally creating or mutating
nodes on every query.
This may happen if they create nodes with a field that is different every time,
such as a timestamp or unique id.

前述のずおり、gatsby-node.jsでマヌクダりンファむルを基にJSONファむルを生成しおいるわけですが、マヌクダりンファむルの曎新をきっかけにホットリロヌドが走り、JSONファむルの生成が無限ルヌプしおしたっおいるようです。圓初はこんなこず起きなかったず思うんですが、い぀からかこうなっおしたいたした😭血涙。

ただ、本番環境で起きる類のものではなくそこたでクリティカルな問題ではなかったこずが救いです。

環境倉数process.env.NODE_ENVを利甚すれば、Gatsbyがどのモヌドで実行されおいるかを取り出せたす。gatsby developならdevelopment、gatsby buildならproductionが栌玍されたす。

今回はgatsby-node.jsを以䞋のように曞き換え、本番環境時gatsby buildが実行された時のみJSONファむルの生成を行うようにしたした。

gatsby-node.js
  // 本番環境のみJSONファむルの生成を行う
  if(process.env.NODE_ENV === 'production') {
    const keywords = queryResult.data.allArticlesForSearching.edges.map(({node}) => {
      return {
        slug: node.fields.slug,
        title: node.frontmatter.title,
        keywords: node.frontmatter.keywords,
      }
    })

    fs.writeFileSync('./static/keywordSearch.json', JSON.stringify(keywords, null , 2))
  }

ロヌカルでgatsby developしおいる時にはfrontmatterのkeywordsを曞き換える頻床は倚くないですし、曞き換えたずしおも䞀床gatsby buildすればJSONファむルを最新にできたす。

デプロむする時にはgatsby buildが行われたすので、本番環境ではJSONファむルはちゃんず最新になっおいたす。

曎新履歎
  • 2022幎12月05日 : シリヌズを「日蚘」から「その他」に倉曎。