はじめに
サイト内検索やアプリ内のインクリメンタル検索を実装しようとして、「Elasticsearchを立てるほどの規模じゃない」「でもArray.filterとインデックスなしの文字列一致では遅すぎる」と悩んだ経験はないでしょうか。特にSPAやドキュメントサイトのように、数千〜数万件のデータをブラウザ側だけで検索したいケースでは、サーバーを介さず動く軽量な全文検索エンジンが欲しくなります。
そこで候補に挙がるのがFlexSearchです。README曰く「他ライブラリと比較して最大100万倍高速なクエリ処理」を謳う、ブラウザとNode.jsの両方で動く全文検索ライブラリです。数字だけ見ると眉唾ものに感じますが、実際にはインメモリの転置インデックスを徹底的にチューニングすることで、この速度を実現しています。今回はその仕組みと使い方を、実際に手を動かしながら見ていきます。
FlexSearchとは
FlexSearchは「Next-Generation full-text search library for Browser and Node.js」を掲げる、ゼロ依存の全文検索ライブラリです。単純なid-テキストの組を検索するIndexと、複数フィールドを持つJSONドキュメントを検索できるDocumentという2つのAPIを軸に構成されています。GitNationで「Best Technology of the Year」にノミネートされるなど、パフォーマンス面での評価が高いのが特徴です。
主な特徴
- 圧倒的な検索速度 - 独自ベンチマークでは、他の主要な全文検索ライブラリと比較して最大100万倍高速なクエリ処理を達成したとされています
- ゼロ依存・軽量 -
light・compact・bundleと用途別のビルドが用意されており、必要な機能だけを選んで読み込めます - Web Worker / Node.jsワーカー対応 -
Workerインデックスを使えば、インデックスの更新や検索処理をメインスレッドから切り離して並列実行できます - 多言語の文字セットに対応 - ラテン文字はもちろん、日本語・中国語・韓国語(CJK)、アラビア語、ヘブライ語、キリル文字などを標準サポート
- 永続化インデックス - IndexedDB・Redis・SQLite・Postgres・MongoDB・Clickhouseなど、様々なデータストアにインデックスを永続化できます
インストール
npmを使う場合は以下のコマンドでインストールできます。
npm install flexsearch
Yarnやpnpmでも同様にインストールできます。
yarn add flexsearch
pnpm add flexsearch
ESM環境では次のように名前付きインポートで各クラスを読み込みます。
import { Index, Document, Worker, Charset } from "flexsearch";
基本的な使い方
もっともシンプルな使い方は、IndexにID付きの文字列を登録して検索するパターンです。ブログ記事の一覧から、キーワードに一致するものだけを絞り込んでみましょう。
日本語のように単語間にスペースがない言語では、素のFlexSearchでは単語の切れ目をうまく認識できません。encoder: Charset.CJKを指定することでCJK向けの分かち書きが行われ、トークナイズの設定を変えなくても単語の途中の文字列で検索にヒットするようになります。
実践的なユースケース
複数フィールドを横断検索する(Document Index)
実際のアプリでは、タイトルと本文のように複数のフィールドを持つデータをまとめて検索したい場面が多くあります。Documentインデックスを使えば、フィールドごとにインデックスを構築しつつ、1回のsearch呼び出しで横断的に検索できます。store: trueを指定すると、検索結果からIDを引くまでもなく元データをそのまま復元(enrich)できます。
index: ['title', 'content']と指定するだけで、両方のフィールドが個別にインデックス化され、どちらかにマッチすれば結果に含まれます。フィールドごとにtokenizeやencoderを変えたい場合は、文字列の代わりにオブジェクトの配列を渡すことで細かく制御できます。
タグで絞り込む(Multi-Tag Search)
キーワード検索に加えて、カテゴリやステータスといったタグで絞り込みたいケースも多いはずです。Documentインデックスのtagオプションを使うと、キーワード検索とタグフィルタを組み合わせられます。
同じキーワードでも、選んだタグによって結果が絞り込まれる様子が確認できます。ECサイトの商品検索や、ブログ記事のカテゴリ絞り込みなど、キーワード×属性の組み合わせが必要な場面でそのまま応用できるパターンです。
入力途中でもヒットする部分一致検索(Tokenizer)
デフォルトのstrictトークナイザは完全一致に近く、入力途中の文字列ではヒットしません。オートコンプリートやインクリメンタルサーチを実装する場合は、tokenizeオプションをforward(前方一致)やfull(部分一致)に変更することで、タイプ中でも結果が更新されるUIを作れます。
searchという単語の一部を入力しただけで、fullトークナイザ側は「Search」を含む3件(FlexSearch・Elasticsearch・MeiliSearch)をヒットさせています。ただしfullは転置インデックスのサイズが大きくなりやすいトレードオフがあるため、データ量や用途に応じてforward(前方一致のみ)と使い分けるのが実践的です。
まとめ
FlexSearchは、Indexによるシンプルなid-テキスト検索から、Documentによる複数フィールド・タグ検索、さらにはWorkerや永続化ストレージへの対応まで、全文検索に必要な機能を一通り備えたライブラリです。サーバーを立てずにブラウザだけで高速な検索体験を作りたい場合や、Node.js側で軽量な検索エンジンが欲しい場合に、まず検討する価値があります。
今回紹介したIndex・Document・tokenizeの3つを押さえておけば、多くの検索UIはカバーできるはずです。より大規模なデータセットを扱う場合は、Web Workerによる並列化や、IndexedDB・Redisなどへの永続化もあわせて検討してみてください。