今更ですが、Groongaを使ってみた


全文検索とかどうなの?ということだったので、Groongaを使ってみました。
 → Groonga

全文検索

「全文検索」とはとっても簡単に言うと 「Google検索」 のようなものです。
キーワードなどを入力してたくさんの文書の中から目的のものを見つけだすあれです。

いろいろあるみたい

というわけで、まずは Googleさんに「全文検索システム」とかで聞いてみるといろいろ出てきます。

  • Fess
  • Elasticsearch
  • Apache Solr
  • Namazu
  • Groonga

などなど
Namazu とか懐かしいなぁと思いながら、いろいろみてみると、どうやら Groonga が良さげだったので、使ってみることにしました。

インストール

ローカル環境に入れるのはちょっとあれだったので、docker-compose を利用してインストールすることにしました。#公式のドキュメントにも書いてあるし。。。

公式に書いてある方法で試してみた

 → Groonga#with-docker-compose

おお、確かに簡単に立ち上がりました。
確認は、ブラウザを立ち上げて、http://localhost:10041 とすると、なんかページが表示されます。

#画像を貼ろうと思ってアップしてみたのだけれど、何度やっても以下のように怒られたので、皆さん脳内補完(想像してみてください)お願いします。。。
Something went wrong

がしかし。。。

% docker-compose run groonga
だと、ターミナル立ち上げっぱなしにしないと落ちてしまいます。
じゃあ再度立ち上げようといって % docker-compose restart とかすると、こんどは、データファイルはすでに存在するよエラー。。。

#理由は command: ["-n", "/mnt/db/data.db"]-n がデータファイルを新規に作成というオプションなので、毎回新規に作成しようとして、怒られてました。

むむむ。

というわけで、Dockerfile を作って立ち上げるようにしてみた

ソースを Github に上げたので参照してください。

簡単に説明します。
まずは、/groonga の下。

docker-compose.yml に構成設定等を記述しています。
中にある build: ./groonga で、./groonga/Dockerfile から生成するようにしています。
volumes でホストマシンからでもデータファイルが見れるようにします。

あとの xxxx.sh は便利ツール的な感じです。
インストールするときは、 $ ./install.sh で docker-compose build とかをしてくれます。
docker プロセスに入りたい場合は $ ./login.sh で入れます。
この場合は $ exit で抜けましょう。
停止は $ ./stop.sh で、再起動は $ ./restart.sh です。
dockerプロセスがいらなくなったら $ ./remove.shdocker rm container をします。

つづいて、/groonga/groonga の下。
Dockerfile に具体的な作り方が書いてあります。

なんかバグっぽいのがあるらしい

普通に groonga をインストールして、http でアクセスしていると、時々エラーになることがあります。ネットで調べてみると次の記事がありました。
 → https://okamuuu.hatenablog.com/entry/2017/11/13/185903

#okamuuuさん ありがとうございます。
#しかしこの記事が 2017年で、2020年の今でも同じエラー。。。

というわけで、stack-fix.c はパッチ。それを含めたものを Dockerfile に書いています。
それから、先ほどの、「再起動時に新規ファイル作成でエラー」を回避するために、起動スクリプトを書いて、データファイルがなければ作り、あればそれを使用するようにしました。 groonga.sh です。

再度インストール

というわけで、$ ./install.sh っと。

http://localhost:10041 にアクセスして、はい。できました。

Ruby からかまってみる。

groonga を ruby から使うにはいくつかあるらしいです。
rroonga が有名ですが、これは同一サーバ上に groonga が動いている場合のライブラリっぽいです。
Docker とか別のサーバ(仮想含む)で動いてる場合には groonga-client っぽいです。

というわけで、インストール。

インストールとかサンプルとか

groonga-client は $ gem install groonga-client
でインストールできます。

次のサンプルを実行してみました。

test.rb
# -*- coding: utf-8 -*-
require "groonga/client"

