株式会社クリアコード > ククログ

ククログ

最新
タグ:

GaaS: Groonga as a Service

4年ぶりにインターンシップを開始することを2月にお知らせしました。そこで予告していた通り、4月からインターンシップを実施します。インターンシップをどのように実施するかを3行で説明すると以下の通りです。

  • 目的: インターンがクリアコードで実践している開発方法を体験する
  • 内容: クリアコードのメンバーと一緒にフリーソフトウェアを開発する
  • 対象: プログラミングが好きでフリーソフトウェアに関心がある16歳以上の方(学生、社会人、休職中などは問わない)

詳細はインターンシップページを確認してください。

現時点で紹介している一緒に開発するフリーソフトウェアは以下の通りです。

1つめはJavaScriptやFirefoxに関連した開発になります。2つめはC、Objective-C、GTK+に関連した開発になります。

今回紹介するフリーソフトウェアはRubyやgroongaに関連した開発になります。

これまで紹介したフリーソフトウェアも含めて、ぜひクリアコードの開発方法を体験しながら開発したいと感じた方はインターンシップページにある方法で応募してください。

GaaS: Groonga as a Service

自分でセットアップしなくてもgroongaを使えたらステキではありませんか?groongaをサービスとして提供する、Groonga as a Service。略してGaaS(ガース)。そんなサービスを実現するためのフリーソフトウェアをインターンシップで開発します*1

GaaSを使えば、Herokuアドオンとしてgroongaを提供することもできます。夢が広がりますね*2

GaaSの構想は数年前からありましたが、groonga本体の開発などに注力しており未着手です。しかし、GitHub上にリポジトリを作成済みだったり、どんな構成にするのがよいかをスケッチしたりと実現に向けた準備は進めています。

GaaSの機能

GaaSは以下の機能を提供します。

  • APIでgroongaサーバーを追加・削除する機能
    • 負荷の増大・縮小に動的に対応できる
    • 追加時は既存のgroongaサーバーと同じデータを持つ
  • 同じデータに対して、複数のgroongaサーバーで独立して検索サービスを提供する機能
    • 負荷の増大に対応できる
  • 複数のgroongaサーバーがいるときの更新処理はすべてのgroongaサーバーで実行する機能
    • レプリケーション機能
  • 認証機能
  • groongaサーバー毎に使用可能なリソース量を制限する機能
  • groongaサーバーで更新されたデータをリアルタイムでバックアップするAPI

一方、以下の機能は提供しません。

  • 同じデータを複数のgroongaサーバーに分散して配置し、分散して1つのクエリを実行する機能
    • シャーディングはできない
    • 分散して検索した結果をマージできない
  • データの永続性を保証する機能
    • groongaサーバーを落としたらそのgroongaサーバーが持っていたデータは消える

このことからGaaSは以下のようなケースで有用です。

  • るりまサーチのようにドキュメントを全文検索するケース
    • マスターデータが別にあるのでデータの永続性がなくても困らない(作りなおせばよい)
    • 検索数が増えたらgroongaサーバーを追加すれば対応できる
  • ブログの全文検索機能
    • マスターデータが別にあるのでデータの永続性がなくても困らない(作りなおせばよい)
    • 検索数が増えたらgroongaサーバーを追加すれば対応できる
  • 中規模(数十万レコード)くらいまでのECサイト
    • groongaサーバーを追加して負荷を分散できる
    • groongaはすぐに更新結果を検索に反映できるので、品切れになった商品はすぐにヒットしなくなる
    • バックアップAPIで更新データのバックアップをとっておけば、groongaサーバーがすべて落ちても復旧可能
    • 複数のgroongaサーバーの管理を任せられる

シャーディング機能がないためレコード数(データ量)が多いケースには対応できませんが、レコード数がそれほど多くない場合は運用が楽になるため有用です。

GaaSの実現方法案

GaaSの以下のそれぞれの機能の実現方法案を説明します。まだ作っていないので、実際に作ったらここから大きく方向が変わる可能性があります。

  • APIでgroongaサーバーを追加・削除する機能
  • 同じデータに対して、複数のgroongaサーバーで独立して検索サービスを提供する機能
  • 複数のgroongaサーバーがいるときの更新処理はすべてのgroongaサーバーで実行する機能
  • 認証機能
  • groongaサーバー毎に使用可能なリソース量を制限する機能
  • groongaサーバーで更新されたデータをリアルタイムでバックアップするAPI
APIでgroongaサーバーを追加・削除する機能

まずは、APIでgroongaサーバーを追加・削除する機能の実現方法案を説明します。

簡単のために、groongaサーバーを追加したい時、空いているサーバーはすでにあるとします*3。問題は初期データをどうするかです。初期データは指定したURLからHTTPでダウンロードすることにします。こうすることで、GaaS側では初期データを管理する必要がなくなります。また、追加するサーバーは常に最新の初期データを参照できるので、どのgroongaサーバーの初期化処理でも手順が同様になりgroongaサーバーの追加が簡単です。

起動時の流れは以下のようになります。

  1. 初期データをHTTPでダウンロード
  2. 初期データをgroongaに投入
  3. groongaサーバー起動
  4. サービス開始

groongaサーバーの削除は、groongaサーバーを終了して関連ファイルを削除するだけです。

APIは作ればよいだけなので省略します。

同じデータに対して、複数のgroongaサーバーで独立して検索サービスを提供する機能

検索サービスを複数のgroongaサーバーで分担して提供する機能の実現方法案を説明します。

groongaはHTTPサーバーとして検索APIを提供できます。そのため、すでにあるHTTP関連の技術を使うことができます。クライアントからのリクエストを複数のgroongaサーバーに振り分ける機能はリバースプロキシを使って実現できるでしょう。

複数のgroongaサーバーがいるときの更新処理はすべてのgroongaサーバーで実行する機能

レプリケーション機能の実現方法案について説明します。

fluent-plugin-groongaを使うことにより、1つのgroongaサーバーで実行した更新処理を他のサーバーでも実行することができます。これによりレプリケーション機能を実現できます。

残る問題は、クライアントからのリクエストが更新リクエストか検索リクエストかを判断する方法です。更新リクエストの場合はfluent-plugin-groongaで処理しなければいけませんが、検索リクエストの場合は別々のgroongaサーバーで処理したいです。これは、以下のような構成にすることで実現可能ではないかと考えています*4

まず検索時の構成案を示します。

検索時
                                    +--> fluentd -----> groonga
クライアント --> リバースプロキシ --+--> fluentd -----> groonga
                                    +--> fluentd -----> groonga

検索時は、fluentdは単にデータを中継します。

更新時は、fluentdが更新クエリを全てのgroongaに配布します。以下は、真ん中のfluentdに更新リクエストが飛んだ場合です。同様に、他のfluentdに更新リクエストが飛んだ場合もすべてのgroongaサーバーに変更が反映されます。

更新時
                                         fluentd   +--> groonga
クライアント --> リバースプロキシ -----> fluentd --+--> groonga
                                         fluentd   +--> groonga
認証機能

1つのGaaSシステムで複数のユーザーをホストするためには、他のユーザーのデータが読めないように認証機能が必要です。認証機能はHTTPの仕組みを使えばどうとでもなりそうです。

groongaサーバー毎に使用可能なリソース量を制限する機能

1つのGaaSシステムで複数のユーザーをホストした場合、1人のユーザーがリソースを使いすぎて他のユーザー用のサービスが提供できなくなることは問題です。そのため、groongaサーバー毎に使用可能なリソース量を制限し、制限を超えた場合は自動で再初期化したり、サービスを停止したりするような仕組みが必要です。GodBluepillなどリソース監視も備えたプロセス監視システムはいくつかあるので、それらを使用すれば実現できるでしょう。あるいは、単に、ulimitとプロセス監視システムの組合せでもよいかもしれません。

groongaサーバーで更新されたデータをリアルタイムでバックアップするAPI

groongaサーバーで更新されたデータをリアルタイムでバックアップするAPIがあれば、初期データを随時更新して、groongaサーバーを追加した時に使う初期データを最新のデータにしておくことができます。

GaaSのレプリケーション機能はfluent-plugin-groongaで実現するので、fluentdのcopy outputプラグインとforward outputプラグイン機能を使ってこの機能を実現できます。

まとめ

groongaをサービスとして提供する仕組みGaaS(Groonga as a Service。ガース。)の構想とそれの実現方法案を紹介しました。

GaaSを一緒に開発しながらクリアコードの開発方法を体験したいという方はインターンシップページを確認の上、応募してください。

他のインターンシップ対象のフリーソフトウェアを再掲します。

*1  ここでは「GaaS」という単語をgroongaの機能を提供するサービスおよびそのサービスを実現するソフトウェアの両方を示すものとして使っています。サービスだけを提供するのではなく、サービスを提供するソフトウェアも提供することで、自分の環境にサービスを構築することもできるようにします。データを外部に出したくない場合でも使えるようになります。

*2  groongaをHerokuアドオンとして提供することに(インターンシップとは関係なく)興味のある方はお問い合わせフォームからご連絡ください。一緒に実現させましょう。

*3  Amazon EC2などを使えば追加したい時に動的にサーバーを追加することはできるので、このあたりはそんなに困らないはず。groongaサーバーを設定するためのChefのcookbookを作っていたりします。

*4  要検討

2013-04-01

groonga 2.0.0, mroonga 2.00リリース

4年に1度のうるう肉の日ということもあり、groongaとmroongaがメジャーバージョンアップしてリリースされました。

groonga, mroongaは毎月定期的にリリースされており、このリリースで劇的に変化したわけではありません。しかし、1.0.0がリリースされた時点からは劇的に改良されています。1.0.0の頃からしばらく忘れていたなぁという方にぜひ確認してもらいたいリリースです。

ロゴの更新

バージョンだけではなく見た目も変えてアピールしよう!ということで、メジャーバージョンアップにあわせてロゴも更新しました。

準備中のロゴ

前のロゴを作ったときは、たくさんのgroonga関連のプロジェクトがXroongaという名前になることを予想していませんでした。前のロゴはお面をモチーフにしており、関連プロジェクトで同じようなロゴを作りづらいという問題がありました。

そこで、今回のロゴは最初の1文字をカスタマイズしやすいデザインになっています。実は、"a"から"z"までの文字を別途用意してあるので、「proonga」など新しくXroongaな名前のプロジェクトを作ったときも再利用しやすくなっています。

ロゴは誰でも自由に利用できるように準備を進めているので、groonga関連のプロジェクトに関わっている方は期待してもう少しお待ちください!

まとめ

リリースに関する情報はgroongaやmroongaのサイトで紹介しているので、ここでは、新しいロゴについて紹介しました。サイトデザインの更新も進めているので、そちらも楽しみにしていてください。

また、groongaを使った検索システムの検討から運用まで支援するサービスもはじめました。興味のある方はお気軽にお問い合わせください。

(プログラミングが好きでgroonga関連の開発に興味のある方は採用情報をご覧ください。)

つづき: 2013-01-08
タグ: groonga
2012-02-29

モーショノロジー2012 #1: rroongaによる検索サービスの実装

注意: 長いです。

簡単まとめ: 検索サービスを作るにはrroongaが便利です。groongaサポートサービスをはじめます。

CROOZ株式会社が主催する「モーショノロジー2012 #1 全文検索&検索を利用したサービスの使命、利用プロダクト、事例紹介」が開催されました。今回のテーマは検索ということでgroonga開発チームに声をかけてもらいました。groonga関連の枠がいくつかあったのですが、ここではRubyとgroongaを使った検索サービスの作り方についての枠の内容を紹介します。

rroongaによる検索サービスの実装

以下、多少省略しながらスライドの内容を紹介します。

概要

話すこと

紹介する内容はrroongaを使った場合のメリット・デメリットと入力補完についてです。メリットは事例も交えながら紹介します。入力補完は「Ruby + groongaだからできる」という機能ではなくgroonga単体でも利用できる機能なのですが、最近の検索サービスでは当たり前になっている大事な機能なので、あわせて紹介します。

rroonga?

rroonga?

Rubyとgroongaを一緒に使う方法には以下の2つの方法があります。

  • groongaサーバーを起動し、HTTPで通信してgroongaの機能を使う方法
  • groongaをライブラリとして使用し、API経由でgroongaの機能を使う方法

全文検索システムはgroonga以外にもたくさんありますが、その中にSolrという全文検索システムがあります。Solrは全文検索エンジンとしてLuceneを利用したシステムです。groongaはLuceneと同じ全文検索エンジン機能もSolrと同じサーバー機能も備えています。groongaサーバーを起動する使い方はSolrのように使う使い方で、ライブラリとして使う使い方はLuceneのように使う使い方になります。

