年のはじめなので、2017年にやりたいことと2016年のまとめを書きます。
まず、今年やりたいことです。
設立当時からクリアコードが大事にしていることは次の2つです。
これらをもっと実現するために2017年は仲間を増やしていきます。仲間は、クリアコードのメンバーという形はもちろん、そうでない形でもよいです。そうでない形というのは、たとえば、OSS開発支援サービスを通じて一緒に開発をする人たちやOSS Gate(サイトがカッコよくなりました!)で一緒に活動する人たちです。
クリアコードのメンバーとして取り組みたいという方は採用情報を確認してみてください。
採用情報には古くなっている情報があるので今月中に更新します。今はクリアコードのメンバーが開発に関わっているフリーソフトウェアを一緒に開発する、となっていますが、クリアコードのメンバーが現時点ではまだ開発に関わっていないフリーソフトウェアでもよいことにする予定です。やりたいことは「一緒に開発することでお互いに一緒にクリアコードで仕事をしたい人かを確認する」ことなので、それを達成できるならクリアコードのメンバーがすでに開発に関わっていなくてもよいのです。
それでは、2016年のことを月ごとにふりかえります。
2015年10月からはじめたワークライフバランス推進に関する取り組みをまとめました。この取り組みの目的は「クリアコードのメンバー全員が働きやすい環境を用意する」ことです。その結果として「クリアコードのメンバー全員が十分に能力を発揮できる」ことを目指しています。
この取り組みで導入した在宅勤務制度を一番活用しているのは週4日在宅勤務をしている者です。育児のために在宅勤務を利用しています。時短勤務も検討していましたが、通勤時間がなくなることにより勤務時間を減らさずに育児をできています。このケースではワークライフバランスの改善と能力の発揮を両立できています。
他の者は月に数回程度の在宅勤務でワークライフバランスを改善しています。
1月30日には1回目のOSS Gateワークショップを開催しました。ビギナー(OSS開発に参加したことがない人)が4名と少なめでしたがメンター(OSS開発経験者)にとってはワークショップを経験できたことが有意義でした。
2月8日には「『nginx実践入門』出版記念!執筆者らが語る nginx Tech Talks」でgroonga-httpdの紹介をしました。「全文検索nginx」というタイトルを思いついたときは「しめた!」と思ったものでした。
次の日の2月9日(年に一度の肉の日!)には「MySQLとPostgreSQLと日本語全文検索」でMroongaとPGroongaの紹介をしました。このイベントは好評でこのあと続編を2回開催することになります。
3月16日には「第1回 法人向けFirefox導入セミナー」でFirefoxの法人利用について紹介しました。
3月26日には2回目のOSS Gateワークショップを開催しました。この回まではビギナーを集めるのが大変という状況でした。次回以降はビギナーはたくさん集まるようになり、今度はメンターを集めるのが大変という状況に変わっていきます。
Rubyist Magazine(るびま)0053号にクリアコード代表取締役の須藤へのインタビューが掲載されました。
Redmineの全文検索プラグインをフリーソフトウェアとして公開しました。Mroonga・PGroongaを活用しています。
WEB+DB PRESS Vol.86特集「1年目から身につけたい! チーム開発 6つの心得」が全文公開されました。クリエイティブ・コモンズのBY-NC-SA(表示・非営利・継承)で利用できます。営利目的に使う自由はありませんが、変更・再配布は自由です。技術評論社さんにお願いしてできるだけ自由に利用できるようにしてもらいました。
Mozilla BlogにFirefoxアドオンのWebExtensions移行についての記事を寄稿しました。
5月28日には「東京Ruby会議11」でCアプリケーションへのRubyインタープリターの組み込み方を紹介しました。これがきっかけでRuby 2.4にrb_gc_adjust_memory_usage()
が入りました。
なお、東京Ruby会議11と同日・同建物で第3回目のOSS Gateワークショップが開催されました。この回からビギナーの参加が多数になりました。この結果、このワークショップの課題は、「ビギナーをどうやって集めるか」から「多数のビギナーにどう対応するか」に変わってきました。
6月9日には「MySQLとPostgreSQLと日本語全文検索2」で初心者向けにMroongaとPGroongaを紹介しました。
OSS開発支援サービスを開始しました。去年からSpeeeさんにもサービスを提供しています。業務の中でエンジニアがOSS開発に参加することはSpeeeとして価値があるということで利用されています。Speeeさんのエンジニアの中にはこのサービスがきっかけでOSS開発に参加したエンジニアがいます。
自分の会社でもOSS開発に参加していきたいという方はお問い合わせください。
7月22日には「MariaDBコミュニティイベント in Tokyo」でMroongaを紹介しました。これがきっかけとなり、その後、以下の展開がありました。
クリアコード設立記念日である7月25日にはクリアコード10周年祝いを開催しました。会場をOSS開発支援サービスを提供しているSpeeeさんに提供してもらうなどいろいろな方々に協力してもらって開催できました。ありがとうございました。
7月30日には4回目のOSS Gateワークショップを開催しました。この回から多数のビギナーに対応するために「サポートメンター」を導入しました。
8月はTreasure Dataさんから依頼を受けて開発に参加しているFluentd関連の情報をまとめました。
以前はGroongaも未来検索ブラジルさんから依頼を受けて開発に参加していました。(今はクリアコードが自社の業務の一環として開発に参加しています。)
自社のOSSの開発に参加してほしいという方はお問い合わせください。
9月1日には「Speee Cafe Meetup #02」で「OSS開発者を増やしたい!」という話をしました。
9月8日から10日はRubyKaigi 2016にスポンサーとして参加しました。クリアコードは次の2つの発表をしました。
9月24日には札幌で1回目のOSS Gateワークショップを開催しました。初の東京以外での開催です。なお、同日に東京でも5回目のOSS Gateワークショップを開催しました。
9月29日には「MySQLとPostgreSQLと日本語全文検索3」でMroongaとPGroongaの導入方法例を紹介しました。RubyとPythonでの例になっています。
技術情報をいくつかまとめました。
11月26日には東京で6回目のOSS Gateワークショップを開催しました。なお、同日に札幌でも2回目のOSS Gateワークショップを開催しました。
12月3日にはPGConf.ASIA 2016でPGroongaを紹介しました。PostgreSQLでビジネスをしている方々と話をできたのがよかったです。
2017年にやりたいことは仲間を増やすということであると宣言しました。
2016年の活動もまとめました。
2016年はほぼ毎月発表していました。外向けの活動は今後も継続していきます。
2016年のはじめには知見の言語化を強化していきたいとしていたのですが、それはあまりできませんでした。発表内容紹介とツール紹介・機能紹介が多かったです。2017年こそは知見の言語化を強化していきたいです。
2015年の12月に改良されるまで、mrubyの例外のバックトレースは壊れていることがありました。どういうときに壊れるかというと、たとえばrescue
の中でメソッドを呼ぶと壊れました。
どうして壊れていたかと現在はどうやって壊れないようにしているかを説明します。
まず壊れていた理由を説明します。壊れていた理由は、バックトレースを取得するとき(Exception#backtrace
を呼び出すとき)に「そのときのスタックからバックトレース情報を構築していた」からです。
この方法は例外発生時のスタックの状態とバックトレース取得時のスタックの状態が同じなら問題はありません。しかし、メソッドを呼び出す、例外を保存しておいて後でバックトレースを取得する、などスタックの状態が異なるときは壊れたバックトレースになります。
なお、このような実装になっていた理由は(おそらく)パフォーマンスです。このように(バックトレース取得リクエストがあるまでバックトレース構築を遅延)せずに、例外発生時にバックトレースを構築する実装にすると、例外をあげるコストが高くなります。例外のバックトレースは必ず使われるものではなく、使われないこともあります。そのため、バックトレースを構築しないで済ませられるならしないようにすることで例外をあげるコストを低くできます。実際、CRubyも2.0からバックトレース構築を遅延させるようにして例外をあげるコストを低くしています。
それではどのようにして壊れないようにしているかを説明します。
作戦は次の通りです。
mrb_save_backtrace()
。)mrb_restore_backtrace()
。)mrb_exc_raise()
)mrb_exc_set()
)Rubyオブジェクトにするところをいかに遅延させるかがパフォーマンスに影響します。具体的には以下のタイミングまで遅延させます。
Exception#backtrace
を呼び出したとき1つめのタイミングは自明です。必要とされているタイミングだからです。
2つめのタイミングは実装の制限です。動的にメモリーを確保しないで「Rubyオブジェクトを生成しない生のバックトレース情報の取得」を実現するために、あらかじめ用意しておいた領域(mrb_state::backtrace
)に保存するようにしています。この領域が1つしかないので複数の生バックトレース情報を保存できないのです。そのため、次の例外が発生したときは前の例外の生バックトレース情報をRubyオブジェクトにして、代わりに次の例外の生バックトレース情報を保存するようにしています。
こうすることによりmrubyでは例外をあげるコストを低くしています。
mrubyの例外のバックトレースの構築を遅延させている実装がどうしてこうなっているのか(mrb_exc_set()
がどうして必要なのか)わからないという声を聞いたので、どうしてこのような実装になっているかを説明しました。
クリアコードではCRubyだけでなくmrubyに関する開発・開発支援も承っています。mrubyを使ったアプリケーションだけでなく、mruby本体の改良・修正まで対応できますので、興味のある方はお問い合わせください。
2017年2月11日に名古屋Ruby会議03が開催されます。そこで「Apache ArrowのRubyバインディングをGObject Introspectionで」という話をする予定です。
名古屋Ruby会議03とApache ArrowとGObject IntrospectionとRubyバインディングの宣伝も兼ねて関連情報をまとめていきます。
まずは、Apache Arrowについて簡単に紹介します。
Apache Arrowはインメモリーで高速にデータ分析をするためのデータフォーマットの仕様(たぶん)とその実装です。
Apache Arrowが開発されている目的は高速にデータ分析をすることです。そのために以下のことを大事にしています。
それぞれ少し補足します。
まず、データ交換コストについてです。
現在、データ分析用のプロダクトにはApache HBaseやApache SparkやPandasなどがあり、それらが協調してデータ分析をしています。協調するためには、それらのプロダクト間で分析対象のデータを交換する必要があります。現在は各プロダクトでそれぞれ別のデータフォーマットを使っているので、交換するときにはフォーマットを変換する必要があります。
この状況の課題は次の通りです。
Apache Arrowのように各プロダクトで共通で使えるデータフォーマットがあると変換は必要なくなりますし、そのデータに対する処理も同じ実装を共有できます。これがApache Arrowが解決しようとしているやり方です。Apache Arrowのサイトの「Advantages of a Common Data Layer」のところにこの状況のイメージがあるので、ピンとこない人はApache Arrowのサイトも見てみてください。
次に、複数のCPUコアの話です。
現在は1つのマシンで複数のCPUコアを利用できることは当たり前です。同時に複数のCPUコアを有効活用できれば処理速度を向上させることができ、より速くより多くのデータ分析処理を実現できます。
Apache Arrowはカラムベースのデータフォーマットを活用することによりこれを実現します。Apache Arrowのデータフォーマットを各プロダクトで共通に使えれば、この高速な処理も各プロダクトで共有できます。これがApache Arrowが実現しようとしていることです。これについてもApache Arrowのサイトの「Performance Advantage of Columnar In-Memory」のところにイメージがあるので、参照してください。
なお、Apache Arrowに賛同しているデータ分析プロダクトは現時点で13個あります。各プロダクトがApache Arrowを使う未来がきそうな気がしますね。プロダクトの詳細はApache Arrowのサイトを確認してください。
名古屋Ruby会議03で話す内容の関連情報をまとめはじめました。
今回はApache Arrowの話だけでRubyのことは全然でてきませんでした。次はなぜApache ArrowのRubyバインディングがあるとよさそうなのかについて説明します。
2017年1月16日に永和システムマネジメントさんでOSS Gate東京ミートアップを開催しました。
これまでは「ワークショップ」という形で開催していましたが、今年から「ミートアップ」という形での開催もはじめました。去年一年間「ワークショップ」を開催してきてわかった次の課題を解決することが「ミートアップ」開催の目的です。
最初の課題を解決するために、ミートアップには「開発者」という参加枠を設けました。この枠は「時間と場所」を決めるので2歩目以降のOSS開発に使ってね、という位置づけです。これまで2歩目以降を歩いていなかった人たちも「時間と場所」が決まってしまえば歩くのではないかという仮説を検証するための活動です。
2つめの課題を解決するために、ミートアップは平日(月曜日)開催にしました。平日開催でこれまで参加できなかった人が参加することがわかれば「土曜日だと参加できない人たちがいそう」という仮説が本当だったということがわかります。
次の2つを同じ時間・場所で実施しました。
1つめは「ワークショップでOSS開発の1歩目を歩き出した人が継続して2歩目3歩目を歩いていく」ことを支援するためです。普段OSSの開発を(したいけど)できていない人たちが参加して有意義な時間を過ごしていたら成功です。元ビギナーの人が活用していればより成功です。
2つめは「土曜日だとワークショップに参加できない人たち」を支援するためです。土曜日開催のワークショップに参加できていない人たちが参加していたら成功です。前半・後半の2回にわけても土曜日にやっているのと同じくらいビギナーがOSS開発の1歩目を踏み出せていればもっと成功です。
今回のミートアップの参加者は次の通りでした。
このことと参加者のアンケート結果から次のことがわかりました。
前述の課題の解決策として今回の活動は有効であることがわかりました。さらによくするために、以下の課題があることがわかりました。
これを受けて次回(2月20日(月))は次のようにする予定です。
「ミートアップ」という活動をはじめて開催してみました。課題はいくつかありますが、以下の課題は解決できそうな感触を得ました。
「ワークショップ」と同じように、「ミートアップ」も継続して改良してうまく↑の課題を解決できる取り組みにしていく予定です。
前回は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について説明します。
前回はなぜ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を使えるようになるかという点です。
Ruby以外の言語でもApache Arrowを使えるようになると、Rubyだけでしか使えないときよりもうれしい人は多くなり、メンテナンスするメリットがある人も多くなります。そうすると1人あたりのメンテナンスコストを下げられる可能性があります。
Apache Arrowが本当に多くのデータ分析プロダクトで使われるようになると、Ruby以外の言語でもバインディングが必要とされるでしょう。そうなったときにメンテナンスコストを複数人で分配できるとうれしいです。
現状ではデータ分析をするならJava、Python、Rあたりを使います。これらの言語はユーザーが多いため、今後もこれらの言語のデータ分析まわりは活発に改良されていくでしょう。一方、他の言語はユーザーが少なくあまり改良されていかないでしょう。Java、Python、R以外の言語でも必要に応じてデータ分析に使うようになれば状況は変わるかもしれません。バインディングが用意されることによりApache Arrowがいろんな言語で使えるようになるとそうなるかもしれません。
次回からは実際にGObject Introspectionを使ってApache Arrowのバインディングを作る方法を説明します。
前回はなぜApache ArrowのバインディングをGObject Introspectionで作るとよさそうかについて説明しました。今回からはGObject Introspectionを使ったバインディングの作り方について説明します。実際に動くバインディングはkou/arrow-glibにあります。
基本的な作り方はGObject Introspection対応ライブラリーの作り方を参照してください。今回からはもう少し突っ込んだところを説明します。
今回はエラーの扱いについて説明します。
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);
};
}
}
GError
GObject Introspectionを使ってエラーを扱うにはGError
を使います。
まず、一般的なGError
の使い方を説明します。
GError
はエラー情報を表現するオブジェクトで、次の情報を保持します。
エラーのグループはGQuark
で表現します。これはRubyやSchemeで言えばシンボルに相当します。ようは名前が紐付いているIDです。
arrow-glib(現在開発しているApache ArrowのGObject Introspection対応ライブラリー)では次のように定義しています。
#define GARROW_ERROR garrow_error_quark()
GQuark garrow_error_quark(void);
GARROW_ERROR
というマクロを用意しているのはそういう習慣(#{名前空間}_#{ドメイン名}
という命名規則)だからです。直接garrow_error_quark()
を呼ぶAPIとしてもよいですが、習慣に乗ったほうが使う人が使いやすくなるのでマクロを定義することをオススメします。
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
に対応させています。
/**
* 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()
は他にもいくつか亜種があるので必要に応じて使い分けます。
実際の実装では次のように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でのエラーの扱いについて説明しました。次回は戻り値のオブジェクトの寿命について説明します。
MySQL・PostgreSQL・SQLite3の標準機能では日本語テキストの全文検索に難があります。MySQL・PostgreSQLに高速・高機能な日本語全文検索機能を追加するMroonga・PGroongaというプラグインがあります。これらを導入することによりSQLで高速・高機能な日本語全文検索機能を実現できます。詳細は以下を参照してください。
また、データはMySQL・PostgreSQL・SQLite3に保存して日本語全文検索機能は別途全文検索エンジンGroongaサーバーに任せるという方法もあります。詳細は以下を参照してください。
ここではMySQL・PostgreSQL・SQLite3を一切使わずに、データもGroongaに保存して日本語全文検索を実現する方法を紹介します。
Groongaにデータも保存して使うメリットは以下の通りです。
一方、デメリットは以下の通りです。
このデメリットのうち学習コストの方をできるだけ抑えつつGroongaを使えるようにするためのライブラリーがあります。それがgroonga-client-modelです。groonga-client-modelがGroongaを使う部分の多くをフォローしてくれるため利用者は学習コストを抑えたままGroongaを使って高速な日本語全文検索システムを実現できます。
この記事ではRuby on Railsで作ったアプリケーションからGroongaを使って日本語全文検索機能を実現する方法を説明します。実際にドキュメント検索システムを開発する手順を示すことで説明します。ここではCentOS 7を用いますが、他の環境でも同様の手順で実現できます。
まず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
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をインストールします。
% 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
にアクセスし、データが入っていることを確認します。
ビューにヒット件数表示機能と検索フォームをつけてコントローラーで全文検索するようにします。
検索フォームでは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が効いていることがわかります。
これで基本的な全文検索機能は実現できていますが、せっかく全文検索エンジンを直接使って検索しているので全文検索エンジンならではの機能も使ってみましょう。
まずはドリルダウン機能を使います。ドリルダウンとはある軸に注目して情報を絞り込んでいくことです。例えば、商品カテゴリーに注目して商品を絞り込む(例:家電→洗濯機→ドラム式)、タグに注目して記事を絞り込むといった具合です。
まずは各ドキュメントにタグを付けられるようにしましょう。
タグ用のテーブルを作成し、ドキュメント用のテーブルからそのテーブルを参照するようにします。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件になっています。
全文検索エンジンの機能を使うと簡単・高速にドリルダウンできるようになります。
検索結果を確認しているとき、キーワードがどこに含まれているかがパッとわかると目的のドキュメントかどうかを判断しやすくなります。そのための機能も全文検索エンジンならではの機能です。
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」で全文検索した状態では次のようになります。どこにキーワードがあるかすぐにわかりますね。
検索結果の表示順はユーザーが求めていそうな順番にするとユーザーはうれしいです。
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関連の開発・サポートを仕事にしたい方は採用情報を確認の上ご応募ください。
2017年1月28日にクラウドワークスさんでOSS Gate東京ワークショップを開催しました。東京でのワークショップ開催は今回で7回目です。今回は19名の参加でした。当日無断欠席率は相変わらず50%くらいです。
集合写真はクラウドワークスのCIOの大場さんが撮影してくれました。
以下、ワークショップ運営視点で今回のワークショップについてまとめます。
私たちは毎回課題の解決にチャレンジしながらワークショップを開催しています。これはワークショップはもっとよくしていくためです。課題はワークショップを開催すると毎回見つかります。
今回のチャレンジは次の1点です。
私たちは特定のだれかがいなくてもOSS Gateワークショップを開催できるように整備を進めています。このワークショップには「進行役」という役割があるのですが、その役割をいろんな人ができるようになれば、「特定のだれかがいなくても開催できるようになる」に近づきます。
前回の開催でプレゼン慣れしている人ならほとんど事前準備なしで「進行役」をできることがわかりました。今回はプレゼン慣れしていない人でも「進行役」をできるかチャレンジしました。
今回「進行役」にチャレンジしたのはknokmki612さんです。これまでは「進行役」は1人で担当していましたが、今回は進行役経験者が「サポート進行役」としてサポートすることにしました。「進行役」で実現したいことはスムーズな進行であり、1人で進行することは大事なことではありません。そのため、「サポート進行役」がいればプレゼン慣れしていない人でも「進行役」をできるかをチャレンジしました。
実際に開催してみて次のことがわかりました。
これをふまえて、3月25日開催の次回のOSS Gate東京ワークショップでは次のことにチャレンジします。
今回のワークショップで「進行役」以外の箇所で見つかった課題は次の通りです。
前者に関して少し補足します。私たちは「ふりかえり」はこのワークショップをよりよくするための大事な機会だと考えています。そのため、できるだけ参加した人には協力して欲しいのです。対策は「作業時間を短くしてでも時間が遅れないようにする」がよいかもしれません。
後者に関してはもう少しまとめます。
後者は、全体像を知るために時間を使うと、「OSS開発への参加を体験する」に使う時間が足りなくなってしまうので、そのバランスが難しいということです。
私たちは、このようなサポートが難しそうな場面になったら「『メンター』と『サポートメンター』で相談して協力してサポートしてみよう」というやり方をチャレンジしようとしていたのですが、今回のワークショップではそれができませんでした。次回以降チャレンジできるような進め方を考えた方がよさそうです。
たとえば、午後のフィードバック作業時間の最初に、「サポートメンター」はすべての「ビギナー」を回って、「ビギナー」と「メンター」と3人(「メンター」も含んでいることが重要!)でフィードバック対象を1つ決める、というステップを入れるとよいかもしれません。このタイミングで必ず「メンター」と「サポートメンター」が相談することになるからです。
進め方とは別に「OSS開発の全体像を知りたいビギナーのサポート」の対策を考えてもよいかもいしれません。このワークショップは「いろいろ考えるよりも1回やってみよう。そっちの方がいろいろわかるから。」という前提で「体験する」ことを重視しています。これまでの活動の経験からその前提があっていることは確認していますが、「最初に知識を得た方が次へ進みやすい」という人もいそうではあります。その人たちをサポートするために、このワークショップとは別に「知識を得る」を重視したなにかをOSS Gateとして取り組んでみてもよいのかもしれません。(興味がある人がいれば取り組んでみてください!)
2月・3月のOSS Gateの活動は以下の通りです。大阪開催は今回からです!
1月28日に東京では7回目になるOSS Gateワークショップを開催しました。
今回は次のことにチャレンジしました。
よさそうな結果だったので今後も継続します。今回のチャレンジで新しい課題が見つかったので次回はそれらに取り組みます。
前回はGObject Introspectionでのエラーの扱いについて説明しました。今回は戻り値のオブジェクトの寿命について説明します。と、考えていたのですが、書いていたら、間違って「戻り値の」オブジェクトの寿命ではなく「一般的な」オブジェクトの寿命について説明してしまっていました。。。なので、今回は「一般的な」オブジェクトの寿命についての説明で、「戻り値の」オブジェクトの寿命は次回にします。。。実際に動くバインディングはkou/arrow-glibにあります。
std::shared_ptr
Apache Arrowはstd::shared_ptr
でオブジェクトの寿命を管理しています。バインディングは該当オブジェクトを使っているときは参照を増やして、使わなくなったら参照を減らせばよいです。
GObject
GObject Introspection対応ライブラリーはC言語で実装しますが、オブジェクト指向な機能を使えます。たとえば、カプセル化・継承・ポリモルフィズムなどの機能があります。
これらの機能はGObject IntrospectionがベースにしているGObjectというライブラリーが提供しています。GObject Introspectionでのオブジェクトの寿命の管理はこのGObjectの機能を使います。
GObjectではオブジェクトの寿命はリファレンスカウントで管理します。具体的にはGObject
を継承したクラスを作り、そのオブジェクトに対してg_object_ref()
/g_object_unref()
を使うことで寿命を管理します。
Apache ArrowのオブジェクトとGObject
のオブジェクトを適切な寿命でなじませるには次のようにします。
GObject
のクラスを1対1で対応させる。(例:arrow::Array
をGArrowArray
(GObject
ベースのクラス)に対応させる。)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()
は循環参照を解決するために利用します。)
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での「一般的な」オブジェクトの寿命について説明しました。次回は本当に戻り値のオブジェクトの寿命について説明します。