Help us understand the problem. What is going on with this article?

【JSの全文検索エンジン】elasticlunr.jsを触ってみた~インストールからパイプライン、日本語対応など~

More than 1 year has passed since last update.

elasticlunr.jsを使う機会があったのでまとめた。

Elasticlunr.jsとは

JavaScriptで記述された、軽量な全文検索エンジン
ブラウザ上で動作する。

全文検索とは
複数の文書(ファイル)から特定の文字列を検索する事

Lunr.jsをベースに開発されている。
Lunr.jsと比べると、インデックスにトークンコーパスとベクターを含まないため、軽量で早い
(トークナイズ用の単語の辞書を持たないから軽量?この辺よくわかりませんでした。)

Elasticsearchやluceneと同じ採点メカニズムを採用している。

デフォルトでは日本語に対応していない。

サンプルコード

実験用にelasticlunrだけを動かせるコードを以下のリポジトリにおいてます。
とりあえず動かしてみたい方はご活用ください。

GitHub - elasticlunr-sample

起動コマンド
git clone https://github.com/t-kuni/elasticlunr-sample.git
cd elasticlunr-sample
npm install
node index.js

基本的な使い方

めちゃシンプルなのでコード見た方が早い
これだけでインデックスの構築と検索ができる。

const elasticlunr = require('elasticlunr');

// インデックス初期化
const index = elasticlunr(function () {
    this.addField('body');
    this.setRef('id');
});

// 文書追加
index.addDoc({
    "id": 1,
    "body": "apple block green",
});
index.addDoc({
    "id": 2,
    "body": "camera diary host"
});

// 検索
const r = index.search("camera", {
    fields: {
        body: {boost: 1},
    },
});

console.log(r);
// 出力: [ { ref: '2', score: 0.5773502691896258 } ]

主な機能

大まかに7つある。

  • search index
  • Query-Time boosting
  • Boolean Model
  • Token Expandation
  • Tokenization
  • Pipeline
  • TF/IDF Model, Vector Space Model

search index

全文検索に必要なインデックスを構築する。
文書追加(addDoc)時に、文書を解析してインデックスを構築する。

// インデックス初期化
var index = elasticlunr(function () {
    this.addField('body');
    this.setRef('id');
});

// 文書追加
index.addDoc({
    "id": 1,
    "body": "Yestaday Oracle has released its new database Oracle 12g, this would make more money for this company and lead to a nice profit report of annual year."
});

Query-Time boosting

elasticlunrでは、検索単語に対してどの程度マッチしているのかを表すスコアを算出する。
このスコアを算出する際の、フィールド毎の重みを設定できる。
boostプロパティを使用する。
インデックスの構築時ではなく、検索時に設定出来る。

// 検索実行
const r = index.search("test", {
    fields: {
        title: {boost: 0}, // 重みを0にしたフィールドはスコア算出の対象外となる。
        body: {boost: 1},
    }
});

Boolean Model

OR検索AND検索ができる
検索時にboolプロパティで設定する。

// 全てのフィールドに対してOR検索
const r = index.search("America Canada", {
    fields: {
        title: {boost: 1},
        body: {boost: 1},
    },
    bool: 'OR' // "America" "Canada"の2単語でOR検索します。(boolオプションを省略した場合もOR検索になります
});

// 全てのフィールドに対してAND検索
const r = index.search("America Canada", {
    fields: {
        title: {boost: 1},
        body: {boost: 1},
    },
    bool: 'AND' // 'America" "Canada"の2単語でAND検索します。
});

// titleフィールドに対してAND検索、それ以外はOR検索
const r = index.search("America Canada", {
    fields: {
        title: {boost: 1, bool: 'AND'},
        body: {boost: 1},
    },
    bool: 'OR'
});

Token Expandation

単語の部分一致を含めるかどうか
例えば、当該機能を有効化して、"micro"で検索した場合、"microwave", "microscope"も一致した単語として扱われる。
この場合、部分一致した文書のスコアは完全一致した場合と比べて低くなる。
デフォルトは無効になっている。
検索時にexpandプロパティで設定する

const elasticlunr = require('elasticlunr');

// インデックス初期化
const index = elasticlunr(function () {
    this.addField('body');
    this.setRef('id');
});

// 文書追加
index.addDoc({
    "id": 1,
    "body": "microwave"
});
index.addDoc({
    "id": 2,
    "body": "microscope"
});
index.addDoc({
    "id": 3,
    "body": "micro"
});

// 部分一致を含めて検索
const r = index.search("micro", {
    fields: {
        body: {boost: 1},
    },
    expand: true,
});

console.log(r);
// 出力: [ { ref: '3', score: 1.4054651081081644 },
//          { ref: '1', score: 0.1317623538851404 },
//          { ref: '2', score: 0.11712209234234702 } ]

// 部分一致を含めず検索
const r2 = index.search("micro", {
    fields: {
        body: {boost: 1},
    },
    expand: false,
});

console.log(r2);
// 出力: [ { ref: '3', score: 1.4054651081081644 } ]

Tokenization(分かち書き)

文書をトークンに分解すること。
デフォルトのトークナイザーは英語を扱える。