host = "127.0.0.1"
port = 10041
Groonga::Client.open(host: host, port: port, protocol: :http) do |client|
  tables = client.table_list
  unless tables.map{|m| m.name}.include?("docs")
    # ---- create normal table ----
    client.table_create(name: "docs",
                        flags: "TABLE_HASH_KEY",
                        key_type: "ShortText")
    client.column_create(table: "docs",
                         name: "body",
                         flags: "COLUMN_SCALAR",
                         type: "Text")

    # ---- data insert to table ----
    values = [
      { "_key" => "/path/to/document/1",
        "body" => "メロスは激怒した。" },
      { "_key" => "/path/to/document/2",
        "body" => "メロスには政治がわからぬ。" },
      { "_key" => "/path/to/document/3",
        "body" => "メロスには竹馬の友があった。" },      
    ]   
    client.load(table: "docs",
                values: values.to_json)
  end

  # ---- data search ----
  query = "激怒"
  response = client.select(table: "docs",
                           query: query,
                           match_columns: "body")
  puts "hits: #{response.n_hits} (query: #{query} -> body)"
  response.records.each do |record|
    p record
  end

  query = "政治"
  response = client.select(table: "docs",
                           query: "body:@#{query}")
  puts "hits: #{response.n_hits} (query: #{query} -> body)"
  response.records.each do |record|
    p record
  end

  filter = "/path/to/document/3"
  response = client.select(table: "docs",
                           filter: "_key == '#{filter}'")
  puts "hits: #{response.n_hits} (filter: #{filter} -> _key)"
  response.records.each do |record|
    p record
  end
  query = "/document"
  response = client.select(table: "docs",
                           query: "_key:@#{query}")
  puts "hits: #{response.n_hits} (query: #{query} -> _key)"
  response.records.each do |record|
    p record
  end

end

では実行。

$ ruby ./test.rb
hits: 1 (query: 激怒 -> body)
{"_id"=>1, "_key"=>"/path/to/document/1", "body"=>"メロスは激怒した。"}
hits: 1 (query: 政治 -> body)
{"_id"=>2, "_key"=>"/path/to/document/2", "body"=>"メロスには政治がわからぬ。"}
hits: 1 (filter: /path/to/document/3 -> _key)
{"_id"=>3, "_key"=>"/path/to/document/3", "body"=>"メロスには竹馬の友があった。"}
hits: 3 (query: /document -> _key)
{"_id"=>1, "_key"=>"/path/to/document/1", "body"=>"メロスは激怒した。"}
{"_id"=>2, "_key"=>"/path/to/document/2", "body"=>"メロスには政治がわからぬ。"}
{"_id"=>3, "_key"=>"/path/to/document/3", "body"=>"メロスには竹馬の友があった。"}

なんか良さそうですね。

'激怒' と '政治' の検索で query の書き方を2通りの方法で書いてみました。
match_columns を使うのと、column:@query のように書く書き方です。
とりあえず、どちらでもいい感じです?

。。。っていうか、テーブル作成時に、トークナイザーとか設定してないし、インデックステーブルも作ってないけど、bodyカラムの中を "%激怒%" 的に検索できてるなぁ。。。 query っていうのがそういうものなのだろうか。。。

っていうか、そういうものっぽいですね。はい。

インデックステーブルを作成してみた

というわけで、インデックステーブルを作成してみました。
これを作ると検索時間がとても早くなるっぽい。

test2.rb
# -*- coding: utf-8 -*-
require "groonga/client"

host = "127.0.0.1"
port = 10041
Groonga::Client.open(host: host, port: port, protocol: :http) do |client|
  tables = client.table_list
  unless tables.map{|m| m.name}.include?("doc_indexes")
    # ---- create indexes ----
    client.table_create(name: "doc_indexes",
                        flags: "TABLE_PAT_KEY",
                        key_type: "ShortText",
                        default_tokenizer: "TokenBigram",
                        normalizer: "NormalizerAuto")
    client.column_create(table: "doc_indexes",
                         name: "body_index",
                         flags: "COLUMN_INDEX|WITH_POSITION",
                         type: "docs",
                         source: "body")
  end

  query = "わからぬ"
  response = client.select(table: "docs",
                           query: query,
                           match_columns: "doc_indexes.body_index")
  puts "hits: #{response.n_hits} (query: #{query} -> doc_indexes.body_index)"
  response.records.each do |record|
    p record
  end  
end

で実行すると

$ ruby ./test2.rb
hits: 1 (query: わからぬ -> doc_indexes.body_index)
{"_id"=>2, "_key"=>"/path/to/document/2", "body"=>"メロスには政治がわからぬ。"}

取れましたね。
これがもしかすると正しいやり方なのかもしれない。。。

うむ。

Groongaについて

そうそう、大事なことを忘れてました。groonga は全文検索に最適化したデータベースシステムです。なので、いつも使っている RDB とか KVS とも少し違うので、慣れるまでが大変です。

RDBとかではテーブルを作成するときにキーにするカラムとかデータを格納するカラムとかいろいろ いっしょくた に作成できるのですが、groonga は最初にテーブルを作るときにキーの構成や全文検索用の機能等を指定しておき、後にカラムをテーブルに追加していく、という作り方をします。
それから、インデックスの作成とか検索の仕方とか、とにかく独特な印象です。
使いこなせたらきっと便利なんだろうと思います。

以上です。