今回は後者のgroongaをライブラリとして使用する方法でのメリット・デメリットを事例を交えながら紹介します。

Rubyからgroongaをライブラリとして使うためにはrroongaというRubyのライブラリを使います。この方法ではアプリケーションがデータベースを持つことになります。

システム構成

システム構成

groongaをライブラリとして使う方法ではアプリケーションサーバーがデータベースを持つことになります。これは、よくあるアプリケーションサーバーとデータベースサーバーが分離している構成とは異なります。このあたりがメリット・デメリットにつながってきます。

メリット

メリット

groongaをライブラリとして使う場合、以下のようなメリットがあります。

  • 通信コストがないため、小さいコストで細かくデータを読み書きできる
  • 低レベルのAPIから高レベルのAPIまで使えるため柔軟に演算を組み合わせた検索ができる

小さいコストで細かいデータの読み書きができるメリットを活かした例は後で紹介します。まずは、柔軟に演算を組み合わせられるメリットを活かした例を紹介します。

組み合わせ例: 多段ドリルダウン

組み合わせの例の1つが「多段ドリルダウン」です*1。「ドリルダウン」は「ファセット」と呼ばれることの方が多いのですが*2、ECサイトなどでよく使われている機能です。

amazon.co.jpなどで絞り込める条件がリンクになっているインターフェイスを見たことがないでしょうか。あれがドリルダウン(ファセット)です。

ドリルダウン=ファセット

このスクリーンショットは「本」で検索した状態です。「本」カテゴリーのうち、さらに絞り込める項目(「コンピュータ・IT」、「ビジネス・経済」など)がリンクとしてリストされています。ここをクリックすると検索語などを入力せずに簡単に絞り込んでいくことができます。

また、各項目の横に「(5,248)」などヒット件数も表示されていることに気づいたでしょうか。これは「コンピュータ・IT」、「ビジネス・経済」などサブカテゴリーで絞り込んだ結果ヒットする件数を示しています。事前に検索システム側で検索して、0件ヒットする項目はそもそもこのリストに入らないようになっています。そのため、「絞り込んだけど0件ヒット」という無駄な検索を避けることができます。

このような点でより効率的に検索できるような機能なので、より広く使われるようになりました。

ドリルダウン

さて、多段ドリルダウンはどう違うかというと最終的な検索結果を求める途中でもドリルダウンをするという点が違います。

多段ドリルダウン

高レベルな検索機能しかない場合は、多段ドリルダウンを実現するために「全データ→途中結果」までの検索と「全データ→最終結果」までの検索を2回実施し、それぞれの検索結果に対してドリルダウンする必要があります。これは、高レベルな検索機能では検索の途中結果を保存しておいて再利用するような機能がないためです。

低レベルな検索機能も使えると「全データ→途中結果→ドリルダウン」をしてから「途中結果→最終結果→ドリルダウン」というように効率のよい処理を実現できます。

では、多段ドリルダウンが有用なケースはどのようなケースでしょうか。

用途

多段ドリルダウンは、絞り込み後に値を変更することが多い条件に有用です。amazon.co.jpのカテゴリーの例でいえば『「コンピュータ・IT」で絞り込んだ後に、やっぱり「ビジネス・経済」に変更しよう』ということが多いかどうかということになります。多段ドリルダウンを実施しておけば「絞り込み→解除→再絞り込み」という操作ではなく「絞り込み→再絞り込み」という操作を実現でき、少ない手順で検索できます。

例えば、「価格帯」が再絞り込みをしたくなるような条件です。最初は安めの価格帯で絞り込んでいたけど、よいのがなかったからもう少し高めのものも見てみよう、ということはよくありますよね。

デメリット

デメリット

デメリットはスケールアウトする標準的な仕組みがないことです。そのため、レプリケーションの仕組みを自分で作り込む必要があります。

開発事例

開発事例

Rubyとgroongaでテレビ番組を検索するWeb APIを開発しています。別のシステムから提供される番組情報をrroongaを使ってgroongaのデータベースへ取り込み、番組情報を検索するためのHTTP + JSONベースのWeb APIを提供するシステムです。このシステムが直接視聴者から利用されることはなく、番組検索APIを利用した連携アプリケーションがユーザー用のインターフェイスを提供します。

このシステムは外部からの更新がないとてもシンプルな構成のため、番組情報ソースを各アプリケーションサーバーにコピーし、各サーバーでそのソースを元にデータベースを構築することで冗長化・スケールアウトを実現しています。

このAPIを利用したアプリケーションの1つがテレコ!です。地上波・BS放送・CS放送の番組をメディア横断で検索できるテレビ番組情報サービスです。ぜひ利用してみてください。

メタデータの抽出

メタデータ抽出

番組検索Web APIではデータロードのときにもrroongaが活躍しています。その1つがメタデータの抽出処理です。

メタデータはドリルダウン条件として使えるため検索しやすいシステムを構築するためには重要な情報になります。番組情報の場合は出演者やカテゴリなどがメタデータとなります。メタデータは重要な情報なのですが、用意するにはそれなりの手間がかかるため、なかなか充実させることができません。そのため、ある程度機械的に抽出することで補うことが有効です。番組検索Web APIでは以下のようなコードで番組説明から出演者情報を抽出しています。

1
2
3
4
5
6
7
8
9
10
11
names = []
# 番組説明
description = "出演者: ビートたけし・所ジョージ"
# 人物テーブル
people = Groonga["People"]
# people.records -> ["ビートたけし", "明石家さんま"]
people.scan(description) do |record,|
  # 番組説明内に人物テーブル内の人物名があったら抽出
  names << record.key # "ビートたけし"
end
p names # -> ["ビートたけし"]

やっていることは「はてなダイアリーでのキーワード自動リンク」と同じことです。テキスト(番組説明)中から事前に用意した語(人物名)を抽出しています。なお、この処理のことを「multiple string matching」と呼ぶそうです。

これはgroongaの低レベルのAPIを使って実現します。ただ、この処理のときはデータベース内のデータを頻繁に参照することになるので、データロードするアプリケーションがデータベースを持っていないと時間がかかって実用的にはならないでしょう。

入力補完

最後に入力補完の実現方法について説明します。

入力補完

groonga本体にはサジェスト機能があり、この機能は以下の機能を提供します。

  • 補完: 一部分を入力するだけで完全な検索語を入力できるようにする機能(Googleの検索ボックスにもある機能)
  • 補正: 検索語の一部が間違っていても修正して正しい検索語にする機能(Googleでいえば「もしかして」機能)
  • 提案: 検索結果が多いときに絞り込み用の追加の検索語を提示する機能(Googleでいえば「他のキーワード」機能)
  • ユーザーの入力から統計的に学習し↑の3つを強化する機能

このうち補完機能を使った入力補完の実現方法について説明します。

補完例

groongaの補完機能は補完候補そのものの字面(例えば「万葉集」が補完候補なら「万」など)でなくても、ローマ字やひらがな・カタカナで入力しても補完候補を提示できます。これはIMEがOFFの状態でも利用できて日本語を利用した検索システムではとても便利です。Googleでも同様のことをできますが、amazon.co.jpではできないようです。

補完方法

補完方法には大きく分けて以下の2つの方法があります。

  • コンテンツベースの方法
  • 統計情報ベースの方法

コンテンツベースの方法ではすでにデータベース内にある既知の情報を補完候補とする方法です。番組検索Web APIの場合は番組名や人物名などが適切な補完候補になります。この方法ではデータベース内にある正しい補完候補のみを利用するので、間違った候補を出すことがないというメリットがあります。一方、候補数が少なくて思ったより補完してくれないということもありえます。例えば、「コナ」では「名探偵コナン」を補完してくれなくて「名探偵」と入力しないといけない、といったことがあります。

統計情報ベースの方法ではユーザーの検索履歴をアクセスログなどから収集・解析し、多くのユーザーが検索した語などを補完候補とします。この方法では「コナ」で「コナン」を補完候補とできる可能性があります。一方、ある程度の統計情報がないと適切な補完候補を抽出できなかったり、補完候補の精度が低くなってしまう可能性もあり、調整が必要になります。例えば、特定のキーワードをわざと多く検索してくるようなアクセスがあった場合はそのようなアクセスを無視するといったことが必要になるかもしれません。

まとめ

おさらい

rroongaを使った検索システムの構成とその構成ならではのメリットとデメリットを紹介しました。メリットはメタデータの抽出などgroongaの低レベルのAPIも利用しながら検索システムを構築できることです。デメリットは標準的なレプリケーションの仕組みがないため冗長化やスケールアウトの仕組みを自作する必要があることです。

また、rroongaを使った検索システムの構成とは関係ないのですが、入力補完の実現方法についても紹介しました。

お知らせ

お知らせ

groongaの開発元である有限会社未来検索ブラジルとMySQLからgroongaを使うためのソフトウェアmroongaの開発に参加している斯波さんとクリアコードでgroongaのサポートサービスを提供することにしました。サポート開始は2/29の予定ですがすでにお問い合わせは受け付けていますので、groongaサポートサービスに興味のある方はぜひお問い合わせフォームからご連絡ください。

*1  「多段ドリルダウン」は一般的な用語ではないので注意してください。名前がなかったので今回名前をつけただけです。

*2  Solrでもファセットと呼んでいます。

つづき: 2013-01-08
タグ: Ruby | groonga
2012-01-26

全文検索エンジンgroongaを囲む夕べ 2: 「groonga村」と「mroongaのベンチマーク」用資料

今年も11月29日に「全文検索エンジンgroongaを囲む夕べ」が開催されました。1年ぶりの開催です。会場は株式会社VOYAGE GROUP(10月に株式会社ECナビから社名変更)でした。会場提供ありがとうございます!とても助かりました。会場提供にあたりこしばさんにとてもお世話になりました。ありがとうございます。

29日なので、もちろん新しいバージョンのリリースも行われています。

今年は、会の内容の概要を紹介する導入用のセッション「groonga村」と、mroongaの性能特性がわかるベンチマーク結果を紹介するセッション「mroongaのベンチマーク」を担当しました。

groonga村

mroongaのベンチマーク

「mroongaのベンチマーク」で紹介した結果はOSC2011.DBで紹介した結果とほとんど同じなので、解説はそちらを参照してください。ただし、OSC2011.DBで紹介した位置情報検索のベンチマークデータが間違っていました。

間違っていた位置情報検索のベンチマーク結果

正しくは、「mroongaの方が劇的に速い」という結果ではなく、以下のように「mroongaの方が速い」という結果でした。

正しい位置情報検索のベンチマーク結果

ここでは、groonga周辺の技術の整理にもなるので「groonga村」の内容を紹介します。

groonga開発チームの期待

まずはじめにこの会でgroonga開発チームが参加者のみなさんに期待していることを伝えました。

groonga開発チームの期待

期待していることは以下の2つです。

  • groonga開発者が増えること
  • groongaユーザーが増えること

どちらもgroongaがよくなることにつながります。

森さんの資料(PDF)や矢田さんの資料(PDF)にもあるとおり、groonga開発チームでやりたいことはいろいろあるのですが、そこまで手がまわっていません。groongaを開発してくれる人が増えると、groongaがより使いやすくより速い全文検索エンジンになります。

ユーザーが増えるといろんな環境でのテストにもなります。問題があったとフィードバックをもらって、それを修正できればgroongaの品質が向上します。また、実際に使うにはどのような機能が求められているかを聞かせてもらえれば、よりgroongaを使いやすくすることにも役立ちます。それ以外にも「このように使っています」や「groongaを使うために便利なツールを作りました」などといった情報を公開してくれるというのも、groongaユーザーにとって有益な情報になります。ぜひ、使って、フィードバックや情報公開をお願いします!

groongaの開発に参加したいという方も、groongaの事例紹介ページに載せてもいいという方も、groongaのメーリングリストまたはgroonga at razil.jp(またはkou@clear-code.com)へご連絡ください!

参加者の期待

今年の会は、参加者のみなさんが以下を期待しているのではないかということで構成しました。

参加者のみなさんの期待

それぞれについて以下のセッションで扱いました。資料は公開できるように調整中です。すでに公開されているものはリンクしています。まだ資料が公開されていないものはUstreamの録画を観てください。

groonga関連ソフトウェアの位置づけ

