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

ククログ

タグ:

名古屋Ruby会議03:Apache ArrowのRubyバインディングをGObject Introspectionで #nagoyark03

2017年2月11日に名古屋Ruby会議03大須演芸場で開催されました。ここで咳さんの並列処理啓蒙活動話の前座の1人として話してきました。内容は、Apache ArrowGObject IntrospectionRroongaを活用すれば自然言語のデータ分析の一部でRubyを活用できるよ!、です。

関連リンク:

リポジトリーには今回のスライド内で使ったスクリプトも入っています。

内容

当初は以下にいろいろまとめていた通り、Apache Arrow・GObject Introspectionはどんな特徴でどういう仕組みでそれを実現しているかといったことも説明するつもりでした。

ただ、内容をまとめていく過程で、特徴や仕組みの説明が多いと大須演芸場で話すには勢いが足りないと判断しました。その結果、詳細をもろもろ省略したストーリーベースの内容にしました。そのため、↑の内容はほぼ使っていません。

ストーリーベースの内容にしたことで「あぁ、たしかにデータ分析の一部でRubyを使えるかも」という雰囲気は伝わりやすくなったはず(どうだったでしょうか?)ですが、詳細の説明を省略したので詳細が気になったまま終わった人もいるかもしれません。そんな人たち向けに詳細がわかる追加情報を紹介します。

Apache ArrowとApache Parquet

まず、Apache ArrowとApache Parquetの連携についてです。両者はどちらもカラムストアのデータについて扱いますが、Apache Arrowはメモリー上での扱い、Aapache Parquetはストレージ上での扱いという違いがあります。この違いのためにトレードオフのバランスが変わっています。詳細はThe future of column-oriented data processing with Arrow and Parquetを見てください。2016年11月にニューヨークで開催されたDataEngConf NYCでのApache Parquetの作者の発表資料です。

ざっくりとまとめると次の通りです。

  • Apache Parquetはストレージ上でのカラムストアのデータの扱いなので、次の傾向がある。
    • CPUを活用するよりもI/Oを減らすほうが重要(たとえば、データを圧縮するとI/Oは減る一方CPU負荷は増えてしまうが、I/Oを減らしたいので圧縮をがんばる)
    • シーケンシャルアクセスが多い
  • Apache Arrowはメモリー上でのカラムストアのデータの扱いなので、次の傾向がある。
    • I/Oを減らすよりもCPUを活用するほうが重要(たとえば、CPUキャッシュミスが少なくなるようなデータ配置にする)
    • シーケンシャルアクセスもあるしランダムアクセスもある

データの配置や高速化の工夫なども前述の発表資料で説明しているので興味のある人は発表資料も確認してください。

Apache ArrowとApache Parquetは連携できます。具体的に言うと、Apache ParquetのC++実装にはApache Arrowのデータを読み書きできる機能があります。これを使うとApache ParquetのデータをApache Arrowのデータとして扱うことができます。

GObject Introspection

GObject Introspectionに関する情報は次の記事を参考にしてください。

GObject Introspectionに対応するとバインディングを書かなくてもCライブラリーのテストをRubyで書けるようになります。Cライブラリーの開発を捗らせるためにGObject Introspectionに対応させるというのもアリです。(これも当初は話したかったけど省略した話題です。)

GObject IntrospectionのRubyバインディングであるgobject-introspection gemについてはここで少し補足します。

話の中で、GI.loadした後、よりRubyっぽいAPIになるように一手間かけるとグッと使いやすくなると説明しました。当日は次のようにエイリアスを使う方法だけを紹介したのですが、別の方法もあってそれを紹介することをすっかり忘れていました。

require "gi"
Arrow = GI.load("Arrow")
class Arrow::Array
  def [](i)
    get_value(i)
  end
end

実はGObjectIntrospection::Loaderには定義するメソッド名を変える機能があります。上述のケースではエイリアスを作るのではなく、最初からget_value[]として定義するとよいです。

class Arrow::Loader < GObjectIntrospection::Loader
  private
  def rubyish_method_name(function_info, options={})
    # 引数が1つで、メソッド名がget_valueならメソッド名を[]にする
    if function_info.n_in_args == 1 and function_info.name == "get_value"
      "[]"
    else
      super
    end
  end
end

