JSの全文検索lunrをNode.jsで使う

JavaScriptで全文検索出来るlunrをNode.jsで使ってみました。

  • とりあえず使ってみる
  • 日本語に対応させる
  • インデックスを保存して高速化する

とりあえず使ってみる

lunrは素の状態では日本語に対応していないのでとりあえず英文で使ってみます。

準備

npm install lunr

コード

utという単語を検索してみます。

const lunr = require('lunr')

const documents = [{
    title: "ipsum1",
    text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
  },{
    title: "ipsum2",
    text: "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
  },{
    title: "ipsum3",
    text: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."
}]

const idx = lunr(function () {
  this.ref('title')
  this.field('text')

  documents.forEach(function (doc) {
    this.add(doc)
  }, this)
})

console.log(idx.search('ut'))

実行結果

utという単語が含まれている文書が検索されました。score順に出力されます。

[ { ref: 'ipsum2',
    score: 0.639,
    matchData: { metadata: [Object] } },
  { ref: 'ipsum1',
    score: 0.451,
    matchData: { metadata: [Object] } } ]

日本語に対応させる

日本語に対応させる為に追加でlunr-languagesをインストールします。

npm install lunr-languages

コード

以下のようにrequireでいくつか指定をして、lunrの設定時にthis.use(lunr.ja)を追加します。

const lunr = require('lunr')
require('lunr-languages/lunr.stemmer.support.js')(lunr)
require('lunr-languages/tinyseg.js')(lunr)
require('lunr-languages/lunr.ja.js')(lunr)

const documents = [{
    title: "吾輩は猫である",
    text: "吾輩は猫である。名前はまだ無い。どこで生れたかとんと見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。吾輩はここで始めて人間というものを見た。"
  },{
    title: "学問のすゝめ",
    text: "「天は人の上に人を造らず人の下に人を造らず」と言えり。されば天より人を生ずるには、万人は万人みな同じ位にして、生まれながら貴賤上下の差別なく、万物の霊たる身と心との働きをもって天地の間にあるよろずの物を資り、もって
衣食住の用を達し、自由自在、互いに人の妨げをなさずしておのおの安楽にこの世を渡らしめ給うの趣意なり。"
  },{
    title: "蜘蛛の糸",
    text: "ある日の事でございます。御釈迦様は極楽の蓮池のふちを、独りでぶらぶら御歩きになっていらっしゃいました。池の中に咲いている蓮の花は、みんな玉のようにまっ白で、そのまん中にある金色の蕊からは、何とも云えない好い匂が、絶
間なくあたりへ溢れて居ります。極楽は丁度朝なのでございましょう。"
}]

const idx = lunr(function () {
  this.ref('title')
  this.field('text')
  this.use(lunr.ja)

  documents.forEach(function (doc) {
    this.add(doc)
  }, this)
})

console.log(idx.search('人'))

実行結果

[ { ref: '学問のすゝめ',
    score: 1.828,
    matchData: { metadata: [Object] } } ]

インデックスを保存して高速化する

これまでのやり方だと検索時にインデックスを毎回作成しているので大量のインデックスになってくると時間がかかってしまいます。この為事前にインデックスを作って保存しておき、実行時にはインデックスファイルを読み込んでインデックスの作成の処理時間を省略するようにて高速化します。

インデックスの書き出し

lunr-index.jsonというファイル名でインデックスを書き出します。`

const lunr = require('lunr')
const fs = require('fs')
require('lunr-languages/lunr.stemmer.support.js')(lunr)
require('lunr-languages/tinyseg.js')(lunr)
require('lunr-languages/lunr.ja.js')(lunr)

const documents = [{
    title: "吾輩は猫である",
    text: "吾輩は猫である。名前はまだ無い。どこで生れたかとんと見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。吾輩はここで始めて人間というものを見た。"
  },{
    title: "学問のすゝめ",
    text: "「天は人の上に人を造らず人の下に人を造らず」と言えり。されば天より人を生ずるには、万人は万人みな同じ位にして、生まれながら貴賤上下の差別なく、万物の霊たる身と心との働きをもって天地の間にあるよろずの物を資り、もって
衣食住の用を達し、自由自在、互いに人の妨げをなさずしておのおの安楽にこの世を渡らしめ給うの趣意なり。"
  },{
    title: "蜘蛛の糸",
    text: "ある日の事でございます。御釈迦様は極楽の蓮池のふちを、独りでぶらぶら御歩きになっていらっしゃいました。池の中に咲いている蓮の花は、みんな玉のようにまっ白で、そのまん中にある金色の蕊からは、何とも云えない好い匂が、絶
間なくあたりへ溢れて居ります。極楽は丁度朝なのでございましょう。"
}]

const idx = lunr(function () {
  this.ref('title')
  this.field('text')
  this.use(lunr.ja);

  documents.forEach(function (doc) {
    this.add(doc)
  }, this)
})

fs.writeFile('lunr-index.json', JSON.stringify(idx), (err)=>{
  if(err){
    console.log('write file error')
    throw err
  }else{
    console.log('success')
  }
})

検索

インデックスファイルを読み込んで検索してみます。 lunr-languagesは分かち書き時に使われるので検索のみ行うときはrequireの必要はありません。

const lunr = require('lunr')
const fs = require('fs')

fs.readFile('lunr-index.json', 'utf-8', (err, indexData) => {
  if (err) {
    console.log('read file error')
    throw err
  }

  const idx = lunr.Index.load(JSON.parse(indexData))
  console.log(idx.search('人'))
})

実行結果

無事同じ検索結果が出ました🎉

[ { ref: '学問のすゝめ',
    score: 1.828,
    matchData: { metadata: [Object] } } ]

参照