groongaはライブラリとして利用できるため、MySQLやPostgreSQL、Rubyなど様々なソフトウェアと連携できます。そのため、今回の会でもたくさんの分野の話がでてきます。話をきいているうちにどこの話をしているのかわからなくなってしまうと、話についていけなくなってしまうかもしれません。それを防ぐためにgroongaとその関連ソフトウェアの位置づけを説明しました。

groonga村

これが全体像です。それでは、それぞれの部分を説明します。

コア機能

コア機能

キー管理機能(レコードを参照するときに利用)や転置索引、キーストアなどgroongaが提供する機能のベースになる機能を提供するレイヤーがあります。

DB API

DB API

コア機能の上にデータベース機能を提供するDB APIがあります。このAPIを使うとSQLiteのようにプロセス内にデータベースを持ったアプリケーションを開発することができます。

rroonga

rroonga

DB APIを使ったソフトウェアがrroongaです。RubyからDB APIを使えるようになります。buzztterるりまサーチMilkodeなどがrroongaを使っています。

mroonga

mroonga

MySQLからDB APIを使うためのソフトウェアがmroongaです。SQLを使ってgroongaを使えるようになるため、既存のSQL関連の技術をそのまま使えることが魅力です。例えば、Ruby on RailsでWebアプリケーションを使っている場合はActiveRecord経由でgroongaの高速な全文検索機能を使えることになります。

クエリAPI

クエリAPI

groongaは独自のクエリ言語を持っています。RDBMSでいうSQLのようなものです。このAPIを使うと文字列をやり取りすることでgroongaの機能を使うことができます。

groongaコマンド

groongaコマンド

クエリAPIをネットワーク越しやコマンドライン上で使うためにgroongaコマンドを提供しています。groongaコマンドはHTTPサーバーにもなるため、特別なライブラリではなく通常のHTTPライブラリを使うだけで任意のプログラミング言語からgroongaの機能を使うことができます。塩畑さんの発表にもある通り、ぐるなびさんではHTTPでgroongaの機能を利用しています。

nroonga

nroonga

クエリAPIをNode.jsから使えるようにするライブラリがnroongaです。Node.jsからgroongaの機能を利用するためのnode-groongaというライブラリもありますが、こちらはgroongaコマンドのHTTPサーバーと通信するもので、直接クエリAPIを叩くnroongaとはレイヤーが違います。node-groongaはgroongaコマンドをより便利に使うためのもので、nroongaはgroongaコマンドのHTTPサーバー機能の代替になりうるものです。

groongaコマンドでもHTTPサーバー機能を実現できますが、実は単純なHTTPサーバー機能しかありません。例えば、認証機能もありませんし、WebSocketを実現することもできません。それらの機能も実現できるリッチなHTTPサーバーはNode.jsが提供する機能を利用するのがよいのではないか、というアイディアから生まれたものです。まだ開発が始まったばかりのプロジェクトですが、興味のある方はぜひ開発に参加してください!

textsearch_groonga

groonga with PostgreSQL

PostgreSQLからgroongaの機能を使うためのソフトウェアがtextsearch_groongaです。mroongaと違いDB APIとクエリAPIをうまく組み合わせてgroongaの機能を使っています。

まとめ

groonga村

groongaはライブラリとして利用できるため、このように多くのソフトウェアと連携し、ニーズにあわせた使い方ができるようになっています。ぜひgroongaを使ってみてフィードバックをお願いします!

それでは、また、来年のいい肉の日に会いましょう*1。会場提供をしてくれたVOYAGE GROUPのみなさん、Ustreamで放送してくれたグニャラくんさん、司会をしてくれた坂井さん、受付をしてくれたRuby会議実行委員会のしまださん、すずきさん、発表者のみなさん、会場に来てくれたみなさん、Ustreamで参加してくれたみなさん、ありがとうございました!

おまけ: 参加状況について

今回も前回同様にATNDを使ってイベント告知・参加者登録を行いました。定員100名で告知をし、前日までに160名くらいの登録でキャンセル数は1桁でした。ですが、前回の当日キャンセル数とキャンセルなしで不参加の傾向を考えると、事前に160名の参加登録があったとしても当日はちょうど定員である100名くらいになるのではないかと予想し、「補欠の人も会場にきても大丈夫です」ということにしました。実際、当日にキャンセル数が30くらいになり、キャンセルなしで不参加の人も50人くらいいたため、最終的には80-90名くらいの参加になりました。(当日に参加登録した方もいたようです。)

「補欠の人も会場にきても大丈夫です」というスタイルは会場提供のVOYAGE GROUPさんがOKを出してくれたので実現できたのですが、このくらいのゆるさでやったほうがちょうどよくなるのかもしれませんね。柔軟な対応、ありがとうございました!

*1  groonga開発チーム主催のgroonga勉強会は年に一度ですが、ユーザーのみなさんはいい肉の日以外にもgroonga勉強会を開催して大丈夫です!

つづき: 2011-12-26
タグ: groonga
2011-12-01

すべてのMySQLユーザーに高速な全文検索機能を! - OSC2011.DB用資料

オープンソースカンファレンス2011 DBOSSDB MySQLセッションでgroongaストレージエンジンについて紹介してきました。

すべてのMySQLユーザーに高速な全文検索を!

内容はgroongaストレージエンジンが得意なシチュエーションについてベンチマークデータを紹介するというものです。どういうときにgroongaストレージエンジンが高速に動作するかがわかります。

groongaストレージエンジンが得意なシチュエーション

groongaストレージエンジンは以下のような処理が得意です。

  • 全文検索
  • 位置情報検索
  • リアルタイム更新

groongaストレージエンジンの性能特性を紹介するためにベンチマークデータを紹介しました。ベンチマークはこれらの得意な処理を実行するシチュエーション向けに複数のパターンで行いました。

高速な全文検索

groongaの全文検索処理の性能を示すためにtwitterから取得したデータを利用しました。測定する処理はフレーズ検索です。約100万件のtweetに対して「"facebook saved"」というような2単語でフレーズ検索します。このようなフレーズ検索を1万回(1万パターン)実行するためにかかった時間が縦軸になっており、グラフが短いほど高速に全文検索が実行されていることを示しています。

約100万件のtweetに対して1万回のフレーズ検索にかかった時間

groongaストレージエンジンの方がMySQLの開発版5.6.3-labs-innodb-ftsに含まれるInnoDBの全文検索機能よりも10倍程度高速で、MyISAMよりは2倍程度高速でした。

高速な位置情報検索

groongaの位置情報検索処理の性能を示すために国土交通相の位置参照情報ダウンロードサービスの2010年版のデータを利用しました。測定する処理は「MBRContains(GeomFromText('LineString(139.850124 38.718204, 140.447158 37.817489)'), location)」というように位置情報でレコードを絞り込み、「ORDER BY name」というように住所でソートする検索です。このような検索を千回(千パターン)実行するためにかかった時間が縦軸になっており、グラフが短いほど高速に位置情報検索が実行されていることを示しています。

約1100万件の番地データに対して千回のMBRContains + ORDER BY検索にかかった時間

groongaストレージエンジンのほうがMyISAMよりも40倍程度高速でした。

リアルタイム更新

groongaはリアルタイム更新が得意です。リアルタイムで更新するために以下の2点を重視しています。

  • 高速に更新できること。
  • 検索負荷が高いときでも更新性能が落ちないこと。

まず、更新性能が高いことを確認し、次に検索負荷が高いときの更新性能を確認します。

高速な更新

高速に更新できることを示すために、98万件のtweetが登録されたデータベースを用意します。このデータベースに対して、2万件のtweetを登録します。このとき1秒あたりに追加したレコード数が縦軸になっており、グラフが長いほど高速に登録されていることを示しています。

98万件のtweetが保存されている状態で2万件のtweetを追加したときの更新スループット

groongaストレージエンジンのほうがInnoDBよりも3倍程度高速、MyISAMよりも2倍程度高速、Sphinxよりも3倍程度高速でした。

参照ロックフリーな更新

検索負荷が高いときでも更新性能が落ちないことを示すために、98万件のtweetが登録されたデータベースを用意します。このデータベースに対して、検索負荷(クエリ数/秒)を変えながら2万件のtweetを登録します。このグラフはそれぞれのストレージエンジン毎に見ます。横軸が検索負荷を表していて、左側になるほど検索負荷が小さく、右側になるほど検索負荷が高いことを示しています。グラフが水平になっているほど検索負荷が高くなっても更新性能が落ちていないことを示しています。

98万件のtweetが保存されている状態で検索負荷を変えながら2万件のtweetを追加したときの更新スループット

groongaストレージエンジンとInnoDBは検索負荷が高くなっても更新性能はそれほど落ちておらず、MyISAMとSphinxは更新性能が落ちていました。

groongaストレージエンジンの機能制限

このように、高速に動作するgroongaストレージエンジンですが、以下のように機能制限があります。

  • トランザクションをサポートしていない。
  • 位置情報はPOINTしかサポートしていない。

そのため、どのようなケースにでも利用できるわけではありません。しかし、groongaストレージエンジンは他のストレージエンジンと組み合わせて使うことができるため、上記の機能制限の一部を解消することができます。

groongaストレージエンジンを他のストレージエンジンと使う場合は全文検索処理・位置情報検索処理のみをgroongaストレージエンジンが行い、それ以外の処理は連携した他のストレージエンジンが行います。そのため、groongaストレージエンジンとInnoDBを一緒に使うと、トランザクションはInnoDBの機能を用いて、全文検索はgroongaストレージエンジンを用いる、ということができます。この仕組みを使うと「トランザクションをサポートしていない」というgroongaストレージエンジンの機能制限を解消することができます。

groongaストレージエンジンと他のストレージエンジンを組み合わせて使う方法

ただし、更新性能は組み合わせて使うストレージエンジンの性能に依存するため、groongaストレージエンジンの得意な処理である「リアルタイム更新」性能は発揮できません。「高速な全文検索機能」と「高速な位置情報検索機能」のみ利用できます。

InnoDBと組み合わせて利用した場合のベンチマークデータを以下に示します。

まず、全文検索の性能です。

約100万件のtweetに対して1万回のフレーズ検索にかかった時間(InnoDBと組み合わせた場合のデータ付き)

groongaストレージエンジン単体で使った場合とほとんど同じ性能がでています。

次に、位置情報検索の性能です。

約1100万件の番地データに対して千回のMBRContains + ORDER BY検索にかかった時間(InnoDBと組み合わせた場合のデータ付き)

こちらもgroongaストレージエンジン単体で使った場合とほとんど同じ性能がでています。

次に、更新性能です。

98万件のtweetが保存されている状態で2万件のtweetを追加したときの更新スループット(InnoDBと組み合わせた場合のデータ付き)

groongaストレージエンジン単体で使った場合よりも大きく性能が落ちて、組み合わせて使っているInnoDBと同じ程度の性能になっています。

最後に検索負荷が高いときの更新性能です。

98万件のtweetが保存されている状態で検索負荷を変えながら2万件のtweetを追加したときの更新スループット(InnoDBと組み合わせた場合のデータ付き)

InnoDBが検索負荷が高くても更新性能がほとんど落ちないため、groongaストレージエンジンとInnoDBを組み合わせて使った場合でも更新性能がほとんど落ちていません。

まとめ

OSC2011.DBでgroongaストレージエンジンが得意なシチュエーションのベンチマーク結果を紹介してきました。もちろん、groongaストレージエンジンが得意ではないシチュエーションもあり、そのようなケースでは他のストレージエンジンの方が性能がよくなります。groongaストレージエンジンが苦手なケースについては今月末(2011/11/29)開催の全文検索エンジンgroongaを囲む夕べ 2で紹介する予定です。興味のある方はこちらに参加してみてください。すでに定員を超えていますが、前回の全文検索エンジンgroongaを囲む夕べ #1では最終的に35名のキャンセルになっていましたので、今からでもギリギリ参加できるのではないでしょうか。

groongaストレージエンジンのより詳しい情報についてはgroongaストレージエンジンのサイトも参照してください。

タグ: groonga
2011-11-07

groongaの全文検索処理の流れ

groongaにデータを登録して、インデックスを更新すると全文検索をすることができます。ここでは、groongaが内部でどのような処理をして全文検索をしているかを説明します。

前提

まず、以下のように「Yes good」と「Hey good」という文書が登録されているとします。

登録されている文書と転置インデックス

このとき、「Yes good」で検索したらどうなるかを説明します。

トークナイズ

