はじめに
このブログに、検索機能を実装してみたので、軽く紹介したいと思います。
実装の詳細については、下記のサンプルアプリを参照してください。下記のサンプルは、記事以外はこのブログとほとんど同じ内容になっています。
GitHub - monda00/next-app-router-mdx-blog: This is a sample blog application using Next.js with App Router with next-mdx-remote.
This is a sample blog application using Next.js with App Router with next-mdx-remote. - monda00/next-app-router-mdx-blog
どう検索機能を実装するか
まず、検索機能を実装するにあたって、どのように実装するか考えました。
検討した実装方法は、大きく分けて下記の3つです。
- クライアントサイドで検索
- サーバーサイドで検索
- 外部サービスを利用
今回は、コストとサーバーの負荷を考慮して、クライアントサイドで検索することにしました。
検索に使うタイトルと本文を含めたjsonファイルは2MB程度だったので、現時点ではクライアントサイドで検索しても問題ないかなと思いました。もし、記事数が増えてきたら、サーバーサイドで検索するか、外部サービスを利用することを検討したいと思います。もしくは、タイトルだけでjsonファイルを作成すれば、記事がそれなりに増えてもクライアントサイドで検索できるかもしれません。(タイトルだけで検索することになりますが)
Fuse.js
今回は、クライアントサイドで検索するのに、Fuse.jsを利用しました。
Fuse.jsは、軽量な検索ライブラリで、バックエンドに検索エンジンを必要とせず、クライアントサイドで検索を実行できます。
Fuse.js | Fuse.js
Lightweight fuzzy-search library, in JavaScript
Fuse.jsで検索機能を実装
それでは、Fuse.jsを使った検索機能の実装をざっと紹介していきます。
検索対象となるデータ
まずは、検索対象となるデータを用意します。今回は、ブログの記事のタイトルと本文を検索対象にしました。
下記のスクリプトをビルド時に実行して、記事のタイトルと本文を含めたjsonファイルを作成します。
1const fs = require('fs')
2const path = require('path')
3const matter = require('gray-matter')
4
5const postsDirectory = path.join(process.cwd(), 'contents/posts')
6const outputPath = path.join(process.cwd(), 'public/posts.json')
7
8function getAllPostsJson() {
9 const fileNames = fs.readdirSync(postsDirectory)
10 return fileNames.map((fileName) => {
11 const slug = fileName.replace(/\.mdx?$/, '')
12 const fullPath = path.join(postsDirectory, fileName)
13 const fileContents = fs.readFileSync(fullPath, 'utf8')
14 const { data, content } = matter(fileContents)
15
16 return {
17 slug,
18 title: data.title || '',
19 body: content,
20 }
21 })
22}
23
24const posts = getAllPostsJson()
25fs.writeFileSync(outputPath, JSON.stringify(posts, null, 2))
Fuse.jsで検索
次に、記事のタイトルと本文のjsonファイルを読み込んで、Fuse.jsで検索できるようにします。
1...
2
3export default function SearchModal() {
4 const [query, setQuery] = useState('')
5 const [posts, setPosts] = useState<SearchResultPost[]>([])
6 const [results, setResults] = useState<FuseResult<SearchResultPost>[]>([])
7
8 // 検索対象のデータを取得
9 useEffect(() => {
10 fetch('/posts.json')
11 .then((res) => res.json())
12 .then((data: SearchResultPost[]) => setPosts(data))
13 .catch((err) => console.error('❌ 検索の読み込みに失敗:', err))
14 }, [])
15
16 // Fuse.jsのインスタンスを作成
17 const fuse = useMemo(() => {
18 return new Fuse(posts, {
19 keys: ['title', 'body'],
20 threshold: 0.3,
21 })
22 }, [posts])
23
24 // 検索
25 useEffect(() => {
26 if (query.length > 1) {
27 setResults(fuse.search(query))
28 } else {
29 setResults([])
30 }
31 }, [query, fuse])
32
33...
これだけで、検索の機能については実装できました。あとは、検索結果を表示すればOKです。
まとめ
今回は、本ブログに実装した検索機能について簡単に紹介しました。便利なので、ぜひ使ってみてください。
Fuse.jsを使うことで、簡単にクライアントサイドでの検索機能を実装することができました。より詳細な実装については、こちらのコミットから確認できます。
参考
- Fuse.js | Fuse.js
- monda00/next-app-router-mdx-blog: This is a sample blog application using Next.js with App Router with next-mdx-remote.