日本語による全文検索を導入したいという気持ちで調べたところ、AlgoliaやElasticsearch、Supabaseなど、多くのソフトウェアが出てきました。
少し前であれば、自前でMySQLを用意してMroongaあたりをインストールしていましたが、現在では大きく進歩しています。しばらく全文検索を実装する機会がなかったのでワクワクです。
はじめはElasticsearchを導入しようとしました。しかし、いくらか触ったところ「高機能だが学習コスト高め」という結論に至りました。(導入自体はDockerで簡単になっていました)
そこで、「簡単に構築できる全文検索環境」のキャッチコピーに惹かれ、Fessへ入門することに決めたので備忘録を残します。
追記
Java製のため、かなりCPUを消費することが判明。Cloud Runで動かすことを予定していたため、コスト面で不安が残るため一旦導入は断念。
Fessとは
株式会社コードリブズという企業が提供するオープンソースの全文検索サーバーです。
とにかく簡単に導入でき、それなのに高機能な全文検索システムです。裏側はElasticsearchかOpenSearchで選べるようになっています。
Fessのインストール
早速、Fessをインストールしていきます。基本的に下記リンクの公式ドキュメントに沿って進めていくので、どちらかと言うと公式ドキュメントをご覧になることをおすすめします。
今回はmacOS+Docker環境で最低限使える環境を作っていきます。
Docker版はGitHubリポジトリにあるDocker Composeのyamlファイルを使います。
git clone
してみましょう。
$ docker compose -f compose.yaml -f compose-opensearch2.yaml up -d
たったこれだけで完了です。
起動動作確認
ブラウザでhttp://localhost:8080
にアクセスしてみましょう。
下の画像のような画面が出てきます。
次に右上のLogin
からログインします。デフォルトでユーザー名とパスワードはadmin
ですが、すぐにパスワードを変更できます。
パスワード変更が済んだら、右上のメニューからAdministration
をクリックして管理画面に入りましょう。下のスクリーンショットのような管理画面が表示されます。
起動が確認できたら、次のステップとして実際にインデックスを作成してみます。
ブラウザの言語設定が日本語になっていれば、自動的に日本語で表示されます。
※当方のブラウザの言語設定が英語のため、英語のまま進めます。
インデックスの作成
今回はFessのWebクローリング機能を使っていきます。(とても便利)
ただ、注意点として自社や自身が保有するWebサイトを対象にしてください。他社のWebサイトに余計な不可を加えることは避けましょう。
以下の画面から対象のURLを設定します。
作成したら、"Scheduler"メニューから"Default Crawler"をクリックし、"Start now"で開始しましょう。
下のスクリーンショットのように、"Crawling Info"でクローラーのステータスを見ることができます。(スクリーンショットは完了後)
検索動作確認
次に検索できるかを確認しましょう。
右上にあるツールバーの"Search View"、またはサイドバー上部にある検索窓からクローリングしたWebサイトに掲載されていそうな語句を検索してみます。
検索結果にスコア(検索語句と対象の近似性)付きで検索結果が表示されます。これで検索が動作していることがわかりました。
Next.jsから使ってみる
「GUI上で検索結果が見られる=APIで利用できる」状態なので、Next.jsから使ってみましょう。
まずはAPIの確認です。公式ドキュメントでは
こちらに詳しく書かれています。
API動作確認
先ほど、"Python"を検索語句とした結果を画面上で表示しました。同じようにAPIで叩いてみます。
検索APIはapi/v1/documents?q={検索語句}
なので、以下のコマンドを叩きます。
結果のJSONを見やすくするために
jq
を使っています。
インストールしていない人は、とても便利なので
こちらからインストールしてみてください。
$ curl -X GET 'localhost:8080/api/v1/documents?q=Python' | jq .
下記画像のように、良い雰囲気で取得できています。
Next.jsにコンポーネントを作る
Next.jsで上記APIを使うために、検索フォーム用コンポーネントを作成します。
なお、前提としてAPIへリクエストするため、Next.jsの設定はSSRであるとします。また、今回は簡略化のために
Mantine UIを利用しています。
Search.tsx
import { useState } from 'react'
import { useForm } from '@mantine/form'
import { Box, Button, TextInput } from '@mantine/core'
export function Search() {
const [items, setItems] = useState([])
const form = useForm({
initialValues: {
query: '',
}
})
const fessSearch = async () => {
const res = await fetch(`/api/fess?q=${form.values.query}`)
const data = await res.json()
setItems(data.items)
}
return (
<Box>
<form onSubmit={form.onSubmit(() => fessSearch())}>
<TextInput
label="キーワード"
placeholder="キーワード"
{...form.getInputProps('query')}
/>
<Button type="submit">検索</Button>
</form>
{items.length > 0 && (
<p>検索結果</p>
<ul>
{items.map((item: Item) => (
<li key={item.url}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
)}
</Box>
)
}
最終的に/api/fess
へリクエストします。
/api/fess
の内容は以下です。Next.js 13系で使えるApp Routerの場合だと実装が変わるので、それぞれ記載します。
pages/api/fess.ts
type Item = {
title: string
url: string
}
type ResponseData = {
items: Item[]
}
export default async function handler( req, res ) {
const keyword = req.query.q
const endpoint = `http://localhost:8080/api/v1/documents?q=${keyword}`
const response = await fetch(endpoint)
const data = await response.json()
const items = data.data.map((item: any) => ({
title: item.title,
url: item.url,
}))
res.status(200).json({ items })
}
app/api/fess/route.ts
import { NextResponse } from 'next/server'
export async function POST(req: Request) {
const json = await req.json()
const res = await fetch(`http://localhost:8080/api/v1/documents?q=${json.q}`)
const data = await res.json()
const items = data.data.map((item: any) => ({
title: item.title,
url: item.url,
}))
return NextResponse.json({ items })
}
若干デザインはいじっていますが、以下のように検索結果が表示されれば完了です。
簡単に導入できるFessでお手軽全文検索
今回は最短距離で試していきましたが、実際に本番運用する際はAPI Keyの発行やインデックス作成のワークフローを考慮する必要があります。
そういった点を念頭に置いても、Fessは非常に簡潔な導入方法で利用できるためおすすめです。
よりビジネスシーンに活用したい場合は、以下のプランもあります。
他の全文検索サービスと比べても安価なので、「全文検索だけ導入したい」というユースケースでぜひ活用していきましょう。