このようにGObjectIntrospection::Loaderをカスタマイズするやり方には次のメリットがあります。

  • 余計なメソッドを増やさない(今回のケースではget_value
  • 新しく同じパターンのメソッドが増えてもエイリアスを追加する必要がない(たとえば、Arrow::Array以外にget_value(i)なメソッドが増えてもバインディングを変更する必要がない)

この実装はRed Arrow(RArrowから名前を変更、由来はRubyは赤いからというのと西武新宿線の特急列車)にあります。open {|io| ...}を実現する方法も面白いので、GObject Introspectionが気になってきた方はぜひ実装も見てみてください。

まとめ

名古屋Ruby会議03で「Apache ArrowとGObject IntrospectionとRroongaを使って自然言語のデータ分析の一部でRubyを活用する」という話をしました。(使い方を間違っていましたが)はじめて小拍子を使ったり、はじめてマクラの後に羽織(?)を脱いだりできて、楽しかったです。貴重な経験になりました。声をかけてもらってありがとうございます。名古屋のみなさん(名古屋外からの参加の方も多かったですが)に楽しんでもらえていたなら、とてもうれしいことです。

今回の話では詳細をもろもろ省略しましたが、そのあたりに興味のある方がいたらぜひお声がけください。

また、クリアコードと一緒にRubyでデータ分析できる環境を整備していきたい!という方はぜひお問い合わせください。

タグ: Ruby
2017-02-15

Groonga Meatup 2017:Groonga族2016 #groonga

2017年2月9日(年に一度の肉の日!)にGroonga Meatup 2017が開催されました。ここでGroongaMroongaPGroongaの2016一年間での進化の様子を紹介しました。最近のGroonga・Mroonga・PGroongaの情報をざっと把握できるのでご活用ください。

関連リンク:

内容

2016年はGroonga・PGroongaの改良が多く、Mroongaの改良は少なめでした。ただ、MroongaはGroongaを使っているので、Groongaの改良(の多く)はそのままMroongaでも利用できます。そのため、Groonga・PGroongaだけでなくMroongaも進化しています。

Groongaの大きな改良はこのあたりです。

Mroongaの大きな改良はこのあたりです。

  • FOREIGN KEY制約をサポート
  • マルチカラムインデックスの更新性能劣化を解消
  • *SSプラグマを追加(MATCH AGAINSTないでGroongaの検索条件を使える。Groongaの検索条件では複数インデックスを使えるのでMySQLで検索するよりも圧倒的に速くなる。)

PGroongaの大きな改良はこのあたりです。

  • ストリーミングレプリケーションをサポート
  • 検索の高速化(PostgreSQLがより適切な実行計画を選べるように、PostgreSQLにより精度の高い情報を提供するようになった)
  • Zstandardによるカラム圧縮をサポート
  • 各種便利演算子を追加(類似文書検索演算子・前方一致検索演算子・前方一致RK検索演算子)

他にもいろいろあるのでぜひスライドも確認してください。

まとめ

Groonga Meatup 2017で2016年のGroonga族の様子を紹介しました。

なお、今回のGroonga Meatup 2017は東京で開催しましたが、今月、名古屋と大阪でも開催されるので近辺の方はぜひお越しください!

他の発表については以下を参照してください。

おまけみたいな書き方になってしまいますが、同日、初心者向けのMroongaの電子書籍Groongaではじめる全文検索がリリースされました!初心者の人がくじけずに始められるようにできるだけ短い内容(約30ページ!)で動くものが作れる、という内容になっています。具体的にはPHPでMroongaを使ってPDF検索システムを作っています。全文検索をしたいけどどこから始めればいいかピンときていない…という方はまずはこれを読んでみてください。

タグ: Groonga
2017-02-09

名古屋Ruby会議03:Apache ArrowのRubyバインディング(6) #nagoyark03

前回はGObject Introspectionでのオブジェクトの寿命の扱いについて説明しました。今回は戻り値のオブジェクトの寿命について説明します。実際に動くバインディングはkou/arrow-glibにあります。

戻り値のオブジェクトの寿命

一般的に戻り値のオブジェクトの寿命には次のパターンがあります。

  • 所有権は関数実行側にあるパターン:戻り値のオブジェクトが必要なくなったら戻り値を受け取った側が責任を持って解放する。
  • 所有権は関数側にあるパターン:戻り値を受け取った側は解放してはいけない。

話を単純にするために、戻り値のオブジェクトとして文字列(char *)を考えてみましょう。

strdup(3)(引数の文字列をコピーして返す関数)は前者(所有権は関数実行側にあるパターン)です。strdup(3)の戻り値はstrdup(3)を呼び出した側が責任を持って解放しなければいけません。

char *copy;
copy = strdup("Hello");
free(copy); /* 呼び出し側がfree()する。 */

getenv(3)は後者(所有権は関数側にあるパターン)です。getenv(3)の戻り値はgetenv(3)を呼び出した側が解放してはいけません

char *path;
path = getenv("PATH");
/* free(path); */ /* 呼び出し側がfree()してはダメ! */

これが基本です。

ただし、前者の「所有権は関数実行側にあるパターン」にはもう少しパターンがあります。それは戻り値のオブジェクトがコンテナーの場合です。コンテナーというのは配列・リスト・ハッシュテーブルなどのように複数のオブジェクトを格納するオブジェクトのことです。

コンテナーの場合は、次のパターンがあります。

  • コンテナーの所有権のみ関数実行側にあるパターン
  • コンテナーの所有権もコンテナーの中のオブジェクトの所有権も関数実行側にあるパターン

コンテナーの所有権のみの場合はコンテナーのみを解放します。

GObject Introspectionでの戻り値のオブジェクトの寿命の指定

GObject Introspectionではこれらのそれぞれのパターンを指定できます。指定すると後はGObject Introspection(を使ったバインディング、Rubyならgobject-introspection gem)が適切に寿命を管理してくれます。

パターンはそれぞれの関数のドキュメントで指定します。

次の関数は新しくGArrowArrayオブジェクトを作って返す関数です。新しく作ったオブジェクトの所有権は関数呼び出し側にあります。そのため、「所有権は関数実行側にあるパターン」です。

/**
 * garrow_array_builder_finish:
 * @builder: A #GArrowArrayBuilder.
 *
 * Returns: (transfer full): The built #GArrowArray.
 */
GArrowArray *
garrow_array_builder_finish(GArrowArrayBuilder *builder)
{
  auto arrow_builder = garrow_array_builder_get_raw(builder);
  std::shared_ptr<arrow::Array> arrow_array;
  arrow_builder->Finish(&arrow_array);
  return garrow_array_new_raw(&arrow_array);
}

ポイントは次の箇所です。Returns:が戻り値に関する情報のドキュメントという意味で、(transfer full)が「所有権は呼び出し側にある」という意味です。

/**
 * Returns: (transfer full): The built #GArrowArray.
 */

「所有権は関数側にあるパターン」も見てみましょう。

次の関数はフィールド名を返す関数です。フィールド名はフィールドオブジェクトに所有権があるので関数呼び出し側は解放してはいけません。(gcharはGLibが提供しているchar型です。charと同じです。)

/**
 * garrow_field_get_name:
 * @field: A #GArrowField.
 *
 * Returns: The name of the field.
 */
const gchar *
garrow_field_get_name(GArrowField *field)
{
  const auto arrow_field = garrow_field_get_raw(field);
  return arrow_field->name.c_str();
}

ポイントは次の箇所です。戻り値の型にconstがついている場合はGObject Introspectionは所有権は関数側にあると推測します。そのため、明示的にReturns: (transfer none):と書く必要はありません。

const gchar *

「コンテナーの所有権のみ関数実行側にあるパターン」も見てみましょう。

arrow-glibでは使っていないのでGTK+にある関数を持ってきました。

/**
 * gtk_print_backend_load_modules:
 *
 * Returns: (element-type GtkPrintBackend) (transfer container):
 */
GList *
gtk_print_backend_load_modules (void)
{
  /* ... */
}

ポイントは(transfer container)です。これでコンテナーの所有権のみ関数呼び出し側にある、ということを指定します。

(element-type GtkPrintBackend)はコンテナー内の要素の型を指定しています。)

最後に「コンテナーの所有権もコンテナーの中のオブジェクトの所有権も関数実行側にあるパターン」を見てみましょう。

/**
 * garrow_record_batch_get_columns:
 * @record_batch: A #GArrowRecordBatch.
 *
 * Returns: (element-type GArrowArray) (transfer full):
 *   The columns in the record batch.
 */
GList *
garrow_record_batch_get_columns(GArrowRecordBatch *record_batch)
{
  const auto arrow_record_batch = garrow_record_batch_get_raw(record_batch);

  GList *columns = NULL;
  for (auto arrow_column : arrow_record_batch->columns()) {
    GArrowArray *column = garrow_array_new_raw(&arrow_column);
    columns = g_list_prepend(columns, column);
  }

  return g_list_reverse(columns);
}

ポイントは(transfer full)です。(transfer full)はコンテナーなオブジェクトにもそうでないオブジェクトにも使えるということです。

まとめ

GObject Introspectionでの戻り値のオブジェクトの寿命について説明しました。次回はインターフェイスについて説明します。

つづき: 2017-02-15
タグ: Ruby
2017-02-01

名古屋Ruby会議03:Apache ArrowのRubyバインディング(5) #nagoyark03

前回はGObject Introspectionでのエラーの扱いについて説明しました。今回は戻り値のオブジェクトの寿命について説明します。と、考えていたのですが、書いていたら、間違って「戻り値の」オブジェクトの寿命ではなく「一般的な」オブジェクトの寿命について説明してしまっていました。。。なので、今回は「一般的な」オブジェクトの寿命についての説明で、「戻り値の」オブジェクトの寿命は次回にします。。。実際に動くバインディングはkou/arrow-glibにあります。

Apache Arrowでのオブジェクトの寿命の管理:std::shared_ptr

Apache Arrowはstd::shared_ptrでオブジェクトの寿命を管理しています。バインディングは該当オブジェクトを使っているときは参照を増やして、使わなくなったら参照を減らせばよいです。

GObject Introspectionでのオブジェクトの寿命の管理:GObject

GObject Introspection対応ライブラリーはC言語で実装しますが、オブジェクト指向な機能を使えます。たとえば、カプセル化・継承・ポリモルフィズムなどの機能があります。

これらの機能はGObject IntrospectionがベースにしているGObjectというライブラリーが提供しています。GObject Introspectionでのオブジェクトの寿命の管理はこのGObjectの機能を使います。

GObjectではオブジェクトの寿命はリファレンスカウントで管理します。具体的にはGObjectを継承したクラスを作り、そのオブジェクトに対してg_object_ref()/g_object_unref()を使うことで寿命を管理します。

Apache ArrowのオブジェクトとGObjectのオブジェクトを適切な寿命でなじませるには次のようにします。

  • Apache ArrowのクラスとGObjectのクラスを1対1で対応させる。(例:arrow::ArrayGArrowArrayGObjectベースのクラス)に対応させる。)
  • GObjectのオブジェクトを作るときに対応するApache Arrowのオブジェクトの参照を増やす。
  • GObjectのオブジェクトを解放するときに対応するApache Arrowのオブジェクトの参照を減らす。

