国産の全文検索エンジンGroongaをNode.jsで使う

全文検索エンジンGroongaのインストール方法や使い方とNode.jsから利用する方法です。

目次

環境

  • Ubuntu 16.04.6 LTS
  • Groonga v9.0.9
  • Node.js v8.11.3
  • nroonga v0.4.1

Groongaのインストール

aptでインストール:

$ sudo apt-get -y install software-properties-common
$ sudo add-apt-repository -y universe
$ sudo add-apt-repository -y ppa:groonga/ppa
$ sudo apt-get update

# groonga本体のインストール:
$ sudo apt-get -y install groonga

# MeCabトークナイザーのインストール:
$ sudo apt-get -y install groonga-tokenizer-mecab
# `TokenFilterStem`トークンフィルターのインストール:
$ sudo apt-get -y install groonga-token-filter-stem
# MySQL互換ノーマライザーのインストール:
$ sudo apt-get -y install groonga-normalizer-mysql

または、ソースからインストール:

$ sudo apt-get -V -y install wget tar build-essential zlib1g-dev liblzo2-dev libmsgpack-dev libzmq-dev libevent-dev libmecab-dev
$ wget https://packages.groonga.org/source/groonga/groonga-9.0.9.tar.gz
$ tar xvzf groonga-9.0.9.tar.gz
$ cd groonga-9.0.9
$ ./configure
$ make -j$(grep '^processor' /proc/cpuinfo | wc -l)
$ sudo make install

CLIクライント

Groongaのインストールが成功するとCLIクライアントも使えるようになっています:

$ groonga --version

Groonga 9.0.9 [linux-gnu,x86_64,utf8,match-escalation-threshold=0,nfkc,mecab,msgpack,mruby,onigmo,zlib,lz4,zstd,epoll,rapidjson]
...

さっそくテスト用DBを作ってみます:

# テキトーにテストDB用フォルダを作成:
$ mkdir -p ~/groonga-db/test
# テキトーなテストDBを新規作成しクライアントで開く:
$ groonga -n ~/groonga-db/test/foo.db
> 

テスト用DBにテーブルを作成し、テスト用データを投入します:

# blogテーブルを作成:
> table_create blog TABLE_HASH_KEY UInt64 
> column_create blog url COLUMN_SCALAR ShortText
> column_create blog title COLUMN_SCALAR ShortText
> column_create blog content COLUMN_SCALAR LongText
# blog_indexテーブルを作成:
> table_create blog_index TABLE_PAT_KEY ShortText --default_tokenizer TokenMecab --normalizer NormalizerAuto
> column_create blog_index search COLUMN_INDEX|WITH_POSITION|WITH_SECTION blog url,title,content

# テストデータを投入:
load --table blog
[
{"_key":1,"url": "http://example.com/blog/1","title": "ブログその1","content": "これはブログその1のコンテンツです。Groongaを使ってみましょう。"},
{"_key":2,"url": "http://example.com/blog/2","title": "ブログその2","content": "これはブログその2のコンテンツです。nroongaを使ってみましょう。"},
{"_key":3,"url": "http://example.com/blog/3","title": "ブログその3","content": "これはブログその3のコンテンツです。Groongaで全文検索。"},
]

データの準備が整いました。さっそく検索してみましょう:

# "Groonga"というキーワードで全文検索:
> select blog --match_columns blog_index.search --query 'Groonga'
[
  [0, 1572582912.416774, 0.03873062133789062],
  [
    [
      [2],
      [
        ["_id", "UInt32"],
        ["_key", "UInt64"],
        ["content", "LongText"],
        ["title", "ShortText"],
        ["url", "ShortText"]
      ],
      [1, 1, "これはブログその1のコンテンツです。Groongaを使ってみましょう。", "ブログその1", "http://example.com/blog/1"],
      [3, 3, "これはブログその3のコンテンツです。Groongaで全文検索。", "ブログその3", "http://example.com/blog/3"]
    ]
  ]
]

# "http://example.com/blog/3"というキーワードで全文検索:
> select blog --match_columns blog_index.search --query "http\\://example.com/blog/3"
[
  [0, 1572583113.978827, 0.000293731689453125],
  [
    [
      [1],
      [
        ["_id", "UInt32"],
        ["_key", "UInt64"],
        ["content", "LongText"],
        ["title", "ShortText"],
        ["url", "ShortText"]
      ],
      [3, 3, "これはブログその3のコンテンツです。Groongaで全文検索。", "ブログその3", "http://example.com/blog/3"]
    ]
  ]
]