まず、入力の「Yes good」をトークナイズします。このとき使用するトークナイザーは使用する転置インデックスと同じものです。転置インデックスが使用するトークナイザーは語彙表(lexcion)を見ればわかります。今回はTokenDelimitトークナイザーですね。

TokenDlimitトークナイザー

TokenDlimitは空白区切りでトークナイズするトークナイザーなので「Yes good」は「Yes」と「good」にトークナイズされます。

転置インデックスの参照

トークナイズしたら、それぞれの単語について転置インデックスを参照します。転置インデックスの参照は2段階あります。

まず、単語をキーとしてlexiconを検索し、単語ID(= lexiconのレコードID)を取得します。「Yes」の場合は単語IDは「1」です。

「Yes」の単語IDを検索

次に、単語IDを使って単語に対応する転置インデックスの値を取得します。単語ID「1」に対応する転置インデックスの値は文書ID「1」です。つまり、「Yes」という単語を含む文書は文書IDが「1」の文書だということです。

単語ID「1」の転置インデックスの値を取得

続いて、単語「good」の転置インデックスを参照します。「Yes」のときと同様にまずはlexiconを検索します。「good」の単語IDは「2」です。

「good」の単語IDを検索

「Yes」の時と同様に、単語IDを使って単語に対応する転置インデックスの値を取得します。単語ID「2」に対応する転置インデックスの値は文書ID「1」と「2」です。つまり、「good」という単語を含む文書は文書IDが「1」の文書と「2」の文書だということです。

単語ID「2」の転置インデックスの値を取得

元の入力は「Yes good」だったので、「Yes」の転置インデックスにも「good」の転置インデックスにも両方含まれている文書ID「1」だけがヒットした文書になります*1。文書ID「2」は「good」の転置インデックスに含まれていますが、「Yes」の転置インデックスには文書ID「2」が含まれていないので「Yes good」ではヒットしません。

ヒットした文書

部分文字列での全文検索

このケースでは「ood」など部分文字列ではヒットしません。どうしてヒットしないかを説明します。

まず、「ood」をトークナイズすると空白がないので「ood」という1単語にトークナイズされます。次に、「ood」という単語でlexiconを検索するとそのような単語は登録されていないので、単語IDを取得できません。そのため、この時点でヒットする文書がないと判断し、転置インデックスは参照しません。

それでは、部分文字列でも検索できるようにするにはどうすればよいかというと、トークナイザーを変更します。どうしてトークナイザーがでてくるかというのは、先に説明した転置インデックスを参照する処理の流れを考えるとわかります。

転置インデックスを参照する処理は以下の2つの処理にわけられます。

  1. lexiconから単語IDを取得する。
  2. 単語IDを使って転置インデックスの値を取得する。

「ood」で検索する例で確認した通り、「lexiconから単語IDを取得する」ことができないために部分文字列で検索できていません。よって、部分文字列でも「lexiconから単語IDを取得する」ことができるようにすれば、部分文字列でも検索できるようになります。

ここでトークナイザーの出番です。lexiconに登録される単語は文書をトークナイズして得られた単語です。そのため、トークナイズするときに部分文字列も単語としてトークナイズすればlexiconに単語の部分文字列も登録されます。すると、検索時に部分文字列でlexiconを参照しても単語IDを取得することができます。

groongaにはいくつか組み込みのトークナイザーがあります*2。部分文字列でも検索できるようにするには空白区切りで単語にトークナイズするTokenDelimitではなく、2文字単位で単語にトークナイズするTokenBigramを使います*3

トークナイザーとしてTokenBigramを使うと「Yes good」は「Ye」・「es」・「s 」・「 g」・「go」・「oo」・「od」・「d」にトークナイズされます。

TokenBigramを利用

このとき、「ood」で検索したらどうなるかを説明します。

まず、転置インデックスが使っているのと同じトークナイザーTokenBigramでトークナイズします。「ood」は「oo」・「od」にトークナイズされます。lexiconを検索すると「oo」の単語IDは6で「od」の単語IDは7です。どちらも登録されている単語なのでヒットする文書がありそうです。

「oo」と「od」の単語IDを検索

次に、単語ID「6」と単語ID「7」の転置インデックスを参照します。「oo」も「od」もどちらも文書ID「1」に含まれていることがわかります。よって、文書ID「1」の文書は「ood」という文字列を含んでいることがわかります*4

単語ID「1」と「2」の転置インデックスの値を取得

検索時はこのように動作するため、トークナイザーによって検索結果が異なります。

トークナイザーの使い分け

では、トークナイザーはどのような基準で選ぶとよいのでしょうか。一見すると、部分文字列でも検索できるトークナイザーの方がよさそうに見えます。しかし、必ずしもそうとは限りません。部分文字列でもヒットするということは、望んでいない文書もヒットする可能性が増えるということです。

例えば、「cat」で「category」もヒットするようになります。「cat」で検索しているときに「category」に関する文書もヒットすると、それはノイズとなります。ノイズが多いと目的の文書を見つけづらくなってしまうため、使い勝手が悪くなります*5。よって、部分検索もできるようにしたほうがよいかどうかはアプリケーションに依る、ということになります。

なお、groongaは1つの全文検索で複数の転置インデックスを使うことにより、「完全一致した文書はスコアを高めにつけて、部分一致した文書はスコアを低めにつける」ということもできます。これは、転置インデックス毎*6に異なるトークナイザーを利用できるためです。

タグ検索

転置インデックスを用いると全文検索と同じ方法でタグ検索も実現できます。

実は、上記のTokenDelimitを使った全文検索の説明はタグ検索の動作そのものになっています。「Yes」や「good」をタグだと考えてもう一度読みなおしてみてください。

まとめ

groongaでの(簡略化した)全文検索処理の流れを説明しました。思ったように検索ができない場合は、全文検索の処理の流れを考えながら挙動を確認していくと、どこが問題かをみつけやすくなるはずです。

*1  実際は「両方含まれている」だけではなく「入力と同じ順序で両方含まれている」文書だけを選びます。そのため、転置インデックスには単語が文書中のどこで現れたのかを記録しておく必要があります。groongaでは位置情報も含めるかどうかはカラム定義時のオプションで指定することが可能です。なお、位置情報も含んだ転置インデックスを完全転置インデックスと呼びます。

*2  ここでgroongaのドキュメントにトークナイザー一覧ページを作ってリンクを貼れると嬉しい。

*3  KEY_NORMALIZEを指定すると必ずしも2文字単位ではなくなるので注意すること、ということはここでは省略する。

*4  実際は「両方含まれている」だけではなく「入力と同じ順序で両方含まれている」ことも確認します。つまり、「oo」の次に「od」が出現していることも確認します。

*5  キーワードは「適合率」と「再現率」。

*6  実際は転置インデックス毎ではなくてlexicon毎。

つづき: 2011-12-26
タグ: groonga
2011-10-20

groongaにデータを登録してからインデックスが更新されるまでの流れ

全文検索エンジンgroongaを囲む昼下がり@札幌はたっぷり3時間もあるので、「groongaがどのように動いているか」、「より効率的に検索するためにはどうしたらよいか」などといった話ができるはずです。

この文書は、札幌でのgroonga勉強会で使うための「groongaがどのように動いているか」を説明に使うための文書です。後でgroongaのドキュメントにマージする予定です。

それでは、groongaがどのように全文検索用のインデックスを作成しているかを説明します。まず、全文検索機能で重要なオブジェクトを説明して、その後にそれらを使ってどのようにインデックスを作成しているかを説明します。

主要オブジェクト

groongaの全文検索機能で大事なオブジェクトは以下の3つです。

  1. テーブル
  2. カラム
  3. トークナイザー

それぞれ順に説明します。

テーブル

groongaでは、ひとまとまりのデータを「レコード」と呼びます。これはRDBと同じです。RDBでも行ごとにまとまったデータをレコードと呼んでいます。

レコード

groongaのテーブルは「レコードID」を管理するオブジェクトです。「レコードID」とはレコードを一意に識別する数値です。

レコードID

なお、1つのテーブルで管理できるレコード数(レコードID)の理論的な上限値は約2億6千万レコードです。

テーブルには以下の3つの種類があります。

  1. 配列
  2. ハッシュテーブル
  3. パトリシアトライ

配列はID列を持ったテーブルです。レコードを追加すると新しいレコードIDを払い出します。基本的にレコードIDは1, 2, 3, ...というように順に払い出されます。

配列

ハッシュテーブルはID列とIDと1対1に対応するキーを持ったテーブルです。レコードを追加するときは必ずキーも一緒に指定します。レコードが追加されるとキーに対応したレコードIDを払い出します。ハッシュテーブルが払い出すレコードIDも1, 2, 3, ...というように順に払い出されます。もし、レコードを追加するときに指定したキーが既存のレコードと同じキーだった場合は新しくレコードIDを払い出さず、既存のレコードと同じIDを返します。

ハッシュテーブル

パトリシアトライもハッシュテーブルと同様にID列とIDと1対1に対応するキーを持ったテーブルです。レコードの追加も同じように動きます。

パトリシアトライ

配列ではレコードIDでのみレコードを特定できますが、ハッシュテーブルとパトリシアトライはレコードIDだけではなくキーでもレコードを特定できます。

パトリシアトライとハッシュテーブルとの違いはキーの検索方法です。

ハッシュテーブルではキーの検索方法は完全一致検索のみです。つまり、キーからレコードIDを求めるには、求めたいレコードのキーと同一のキーを指定するしかないということです。一方、パトリシアトライでは、完全一致検索だけではなく前方一致検索もできます。

例えば、ククログのデータがgroongaのデータベースに入っているとします。1エントリが1レコードに対応し、エントリが書かれた日付をキーにしているとします。すると、ここ2ヶ月では以下のようなキーになります。

  • "2011-8-11"
  • "2011-8-24"
  • "2011-9-5"
  • "2011-9-13"
  • "2011-9-29"

この中から2011/9に書かれたエントリのみを表示したいとします。すると、2011/9に書かれたエントリのレコードIDの一覧を取得しなければいけません。この場合、パトリシアトライを使っていると"2011-9-"でキーを前方一致検索することで実現できます。しかし、ハッシュテーブルではこのようなことはできません。

前方一致検索

また、パトリシアトライを使うとキーワードリンクなどといったこともできるようになります。

なお、レコードの参照速度の速い順にテーブルの種類を並べると以下のようになります。

  1. 配列
  2. ハッシュテーブル
  3. パトリシアトライ

配列と他の2つのテーブルの使い分けは、ID以外にレコードを特定するキーが欲しいかどうかで考えます。ハッシュテーブルとパトリシアトライの使い分けは、キーを完全一致検索だけで使うかどうかで考えます。

カラム

テーブルはレコードIDを管理するだけで、レコードが持つ値はカラムに保存します。1つのレコードに対して複数のカラムをひもづけることができるため、レコードは複数の値を持つことができます。

カラム

カラムには以下の3つの種類があります。

  1. スカラーカラム
  2. ベクターカラム
  3. インデックスカラム

スカラーカラムは1つの値だけを保存できるカラムです。数値を保存するカラムなら「29」や「2929」などを保存できます。文字列を保存するカラムなら「"groonga"」や「"札幌"」などを保存できます。

スカラーカラム

ベクターカラムは複数の値を保存できるカラムです。同じ種類の値だけを保存できる配列と考えるのがよいでしょう。数値を保存するカラムなら「[2, 29, 292]」などを保存できます。文字列を保存するカラムなら「["groonga", "札幌"]」などを保存できます。

ベクターカラム

インデックスカラムは転置インデックスを保存するカラムです。転置インデックスは単語IDとその単語IDが含まれている文書IDをひもづけたデータ構造です。

転置インデックス

groongaのインデックスカラムでは、単語IDはインデックスカラムのあるテーブルのレコードIDに対応します。文書IDは検索対象のテーブルのレコードIDに対応します。なお、インデックスカラムがあるテーブルのことを「語彙表(lexicon)」と呼びます。

インデックスカラム

スカラーカラムとベクターカラムはどのテーブルでも一緒に使えますが、インデックスカラムは配列と一緒に使うことはできません。ハッシュテーブルかパトリシアトライと一緒に使う必要があります。これは、groongaでは単語をキーとしたレコードを作成することによりレコードID(= 単語ID)を作成しているためです。

トークナイザー

トークナイザーとは文書から単語を切り出すオブジェクトのことです。例えば、"I am a boy"という文書から「I」、「am」、「a」、「boy」という4つの単語を切り出したりします。この切りだすことを「トークナイズ」と呼びます*1。転置インデックスを作成するときは、単語IDと文書IDをひもづけるために文書内の単語を抽出する必要があります。それを行うのがトークナイザーです。

groongaでは、語彙表用のテーブル毎にトークナイザーを指定します*2

トークナイザー

転置インデックスの更新は元の文書を登録・更新・削除したときにgroongaが自動的に行います。そのため、ユーザが明示的にトークナイザーを使うことはありません。単に指定するだけです。

groongaで利用できるトークナイザーの一部は以下の通りです。

  • TokenBigram: バイグラムでトークナイズする。
  • TokenMecab: MeCabを使ってトークナイズする。
  • TokenDelimit: 空白区切りでトークナイズする。

データ更新時の動作

データを更新するとgroonga内部で自動的に転置インデックスが更新されます。そのときの動作を説明します。

まず、最初は、検索対象のテーブル(= 文書を保存するテーブル)にも語彙表のテーブルにもなにもレコードがありません。

データが何もない初期状態

それでは、検索対象のテーブルにデータを保存しましょう。

1つ目の文書を保存

"Yes good"という文書を保存しました。最初の文書なのでレコードID(= 文書ID)は1になっています。データが保存されるとgroongaが内部で自動的に転置インデックスを更新します。

1つ目の文書の1つ目の単語を処理

"Yes good"は「Yes」と「good」という2つの単語にトークナイズされます。まずは、「Yes」が語彙表に登録されます。これは最初の単語なのでレコードID(= 単語ID)は1になっています。続いて、単語IDが1に対応するインデックスカラムには保存された文書の文書IDとして1を登録します。

次に2つ目の単語である「good」を処理します。

1つ目の文書の2つ目の単語を処理

「good」も「Yes」と同様に処理します。まずは、「good」が語彙表に登録されます。「good」の単語IDは2になります。続いて、単語IDが2に対応するインデックスカラムに保存された文書の文書IDである1を登録します。

もうひとつ文書を保存します。

2つ目の文書を保存

2番目の文書なので文書IDは2になりました。データが保存されるとgroongaが内部で自動的に転置インデックスを更新します。

2つ目の文書の1つ目の単語を処理

"Hey good"は「Hey」と「good」という2つの単語にトークナイズされます。まずは、「Hey」が語彙表に登録され、単語IDが3になります。続いて、単語IDが3に対応するインデックスカラムには保存された文書の文書IDとして2を登録します。

次に2つ目の単語である「good」を処理します。

2つ目の文書の2つ目の単語を処理

「good」はすでに登録されている単語なので、新しく追加せずに既存の「good」と同じ単語IDを使います。「good」の単語IDは2なので、対応するインデックスカラムに文書IDとして2を登録します。このとき、すでに文書ID 1も登録されているので文書ID 2は追加します。

このようにして転置インデックスが作成されます。

まとめ

groonga内部でどのように転置インデックスを更新しているかを説明しました。この動作がわかっていると検索が期待通りに動かないときにどこが問題かを見つけやすくなります。例えば、語彙表のキーに期待通りの単語が入っていなかったら、間違ったトークナイザーを指定しているかもしれません。もし、そもそも語彙表に単語が入っていなかったら、転置インデックスの自動更新が動いていないのでしょう。異なるカラム用にインデックスカラムを作成していないかを確認する必要があります。

それでは、全文検索エンジンgroongaを囲む昼下がり@札幌で会いましょう。

*1  トークナイズして切り出されたものは「トークン」と呼びますが、ここでは「単語」で統一します。「トークン」と呼ばれるのは切り出されたものが必ずしも「単語」単位とはならないからです。

*2  今後、インデックスカラム毎に指定できるようになるかもしれませんが、今のところテーブル毎の設定でとても困っているという声がないため、近い将来に実現されることはないでしょう。

タグ: groonga
2011-10-05

全文検索エンジンgroongaを囲む昼下がりと夕べのお知らせ

今月も全文検索エンジンgroongaと、groongaをMySQLから使うためのモジュールであるgroongaストレージエンジンがリリースされました。

そして、groonga勉強会の開催が決まりました。

昼下がりの方は来週の土曜日に札幌で開催されます。夕べの方は2ヶ月後の29日に東京で開催されます。夕べは昨年同じ日にちに開催したgroonga勉強会の第2回目という位置づけで、前回と同様に開発している側からのgroongaと関連プロダクトの説明が主になります。一方、昼下がりの方は時間的なゆとりがあることもあり、単に説明を聞くだけではなく、質疑応答にも十分な時間をとれそうです。

groongaに興味のある方はぜひご参加ください。

タグ: groonga
2011-09-29

groongaで高速な位置情報検索

groongaのドキュメントにも位置情報検索について書かれているのですが、情報の更新が追いついていないため情報が不足しています。そこで、ここに現状に合わせたgroongaの位置情報検索についての情報をまとめておきます。なお、ここにまとめた内容もドキュメントに反映させる予定です。

できること

groongaには位置情報を用いた検索機能がついています。位置情報を用いた検索では索引を利用するため、全文検索と同じように高速に検索することができます。ただし、PostGISMySQLのように*1線や面などもデータとして保持できるというわけではなく、点のみをデータとして保持できます。よって、groongaにできることは以下の通りです。

  1. 指定した四角の中に含まれている座標を持つレコードを検索する。
  2. 指定した円の中に含まれている座標を持つレコードを検索する。
  3. 座標間の距離を計算する。
  4. ある座標からの距離が近い順にレコードをソートする。

つまり、以下のようなユースケースにはgroongaの位置情報検索機能を使うことができます。

  • 東京ドーム周辺のコンビニを検索する。
  • 新宿駅東口から近い順に和菓子屋をソートして表示する。
  • 今いる場所の近くにあるラーメン屋をリストアップして、近い順にソートして、今いる位置からの距離も計算する。

一方、以下のようなユースケースでは使えません。

  • 杉並区内にある駅を検索する。(1つの四角または1つの円で表現できない領域では検索できない。)
  • 湖を点ではなく領域で表現する。(レコードは点としてしか位置情報を持てない。)

文章だけだとピンとこないはずなので、図も用意しました。

まず、以下の図を見てください。黒い点がレコードを表しています。それぞれの操作でレコードがどのように扱われるかを示します。

レコードのみ

以下の図は「指定した四角の中に含まれている座標を持つレコードを検索」したところです。赤い四角が「指定した四角」で、赤い点が検索されたレコードです。

指定した四角の中に含まれている座標を持つレコードを検索

以下の図は「指定した円の中に含まれている座標を持つレコードを検索」したところです。赤い円が「指定した円」で、赤い点が検索されたレコードです。

指定した円の中に含まれている座標を持つレコードを検索

以下の図は「座標間の距離を計算」したところです。赤い点が基準点で、基準点とレコードの座標の間の距離を計算しています。

座標間の距離を計算

以下の図は「ある座標からの距離が近い順にレコードをソート」したところです。赤い点が基準点で、基準点からの距離が近い順にレコードを順番に選んでいます。赤い数字が選ばれた順番です。

ある座標からの距離が近い順にレコードをソート

表現方法

前述の通り、groongaで保持できる位置情報は点だけです。点を格納するカラムは以下のどちらかの型にしなければいけません。

  • TokyoGeoPoint: 日本測地系での座標のときに利用する。
  • WGS84GeoPoint: 世界測地系(WGS 84のWGSはWorld Geodetic Systemの略)での座標のときに利用する。

どちらの型を用いた場合でも、緯度と経度を格納するという点は変わりません。そのため、どちらの型の値も同じ表現方法を用います。サポートしている表現方法は以下のフォーマットの文字列です。

  • "#{緯度}x#{経度}"
  • "#{緯度},#{経度}"

緯度・経度は「ミリ秒」または「度」で表現します。ミリ秒表記はあまりなじみがないかもしれませんが、度表記はGoogle Mapsでも使われている表記なので見たことがあるかもしれません。たとえば、東京駅は緯度が35度40分52.975秒、経度が139度45分57.902秒ですが、これは以下のように表現します。

ミリ秒表記:

  • 35度40分52.975秒 → ((35 * 60 * 60) + (40 * 60) + 52.975) * 1000 → 128452975
  • 139度45分57.902秒 → ((139 * 60 * 60) + (45 * 60) + 57.902) * 1000 → 503157902
  • 座標: "128452975x503157902" または "128452975,503157902"

度表記:

  • 35度40分52.975秒 → 35 + ((40 + (52.975 / 60.0)) / 60.0) → 35.6813819444444
  • 139度45分57.902秒 → 139 + ((45 + (57.902 / 60.0)) / 60.0) → 139.766083888889
  • 座標: "35.6813819444444x139.766083888889"または"35.6813819444444,139.766083888889"(これはGoogle Mapsと同じ表記。つまり、Google Mapsで表示した座標情報をそのまま利用できる。)

以下は使用例です。まず、テーブルとカラムを定義します。

% groonga -n /tmp/geo-point
> table_create Stations TABLE_HASH_KEY ShortText
[[0,1315881737.57395,0.055109867],true]
> column_create Stations location COLUMN_SCALAR WGS84GeoPoint
[[0,1315881759.65377,0.081054688],true]

データを取り込みます。上記の4パターンすべてを取り込んでいます。

> load --table Stations
> [
> ["_key", "location"],
> ["東京駅(ミリ秒 + x表記)", "128452975x503157902"],
> ["東京駅(ミリ秒 + ,表記)", "128452975,503157902"],
> ["東京駅(度 + x表記)", "35.6813819444444x139.766083888889"],
> ["東京駅(度 + ,表記)", "35.6813819444444,139.766083888889"]
> ]
[[0,1315881767.69242,127.240681505],4]

格納されているデータを確認します。groonga内部では緯度・経度をミリ秒として保持しているため、ミリ秒表記で出力されます。groonga内部でミリ秒として保持しているのは浮動小数点数ではなく整数として処理したいからです。

> select Stations --output_type xml
<?xml version="1.0" encoding="utf-8"?>
<SEGMENTS>
<SEGMENT>
<RESULTPAGE>
<RESULTSET OFFSET="0" LIMIT="4" NHITS="4">
<HIT NO="1">
<FIELD NAME="_id">1</FIELD>
<FIELD NAME="_key">東京駅(ミリ秒 + x表記)</FIELD>
<FIELD NAME="location">128452975x503157902</FIELD>
</HIT>
<HIT NO="2">
<FIELD NAME="_id">2</FIELD>
<FIELD NAME="_key">東京駅(ミリ秒 + ,表記)</FIELD>
<FIELD NAME="location">128452975x503157902</FIELD>
</HIT>
<HIT NO="3">
<FIELD NAME="_id">3</FIELD>
<FIELD NAME="_key">東京駅(度 + x表記)</FIELD>
<FIELD NAME="location">128452974x503157901</FIELD>
</HIT>
<HIT NO="4">
<FIELD NAME="_id">4</FIELD>
<FIELD NAME="_key">東京駅(度 + ,表記)</FIELD>
<FIELD NAME="location">128452974x503157901</FIELD>
</HIT>
</RESULTSET>
</RESULTPAGE>
</SEGMENT>
</SEGMENTS>

>

目視でデータを確認する場合は「--output_type xml」を指定して、XMLとして出力した方が確認しやすいです。

使い方

groongaの位置情報検索機能で利用できる以下のことについてその実現方法を説明します。

  1. 指定した四角の中に含まれている座標を持つレコードを検索する。
  2. 指定した円の中に含まれている座標を持つレコードを検索する。
  3. 座標間の距離を計算する。
  4. ある座標からの距離が近い順にレコードをソートする。

説明にあたって、店舗を検索するアプリケーションを考えます。各店舗がそれぞれ1レコードに対応します。

スキーマ定義

まず、店舗を格納する「Shops」テーブルを定義します。今回は説明用なので、各店舗には必要最小限の情報として店舗名と位置情報のみを格納することとします。

table_create Shops TABLE_HASH_KEY ShortText
column_create Shops location COLUMN_SCALAR WGS84GeoPoint

位置情報で高速に検索できるようにインデックスを張ります。

table_create Locations TABLE_PAT_KEY WGS84GeoPoint
column_create Locations shop COLUMN_INDEX Shops location

インデックス用のテーブル「Locations」はパトリシアトライ(TABLE_PAT_KEY)にします。キーの型はインデックス対象のカラム(Shops.location)と同じ型(WGS84GeoPoint)にすることがポイントです。

以下は実際に実行した結果です。

ddl.grn:

table_create Shops TABLE_HASH_KEY ShortText
column_create Shops location COLUMN_SCALAR WGS84GeoPoint

table_create Locations TABLE_PAT_KEY WGS84GeoPoint
column_create Locations shop COLUMN_INDEX Shops location

データベースの作成:

% rm -rf /tmp/shops
% mkdir -p /tmp/shops/
% groonga -n /tmp/shops/db < ddl.grn
[[0,1315883158.10711,0.05206624],true]
[[0,1315883158.1593,0.067047364],true]
[[0,1315883158.22642,0.056288895],true]
[[0,1315883158.28277,0.11994776],true]
サンプルデータ

たまたまたいやき屋(+α)のデータがあった*2のでそのデータを使います。

shops.grn:

load --table Shops
[
["_key", "location"],
["根津のたいやき", "35.720253,139.762573"],
["たい焼 カタオカ", "35.712521,139.715591"],
["そばたいやき空", "35.683712,139.659088"],
["車", "35.721516,139.706207"],
["広瀬屋", "35.714844,139.685608"],
["さざれ", "35.714653,139.685043"],
["おめで鯛焼き本舗錦糸町東急店", "35.700516,139.817154"],
["尾長屋 錦糸町店", "35.698254,139.81105"],
["たいやき工房白家 阿佐ヶ谷店", "35.705517,139.638611"],
["たいやき本舗 藤家 阿佐ヶ谷店", "35.703938,139.637115"],
["みよし", "35.644539,139.537323"],
["寿々屋 菓子", "35.628922,139.695755"],
["たい焼き / たつみや", "35.665501,139.638657"],
["たい焼き鉄次 大丸東京店", "35.680912,139.76857"],
["吾妻屋", "35.700817,139.647598"],
["ほんま門", "35.722736,139.652573"],
["浪花家", "35.730061,139.796234"],
["代官山たい焼き黒鯛", "35.650345,139.704834"],
["たいやき神田達磨 八重洲店", "35.681461,139.770599"],
["柳屋 たい焼き", "35.685341,139.783981"],
["たい焼き写楽", "35.716969,139.794846"],
["たかね 和菓子", "35.698601,139.560913"],
["たい焼き ちよだ", "35.642601,139.652817"],
["ダ・カーポ", "35.627346,139.727356"],
["松島屋", "35.640556,139.737381"],
["銀座 かずや", "35.673508,139.760895"],
["ふるや古賀音庵 和菓子", "35.680603,139.676071"],
["蜂の家 自由が丘本店", "35.608021,139.668106"],
["薄皮たい焼き あづきちゃん", "35.64151,139.673203"],
["横浜 くりこ庵 浅草店", "35.712013,139.796829"],
["夢ある街のたいやき屋さん戸越銀座店", "35.616199,139.712524"],
["何故屋", "35.609039,139.665833"],
["築地 さのきや", "35.66592,139.770721"],
["しげ田", "35.672626,139.780273"],
["にしみや 甘味処", "35.671825,139.774628"],
["たいやきひいらぎ", "35.647701,139.711517"]
]

データのロード:

% groonga /tmp/shops/db < shops.grn
[[0,1315883204.86313,0.005284274],36]

それでは、サンプルデータが用意できたので実際に使ってみましょう。

指定した四角の中に含まれている座標を持つレコードを検索

明日、あなたは初めて浅草に行くことになりました。初めて行く土地にたいやき屋があるかどうか、気になりますよね。そこで、浅草周辺にあるたいやき屋をgroongaで検索することにしました。

地図を見ると、左上が「35.7185,139.7912」で右下が「35.7065,139.8069」となる四角い範囲の中にたいやき屋があるかどうかを調べれば浅草周辺のたいやき屋を見つけられそうです。

浅草周辺のたいやき屋をgeo_in_rectangle()で検索

groongaにはgeo_in_rectangle(カラム名, 四角い範囲の左上の座標, 四角い範囲の右下の座標)という関数があり、この関数を--filterオプションに指定すると指定した四角い範囲内にあるレコードをインデックスを使って高速に検索することができます。

% groonga /tmp/shops/db
> select Shops --filter 'geo_in_rectangle(location, "35.7185,139.7912", "35.7065,139.8069")' --output_type xml
<?xml version="1.0" encoding="utf-8"?>
<SEGMENTS>
<SEGMENT>
<RESULTPAGE>
<RESULTSET OFFSET="0" LIMIT="2" NHITS="2">
<HIT NO="1">
<FIELD NAME="_id">30</FIELD>
<FIELD NAME="_key">横浜 くりこ庵 浅草店</FIELD>
<FIELD NAME="location">128563246x503268584</FIELD>
</HIT>
<HIT NO="2">
<FIELD NAME="_id">21</FIELD>
<FIELD NAME="_key">たい焼き写楽</FIELD>
<FIELD NAME="location">128581088x503261445</FIELD>
</HIT>
</RESULTSET>
</RESULTPAGE>
</SEGMENT>
</SEGMENTS>

>

浅草周辺には、くりこ庵と写楽があるんですね。それでは、写楽の方に行くことにしましょう。

補足: 実際はブラウザ内の地図表示エリアに表示している範囲にあるレコードを検索するためにgeo_in_rectangle()を使うことが多いでしょう。なぜなら、ほとんどの地図表示エリアは四角だからです。

指定した円の中に含まれている座標を持つレコードを検索

浅草周辺を検索するために四角を指定するのは少し面倒ですね。それよりも、「浅草駅から500m以内にあるたいやき屋」の方がわかりやすいです。

地図を見ると、浅草駅は「35.7119,139.7983」にあります。

浅草周辺のたいやき屋をgeo_in_circle()で検索

groongaにはgeo_in_circle(カラム名, 円の中心の座標, 円の半径)という関数があり、この関数を--filterオプションに指定すると指定した円の範囲内にあるレコードをインデックスを使って高速に検索することができます。

% groonga /tmp/shops/db
> select Shops --filter 'geo_in_circle(location, "35.7119,139.7983", 500)' --output_type xml
<?xml version="1.0" encoding="utf-8"?>
<SEGMENTS>
<SEGMENT>
<RESULTPAGE>
<RESULTSET OFFSET="0" LIMIT="1" NHITS="1">
<HIT NO="1">
<FIELD NAME="_id">30</FIELD>
<FIELD NAME="_key">横浜 くりこ庵 浅草店</FIELD>
<FIELD NAME="location">128563246x503268584</FIELD>
</HIT>
</RESULTSET>
</RESULTPAGE>
</SEGMENT>
</SEGMENTS>

> 

浅草駅の近くには、くりこ庵しかないんですね。それでは、くりこ庵に行くことにしましょう。

座標間の距離を計算

東京駅から2km以内にあるたいやき屋を検索し、それぞれのたいやき屋までの距離も取得しましょう。距離を取得するには_scoreカラムとgeo_distance(カラム名, 基準点の座標)関数を使います。

_scoreカラムは擬似カラムの一種で、検索結果レコードに自動的に追加されているカラムです。通常は検索のヒットスコアを入れるのですが、それ以外の値でも任意の値を入れることができるので、東京駅からの距離を入れることにします。

--scorerオプションを指定することにより_scoreカラムに任意の値を設定できます。ここでgeo_distance()を使い、東京駅からの距離を_scoreカラムに入れます。

実行例(読みやすくするため改行が入っていますが実際は改行を入れてはいけません):

> select Shops
    --filter 'geo_in_circle(location, "35.68138194,139.766083888889", 2000)'
    --scorer '_score = geo_distance(location, "35.68138194,139.766083888889")'
    --output_columns '_key,_score,*'
    --sortby _score
    --output_type xml
<?xml version="1.0" encoding="utf-8"?>
<SEGMENTS>
<SEGMENT>
<RESULTPAGE>
<RESULTSET OFFSET="0" LIMIT="7" NHITS="7">
<HIT NO="1">
<FIELD NAME="_key">たい焼き鉄次 大丸東京店</FIELD>
<FIELD NAME="_score">230</FIELD>
<FIELD NAME="location">128451283x503166852</FIELD>
</HIT>
<HIT NO="2">
<FIELD NAME="_key">たいやき神田達磨 八重洲店</FIELD>
<FIELD NAME="_score">407</FIELD>
<FIELD NAME="location">128453259x503174156</FIELD>
</HIT>
<HIT NO="3">
<FIELD NAME="_key">銀座 かずや</FIELD>
<FIELD NAME="_score">990</FIELD>
<FIELD NAME="location">128424628x503139222</FIELD>
</HIT>
<HIT NO="4">
<FIELD NAME="_key">にしみや 甘味処</FIELD>
<FIELD NAME="_score">1310</FIELD>
<FIELD NAME="location">128418570x503188660</FIELD>
</HIT>
<HIT NO="5">
<FIELD NAME="_key">しげ田</FIELD>
<FIELD NAME="_score">1606</FIELD>
<FIELD NAME="location">128421453x503208982</FIELD>
</HIT>
<HIT NO="6">
<FIELD NAME="_key">柳屋 たい焼き</FIELD>
<FIELD NAME="_score">1671</FIELD>
<FIELD NAME="location">128467227x503222331</FIELD>
</HIT>
<HIT NO="7">
<FIELD NAME="_key">築地 さのきや</FIELD>
<FIELD NAME="_score">1765</FIELD>
<FIELD NAME="location">128397312x503174595</FIELD>
</HIT>
</RESULTSET>
</RESULTPAGE>
</SEGMENT>
</SEGMENTS>

>

鉄次が一番近いですね。

ある座標からの距離が近い順にレコードをソート

前の例では_scoreに入れた東京駅からの距離でソートしていましたが、このときはインデックスを使いません。インデックスを使ってソートする場合は--sortbyにgeo_distance()関数を指定します。ただし、--sortby内では緯度・経度の区切りに「,」は使えないことに注意してください。--sortbyでgeo_distance()を使うときは、「"#{緯度},#{経度}"」ではなく「"#{緯度}x#{経度}"」というように緯度・経度の区切りには「x」を使ってください。

  • ×: --filter 'geo_distance(location, "35.68138194,139.766083888889")'
  • ○: --filter 'geo_distance(location, "35.68138194x139.766083888889")'

実行例(読みやすくするため改行が入っていますが実際は改行を入れてはいけません):

> select Shops
    --filter 'geo_in_circle(location, "35.68138194,139.766083888889", 2000)'
    --sortby 'geo_distance(location, "35.68138194x139.766083888889")'
    --output_type xml
<?xml version="1.0" encoding="utf-8"?>
<SEGMENTS>
<SEGMENT>
<RESULTPAGE>
<RESULTSET OFFSET="0" LIMIT="7" NHITS="7">
<HIT NO="1">
<FIELD NAME="_key">たい焼き鉄次 大丸東京店</FIELD>
<FIELD NAME="location">128451283x503166852</FIELD>
</HIT>
<HIT NO="2">
<FIELD NAME="_key">たいやき神田達磨 八重洲店</FIELD>
<FIELD NAME="location">128453259x503174156</FIELD>
</HIT>
<HIT NO="3">
<FIELD NAME="_key">銀座 かずや</FIELD>
<FIELD NAME="location">128424628x503139222</FIELD>
</HIT>
<HIT NO="4">
<FIELD NAME="_key">にしみや 甘味処</FIELD>
<FIELD NAME="location">128418570x503188660</FIELD>
</HIT>
<HIT NO="5">
<FIELD NAME="_key">しげ田</FIELD>
<FIELD NAME="location">128421453x503208982</FIELD>
</HIT>
<HIT NO="6">
<FIELD NAME="_key">柳屋 たい焼き</FIELD>
<FIELD NAME="location">128467227x503222331</FIELD>
</HIT>
<HIT NO="7">
<FIELD NAME="_key">築地 さのきや</FIELD>
<FIELD NAME="location">128397312x503174595</FIELD>
</HIT>
</RESULTSET>
</RESULTPAGE>
</SEGMENT>
</SEGMENTS>

>

インデックスを使わない場合と同じ結果になっていますね。

データ構造

ここまではユーザ向けの説明でしたが、ここからは実装の説明になります。

groongaでは位置情報を高速に検索するために、GeoHashと同じ考え方で緯度経度をエンコードしてパトリシアトライに格納しています。同じ考え方というのは、緯度と経度の情報を交互に含んだバイト列としてエンコードするという点です。

例えば、東京駅はミリ秒表記では「128452975x503157902」になります。groongaは内部では緯度・経度をミリ秒としてデータを持っていて、それぞれ32bit整数として保持しています。東京駅のデータは2進数で表すと以下のようになります。