Apache ArrowのクラスとGObjectのクラスを1対1で対応させるのは簡単です。そうなるようにクラスを設計すればよいだけだからです。

GObjectのオブジェクトを作るときに対応するApache Arrowのオブジェクトの参照を増やすには、GObjectのオブジェクトのインスタンス変数としてstd::shard_ptr<ARROW_CLASS>なインスタンス変数を用意して、GObjectのコンストラクターで代入します。(GArrowArrayではプロパティーという仕組みを利用していますが、利用せずに直接構造体のメンバーを使ってもよいです。)

GObjectのオブジェクトを解放するときに対応するApache Arrowのオブジェクトの参照を減らすには、GObjectのオブジェクトが解放されるときに関数を呼ぶ仕組みがあるのでそれを利用します。(dispose()finalize()の2つ呼ばれる関数がありますが、この場合はfinalize()を利用します。dispose()は循環参照を解決するために利用します。)

arrow-glibでの実装

GArrowArrayを使って実際の実装を説明します。

まず、arrow::Array(Apache Arrowのオブジェクト)の参照を増やすためのインスタンス変数を用意します。このインスタンス変数は外部から参照できる必要はないのでプライベートな構造体に持たせます。(カプセル化)

typedef struct GArrowArrayPrivate_ {
  std::shared_ptr<arrow::Array> array;
} GArrowArrayPrivate;

G_DEFINE_TYPE_WITH_PRIVATE(GArrowArray, garrow_array, G_TYPE_OBJECT)

#define GARROW_ARRAY_GET_PRIVATE(obj)                                   \
  (G_TYPE_INSTANCE_GET_PRIVATE((obj), GARROW_TYPE_ARRAY, GArrowArrayPrivate))

コンストラクターではstd::shard_ptr<arrow::Array>を受け取ってこのインスタンス変数に設定します。(実際はプロパティーを使っているのでもう少し処理が増えています。)

GArrowArray *
garrow_array_new_raw(std::shared_ptr<arrow::Array> *arrow_array)
{
  /* GARROW_ARRAY()はバリデーションつきでGArrowArray *にキャストするマクロ */
  /* GARROW_TYPE_ARRAYはGArrowArray用のGTypeを返すマクロ */
  /* GTypeはGObjectで型情報を表現するデータ */
  auto array = GARROW_ARRAY(g_object_new(GARROW_TYPE_ARRAY, NULL));
  /* GARROW_ARRAY_GET_PRIVATE()は前述のプライベートなデータ用の
     構造体を返すマクロ */
  auto priv = GARROW_ARRAY_GET_PRIVATE(array);
  /* Apache Arrowのオブジェクトの参照を増やす */
  priv->array = *arrow_array;
  return array;
}

デストラクター(finalize())ではコンストラクターで増やした参照を減らします。

static void
garrow_array_finalize(GObject *object)
{
  auto priv = GARROW_ARRAY_GET_PRIVATE(object);
  /* priv->array.reset()でも同じ */
  priv->array = nullptr;
  G_OBJECT_CLASS(garrow_array_parent_class)->finalize(object);
}

まとめ

GObject Introspectionでの「一般的な」オブジェクトの寿命について説明しました。次回は本当に戻り値のオブジェクトの寿命について説明します。

タグ: Ruby
2017-01-31

OSS Gate東京ワークショップ2017-01-28を開催 #oss_gate

2017年1月28日にクラウドワークスさんOSS Gate東京ワークショップを開催しました。東京でのワークショップ開催は今回で7回目です。今回は19名の参加でした。当日無断欠席率は相変わらず50%くらいです。

集合写真

集合写真はクラウドワークスのCIOの大場さんが撮影してくれました。

以下、ワークショップ運営視点で今回のワークショップについてまとめます。

今回のチャレンジ

私たちは毎回課題の解決にチャレンジしながらワークショップを開催しています。これはワークショップはもっとよくしていくためです。課題はワークショップを開催すると毎回見つかります。

今回のチャレンジは次の1点です。

  1. プレゼン慣れしていない人でも「進行役」をできるようになる
進行役

私たちは特定のだれかがいなくてもOSS Gateワークショップを開催できるように整備を進めています。このワークショップには「進行役」という役割があるのですが、その役割をいろんな人ができるようになれば、「特定のだれかがいなくても開催できるようになる」に近づきます。

前回の開催でプレゼン慣れしている人ならほとんど事前準備なしで「進行役」をできることがわかりました。今回はプレゼン慣れしていない人でも「進行役」をできるかチャレンジしました。

今回「進行役」にチャレンジしたのはknokmki612さんです。これまでは「進行役」は1人で担当していましたが、今回は進行役経験者が「サポート進行役」としてサポートすることにしました。「進行役」で実現したいことはスムーズな進行であり、1人で進行することは大事なことではありません。そのため、「サポート進行役」がいればプレゼン慣れしていない人でも「進行役」をできるかをチャレンジしました。

実際に開催してみて次のことがわかりました。

  • 「サポート進行役」がいればプレゼン慣れしていない人でも十分「進行役」をできる。
  • 進行用スライドに大事なことが抜けていそう。(「サポート進行役」がフォローした箇所は進行用スライドに含まれているべきな気がする。)
  • 最後の「ふりかえり」の進行は「進行役」がやらなくてもよい役割かもしれない。(別の人がやってもよさそう。)

これをふまえて、3月25日開催の次回のOSS Gate東京ワークショップでは次のことにチャレンジします。

  • 「サポート進行役」がフォローした箇所と内容を記録する。
    • その記録から進行用スライドに抜けている大事なことがわかるはず。
  • 「ふりかえり」の進行は「進行役」と別の(ふりかえり慣れしている)人がやる。
    • この体制でも「今回のワークショップでわかったことと課題」を洗い出せれば、役割を分けても回ることがわかる。そうすれば「進行役」のハードルが下がってよりいろんな人ができる役割に近づく。

今回わかった課題

今回のワークショップで「進行役」以外の箇所で見つかった課題は次の通りです。

  • 時間が遅れると次の予定があって「ふりかえり」に参加せずに帰る人が増える。
  • OSS開発の全体像を知りたいビギナーのサポートが難しそう。

前者に関して少し補足します。私たちは「ふりかえり」はこのワークショップをよりよくするための大事な機会だと考えています。そのため、できるだけ参加した人には協力して欲しいのです。対策は「作業時間を短くしてでも時間が遅れないようにする」がよいかもしれません。

後者に関してはもう少しまとめます。

後者は、全体像を知るために時間を使うと、「OSS開発への参加を体験する」に使う時間が足りなくなってしまうので、そのバランスが難しいということです。

私たちは、このようなサポートが難しそうな場面になったら「『メンター』と『サポートメンター』で相談して協力してサポートしてみよう」というやり方をチャレンジしようとしていたのですが、今回のワークショップではそれができませんでした。次回以降チャレンジできるような進め方を考えた方がよさそうです。

たとえば、午後のフィードバック作業時間の最初に、「サポートメンター」はすべての「ビギナー」を回って、「ビギナー」と「メンター」と3人(「メンター」も含んでいることが重要!)でフィードバック対象を1つ決める、というステップを入れるとよいかもしれません。このタイミングで必ず「メンター」と「サポートメンター」が相談することになるからです。

進め方とは別に「OSS開発の全体像を知りたいビギナーのサポート」の対策を考えてもよいかもいしれません。このワークショップは「いろいろ考えるよりも1回やってみよう。そっちの方がいろいろわかるから。」という前提で「体験する」ことを重視しています。これまでの活動の経験からその前提があっていることは確認していますが、「最初に知識を得た方が次へ進みやすい」という人もいそうではあります。その人たちをサポートするために、このワークショップとは別に「知識を得る」を重視したなにかをOSS Gateとして取り組んでみてもよいのかもしれません。(興味がある人がいれば取り組んでみてください!)