# "http://example.com/blog/3"というキーワードでurlカラムを検索:
> select blog --match_columns blog_index.search --query "url:http\\://example.com/blog/3"
[
  [0, 1572583156.503164, 0.0002362728118896484],
  [
    [
      [1],
      [
        ["_id", "UInt32"],
        ["_key", "UInt64"],
        ["content", "LongText"],
        ["title", "ShortText"],
        ["url", "ShortText"]
      ],
      [3, 3, "これはブログその3のコンテンツです。Groongaで全文検索。", "ブログその3", "http://example.com/blog/3"]
    ]
  ]
]

NOTE: 検索クエリー構文のより詳しい情報はGroonga本家のマニュアルをご参照ください。

TIP: rlwrapでCLIクライアントを使い安く

GroongaのCLIクライアントはデフォルトではbashのヒストリ機能のようなカーソルキーでの履歴表示に対応していません。同じコマンドを毎度毎度入力するのは流石に面倒なので、クライアント起動時にrlwrapを噛ませると捗ります:

# rlwrapをインストール:
$ sudo apt install rlwrap

# rlwrapを通してクライアント起動:
$ rlwrap groonga ~/groonga-db/test/foo.db
> ## ↑↓キーで入力履歴を辿れます。

Node.jsでGroonga全文検索

今度はNode.jsからGroonga全文検索を行ってみます。 ここではnpmモジュール nroonga を使います。(Node.jsからGroongaを扱えるようにできる素晴らしいモジュールです!)

まずは適当なプロジェクトを作成し、 nroonga をnpmインストールしましょう:

$ mkdir groonga-test && cd groonga-test
$ npm init
$ npm install --save nroonga

TIP: nroongaのnpmインストールでエラーが発生!?

Ubuntu 16.04 LTS な環境でnroongaをインストールしたところ、下記のようなエラーが発生しました:

...
> nroonga@0.4.1 install /home/user/node_modules/nroonga
> node-gyp rebuild

Package groonga was not found in the pkg-config search path.
Perhaps you should add the directory containing `groonga.pc'
to the PKG_CONFIG_PATH environment variable
No package 'groonga' found
gyp: Call to 'pkg-config --libs-only-l groonga' returned exit status 1 while in binding.gyp. while trying to load binding.gyp
gyp ERR! configure error 
gyp ERR! stack Error: `gyp` failed with exit code: 1
...

原因は不明ですが、とりあえずGroonga本体をソースからインストールすることで上手く行くようになります。

全文検索コードのサンプル

先ほどのCLIクライアントで実行した3つ目のコマンドをnroongaで実行するサンプルコードです:

// app.js
const nroonga = require('nroonga');

const db = new nroonga.Database('/home/user/groonga-db/test/foo.db');
const cmd = 'select blog --match_columns blog_index.search --query "url:http\\\\://example.com/blog/3"';
db.command(cmd, (err, data) => {
  console.log(`*** "${cmd}" の実行結果:`, JSON.stringify(data, null, 2));
});

サンプルコードを実行

$ node app.js 

*** "select blog --match_columns blog_index.search --query "url:http\\://example.com/blog/3"" の実行結果: [
  [
    [
      1
    ],
    [
      [
        "_id",
        "UInt32"
      ],
      [
        "_key",
        "UInt64"
      ],
      [
        "content",
        "LongText"
      ],
      [
        "title",
        "ShortText"
      ],
      [
        "url",
        "ShortText"
      ]
    ],
    [
      3,
      3,
      "これはブログその3のコンテンツです。Groongaで全文検索。",
      "ブログその3",
      "http://example.com/blog/3"
    ]
  ]
]

コメント

MySQLがビルトインしている全文検索機能は、データ規模が巨大になると検索が遅すぎて使い物になりません。そのような場面では何らかの全文検索システムを導入する必要がありますが、日本人が主体となって開発しているGroongaは日本語の全文検索に強くてオススメです。

Groongaとの通信には、nroongaを使う方法の他に、mroongaというMySQLに組み込む感じ(?)のライブラリもありますが、今回は前者の nroonga を使ってGroongaと直接やりとりしました。nroongaを選んだ理由としては2つあります。ひとつは、nroongaは間にMySQLを噛まさないため処理が速そうなこと。そしてもうひとつは、母体としてのRDBMS(MySQL)のデータとGroongaのデータを切り分けたかったことです。RDBMSとGroongaを切り分けることで、万が一Groongaのデータが壊れてもRDBMS側から比較的簡単に復旧させることができ、また全文検索に必要なカラムだけをGroonga側に持たせることもできます。(RDBMSデータとGroongaデータを同期させる仕組みを開発しなければなりませんが、それはそんなに大変ではないと思います。)

ちなみに当サイト運営謹製の国会会議録検索エンジン「くにさく」でもGroongaを利用させていただいております。 Groonga開発チームに感謝!

芽萌丸プログラミング部 @programming
プログラミング関連アカウント。Web標準技術を中心に書いていきます。フロントエンドからサーバサイドまで JavaScript だけで済ませたい人たちの集いです。