Gridsomeで作成したブログにFlexsearch.jsで全文検索を導入する

2019.08.02

#Gridsome
このブログはまだ全文検索を必要とするほど記事数はありませんが、勉強になるかなと思いGridsomeで出力しているこのブログにFlexSearch.jsを用いた全文検索機能を追加してみました。
分かち書き(もどき)による日本語検索への対応と英数の前方一致検索が可能になりました。

FlexSearch.jsとは

FlexSearch.jsとは、Javascript製の全文検索ライブラリです。 依存関係がまったくなく他の全文検索ライブラリに比べて高速なことやパフォーマンスやメモリ効率のカスタマイズ性が売りのようです。


FlexSearch(Github)


フロントエンドは素人なので他のライブラリやalgoliaのような全文検索サービスはどんなものかわかりませんが、依存関係がないというのが魅力的だったのでこれに決めました。

Gridsomeへの導入

普通にnpmでインストールして、コンポーネントでインポートして使用します。

npm i flexsearch

仮にFlexSearch.vueというコンポーネントを作成して使うとします。

// ~/components/FlexSearch.vue

import Flexsearch from 'flexsearch'

export default {
  data() {
    return {
      index: null,
      // 検索フォームにバインドする変数
      searchTerm: '',
    }
  },
  beforeMount() {
    this.index = new Flexsearch({
      tokenize: 'forward'
      doc: {
        id: 'id',
        field: ['title', 'description'],
      },
    })
    this.index.add(this.$static.posts.edges.map(e => e.node))
  },
  computed: {
    // 検索結果を返す算出プロパティ
    searchResults() {
      if (this.index === null) return []
      return this.index.search({
        query: this.searchTerm,
        limit: 10,
      })
    },
  },
}
# ~/components/FlexSearch.vue

query Posts {
  posts: allPost {
    edges {
      node {
        id
        path
        title
        description
        ...
      }
    }
  }
}

こんな感じで割とすぐに全文検索が使えるようになります。

公式を見るといろいろカスタマイズできそうな項目はあるのですが、勉強不足のためとりあえず最低限で使ってみます。

日本語をインデックスに含める

導入は簡単なんですが、上記の設定だと日本語の文字列は検索できません。 流石にそれだと検索機能を追加する意味がないので、日本語に対応させます。


日本語を検索できるようにするには日本語を分かち書きしてインデックスに含めないといけません。

分かち書きや形態素解析のライブラリはいろいろあるようですが(TinySegmenterやKuromoji.jsなどが有名なんでしょうか)、せっかく依存性のないFlexSearchを使っているのでここはライブラリは使用せず自分で書いてみようと思います。

tokenize: 'forward' としたところを以下のように修正します。

      tokenize: str => [
        ...new Set(
          str
            // 処理前にアルファベットを小文字に変換
            .toLowerCase()
            // 漢字、カナ、半角英数の連続する塊を切り出し
            // かなと全角英数は対象外
            .match(/[-]+|[-]+|[a-z0-9]+/g)
            // 1文字の要素を削除する
            .filter(word => word.length > 1)
            // 半角英数の場合、前方一致検索ができるように処理
            .map(word => {
              if (word.match(/[a-z0-9]+/g)) {
                let token = ''
                return Array.from(word)
                  .map(char => (token += char))
                  .filter(token => token.length > 1)
              } else {
                return word
              }
            })
            .flat()
        ),
      ],

上記の設定で、例えばこの投稿のdescription(目次の上にある文章)は以下のようにtokenizeされます。

[
  'ブログ', '全文検索', '必要', '記事数', '勉強',
  'gr', 'gri', 'grid', 'grids', 'gridso', 'gridsom', 'gridsome',
  '出力', 'fl', 'fle', 'flex', 'flexs', 'flexse', 'flexsea',
  'flexsear', 'flexsearc', 'flexsearch', 'js', '全文検索機能',
  '追加', '日本語検索', '対応', '英数', '前方一致検索', '可能',
]

これらのいずれかにマッチする文字列が検索フォームに入力されたとき、この記事が検索結果として表示されるようになります。

英語については tokenize: 'forward' を指定すると前方一致検索ができるのですがそれと同じにしたかったのでこのようにしました。

searchResultsの算出プロパティに検索結果が代入されるので、これを画面に組み込めば全文検索機能の完成です。

あとはおいおい挙動やパフォーマンスの様子をみつつ調整していければと思います。