今後のOSS Gate

2月・3月のOSS Gateの活動は以下の通りです。大阪開催は今回からです!

まとめ

1月28日に東京では7回目になるOSS Gateワークショップを開催しました。

今回は次のことにチャレンジしました。

  1. プレゼン慣れしていない人でも「進行役」できるようにする

よさそうな結果だったので今後も継続します。今回のチャレンジで新しい課題が見つかったので次回はそれらに取り組みます。

2017-01-30

Ruby on RailsでGroongaを使って日本語全文検索を実現する方法

MySQL・PostgreSQL・SQLite3の標準機能では日本語テキストの全文検索に難があります。MySQL・PostgreSQLに高速・高機能な日本語全文検索機能を追加するMroongaPGroongaというプラグインがあります。これらを導入することによりSQLで高速・高機能な日本語全文検索機能を実現できます。詳細は以下を参照してください。

また、データはMySQL・PostgreSQL・SQLite3に保存して日本語全文検索機能は別途全文検索エンジンGroongaサーバーに任せるという方法もあります。詳細は以下を参照してください。

ここではMySQL・PostgreSQL・SQLite3を一切使わずに、データもGroongaに保存して日本語全文検索を実現する方法を紹介します。

Groongaにデータも保存して使うメリットは以下の通りです。

  • Groongaのフル機能を使える
  • 検索とデータの取得を一度にできるので速い

一方、デメリットは以下の通りです。

  • MySQL・PostgreSQL・SQLite3を使うだけの場合と比べて学習コストが高い(最初にGroongaのことを覚えないといけない)
  • マスターデータを別途安全に管理する必要がある(Groongaにはトランザクション・クラッシュリカバリー機能がないため)

このデメリットのうち学習コストの方をできるだけ抑えつつGroongaを使えるようにするためのライブラリーがあります。それがgroonga-client-modelです。groonga-client-modelがGroongaを使う部分の多くをフォローしてくれるため利用者は学習コストを抑えたままGroongaを使って高速な日本語全文検索システムを実現できます。

この記事ではRuby on Railsで作ったアプリケーションからGroongaを使って日本語全文検索機能を実現する方法を説明します。実際にドキュメント検索システムを開発する手順を示すことで説明します。ここではCentOS 7を用いますが、他の環境でも同様の手順で実現できます。

Groongaのインストール

まずGroongaをインストールします。CentOS 7以外の場合にどうすればよいかはGroongaのインストールドキュメントを参照してください。

% sudo -H yum install -y http://packages.groonga.org/centos/groonga-release-1.2.0-1.noarch.rpm
% sudo -H yum install -y groonga-httpd
% sudo -H systemctl start groonga-httpd

Rubyのインストール

CentOS 7にはRuby 2.0のパッケージがありますが、Ruby on Rails 5.0.1はRuby 2.2以降が必要なのでrbenvとruby-buildでRuby 2.4をインストールします。

% sudo -H yum install -y git
% git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
% git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
% echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
% echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
% exec ${SHELL} --login
% sudo -H yum install -y gcc make patch openssl-devel readline-devel zlib-devel
% rbenv install 2.4.0
% rbenv global 2.4.0

Ruby on Railsのインストール

Ruby on Railsをインストールします。

% sudo -H yum install -y sqlite-devel nodejs
% gem install rails

ドキュメント検索システムの開発

いよいよ日本語全文検索機能を持ったドキュメント検索システムを開発します。

まずはrails newで雛形を作ります。Active Recordを一切使わないので--skip-active-recordを指定しています。

% rails new document_search --skip-active-record
% cd document_search

Gemfileにgroonga-client-model gemを追加します。

gem 'groonga-client-model'

groonga-client-model gemをインストールします。

% bundle install

検索対象のドキュメントを格納するテーブルとそれを高速に検索するためのインデックスを定義します。定義はdb/schema.grnにGroongaのコマンドの書式で書きます。参考になるドキュメントは後で示すのでまずは実際の定義を確認しましょう。

db/schema.grn:

# ドキュメントを格納するテーブル。キーなし。
table_create \
  --name documents \
  --flags TABLE_NO_KEY
# ドキュメントのタイトルを格納するカラム。
column_create \
  --table documents \
  --name title \
  --flags COLUMN_SCALAR \
  --type ShortText
# ドキュメントの内容を格納するカラム。
column_create \
  --table documents \
  --name content \
  --flags COLUMN_SCALAR \
  --type Text

# 全文検索インデックス用のテーブル。
table_create \
  --name terms \
  --flags TABLE_PAT_KEY \
  --key_type ShortText \
  --normalizer NormalizerAuto \
  --default_tokenizer TokenBigram
# ドキュメントのタイトルと内容を全文検索するためのインデックス。
# Groongaではインデックスはカラムの一種。
column_create \
  --table terms \
  --name documents_index \
  --flags COLUMN_INDEX|WITH_POSITION|WITH_SECTION \
  --type documents \
  --source title,content

以下は参考になるドキュメントです。

作成したテーブル・インデックス定義はgroonga:schema:loadタスクでGroongaに取り込めます。

% bin/rails groonga:schema:load

これでGroongaに検索対象のドキュメントを格納するテーブルができたので対応するモデルを作ります

% bin/rails generate scaffold document title:text content:text

これでDocumentクラスがapp/models/document.rbに生成されます。DocumentオブジェクトはActive RecordのようなAPIを提供するのでActive Recordと同じような感じで使えます。

動作を確認するためにQiitaから検索対象のドキュメントを取得するRakeタスクを作ります

lib/tasks/data.rake:

require "open-uri"
require "json"

namespace :data do
  namespace :load do
    desc "Load data from Qiita"
    task :qiita => :environment do
      tag = "groonga"
      url = "https://qiita.com/api/v2/items?page=1&per_page=100&query=tag:#{tag}"
      open(url) do |entries_json|
        entries = JSON.parse(entries_json.read)
        entries.each do |entry|
          Document.create(title:   entry["title"],
                          content: entry["body"])
        end
      end
    end
  end
end

実行して検索対象のドキュメントを作ります。

% bin/rails data:load:qiita

http://localhost:3000/documentsにアクセスし、データが入っていることを確認します。

Qiitaのデータをロード

ビューにヒット件数表示機能と検索フォームをつけてコントローラーで全文検索するようにします。

検索フォームではqueryというパラメーターに検索クエリーを指定することにします。

@documents@requestに変更してビューで@request.responseとしているのは、コントローラーの時点ではまだGroongaにリクエストを発行せず、ビューで必要になった時点で発行するためです。(Active Recordも同じことをやっていますが、Active Recordはto_aが必要になった時点で暗黙的に行っているのでユーザーが気にすることはありません。groonga-client-modelも同じようにすることができるのですが…長くなるので別の機会に説明します。)

@request.responseとしている理由はもう1つあります。groonga-client-modelとActive Recordで検索結果が違うからです。Active Recordはヒットしたモデルの配列を返しますが、groonga-client-modelはそれだけではなくさらに追加の情報も返します。たとえば、「ヒット数」(@request.response.n_hits)も持っています。SQLでは別途SELECT COUNT(*)を実行しないといけませんが、Groongaでは1回の検索で検索結果もヒット数も両方取得できるので効率的です。

app/views/documents/index.html.erb:


 <h1>Documents</h1>

+<p><%= @request.response.n_hits %> records</p>
+
+<%= form_tag(documents_path, method: "get") do %>
+  <%= search_field_tag "query", @query %>
+  <%= submit_tag "Search" %>
+<% end %>
+
 <table>
   <thead>
     <tr>
@@ -12,7 +19,8 @@
   </thead>

   <tbody>
-    <% @documents.each do |document| %>
+    <% @request.response.records.each do |document| %>
       <tr>
         <td><%= document.title %></td>
         <td><%= document.content %></td>

app/controllers/documents_controller.rb:

@@ -4,7 +4,11 @@ class DocumentsController < ApplicationController
   # GET /documents
   # GET /documents.json
   def index
-    @documents = Document.all
+    @query = params[:query]
+    @request = Document.select.
+      query(@query)
   end

   # GET /documents/1

この状態で次のようにレコード数とフォームが表示されるようになります。

フォームを追加

また、この状態で日本語全文検索機能を実現できています。確認してみましょう。

フォームに「オブジェクト」と日本語のクエリーを入力します。元のドキュメントは100件あり、「オブジェクト」で絞り込んで4件になっています。日本語で全文検索できていますね。

「オブジェクト」で検索

次のようにOR検索もできます。「オブジェクト」単体で検索したときの4件よりも件数が増えているのでORが効いていることがわかります。

「オブジェクト OR API」で検索

全文検索エンジンならではの機能を利用

これで基本的な全文検索機能は実現できていますが、せっかく全文検索エンジンを直接使って検索しているので全文検索エンジンならではの機能も使ってみましょう。

ドリルダウン

まずはドリルダウン機能を使います。ドリルダウンとはある軸に注目して情報を絞り込んでいくことです。例えば、商品カテゴリーに注目して商品を絞り込む(例:家電→洗濯機→ドラム式)、タグに注目して記事を絞り込むといった具合です。

まずは各ドキュメントにタグを付けられるようにしましょう。

タグ用のテーブルを作成し、ドキュメント用のテーブルからそのテーブルを参照するようにします。RDBMSと違い、Groongaは直接他のテーブルを参照する機能があります。

db/schema.grnに以下を追加します。

db/schema.grn:

# タグを格納するテーブル。正規化したタグ名がキー。
table_create \
  --name tags \
  --flags TABLE_HASH_KEY \
  --key_type ShortText \
  --normalizer NormalizerAuto
# 表示用のタグ名。たとえば、タグのキーは「rails」でラベルは「Rails」にする。
column_create \
  --table tags \
  --name label \
  --flags COLUMN_SCALAR \
  --type ShortText

# ドキュメントテーブルにタグテーブルを参照するカラムを追加。
# タグは複数設定できる。
column_create \
  --table documents \
  --name tags \
  --flags COLUMN_VECTOR \
  --type tags

# タグ検索を高速にするためのインデックスカラム。
column_create \
  --table tags \
  --name documents_tags \
  --flags COLUMN_INDEX \
  --type documents \
  --source tags

以下は参考になるドキュメントです。

更新したスキーマをロードします。

% bin/rails groonga:schema:load

タグを作ります

% bin/rails generate scaffold tag _key:string label:string

Qiitaのデータからタグ情報もロードするようにします。Tagを毎回createして大丈夫なのかと思うかもしれませんが、大丈夫です。groonga-client-modelはレコード保存にGroongaのloadコマンドを使っています。このloadコマンドの挙動はupsert(すでに同じキーのレコードがなかったら追加、あったら上書き)なのです。

lib/tasks/data.rake:

@@ -10,8 +10,12 @@ namespace :data do
       open(url) do |entries_json|
         entries = JSON.parse(entries_json.read)
         entries.each do |entry|
+          tags = entry["tags"].collect do |tag|
+            tag_name = tag["name"]
+            Tag.create(_key: tag_name, label: tag_name)
+          end
           Document.create(title:   entry["title"],
-                          content: entry["body"])
+                          content: entry["body"],
+                          tags:    tags)
         end
       end
     end

データベース内のデータを削除してQiitaのロードし直します。

% bin/rails runner 'Document.all.each(&:destroy)'
% bin/rails data:load:qiita

ビューにタグ情報も表示します。コントローラーでoutput_columnsを指定しているのは(参照先の)タグテーブルのラベルカラムも取得するためです。デフォルトではタグテーブルのキーしか取得しないので明示的に指定しています。

app/controllers/documents_controller.rb:

@@ -6,6 +6,7 @@ class DocumentsController < ApplicationController
   def index
     @query = params[:query]
     @request = Document.select.
+      output_columns(["_id", "_key", "*", "tags.label"]).
       query(@query)
   end

app/views/documents/index.html.erb:

@@ -14,6 +14,7 @@
     <tr>
       <th>Title</th>
       <th>Content</th>
+      <th>Tags</th>
       <th colspan="3"></th>
     </tr>
   </thead>
@@ -24,6 +25,13 @@
       <tr>
         <td><%= document.title %></td>
         <td><%= document.content %></td>
+        <td>
+          <ul>
+          <% document.tags.each do |tag| %>
+            <li><%= tag.label %></li>
+          <% end %>
+          </ul>
+        </td>
         <td><%= link_to 'Show', document %></td>
         <td><%= link_to 'Edit', edit_document_path(document) %></td>
         <td><%= link_to 'Destroy', document, method: :delete, data: { confirm: 'Are you sure?' } %></td>

「Tags」カラムにタグがあるのでタグがロードされていることを確認できます。

タグがロードされている

実はすでにタグで高速に検索できるようにもなっています。フォームに「tags:@全文検索」と入力すると「全文検索」タグで絞り込めます。(tags:@...は「tagsカラムの値を検索する」というGroongaの構文です。Googleのsite:...に似せた構文です。)

「全文検索」タグで検索

それではこのタグ情報を使ってドリルダウンできるようにします。

ユーザーにとっては、タグをキーボードから入力して絞り込む(ドリルダウンする)のは面倒なので、クリックでドリルダウンできるようにします

コントローラーには次の2つの処理を追加しています。

  • クエリーパラメーターとしてtagが指定されていたらfilter("tags @ %{tag}", tag: tag)でタグ検索をする条件を追加する。
  • タグでドリルダウンするための情報(どのタグ名で絞りこめるのか、また、絞り込んだらどのくらいの件数になるのか、という情報)を取得する

「タグでドリルダウンするための情報を取得する」とはSQLでいうと「GROUP BY tagの結果も取得する」という処理になります。SQLではGROUP BYの結果も取得すると追加でSQLを実行しないといけませんが、Groongaでは1回のクエリーで検索もヒット数の取得もドリルダウン用の情報も取得できるので効率的です。

app/controllers/documents_controller.rb:

@@ -5,9 +5,18 @@ class DocumentsController < ApplicationController
   # GET /documents.json
   def index
     @query = params[:query]
-    @request = Document.select.
+    @tag = params[:tag]
+
+    request = Document.select.
       output_columns(["_id", "_key", "*", "tags.label"]).
       query(@query)
+    if @tag.present?
+      request = request.filter("tags @ %{tag}", tag: @tag)
+    end
+    @request = request.
+      drilldowns("tag").keys("tags").
+      drilldowns("tag").sort_keys("-_nsubrecs").
+      drilldowns("tag").output_columns(["_key", "_nsubrecs", "label"])
   end

ビューではクリックでドリルダウンできる(タグで絞り込める)ようにリンクを表示します。

app/views/documents/index.html.erb:

@@ -5,10 +5,21 @@
 <p><%= @request.response.n_hits %> records</p>

 <%= form_tag(documents_path, method: "get") do %>
+  <%= hidden_field_tag "tag", @tag %>
   <%= search_field_tag "query", @query %>
   <%= submit_tag "Search" %>
 <% end %>

+<nav>
+  <% @request.response.drilldowns["tag"].records.each do |tag| %>
+  <%= link_to_unless @tag == tag._key,
+                     "#{tag.label} (#{tag._nsubrecs})",
+                     url_for(query: @query, tag: tag._key) %>
+  <% end %>
+  <%= link_to "タグ絞り込み解除",
+              url_for(query: @query) %>
+</nav>
+
 <table>
   <thead>
     <tr>
@@ -27,7 +38,9 @@
         <td>
           <ul>
           <% document.tags.each do |tag| %>
-            <li><%= tag.label %></li>
+            <li><%= link_to_unless @tag == tag._key,
+                                   tag.label,
+                                   url_for(query: @query, tag: tag._key) %></li>
           <% end %>
           </ul>
         </td>

これで次のような画面になります。「全文検索 (20)」というリンクがあるので、「全文検索」タグでドリルダウンすると「20件」ヒットすることがわかります。

タグでドリルダウンできる

「全文検索 (20)」のリンクをクリックすると「全文検索」タグでドリルダウンできます。たしかに20件ヒットしています。

「全文検索」タグでドリルダウン

ここからさらにキーワードで絞り込むこともできます。以下はさらに「ruby」で絞り込んだ結果です。ヒット数がさらに減って3件になっています。

「全文検索」タグでドリルダウンして「ruby」で全文検索

全文検索エンジンの機能を使うと簡単・高速にドリルダウンできるようになります。

キーワードハイライト

検索結果を確認しているとき、キーワードがどこに含まれているかがパッとわかると目的のドキュメントかどうかを判断しやすくなります。そのための機能も全文検索エンジンならではの機能です。

highlight_html()を使うとキーワードを<span class="keyword">...</span>で囲んだ結果を取得できます。

snippet_html()を使うとキーワード周辺のテキストを取得できます。

これらを使ってキーワードをハイライトするには次のようにします。

app/controllers/documents_controller.rb:

@@ -8,7 +8,14 @@ class DocumentsController < ApplicationController
     @tag = params[:tag]

     request = Document.select.
-      output_columns(["_id", "_key", "*", "tags.label"]).
+      output_columns([
+                       "_id",
+                       "_key",
+                       "*",
+                       "tags.label",
+                       "highlight_html(title)",
+                       "snippet_html(content)",
+                     ]).
       query(@query)
     if @tag.present?
       request = request.filter("tags @ %{tag}", tag: @tag)

app/views/documents/index.html.erb:

@@ -33,8 +33,16 @@
   <tbody>
     <% @request.response.records.each do |document| %>
       <tr>
-        <td><%= document.title %></td>
-        <td><%= document.content %></td>
+        <td><%= document.highlight_html.html_safe %></td>
+        <td>
+          <% if document.snippet_html.present? %>
+            <% document.snippet_html.each do |chunk| %>
+              <div>...<%= chunk.html_safe %>...</div>
+            <% end %>
+          <% else %>
+            <%= document.content %>
+          <% end %>
+        </td>
         <td>
           <ul>
           <% document.tags.each do |tag| %>

app/assets/stylesheets/documents.scss:

@@ -1,3 +1,7 @@
 // Place all the styles related to the documents controller here.
 // They will automatically be included in application.css.
 // You can use Sass (SCSS) here: http://sass-lang.com/
+
+.keyword {
+  color: red;
+}

「全文検索」タグでドリルダウンして「ruby」で全文検索した状態では次のようになります。どこにキーワードがあるかすぐにわかりますね。

「全文検索」タグでドリルダウンして「ruby」で全文検索した結果をハイライト

スコアでソート

検索結果の表示順はユーザーが求めていそうな順番にするとユーザーはうれしいです。

Groongaはスコアという数値でどれだけ検索条件にマッチしていそうかという情報を返します。スコアでソートすることでユーザーが求めていそうな順番にできます。

@@ -15,8 +15,12 @@ class DocumentsController < ApplicationController
                        "tags.label",
                        "highlight_html(title)",
                        "snippet_html(content)",
-                     ]).
-      query(@query)
+                     ])
+    if @query.present?
+      request = request.
+        query(@query).
+        sort_keys(["-_score"])
+    end
     if @tag.present?
       request = request.filter("tags @ %{tag}", tag: @tag)
     end
ページネーション

groonga-client-modelは標準でページネーション機能を提供しています。Kaminariと連携することでページネーションのUIもすぐに作れます。

Gemfile:

@@ -53,3 +53,4 @@ end
 gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

 gem 'groonga-client-model'
+gem 'kaminari'

app/controllers/documents_controller.rb:

@@ -27,7 +27,8 @@ class DocumentsController < ApplicationController
     @request = request.
       drilldowns("tag").keys("tags").
       drilldowns("tag").sort_keys("-_nsubrecs").
-      drilldowns("tag").output_columns(["_key", "_nsubrecs", "label"])
+      drilldowns("tag").output_columns(["_key", "_nsubrecs", "label"]).
+      paginate(params[:page])
   end

app/views/documents/index.html.erb:

@@ -2,7 +2,7 @@

 <h1>Documents</h1>

-<p><%= @request.response.n_hits %> records</p>
+<p><%= page_entries_info(@request.response) %></p>

 <%= form_tag(documents_path, method: "get") do %>
   <%= hidden_field_tag "tag", @tag %>
@@ -62,4 +62,6 @@

 <br>

+<%= paginate(@request.response) %>
+
 <%= link_to 'New Document', new_document_path %>

RubyGemsを追加したのでGemfile.lockを更新します。アプリケーションサーバーを再起動することも忘れないでください。

% bundle install

画面の上にはページの情報が表示されます。

ページの情報

画面の下にはページを移動するためのリンクが表示されます。

ページネーション

まとめ

MySQL・PostgreSQL・SQLite3を一切使わずにRuby on Railsアプリケーションで日本語全文検索機能を実現する方法を説明しました。データの保存も取得も検索もすべてGroongaで実現しました。単に全文検索できるようにするだけではなく、ドリルダウンやハイライトといった全文検索ならではの機能の実現方法も紹介しました。

Groongaを使いたいけど学習コストが増えそうだなぁと思っていた人は試してみてください。実際に試してみて詰まった場合や、ここには書いていないこういうことをしたいけどどうすればいいの?ということがでてきた場合は以下の場所で相談してください。

Groongaを用いた全文検索アプリケーションの開発に関するご相談は問い合わせフォームからご連絡ください。

Groonga関連の開発・サポートを仕事にしたい方は採用情報を確認の上ご応募ください。

タグ: Ruby | Groonga
2017-01-27

名古屋Ruby会議03:Apache ArrowのRubyバインディング(4) #nagoyark03

前回はなぜApache ArrowのバインディングをGObject Introspectionで作るとよさそうかについて説明しました。今回からはGObject Introspectionを使ったバインディングの作り方について説明します。実際に動くバインディングはkou/arrow-glibにあります。

基本的な作り方はGObject Introspection対応ライブラリーの作り方を参照してください。今回からはもう少し突っ込んだところを説明します。

今回はエラーの扱いについて説明します。

Apache Arrowのエラー:arrow::Status

Apache ArrowはC++で実装されていますが、エラーの通知は例外ではなくarrow::Statusというオブジェクトを返すことで実現しています。たとえば、arrow::io::FileOutputStream::Open()は次のようになっています。パスにあるファイルを開けなかったらarrow::Statusで理由を返します。

namespace arrow {
  namespace io {
    class FileOutputSTream {
      // When opening a new file, any existing file with the indicated path is
      // truncated to 0 bytes, deleting any existing memory
      static Status Open(const std::string& path, std::shared_ptr<FileOutputStream>* file);
    };
  }
}

GObject Introspectionのエラー:GError

GObject Introspectionを使ってエラーを扱うにはGErrorを使います。

まず、一般的なGErrorの使い方を説明します。

GErrorはエラー情報を表現するオブジェクトで、次の情報を保持します。

  • エラーのグループ(ドメインと呼んでいる)
  • エラーコード
  • エラーメッセージ

エラーのグループはGQuarkで表現します。これはRubyやSchemeで言えばシンボルに相当します。ようは名前が紐付いているIDです。

arrow-glib(現在開発しているApache ArrowのGObject Introspection対応ライブラリー)では次のように定義しています。

arrow-glib/error.h:

#define GARROW_ERROR garrow_error_quark()

GQuark garrow_error_quark(void);

GARROW_ERRORというマクロを用意しているのはそういう習慣(#{名前空間}_#{ドメイン名}という命名規則)だからです。直接garrow_error_quark()を呼ぶAPIとしてもよいですが、習慣に乗ったほうが使う人が使いやすくなるのでマクロを定義することをオススメします。

arrow-glib/error.cpp:

G_DEFINE_QUARK(garrow-error-quark, garrow_error)

G_DEFINE_QUARK()の呼び出しでgarrow_error_quark()を定義しています。ざっくり言うと、g_quark_from_static_string("garrow-error-quark");を実行する関数として定義してくれます。

これでGErrorに設定するエラーのグループを使えるようになりました。

次はエラーコードを用意します。具体的にはenumを用意します。enumの中身はarrow::StatusCodeに対応させています。

arrow-glib/error.h:

/**
 * GArrowError:
 * @GARROW_ERROR_OUT_OF_MEMORY: Out of memory error.
 * @GARROW_ERROR_KEY: Key error.
 * @GARROW_ERROR_TYPE: Type error.
 * @GARROW_ERROR_INVALID: Invalid value error.
 * @GARROW_ERROR_IO: IO error.
 * @GARROW_ERROR_UNKNOWN: Unknown error.
 * @GARROW_ERROR_NOT_IMPLEMENTED: The feature is not implemented.
 *
 * The error code used by all arrow-glib functions.
 */
typedef enum {
  GARROW_ERROR_OUT_OF_MEMORY = 1,
  GARROW_ERROR_KEY,
  GARROW_ERROR_TYPE,
  GARROW_ERROR_INVALID,
  GARROW_ERROR_IO,
  GARROW_ERROR_UNKNOWN = 9,
  GARROW_ERROR_NOT_IMPLEMENTED = 10
} GArrowError;

このenum定義から実行時にenumの名前・値を取得できるようにする情報を自動生成する必要があるのですが、ここでの説明は省略します。

これで以下の情報が揃ったのでGErrorを使うための事前準備は完了です。

  • エラーのグループ(ドメインと呼んでいる)
  • エラーコード

残りの以下は実際にGErrorを使うときに個別に設定します。

  • エラーメッセージ

エラーのグループとエラーコードは次のように使います。エラーメッセージはprintf()のように動的にフォーマットできることを示すためにムダに%dを使っています。

static void
fail_function(GError **error)
{
  g_set_error(error,
              GARROW_ERROR,
              GARROW_ERROR_INVALID,
              "Wrong number of argument: required %d argument",
              1);
}

g_set_error()は他にもいくつか亜種があるので必要に応じて使い分けます。

arrow-glibでの実装

実際の実装では次のようにg_set_error()をラップした関数を使っています。

void
garrow_error_set(GError **error,
                 const arrow::Status &status,
                 const char *context)
{
  if (status.ok()) {
    return;
  }

  g_set_error(error,
              GARROW_ERROR,
              garrow_error_code(status),
              "%s: %s",
              context,
              status.ToString().c_str());
}

次のように使います。

/**
 * garrow_io_file_output_stream_open:
 * @path: The path of the file output stream.
 * @append: Whether the path is opened as append mode or recreate mode.
 * @error: (nullable): Return location for a #GError or %NULL.
 *
 * Returns: (nullable) (transfer full): A newly opened
 *   #GArrayIOFileOutputStream or %NULL on error.
 */
GArrowIOFileOutputStream *
garrow_io_file_output_stream_open(const gchar *path,
                                  gboolean append,
                                  GError **error)
{
  std::shared_ptr<arrow::io::FileOutputStream> arrow_file_output_stream;
  auto status =
    arrow::io::FileOutputStream::Open(std::string(path),
                                      append,
                                      &arrow_file_output_stream);
  if (status.ok()) {
    return garrow_io_file_output_stream_new_raw(&arrow_file_output_stream);
  } else {
    std::string context("[io][file-output-stream][open]: <");
    context += path;
    context += ">";
    garrow_error_set(error, status, context.c_str());
    return NULL;
  }
}

このようにエラーをGErrorで表現しておくと、あとはバインディングレベルでいい感じにしてくれます。RubyならGErrorが設定されたら例外にします。

require "gi"

Arrow = GI.load("Arrow")
ArrowIO = GI.load("ArrowIO")

ArrowIO::FileOutputStream.open("/tmp/nonexistent/xxx", false)
# -> gobject-introspection/loader.rb:110:in `invoke': [io][file-output-stream][open]: </tmp/nonexistent/xxx>: IOError: Failed to open file: /tmp/nonexistent/xxx (Arrow::Error::Io)
#       from gobject-introspection/loader.rb:110:in `block in define_singleton_method'
#       from /tmp/a.rb:6:in `<main>'

まとめ

GObject Introspectionでのエラーの扱いについて説明しました。次回は戻り値のオブジェクトの寿命について説明します。

タグ: Ruby
2017-01-25

名古屋Ruby会議03:Apache ArrowのRubyバインディング(3) #nagoyark03

前回はなぜApache ArrowのRubyバインディングがあるとよさそうかについて説明しました。今回はなぜApache ArrowのRubyバインディングをGObject Introspectionで作るとよさそうかについて説明します。

理由は、Apache Arrowを複数のプログラミング言語から使えるようになることと、バインディングのメンテナンスをしやすくなるからです。

GObject IntrospectionはCライブラリーの各種言語バインディングを作るためのライブラリーです。それぞれの言語バインディングは自動生成するため、GObject Introspectionに対応すれば個別に各言語のバインディングを用意する必要はありません。たとえば、Ruby用のバインディングとLua用のバインディングを個別に用意する必要はありません。GObject Introspectionに対応すればどちらのバインディングも使えるようになります。

もし、SWIGを知っているのであれば、GObject Introspectionに対応することで得られる効果はSWIGを使った場合と同じと思えばだいたいあっています。

Rubyでもデータ分析をしたいのであれば、拡張ライブラリーとしてApache Arrowのバインディングを作るというやり方もあります。それぞれの違いは次の通りです。

GObject Introspectionを使うやり方:

  • Ruby以外の言語でもApache Arrowを使えるようになる。
  • Apache ArrowをGObject Introspectionに対応させる人はGObject Introspectionについて知らないといけない。(使う人は知らなくてもよい。)

拡張ライブラリーとして作るやり方:

  • RubyでだけApache Arrowを使えるようになる。
  • 拡張ライブラリーを作る人は拡張ライブラリーについて知らないといけない。(使う人はあまり知らなくてもよい。)

知らないといけない知識が変わるという違いもありますが、ポイントはRuby以外の言語でもApache Arrowを使えるようになるかという点です。

Ruby以外の言語でもApache Arrowを使えるようになると、Rubyだけでしか使えないときよりもうれしい人は多くなり、メンテナンスするメリットがある人も多くなります。そうすると1人あたりのメンテナンスコストを下げられる可能性があります。

Apache Arrowが本当に多くのデータ分析プロダクトで使われるようになると、Ruby以外の言語でもバインディングが必要とされるでしょう。そうなったときにメンテナンスコストを複数人で分配できるとうれしいです。

現状ではデータ分析をするならJava、Python、Rあたりを使います。これらの言語はユーザーが多いため、今後もこれらの言語のデータ分析まわりは活発に改良されていくでしょう。一方、他の言語はユーザーが少なくあまり改良されていかないでしょう。Java、Python、R以外の言語でも必要に応じてデータ分析に使うようになれば状況は変わるかもしれません。バインディングが用意されることによりApache Arrowがいろんな言語で使えるようになるとそうなるかもしれません。

次回からは実際にGObject Introspectionを使ってApache Arrowのバインディングを作る方法を説明します。

タグ: Ruby
2017-01-23

名古屋Ruby会議03:Apache ArrowのRubyバインディング(2) #nagoyark03

前回はApache Arrowについて説明しました。今回はなぜApache ArrowのRubyバインディングがあるとよさそうか説明します。ちなみに、「Apache ArrowのRubyバインディング」とは「Apache ArrowをRubyから使えるようにするライブラリー」のことです。「バインディング」については興味がある人はRubyKaigi 2016:How to create bindings 2016が参考になるはずです。

Apache Arrowは複数のデータ分析プロダクトで共通で使われることを目指しているデータフォーマットの仕様(とその実装)です。Apache Arrowがうまくいくと書くデータ分析プロダクトはデータを交換するときにApache Arrowを使うということです。つまり、Apache Arrowを使えればその輪の中にまぜてもらえるということです。RubyでApache Arrowを使えればRubyもデータ分析の輪の中にまぜてもらえるということです。

Apache Arrowがまだ広く使われていない現状では、Rubyがデータ分析の輪の中に入るには各データ分析プロダクトと連携する部分を個別に用意していく必要があります。これは結構大変です。

Apache Arrowが広く使われている未来では、Apache Arrowに対応するだけで済みます。個別のプロダクトに対応する必要はありません。

このような理由からApache ArrowのRubyバインディングがあるとよさそうだと考えています。

次回はApache Arrowのバインディングを作るために利用しているGObject Introspectionについて説明します。

タグ: Ruby
2017-01-20

OSS Gate東京ミートアップ2017-01-16を開催 #oss_gate

2017年1月16日に永和システムマネジメントさんOSS Gate東京ミートアップを開催しました。

ミートアップの目的

これまでは「ワークショップ」という形で開催していましたが、今年から「ミートアップ」という形での開催もはじめました。去年一年間「ワークショップ」を開催してきてわかった次の課題を解決することが「ミートアップ」開催の目的です。

  • 「ワークショップ」でOSS開発の1歩目を歩き出した人が継続して2歩目3歩目を歩いていっていない(ように見える)
  • 土曜日だと参加できない人たちがいそう(「ワークショップ」は隔月最終土曜日に開催している)

最初の課題を解決するために、ミートアップには「開発者」という参加枠を設けました。この枠は「時間と場所」を決めるので2歩目以降のOSS開発に使ってね、という位置づけです。これまで2歩目以降を歩いていなかった人たちも「時間と場所」が決まってしまえば歩くのではないかという仮説を検証するための活動です。

2つめの課題を解決するために、ミートアップは平日(月曜日)開催にしました。平日開催でこれまで参加できなかった人が参加することがわかれば「土曜日だと参加できない人たちがいそう」という仮説が本当だったということがわかります。

ミートアップでやったこと

次の2つを同じ時間・場所で実施しました。

  • 「開発者」がそれぞれ開発する
  • 「ビギナー」(OSS開発未経験の人)・「メンター」(ビギナーをサポートする人)でワークショップの前半部分だけやる

1つめは「ワークショップでOSS開発の1歩目を歩き出した人が継続して2歩目3歩目を歩いていく」ことを支援するためです。普段OSSの開発を(したいけど)できていない人たちが参加して有意義な時間を過ごしていたら成功です。元ビギナーの人が活用していればより成功です。

2つめは「土曜日だとワークショップに参加できない人たち」を支援するためです。土曜日開催のワークショップに参加できていない人たちが参加していたら成功です。前半・後半の2回にわけても土曜日にやっているのと同じくらいビギナーがOSS開発の1歩目を踏み出せていればもっと成功です。

ミートアップを開催してわかったこと

今回のミートアップの参加者は次の通りでした。

  • 開発者4名(うち、1名は元ビギナー)
  • ビギナー4名(全員初参加)
  • メンター6名(うち、初メンター3名、初進行役1名、初サポート進行役1名)

このことと参加者のアンケート結果から次のことがわかりました。

  • 2歩目以降を歩き続けるために「時間と場所」を用意するのは有効そう。
    • 「開発者」として参加した人全員が「時間と場所」が用意されているとやるという意見だった。
  • 土曜日だと参加できない人たち(平日だと参加できる人たち)はいそう。
    • 月曜日は参加しやすいとのこと。
  • 進行役の練習の場としてよさそう。
    • ワークショップ全体を一気にやるよりもまずは半分だけやった方が最初の一歩としてはよさそう。

前述の課題の解決策として今回の活動は有効であることがわかりました。さらによくするために、以下の課題があることがわかりました。

  • 「開発者」と「ビギナー」・「メンター」の接点がほとんどない。
    • 同じ時間・場所で活動したら自然になにか接点ができるかと予想していたがそうでもなかった。明示的になにか仕組みを用意した方がよさそう。
  • ワークショップを2回にわけても効果があるか確認できていない。
    • 今回は1回目なので、次回、今回参加したビギナーが再び参加するかを確認する。
    • 次回、再参加したビギナーが続きから再開できるかを確認する。

次のアクション

これを受けて次回(2月20日(月))は次のようにする予定です。

  • 開発者に関して
    • 「ワークショップ」で「時間と場所」があることを告知することで利用者は増えそうなので、2017-01-28のワークショップで忘れずに告知する。元ビギナーの参加が増えて、参加した開発者が有意義な時間を過ごしたら成功。
    • ビギナー・メンターとなにかしらいい感じの接点をもつ仕組みを試す。いい感じに接点を持てたら成功。
  • ビギナーに関して
    • 再参加するかどうかを確認する。再参加していたら成功。
    • 再参加したビギナーが続きから再開できるか。1から(動かすところから)の再開にならずにフィードバックから進められたら成功。
    • 土曜日のワークショプに参加する人がいるかを確認する。参加していたら成功。
  • メンターに関して
    • 土曜日のワークショップに参加できない人が参加しているか確認する。参加していたら成功。
  • 運営に関して
    • ミートアップ企画者がいなくても運営できるか確認する。(次回は企画者が参加できない。)次回のミートアップで今回の課題に対するアクションを実施し、その結果を確認し、次のアクションを決められれば成功。

まとめ

「ミートアップ」という活動をはじめて開催してみました。課題はいくつかありますが、以下の課題は解決できそうな感触を得ました。

  • 「ワークショップ」でOSS開発の1歩目を歩き出した人が継続して2歩目3歩目を歩いていっていない(ように見える)
  • 土曜日だと参加できない人たちがいそう(「ワークショップ」は隔月最終土曜日に開催している)

「ワークショップ」と同じように、「ミートアップ」も継続して改良してうまく↑の課題を解決できる取り組みにしていく予定です。

つづき: 2017-01-30
2017-01-18

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|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|
タグ:
RubyKaigi 2015 sponsor RubyKaigi 2015 speaker RubyKaigi 2015 committer RubyKaigi 2014 official-sponsor RubyKaigi 2014 speaker RubyKaigi 2014 committer RubyKaigi 2013 OfficialSponsor RubyKaigi 2013 Speaker RubyKaigi 2013 Committer SapporoRubyKaigi 2012 OfficialSponsor SapporoRubyKaigi 2012 Speaker RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer badge_speaker.gif RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer
SapporoRubyKaigi02Sponsor
SapporoRubyKaigi02Speaker
RubyKaigi2009Sponsor
RubyKaigi2009Speaker
RubyKaigi2008Speaker