何番目のビットか 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
緯度(128452975) 0 0 0 0 0 1 1 1 1 0 1 0 1 0 0 0
経度(503157902) 0 0 0 1 1 1 0 1 1 1 1 1 1 1 0 1
何番目のビットか 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
緯度(128452975) 0 0 0 0 1 0 0 1 0 1 1 0 1 1 1 1
経度(503157902) 1 0 0 1 0 1 0 0 1 0 0 0 1 1 1 0

この緯度・経度データをエンコードして1つのビット列にし、それをパトリシアトライのキーとします。このとき、緯度のビットと経度のビットを交互に使います。

何番目のビットか 0 1 2 3 4 5 6 7 8 9 10 11 12 13 ...
エンコードされた緯度・経度データ 0 0 0 0 0 0 0 1 0 1 1 1 1 0 ...
緯度の何番目のビットか 0 1 2 3 4 5 6 ...
緯度(128452975) 0 0 0 0 0 1 1 ...
経度の何番目のビットか 0 1 2 3 4 5 6 ...
経度(503157902) 0 0 0 1 1 1 0 ...

こうすることにより、先頭の方から2ビットずつデータを読んでいけば緯度情報と経度情報を両方読み込むことができるデータ構造になります。さらに、先頭の方がより粗い位置情報(より広い範囲を表す情報)となっているので、先頭からデータを読み込むことにより徐々に範囲を絞り込んでいけます。

検索方法

では、このデータ構造を使ってどのように効率よく検索するかを説明します。

以下の図は先頭の2ビットだけを読んだ状態の図です。

先頭2ビットだけを読んだ状態

先頭の2ビットが「00」の場合は右上の赤い範囲を表します。つまり、赤い範囲にあるレコードを検索したい場合は先頭の2ビットが「00」のレコードを検索すればよいことになります。

さらに2ビット読んで先頭の4ビットまで使うことにしたのが以下の図です。

先頭4ビットまで読んだ状態

先頭の4ビットが「0000」であれば、右上の範囲の中のさらに左下の赤い範囲にあることがわかります。

このようにして先頭から2ビット単位でデータを読み込むことによりレコードを絞り込むことができます。先頭ビットでレコードを絞り込んでいく部分はパトリシアトライの前方一致検索機能を使います。そのため、位置情報のインデックス用のテーブルはパトリシアトライである必要があります。

geo_in_rectangle()を使った検索

geo_in_rectangle()がどのように検索しているかを説明します。

geo_in_rectangle()は、まず,指定された四角よりも少しだけ大きい範囲を選びます。例えば、黒塗りの四角が指定された場合は赤い縁になっている2つの範囲を選びます。このとき、できるだけ小さい範囲を選ぶようにがんばります。

geo_in_rectangle()で使う範囲

次に、範囲の中にあるレコードを取り出し、指定された四角の中に本当にレコードが含まれているかを確認します。範囲を少し大きめにとっているため、このチェックをしないと、指定された四角に入っていない(けど近くにある)レコードも検索結果に含めてしまう可能性があるためです。

geo_in_circle()を使った検索

geo_in_circle()も、形が四角ではなく円であるというだけでやっていることはgeo_in_rectangle()とほとんど同じです。違うのはどうやって検索対象とする範囲を選ぶかという部分だけです。

まず、指定された円よりも少しだけ大きい範囲を選びます。例なので、実際の処理よりも大雑把に説明します。以下の図のように黒塗りの円が指定された場合は赤い縁になっている9つの範囲を選びます*3。このとき、できるだけ小さい範囲を選ぶようにがんばります。

geo_in_circle()で使う範囲

次に、範囲の中にあるレコードを取り出し、指定された円の中に本当にレコードが含まれているかを確認します。これはgeo_in_rectangle()でもやっている処理と同じです。

まとめ

groongaのドキュメントの位置情報検索についての情報が不足しているため、現状に合わせた内容をまとめました。ここにまとめた内容は後でgroongaのドキュメントに反映させる予定です。

*1  このあたりに興味のある人はOpen Geospatial Consortiumのサイトもみるとよいでしょう。

*2  元々はgroonga本体のテスト用に用意したデータです。

*3  実際はもっと細かく範囲を分割して、検索範囲をもっと小さくします

つづき: 2011-12-26
タグ: groonga
2011-09-13

*roonga族リリース

肉の日なのでgroongaとその関連プロジェクトがリリースされました。

このうち、mroongaの変更点が大きめなので、mroonga 0.7の変更点について紹介します。

mroongaとは

mroonga*1はMySQLにgroongaの全文検索機能を追加するストレージエンジンです。ストレージエンジンというのはMySQLのデータストア機能・検索機能を担当するモジュールのことです。ここがプラグインとして後から追加できる仕組みになっています。groongaをバックエンドとしてストレージエンジンを実装しているのがmroongaです*2

mroongaを使うと嬉しいこと

mroongaを使うと使い慣れたRDBやSQLを使って全文検索機能が得意なアプリケーションを開発できるようになります。既存の資産を活かしながら本格的な全文検索機能も使えることが嬉しいことですね。

現在は、RDBとSolrを組み合わせて全文検索機能付きのアプリケーションを実現していることも多いかもしれませんが、そうすると管理対象が増えたりデータが分散したりして管理コストが上がってしまいます*3

mroongaはこれまでと同様にMySQLサーバーを管理するだけでよいので、新しく管理対象が増えたりしません。

これまでのmroonga: ストレージモード

mroonga 0.6までは、データストア機能も通常の検索機能も全文検索機能も提供するひとり立ちしたストレージエンジンでした。このように動くモードをストレージモードと呼びます。図にすると以下のようになります。MyISAMやInnoDBと同じように扱える1つのストレージエンジンとしてmroongaが存在しています。

ストレージモード

このモードではカラムストアであるgroongaの特性を活かすことができるため、 不必要なデータアクセスを避けた高速な動作ができるというメリットがあります。しかし、データストアの信頼性という点では、実績のあるInnoDBなどには及ばないというデメリットがあります。

このデメリットを解決するために追加されたのがラッパーモードです*4

0.7で追加されたモード: ラッパーモード

mroonga 0.7では新しくラッパーモードという動作モードを追加しました。このモードでは、他のストレージエンジンと連携して動作します。mroongaは全文検索機能だけ実現し、データストア機能や通常の検索機能は他のストレージエンジンに任せます。図にすると以下のようになります。MySQLからはmroongaしか見えませんが、mroongaの後ろにMyISAMやInnoDBなど既存のストレージエンジンが存在します。mroongaはMySQLと既存のストレージエンジンの間に入り、全文検索機能関連のみ処理し、それ以外は既存のストレージエンジンに処理してもらいます。

ラッパーモード

このモードでは任意の既存のストレージエンジンにgroongaの高性能な全文検索機能を追加できるというメリットがあります。例えば、信頼性のあるInnoDBに全文検索機能を追加して、便利で高速で安心なRDBを実現できます。

まとめ

今月も*roonga族がリリースされました。今回は特にmroongaについて紹介しました。

ラッパーモードの使い方についてはmroongaのラッパーモードのドキュメントを参照してください。なお、同じページに現時点での注意点も説明してあるので、そちらも確認してください。

*1  正式名称は「groongaストレージエンジン」で、「mroonga」は開発コードネームという位置付けだったのですが、最近は「mroongaの方がいいやすいよね?」、「正式名称もmroongaでいいんじゃない?」みたいな流れもでてきています。1.0がリリースされるまでにはどうなるかが決まるはずです。

*2  groongaのSQLインターフェイスと考えてもよい。

*3  Sphinxという選択肢もありますが、構成上それはそれで管理コストが上がってしまいます。

*4  ストレージモードがなくなるわけではありません。

つづき: 2011-12-26
タグ: groonga
2011-06-29

Sphinxの国際化機能を使って複数言語用ドキュメントを用意する方法(使い方)

Sphinxの国際化機能を使って複数言語用ドキュメントを用意する方法(概要)で示した複数言語用ドキュメントを用意する仕組みの使い方を紹介します。本当は仕組みについて説明するつもりだったのですが、使い方を書いていたら長くなったので分けることにしました。この仕組みは実際にgroongaで使っているもので、以下のような使い方になります。

  • ドキュメントを英語で書く。
  • 他の言語への翻訳をPOファイルに書く。
  • それぞれの言語毎にHTMLを生成する。

まずファイル構成を紹介して、その後、実例を示しながら具体的な作業を紹介します。

ファイル構成

まずファイル構成です。これで概要を掴んでください。

groongaはAutomakeなどのGNUビルドシステムを利用しているため、このような構成になっています。違うビルドシステムを利用している場合は違う構成にした方がよいかもしれません。その場合でも英語を特別扱いせずにdoc/locale/以下に各言語毎のディレクトリを作るという方法は真似した方がよいでしょう。この方が規則を単純化できるため、ビルドシステムが単純になるはずです。

.
|-- build
|   `-- makefiles (doc/locale/#{言語}/以下で共有するMakefile)
|       |-- LC_MESSAGES.am (doc/locale/#{言語}/LC_MESSAGES/Makefile.amでinclude)
|       |-- gettext-files.am
|       |-- gettext.am
|       |-- locale.am (doc/locale/#{言語}/Makefile.amでinclude)
|       |-- sphinx-build.am
|       `-- sphinx.am
`-- doc
    |-- Makefile.am
    |-- locale (各言語用ディレクトリ置き場)
    |   |-- Makefile.am
    |   |-- en (英語用ディレクトリ)
    |   |   |-- LC_MESSAGES(翻訳テキストなし。単なる置き場所。)
    |   |   |   |-- Makefile.am
    |   |   |   |-- *.po
    |   |   |   `-- *.mo
    |   |   |-- Makefile.am
    |   |   |-- html/ (生成された英語のHTML)
    |   |   |-- html-build-stamp (HTMLが生成されたことを示すだけのファイル)
    |   |   |-- man/ (生成された英語のman)
    |   |   `-- man-build-stamp (manが生成されたことを示すだけのファイル)
    |   `-- ja (日本語用ディレクトリ)
    |       |-- LC_MESSAGES (英語→日本語の翻訳テキスト)
    |       |   |-- Makefile.am
    |       |   |-- *.po
    |       |   `-- *.mo
    |       |-- Makefile.am
    |       |-- html/ (生成された日本語のHTML)
    |       |-- html-build-stamp (HTMLが生成されたことを示すだけのファイル)
    |       |-- man/ (生成された日本語のman)
    |       `-- man-build-stamp (manが生成されたことを示すだけのファイル)
    |-- source/ (ドキュメント本体)
    `-- sphinx/ (最新のSphinx)

Sphinxの国際化のドキュメント(英語)ではsource/以下にtranslated/#{言語}/LC_MESSAGES/というディレクトリを作って、そこに*.moを置くような例になっています。しかし、上記の構成ではsource/の下ではなく、source/と同じディレクトリにlocale/#{言語}/LC_MESSAGES/を作っています。これは、source/以下に*.moなど自動生成するファイルを置かないようにするためです。

source/以下にファイルがあるとデフォルトでsphinx-buildの処理対象となってしまいます。そのため、source/以下にtranslated/#{言語}/LC_MESSAGES/を置く場合はconf.py内でexclude_patternsを使って処理対象でないことを明示する必要があります。明示的に処理対象外とするくらいなら、はじめからsource/以下ではなく場所に置いた方がよいのではないか、ということでsource/の下ではなく、同じディレクトリにlocale/#{言語}/LC_MESSAGES/を置いています。

使い方

それではこの仕組みを使ったドキュメントの作成方法です。

ドキュメント作成の流れは以下のようになります。ここでは、new-documentというドキュメントを追加するという例で話を進めます。

  1. doc/source/new-document.txtを作成し、英語でドキュメントを書く。
  2. doc/locale/ja/LC_MESSAGES/へ移動する。
  3. make initを実行する。
  4. ↑でnew-document.poができるので、それをリポジトリへ追加する。
  5. new-document.poを翻訳する。
  6. doc/locale/ja/へ移動する。
  7. make htmlを実行する。
  8. ↑でdoc/locale/ja/html/以下に翻訳されたHTMLが生成されるので確認する。

ドキュメントの作成はこの作業を繰り返すことになります。実例を以下に示します。

1. 英語でドキュメントを書く。

まず、英語でドキュメントを作成します。以下の内容のドキュメントにしたとします。

doc/source/new-document.txt:

New Document
============

Hi! This is new document.

次に、既存のページからリンクを張ります。こうしないとどこからも辿れないページになってしまいます。

doc/source/index.txt:

...
* :doc:`new-document`
...

これでオリジナルのドキュメントができました。

2., 3., 4. POファイルの作成

次は、翻訳テキストを書くファイルであるPOファイルを作成します。

[groonga]% cd doc/locale/ja/LC_MESSAGES
[groonga/doc/locale/ja/LC_MESSAGES]% make init
...
ユーザが翻訳に関するフィードバックをあなたに送ることができるように,
新しいメッセージカタログにはあなたの email アドレスを含めてください.
またこれは, 予期せぬ技術的な問題が発生した場合に管理者があなたに連絡が取れる
ようにするという目的もあります.

Is the following your email address?
  kou@clear-code.com
Please confirm by pressing Return, or enter your email address.
kou@clear-code.com ← 入力
http://translationproject.org/team/index.html を検索中... 完了.
A translation team for your language (ja) does not exist yet.
If you want to create a new translation team for ja, please visit
  http://www.iro.umontreal.ca/contrib/po/HTML/teams.html
  http://www.iro.umontreal.ca/contrib/po/HTML/leaders.html
  http://www.iro.umontreal.ca/contrib/po/HTML/index.html

new-document.po を生成.
[groonga/doc/locale/ja/LC_MESSAGES]%

上記のコマンドで以下のような内容のPOファイルが作成されます。

doc/locale/ja/LC_MESSAGES/new-document.po:

# Japanese translations for 1.2.2 package.
# Copyright (C) 2009-2011, Brazil, Inc
# This file is distributed under the same license as the groonga package.
# Kouhei Sutou <kou@clear-code.com>, 2011.
#
msgid ""
msgstr ""
"Project-Id-Version: 1.2.2\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2011-06-19 19:18\n"
"PO-Revision-Date: 2011-06-19 19:18+0900\n"
"Last-Translator: Kouhei Sutou <kou@clear-code.com>\n"
"Language-Team: Japanese\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ja\n"
"Plural-Forms: nplurals=1; plural=0;\n"

#: ../../../source/new-document.txt:2
msgid "New Document"
msgstr ""

#: ../../../source/new-document.txt:4
msgid "Hi! This is new document."
msgstr ""

翻訳対象のメッセージはmsgid "New Document"msgid "Hi! This is new document."の部分です。

内容を確認したらリポジトリに登録しましょう。

[groonga/doc/locale/ja/LC_MESSAGES]% git add new-document.pot
[groonga/doc/locale/ja/LC_MESSAGES]% git commit

(ここでbuild/makefiles/Makefile.amの中にあるpo_filesmo_filesなどを更新する必要があるのですが、ここでは割愛します。)

5. POファイルの更新

生成されたPOファイルはmsgstr ""msgstrの部分が空文字列になっています。ここに翻訳後のテキストを入力します。

doc/locale/ja/LC_MESSAGES/new-document.po:

...
#: ../../../source/new-document.txt:2
msgid "New Document"
msgstr "新しいドキュメント"

#: ../../../source/new-document.txt:4
msgid "Hi! This is new document."
msgstr "わーい!新しいドキュメントだよー。"
6., 7., 8. 翻訳されたドキュメントの確認

以下の手順で翻訳され日本語ドキュメントとなったHTMLを出力できます。

[groonga/doc/locale/ja/LC_MESSAGES]% cd ..
[groonga/doc/locale/ja]% make html
[groonga/doc/locale/ja]% firefox html/new-document.html

以下のようなHTMLがブラウザで表示されたはずです。

...
<h1>新しいドキュメント...</h1>
<p>わーい!新しいドキュメントだよー。</p>
...

まとめ

groongaで採用しているSphinxの国際化機能を使って複数言語用ドキュメントを用意する方法の使い方について紹介しました。なお、新しい言語の追加方法についてはgroongaの翻訳方法のドキュメントで説明しています。

次こそはこの仕組みについて説明できるはずです。

つづき: 2011-07-07
タグ: groonga
2011-06-19

Sphinxの国際化機能を使って複数言語用ドキュメントを用意する方法(概要)

YARDのことの続きを書きたいと思いつつもなかなか辿りつきません。今回はPython製のドキュメントツールSphinxの話です。groongaのケースを例にしてSphinxで複数言語用のドキュメントを生成する方法の概要を紹介します。書いていたら長くなったので、具体的にどうするかというのは次の機会にします。

5/29にgroonga 1.2.2がリリースされました。今後は日本だけではなく世界でも使ってもらえるように、英語でも情報を発信していく方向になりました。そこで、今回のリリースに合わせてサイトデザインをリニューアルし、英語のページも用意しました。ただし、すでにあった日本語のコンテンツを今回のリリースで全部英語にした、というものではありません。今回のリリースでやったのは「日本語と英語という複数の言語で情報を発信する仕組み」を作るところまでです。せっかくなので、今回のリリースで導入した、Sphinxを使って実現している複数言語用のドキュメントを用意する仕組み(の概要)を紹介します。

なお、実際のページは以下のようになります。英語のページでも日本語の文章がまだまだ多く残っていますが、これはおいおい改善していく予定です*1

それぞれのページのヘッダーには他の言語へのリンクを用意してあり、すぐに対応する他の言語のページに行けるようになっています。

概要

まず、複数言語用のドキュメントを用意する仕組みの概要を説明します。

しばらくしたらリリースされるだろうSphinx 1.1には国際化機能がついています。(Sphinx本家の国際化についてのドキュメント(英語)。)今回紹介する仕組みはこの機能をベースにした仕組みになります*2

国際化機能を使うと、ベースとなるドキュメント1つから、そのドキュメントを他の言語に翻訳したドキュメントを複数生成することができます。ふつうはベースとなるドキュメントは英語で記述するので、以下のようなイメージになります。

他の言語に翻訳したドキュメントを複数生成

ポイントは、本文のテキストだけ他の言語に翻訳すればよいというところです。章分けや説明する順序などの文章の構造などはベースとなる英語のドキュメントと同じものを共有します。

以下のようにドキュメント全体を翻訳する方法もありますが、この場合は文章の構造は共有していません。それぞれの翻訳されたドキュメントは翻訳した時点の英語のドキュメントの文書の構造をコピーしています。そのため、英語のドキュメントの文書の構造が変わったら、それにあわせて翻訳したドキュメントの方も変える必要があります*3

ドキュメント全体を翻訳

ドキュメント全体を翻訳する場合は元のドキュメントをコピーして翻訳するだけなので、通常のドキュメント作成作業と作業の流れはそれほど違いはないでしょう。

しかし、Sphinxが採用しているのは本文のみ翻訳する方法なので、通常の作業の流れとはだいぶ異なる流れになります。

翻訳の流れ

Sphinxでは「元の本文」を「翻訳した本文」に置き換えるためにgettextを使っています。gettextは昔からある国際化用ライブラリで、Sphinxが使っているのはgettextのPython実装です。

昔からあるだけあって、周辺ツールがそろっていることが利点です。例えば、元の本文と翻訳した本文とを比較して、変更された本文を自動検出するツールがあったりします。「一度翻訳して終わり」という場合は必要のないツールですが、「元の本文に追従して翻訳も更新する」という場合には有用なツールです。

groongaは継続して開発しているソフトウェアなのでドキュメントも更新されます。この場合は翻訳作業は以下のような流れになります。

翻訳の流れ

「(1)本文を抽出」*4と「(4)本文の差分抽出」*5の部分がツールを使って自動化できる部分です。この部分は手動でやるには大変な部分なのでとても助かります。

「(3)更新」の部分は翻訳とは関係なく通常のドキュメント更新作業と同じです。

残りの(2)と(5)の「日本語へ翻訳」がメインの翻訳作業になりますが、ここの作業が通常の翻訳作業とやり方が違う部分になります。これは、gettextの作法に従う必要があるためです。

翻訳のしかた

gettextはPO (Portable Object)というファイルで「元のテキスト」と「翻訳したテキスト」を関連付けます。POは以下のようなフォーマットのテキストです*6

#: ${元のテキストがあるファイルのパス1}:${行1}
msgid "${元のテキスト1}"
msgstr "${翻訳したテキスト1}"

#: ${元のテキストがあるファイルのパス2}:${行2}
msgid "${元のテキスト2}"
msgstr "${翻訳したテキスト2}"

元の文書から本文を抽出した段階では${翻訳したテキスト}の部分が空になっているので、淡々とそこを埋めていく作業が翻訳作業になります。POファイルはテキストなので好きなエディタで編集できますが、専用の編集ツールもあります。Emacsのpo-modeやGNOMEのGtranslator、KDEのLokalizeなどです。

翻訳ができたら、それと元のドキュメントを使ってHTML形式でドキュメントを生成できます。

まとめ

Sphinxを使った複数言語用のドキュメントを用意する仕組みの概要を紹介しました。具体的な設定などは次の機会に紹介する予定ですが、待ちきれない人はgroongaのリポジトリをのぞいてみてください。

図はblockdiagで作りましたが、便利ですね。

*1  このあたりの改善に興味のある方はgroongaのドキュメントの国際化の方法を参考にチャレンジしてみてください!

*2  未リリースの機能を使っているだけあって、いろいろ大変なところもありました。いくつかは修正してSphinx本体に取り込んでもらいましたが、まだまだ細かくいろいろ問題が残っています。Sphinx推しのみなさんは今のうちに国際化機能を使って、1.1がリリースされる前にもっと改良してみてはいかがでしょうか。

*3  それでもこの方法で複数の言語のドキュメントを用意したい場合はドキュメントの翻訳にSphinxを使うなどを参考にしてみてはいかがでしょうか。

*4  Sphinxが生成したMakefileを使うとmake gettextで抽出できます。

*5  make gettextmsgmergeを組み合わせます。

*6  詳細は本家のThe Format of PO Filesを参照。

タグ: groonga
2011-05-31

検索エンジンはなぜ見つけるのか

1ヶ月ほど前になりますが、検索エンジンについての本が出版されました。GoogleなどのWeb検索システムからAmazonなどのショッピングサイトまで、今では検索システムはなくてはならないものになりました。そんな検索システムのベースとなる考えや知識などを把握したい場合に向いているのがこの本です。検索システムを作ろうとしている人には読んで欲しい一冊です。

検索エンジンはなぜ見つけるのか ―知っておきたいウェブ情報検索の基礎知識
森大二郎
日経BP社
¥ 2,520

「この本を読めば独自の検索エンジンを作れるようになる!」といった類の本ではなく、「検索エンジンの基本がわかる!」という類の本です。そのため、「この検索エンジンをもっとカリカリにチューニングして性能をあげたい!」というときに読む本ではありません。そうではなく、「検索エンジンについてまとまった知識がないので、基本をしっかりおさえたい!」という用途向きです。

内容

著者は全文検索エンジンgroongaのメイン開発者の森さんですが、groongaなど特定の検索エンジンに依存した内容ではなく、検索システム全体の基本について書かれています。つまり、検索エンジンだけではなく、クローラーについても、利用者の入力から質問の意図を汲み取ることについても、検索システムに必要な機能は網羅的に扱っています。

もちろん、一番ボリュームがあるのは検索エンジンの部分で、様々なアルゴリズムが登場します。アルゴリズムの登場の仕方が段階的なので読み進めやすくなっています。「○○という問題があります。」→「△△でこれを解決できます。」→「次は●●という問題があります。」→「▲▲でこれを解決できます。」といった具合です。

プログラムで実装するためにアルゴリズムを説明する場合は細かいことまで説明した方が便利です。しかし、この本ではアルゴリズムの重要なアイディアだけを抜き出して図も使いながら説明しているため、アルゴリズムのコンセプトを理解しやすくなっています。注釈に参考文献が載っているのも嬉しいところです。もっと詳しく知りたいときは参考文献の方をあたることができます。

まとめ

検索エンジンについてのオススメの本を紹介しました。

検索はこれからもっと重要に、そして便利になっていくでしょう。例えば、kdmsnrさんが若い頃はかわいかったことを検索することがもっと重要に、そして便利にできるようになるかもしれませんね。

つづき: 2011-12-26
タグ: groonga
2011-04-17

最新
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|
タグ:
RubyKaigi2008Speaker
RubyKaigi2009Sponsor
RubyKaigi2009Speaker
SapporoRubyKaigi02Sponsor
SapporoRubyKaigi02Speaker
RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer badge_speaker.gif RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer SapporoRubyKaigi 2012 OfficialSponsor SapporoRubyKaigi 2012 Speaker