トークナイザを差し替えるには、elasticlunr.tokenizer関数を置き換える。

const elasticlunr = require('elasticlunr');

elasticlunr.tokenizer = (field) => {
    /*
     * 引数について
     * field = 文書。フィールド単位で渡される。
     *
     * 返り値について
     * トークンの配列を返す。このトークンが後述のパイプラインで処理される。
     */
    return field.split(' ');
};

Pipeline

検索対象の文書検索クエリに対して処理を実行する関数のスタック
文書追加時、または、検索時にトークナイズパイプラインの順で処理される。
デフォルトでは、英語用のstop word filterstemmerが登録される。

  • stop word filter
    • Stop wordとは、「は」「が」などの一般的に使われ、ドキュメントの検索に役にたたない単語の事。 Stop word filterはこれらを除外する仕組み。
  • stemmer
    • 語形の変化を取り除き、同一の単語表現に変換する処理のこと 例えば、'searching', 'searched'は全て'search'となる。

主な用途としては、英語以外の言語に対応する場合に、上記2つを置き換えるのに使用する。
インデックス初期化時のクロージャの中でthis.pipeline.resetthis.pipeline.addを使う。

// インデックス初期化
const index = elasticlunr(function () {
    this.addField('body');
    this.setRef('id');

    this.pipeline.reset();
    this.pipeline.add(function (token, tokenIndex, tokens) {
        /*
         * 引数について
         * token      = 処理対象のトークン
         * tokenIndex = 処理対象のトークンの添え字番号
         * tokens     = トークンの配列
         *
         * 返り値について
         * 修正後のトークンを返す。nullを返すと、そのトークンはインデックスされなくなる(後続のパイプラインの処理も行われない)。
         */
        console.log(token, tokenIndex, tokens);
        return token;
    })
});

TF/IDF Model, Vector Space Model

elasticlunrでは、検索単語に対してどの程度マッチしているのかを表すスコアを算出する。
このスコアを算出するための、いくつかの仕組みこと。

  • TF/IDF Model(単語の出現頻度と逆文書頻度)
    • 文書中に含まれる単語の重要度を評価する手法
  • Vector Space Model(ベクトル空間モデル)
    • 文書をベクトルとして表現し、2文書間の類似度を計算する

このメカニズムはElasticsearch、Luceneと同じ。詳しくは以下を参照してください。
Luceneのスコアリングについて

日本語対応について

デフォルトでは、英語での検索しか出来ない。
多言語対応プラグインを組み込めば、日本語で検索できる様になる。

lunrの多言語対応プラグインをelasticlunr向けにフォークした物を使用する。

weixsong/lunr-languages

※ lunr-languagesリポジトリは2つあるので注意。
MihaiValentin/lunr-languagesはlunr用であり、elasticlunrで使用するとエラーが発生する。
weixsong/lunr-languagesを使用する必要がある。

インストール方法

weixsong/lunr-languagesの、以下2つのファイルを自身のプロジェクトにコピーする。

  • lunr.stemmer.support.js
  • lunr.jp.js

またlunr.jp.jsがTinySegmenterに依存しているため、こちらもダウンロードして自身のプロジェクトにコピーする。

TinySegmenter

完成形はGitHub - elasticlunr-sampleにあるので参考にしてください。

修正箇所

TinySegmenterがrequire()に対応していないため、Nodeで使う場合は、以下の通り修正が必要。
require()で呼び出せる様に、末尾にmodule.exportsを追加する

tiny_segmenter-0.2.js
    p3 = p;
    word += seg[i];
  }
  result.push(word);

  return result;
}

+ module.exports = TinySegmenter;

同様に呼び出し側(lunr.jp.js)もrequireに修正する。

lunr.jp.js
            // change the tokenizer for japanese one
            lunr.tokenizer = lunr.jp.tokenizer;
        };
-        var segmenter = new lunr.TinySegmenter();  // インスタンス生成
+        var segmenter = new (require('./tiny_segmenter-0.2.js'))();  // インスタンス生成

        lunr.jp.tokenizer = function (obj) {

サンプルコード

index.js
const elasticlunr = require('elasticlunr');
require('./lunr.stemmer.support.js')(elasticlunr);
require('./lunr.jp.js')(elasticlunr);

// インデックス初期化
const index = elasticlunr(function () {
    this.use(elasticlunr.jp); // 使用言語の指定
    this.addField('body');
    this.setRef('id');
});

// 文書追加
index.addDoc({
    "id": 1,
    "body": "カメラ 犬"
});
index.addDoc({
    "id": 2,
    "body": "本棚 猫"
});
index.addDoc({
    "id": 3,
    "body": "水 刺身"
});

// 日本語で検索
const r = index.search("", {
    fields: {
        body: {boost: 1},
    },
    expand: true,
});

console.log(r);
// 出力: [ { ref: '2', score: 0.10540988310811232 } ]

まとめ

必要最小限の全文検索エンジン、という感じのライブラリなので
全文検索のコア部分の概要だけ抑えたい~みたいな人の学習用に良いんじゃないかと思った。

t-kuni
主にWEBとか
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした