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

ククログ


Firefoxの独自ビルドの作成方法(2015年版)

概要

弊社はMozilla FirefoxをはじめとしたMozilla製品の技術サポートを提供していますが、希に、自社製品の一部としてFirefoxを顧客向けに配布したいというご相談を頂くことがあります。

結論から先に言うと、このような場合にはFirefoxをソースから独自にビルドする必要があります。 この記事では、Windows上でのFirefox 38.2.1ESRのローカライズ済み(日本語版)インストーラを作成するまでの手順を紹介します。

背景・動機

弊社ではFx Meta Installerというソフトウェアを開発・公開しており、これを使うと、「自組織向けにカスタマイズされたFirefox」と呼べるひとそろいの環境を手軽に展開することができます。 弊社ではサポートビジネスの一環として、顧客組織内での展開用にFx Meta Installerを使ったカスタマイズのご案内をしておりますが、このようなケースは「自組織内で使うための限定的な再配布」にあたるため、Mozilla公式で配布されているFirefoxのインストーラを使用しても問題はありません。

しかし、希に、弊社のサポートサービス提供先の組織内での利用ではなく、その組織(企業)が顧客向けの製品として、Firefoxに特定のアドオンや設定ファイルをバンドルした物をFx Meta Installerベースで作成して提供したいというご相談を頂く事があります。 このようなケースは「自組織外への再配布」となるため、ライセンスの都合上、そこに「Firefox」というブランドが付与された製品を同梱することはできません。

そこで登場するのが「ノーブランド版」です。 「Firefox」というブランドが付与されていない「Firefoxのソースに基づいた別製品」を配布する分には、上記のような制約はありません。 ただ、そのようなバイナリは一般公開されていないため、Firefoxのソースを使って自分で作る必要があります。

以上が、Firefoxの独自ビルド作成の動機ということになります。 (なので、正確にはこの記事の趣旨は「Firefoxのソースに基づいた別製品のビルド方法」と言うことになります。)

情報のありか

Firefoxのビルドに関する情報は、Build Instructionsから辿ることができます。 Windowsでのビルドでは、Windows build prerequisitesSimple Firefox buildを見ると必要な情報が手に入ります。 以下の手順も、基本的にはこれらのページから得られる情報に基づいています。

ビルド環境の構築

Windows上でFirefoxをビルドするために必要なソフトウェアは、すべて無料で入手できます。

Visutal StudioとWindows SDKが特にファイルサイズが大きく、ダウンロードにはそれなりに時間がかかります。

ソースの入手

Firefoxのソースコードはスナップショットのtarballとしても入手できますが、ここではMercurialのリポジトリをcloneする方法で手順を解説します。

環境構築の際に導入したMozillaBuildはMinGWのコマンドラインコンソールを含んでおり、ビルドの作業はこのコンソールから行います。 C:\mozilla-build\start-shell-msvc2013.bat をダブルクリックしてコンソールを開きましょう。

コンソールを開いたら、リポジトリをcloneするためのディレクトリを用意します。

$ mkdir /c/mozilla-source
$ cd /c/mozilla-source

次に、Firefoxのリポジトリをcloneします。

Firefoxやその他のMozilla製品のリポジトリはhttps://hg.mozilla.org/で公開されています。 Nightlyの最新版やESR版など、どのバージョンをビルドしたいかによってどのリポジトリをcloneするかが変わります。 ESRを含むリリース版のFirefoxのソースはreleases/以下にあり、ESRの場合はreleases/mozilla-esr38のようにバージョン番号を含むパスのリポジトリ、一般向けのリリース版の場合はreleases/mozilla-releaseのリポジトリです。 今回はFirefox 38.2.1ESRをビルドしたいので、releases/mozilla-esr38をcloneします。

$ hg clone https://hg.mozilla.org/releases/mozilla-esr38

リポジトリの規模が数GBと大きいので、cloneにはそれなりの時間がかかります。

Firefox本体のリポジトリをcloneできたら、次は言語リソースのリポジトリをcloneします。 言語リソースのリポジトリは言語ごとに分かれており、ESRを含むリリース版のFirefoxの日本語用言語リソースはreleases/l10n/mozilla-release/jaです。

$ hg clone https://hg.mozilla.org/releases/l10n/mozilla-release/ja ja

cloneする際に、hgコマンドの3番目の引数としてディレクトリ名を、明示的に言語コード名のjaと指定します。 これは、MozillaBuildでのビルド時には言語リソースが言語コード名に基づいて検索されるためです。

ビルドの準備

cloneしたリポジトリは、次のリリースに向けての作業が進行している状態になっています。 Firefox 38.2.1ESRのように特定のバージョンをビルドするには、タグに基づいてそのリビジョンのソースをチェックアウトする必要があります。 リポジトリに現在含まれている各リリースのタグは、以下の要領で確認できます。

$ cd /c/mozilla-source/mozilla-esr38
$ hg tags | grep -E -e "FIREFOX_.+_RELEASE" | less

Firefox 38.2.1ESRのタグはFIREFOX_38_2_1esr_RELEASEです。 Firefox本体のリポジトリと言語リソースのリポジトリでタグ名は共通なので、それぞれチェックアウトします。

$ TARGET=FIREFOX_38_2_1esr_RELEASE
$ cd /c/mozilla-source/ja
$ hg checkout $TARGET
$ cd /c/mozilla-source/mozilla-esr38
$ hg checkout $TARGET

次に、ビルドオプションを指定します。 ビルドオプションの指定はFirefox本体のリポジトリのトップレベルのディレクトリに.mozconfigという名前のテキストファイルとして保存します。 今回の例ではc/mozilla-source/mozilla-esr38/.mozconfigの位置です。

ビルドオプションは様々な物がありますが、今回は公式のFirefox 38.2.1ESRのノーブランド版という事以外の違いを持たせる必要はないため、Firefox 38.2.1ESRでabout:buildconfigを開いて確認できるビルドオプション公式のビルドで使用している設定を参考にできます。

(2015年9月28日補記) Firefoxではabout:buildconfigを開くとそのバイナリの元になったビルドオプションの一覧を見ることができますが、実際の.mozconfigには、それらに加えて環境変数の指定なども必要です。 単にビルドオプションだけを指定した状態だと、Visual Studioのランタイムライブラリがインストーラに含まれないなど、一般ユーザの環境で使用するには不都合があるビルド結果となってしまいます。

ビルド環境が64bit版のWindowsで、Visual Studio 2013 Communityを使う場合の基本的なビルド設定は、Firefox自体のリポジトリのbuild/win32/mozconfig.vs2013-win64の位置(※今回ビルドしたいのはFirefox 38.2.1ESRなので、オンラインで例を見る場合はmozilla-esr38リポジトリの物を参照して下さい)にファイルがあります。

このファイルに書かれている設定内容はMozillaで使用しているビルド環境向けの物なのですが、ここまでの手順通りに必要なソフトウェアを導入した場合、Visual Studioのインストール先パスなどが実際の物と異なっています。 ここまでの手順通りに環境を整えた場合のパスを指定するように改めた上で、Firefox 38.2.1ESRのabout:buildconfigに列挙されているオプション群から必要でない物を除外し、ローカライズに必要なオプションを足した物が、以下の例です(行頭の#はコメントアウトです)。

#---------- based on build/win32/mozconfig.vs2013-win64 -------------
_VSPATH="/C/Program Files (x86)/Microsoft Visual Studio 12.0"
export WIN32_REDIST_DIR=${_VSPATH}/VC/redist/x86/Microsoft.VC120.CRT

export MOZ_TOOLS=C:/mozilla-build/moztools

export INCLUDE=/c/Program\ Files\ \(x86\)/Windows\ Kits/8.1/include/shared:/c/Program\ Files\ \(x86\)/Windows\ Kits/8.1/include/um:/c/Program\ Files\ \(x86\)/Windows\ Kits/8.1/include/winrt:/c/Program\ Files\ \(x86\)/Windows\ Kits/8.1/include/winrt/wrl:/c/Program\ Files\ \(x86\)/Windows\ Kits/8.1/include/winrt/wrl/wrappers:${_VSPATH}/vc/include:${_VSPATH}/vc/atlmfc/include:/c/tools/sdks/dx10/include

export LIBPATH=/c/Program\ Files\ \(x86\)/Windows\ Kits/8.1/Lib/winv6.3/um/x86:${_VSPATH}/vc/lib:${_VSPATH}/vc/atlmfc/lib:/c/tools/sdks/dx10/lib
export LIB=/c/Program\ Files\ \(x86\)/Windows\ Kits/8.1/Lib/winv6.3/um/x86:${_VSPATH}/vc/lib:${_VSPATH}/vc/atlmfc/lib:/c/tools/sdks/dx10/lib

export PATH="/c/Program Files (x86)/Windows Kits/8.1/bin/x86:${_VSPATH}/Common7/IDE:${_VSPATH}/VC/BIN/amd64_x86:${_VSPATH}/VC/BIN/amd64:${_VSPATH}/Common7/Tools:${_VSPATH}/VC/VCPackages:/c/mozilla-build/moztools:${PATH}"

export WINDOWSSDKDIR="/c/Program Files (x86)/Windows Kits/8.1/"

. $topsrcdir/build/mozconfig.vs-common

mk_export_correct_style LIB
mk_export_correct_style LIBPATH
mk_export_correct_style PATH
mk_export_correct_style INCLUDE
mk_export_correct_style WIN32_REDIST_DIR

mk_add_options "export MOZ_TOOLS=$MOZ_TOOLS"
#--------------------------------------------------------------------

ac_add_options --enable-crashreporter
ac_add_options --enable-release
#ac_add_options --enable-update-channel=esr
#ac_add_options --enable-update-packaging
ac_add_options --enable-jemalloc
ac_add_options --enable-require-all-d3dc-versions
#ac_add_options --with-google-api-keyfile=/c/builds/gapi.data
#ac_add_options --with-google-oauth-api-keyfile=/c/builds/google-oauth-api.key
#ac_add_options --with-mozilla-api-keyfile=/c/builds/mozilla-desktop-geoloc-api.key
ac_add_options --enable-warnings-as-errors
#ac_add_options --enable-official-branding

mk_add_options MOZ_CO_LOCALES=ja
ac_add_options --enable-ui-locale=ja
ac_add_options --with-l10n-base=/c/mozilla-source

--with-l10n-baseで指定するパスは、言語リソースのリポジトリのパスではなく、その1つ上位のディレクトリのパスです。 MozillaBuildは、ここにMOZ_CO_LOCALESで指定した言語コード名を足したパスの /c/mozilla-source/ja に言語リソースがあることを期待します。

ビルドの実施

準備ができたら、いよいよビルドです。 Firefox本体のリポジトリにcdして、./mach buildを実行すればビルドが始まります。 ビルドにどの程度時間がかかったかを計測するために、timeコマンドを使うことをお薦めします。

$ cd /c/mozilla-source/mozilla-esr38
$ time ./mach build

ビルドに使用するマシンの性能にもよりますが、弊社で使用した環境は以下の通りで、ビルド時間はおよそ1時間でした。

  • Interl Core i5 2.5GHz
  • 8GB RAM
  • Windows 7 Ultimate SP1 64bit

ビルドが完了したら、本当に動作するか確かめてみましょう。 以下のコマンドを実行すると、ビルドされたFirefoxが起動します。 アプリケーション名は「Nightly」になっているはずです。

$ ./mach run

正しく動作することを確認できたら、インストーラを作成しましょう。 これは以下のコマンドで行えます。

$ time ./mach build installer

できあがったインストーラは、カレントディレクトリから見てobj-i686-pc-mingw32/dist/install/sea/の位置、フルパスではC:\mozilla-source\mozilla-esr38\obj-i686-pc-mingw32\dist\install\sea\の位置に出力されます。

ノーブランド化する

実は、この段階ではまだ「ノーブランド版」とは言いにくい状態です。 というのも、言語リソース内で「Firefox」と決め打ちされている箇所があるため、このままではUI上のあちこちで「Firefox」という名前を目にすることになってしまうからです。

というわけで、ソース中に残っている「Firefox」という文字列を置換してみましょう。 これはsedを使って簡単に行えます。

(なお、MinGWに同梱されているsed-iオプションを使うと、ファイルのパーミッションが意図せず変更されてしまうという問題が起こります。 MinGW 上の sed による置換で permission denied を回避するにはで解説されている手順を参考に、あらかじめC:\mozilla-sourceおよびその配下のすべてのファイルについてUsersにフルコントロールの権限を与えておきましょう。)

(2015年10月16日訂正) ノーブランド化後のアプリケーションの正式な名前を「My Custom Browser」、省略した短い名前を「Browser」にすると仮定すると、一括置換の操作は以下のように行えます。

$ cd /c/mozilla-source
$ name="My Custom Browser"
$ shortname="Browser"
$ grep -E -e "\b(Nightly|Firefox)\b" -r ja/browser ja/toolkit mozilla-esr38/browser/branding | \
    grep -v -E -e "Binary" | \
    cut -d ":" -f 1 | sort | uniq | \
    while read path; do sed -i -r -e "s/Mozilla Firefox/$name/g" \
                                  -e "s/((MOZ_APP_DISPLAYNAME|Shorter).*)(Nightly|Firefox)/\1$shortname/g" \
                                  -e "s/(Short.*)(Nightly|Firefox)/\1$name/g" \
                                  -e "s/\bFirefox\b/$shortname/g" \
                                  -e "s/\bNightly\b/$name/g" \
                          "$path"; done

短い名前には空白文字を含められない事に注意が必要です。 「Mozilla Firefox」と書かれている箇所の置換は容易ですが、「Nightly」と書かれている部分はそれが長い名前なのか短い名前なのかが場合によってまちまちなので、ここでは確実に短い名前と断定できる部分(例えばMOZ_APP_DISPLAYNAMEbrandShorterNameの値など)は短い名前にして、それ以外は長い名前にしています。

Gitではgit grepを使うことが多そうな場面ですが、Mercurialではhg grepとすると現在のリビジョンではなく履歴に対する検索となるため、リビジョンによっては検索漏れが出てしまいます。 なので、ここでは通常のgrepを使っています。

この方法は「Nightly」または「Firefox」という文字列を無差別に置換しているため、文脈的におかしな箇所が残っていることがあります。 例えば「My Custom Browser および My Custom Browser ロゴは 米国 Mozilla Foundation の米国およびその他の国における商標です。」のような記述になっているかも知れませんので、こういった例は適宜手作業で自然な内容に書き直す必要があります。

(2015年10月7日追記) その一例として、「about:rights」の内容が挙げられます。 about:rightsには、使用者自身の権利に関する情報、具体的にはプライバシーポリシーであったり、ノーブランド版のFirefoxと組み合わせて使用するWebサービスそのものの利用規約であったりが表示される事になっています。 この情報は初期状態ではプレースホルダ的な内容になっているため、自分で書き換える必要があります。

about:rightsの内容を変えるには、toolkit/content/aboutRights-unbranded.xhtmlの位置にあるファイル(Firefox 38ESRでも同じ位置にあります)を直接書き換えるか、そこで参照しているエンティティを定義している言語リソースの項目を書き換えます。 特定言語のビルドしか提供しないのであれば、aboutRights-unbranded.xhtmlだけを直接書き換えれば十分でしょう。 なお、文章に日本語などの非ASCII文字を含める場合は、ファイルの文字エンコーディングはUTF-8にする必要があります。

また、画像の差し替えも行えます。 ブランドに関する画像はFirefox本体のリポジトリのbrowser/branding/nightly以下にある物が使われるので、必要であればこれらの画像も差し替えておきます。

(2015年10月26日追記) これらの手順で変更できるのは表示上のアプリケーション名のみで、この状態のノーブランド版Firefoxを通常のFirefoxと同時に運用する際には、プロファイルを明示的に分ける必要があるなど、いくつか気をつけないといけない点があります。 そういった点を気にしなくてもよくするためには、内部的なアプリケーション名まで含めたノーブランド化が必要となります。

ソース上でのノーブランド化を終えたら、もう一度ビルドします。

$ cd /c/mozilla-source/mozilla-esr38
$ time (./mach configure &&  ./mach build && ./mach build installer)

ブランド名の一部はconfigureの段階で決定されるため、ブランド名変更後は./mach configureを手動で実行して、configureをやり直す必要があります。 configureの条件が変わるせいで、ここまででビルド済みのバイナリは大部分が使えなくなるため、ビルド時間は新規にビルドした時と同程度かかります。 前述の弊社環境では45分ほどでした。

一方、言語リソースの不自然な箇所を直しただけだったり、画像を差し替えただけだったりの場合は、configureし直す必要はありません。

$ cd /c/mozilla-source/mozilla-esr38
$ time (./mach build && ./mach build installer)

今度は変更が影響する部分だけ再ビルドされるため、全体のビルドに比べて短時間で完了します。 前述の弊社環境では7分ほどでした。

ノーブランド版のベースにするバージョンを更新する

ノーブランド版は、1回作成して提供すれば終わりという性質の物ではありません。 Firefox 38.3ESR、Firefox 38.4ESRといった要領で、その後のセキュリティアップデートにも追従していく必要があります。

セキュリティアップデートの反映は、以下の手順で行います。

  1. ノーブランド化のための変更を元に戻す。
  2. リポジトリを最新の状態に更新する。
  3. セキュリティアップデート版のタグをチェックアウトする。
  4. ノーブランド化のための変更を再反映する。
  5. ビルドし直す。

ノーブランド化のための変更をコミットしていない状態であれば、hg shelveを使えます。 ~/.hgrcを編集(作成)して以下の通り記述し、hg shelveを有効化しておいて下さい。

[extensions]
shelve=

hg shelve/hg unshelveは、git stash/git stash popによく似た機能です。 hg shelveするとコミットしていない変更が一時的に退避され、hg unshelveすると退避していた変更をもう一度反映できます。 ローカルリポジトリをリモートリポジトリの最新の状態に同期するには、hg pullする前にhg shelveで変更を取り消しておく必要があります。

$ cd /c/mozilla-source/mozilla-esr38
$ hg shelve
$ hg pull

リポジトリが最新の状態に更新されたら、新しいリリースのタグ名を調べてチェックアウトします。 新しいリリースがFirefox 38.3.0ESRであれば、恐らくタグ名はFIREFOX_38_3_0esr_RELEASEになっているはずです。 以下は、その前提でFirefox 38.3.0ESRをチェックアウトする例です。

$ TARGET=FIREFOX_38_3_0esr_RELEASE
$ cd /c/mozilla-source/mozilla-esr38
$ hg checkout $TARGET

最新リリースのソースをチェックアウトできたら、先程hg shelveで退避しておいたノーブランド化のための変更を書き戻します。

$ cd /c/mozilla-source/mozilla-esr38
$ hg unshelve

ESR版はセキュリティアップデートのみの提供なので基本的にはブランディング部分のコードに変更はありませんが、通常リリース版では新しいリソースの追加や既存リソースの削除が行われている場合がありますので、hg unshelveの結果が期待通りになっていないかもしれません。 そのような場合は改めて、ノーブランド化の作業をやり直して下さい。

Firefox本体を更新したら、同様の手順で言語リソースも更新します。

$ cd /c/mozilla-source/ja
$ hg shelve
$ hg pull
$ hg checkout $TARGET
$ hg unshelve

その後、インストーラを作り直します。

$ cd /c/mozilla-source/mozilla-esr38
$ time (./mach configure && ./mach build && ./mach build installer)

直前までできていたビルドが失敗するようになってしまったときは?

変更があった箇所だけの差分ビルドは、必要最小限の時間でビルドを完了できるため手軽ですが、変更点が期待通りに検出されなかった場合などに、configureやビルドの段階でエラーになることがあります。

そのような時は、ビルド結果が格納されているディレクトリ(obj-i686-pc-mingw32)を削除してもう一度完全なビルドをやり直すとうまくいきます。

$ cd /c/mozilla-source/mozilla-esr38
$ rm -r ./obj-i686-pc-mingw32
$ time (./mach build && ./mach build installer)

まとめ

以上、Firefox 38.2.1ESRをノーブランド化した物を独自にビルドするための手順を簡単に解説しました。

ちなみに、ここからさらにソースコードに手を加えれば、より自由度の高いカスタマイズが可能になりますが、そうすると元のFirefoxのソースからかけ離れていくため、以後のセキュリティアップデートへの追従が非常に大変な事になります。 基本的にはソースコードに対して行う変更は最小限に留めて、カスタマイズはアドオンや設定ファイルの組み合わせで行うようにすると良いでしょう。

タグ: Mozilla
2015-09-04

Visual Studioなしでmingw-w64だけでWindowsのイベントログに出力する機能を実現する方法

GNU/Linux、*BSD、OS X、Windowsなど複数のプラットフォームに対応するプログラムをC/C++で開発する場合、GCCを利用すると便利です。例に挙げたすべてのプラットフォームに対応している上に、クロスコンパイルもできるからです。特にWindows向けバイナリーのクロスコンパイルは便利です。開発環境(たとえばGNU/Linux)上でビルドできるので、環境を切り替える手間が減って開発効率があがります。

クロスコンパイル関連のことについてはここでは説明しません。必要であれば、Debian GNU/LinuxでWindows用バイナリをビルドする方法や近日中にまとめる予定のCygwinのSSHサーバーに公開鍵認証でログインする方法を参照してください。

これに該当するソフトウェアに全文検索エンジンのGroongaがあります。Groongaは全文検索サーバーとしても動作します。サーバーは長期間動作するプロセスで、状況を確認したり問題を調査するためにログを出力する機能は必須です。ログは確認しやすくないと活用されなくなってしまうため、運用しているシステムとうまく連携するのがベターです。たとえば、UNIX系のシステムなら/var/log/以下のファイルに出力したりSyslogに出力したり、Windowsならイベントログに出力したり、という具合です。

システムと連携する場合、どうしてもその環境特有のコードになってしまいます。ここでは、Windowsでイベントログにログを出力する機能を実現する方法を説明します。ただし、Visual Studioは使わずにmingw-w64だけで実現するという制限をつけることにします。理由は、そうしないとクロスコンパイルできないからです。

なお、以下Windowsのイベントログについて調べた結果を情報源(MSDN)付きで説明していますが、説明している人はWindowsに詳しくない人なので誤った理解を説明しているかもしれません。疑問に思った箇所は情報源を参照してください。

Windows Event Log APIとWindows Logging API

具体的な実現方法を説明する前にイベントログ関連のAPIを説明します。

Windowsイベントを出力するには次のどちらかのAPIを使います。

Event Logging APIはWindows Server 2003、Windows XP、Windows 2000用に開発されたAPIです。

Windows Event Log APIはWindows Server 2008、Windows Vista以降用に開発されたAPIで、Event Logging APIの上位互換になっています。

Event Logging APIはログを出力するだけならコードを書くだけで別途ツールは必要ありません。ただし、ログをキレイに表示するならMC.exe(Message Compilerの略)というツールが必要です。キレイに表示しなくてもよいならツールは必要ありません。

Windows Event Log APIは別途ツールが必要です。

Windows Event Log APIではログを出力するアプリケーションのことをプロバイダーと呼んでいます。プロバイダーには2種類あります。

  • クラシックプロバイダー(classic provider)
  • マニフェストベースのプロバイダー(manifest-based provider)

それぞれログを出力するために使うAPIも違いますし、使うツールも違います。なお、ログを出力することを「イベントを書く」と呼んでいるので、以降ではそのように書いている箇所があります。

クラシックプロバイダーでイベントを書く場合はRegisterTraceGuids()TraceEvent()というAPIとmofcomp(Managed Object Format compilerの略。Managed Object FormatはMOFと略される。)というツールが必要です。

マニフェストベースのプロバイダーでイベントを書く場合はEventRegister()EventWrite()というAPIとMC.exe(Event Logging APIでキレイにログを表示するために使うツールと同じ)というツールを使います。

今はVisual Studioを使わずにmingw-w64だけでログを出力したいので選択肢はEvent Logging APIしかありません。ただし、そのままではログはキレイに表示されません。この回避策については後で触れます。

(なお、Visual Studioと書いていますが、MC.exeはWindows Kits(Windows SDKのこと?)に含まれるもので、mofcompはWMI Administrative Toolsに含まれていそうなものなので、本当はVisual Studioではありません。)

Event Logging APIを使ったログ出力の実装方法

それでは、Event Logging APIを使ったログ出力の実装方法を説明します。ログをキレイに表示するための回避策については次のセクションで説明します。

Event Logging APIでログを出力するために使うAPIは次の3つです。

RegisterEventSource()ReportEvent()を実行するときに使うハンドルを取得して、ReportEvent()でログを出力します。ログは何回出力してもかまいません。ログを出力し終わったらDeregisterEventSource()RegisterEventSource()で取得したハンドルを解放するという流れです。

コードでいうと次のような流れです。

HANDLE event_source;

event_source = RegisterEventSource(/* ... */);
ReportEvent(event_source/*, ... */);
ReportEvent(event_source/*, ... */);
ReportEvent(event_source/*, ... */);
/* ... */
ReportEvent(event_source/*, ... */);
DeregisterEventSource(event_source);

このコードを動くようにするために最低限決めなければいけないことは次のことです。

  • イベントソース名
  • ログの種類
  • メッセージ

イベントソース名はアプリケーション名にするとよいでしょう。Groongaの場合は"Groonga"です。

ログの種類は次から選びます。

  • 成功
  • 認証失敗
  • 認証成功
  • エラー
  • 情報(知っておくといいよ、という情報。たとえば、どのポート番号で起動した、とか。)
  • 警告

多くの場合は「エラー」、「情報」、「警告」から選ぶことになるでしょう。Groongaではこの3つのどれかだけを使っています。

メッセージはログに出力するメッセージです。

次のように決めたとします。

  • イベントソース名:「"MyApp"
  • ログの種類:「エラー」
  • メッセージ:「"File not found"

このときのコードは次のようになります。ReportEvent()の引数が多いですが、ほとんどの引数は0NULLといった値を使うことができます。

#include <windows.h>

int
main(int argc, char **argv)
{
  const char *event_source_name = "MyApp";
  HANDLE event_source;
  WORD type = EVENTLOG_ERROR_TYPE;
  WORD category = 0;
  DWORD event_id = 0;
  SID *user_sid = NULL;
  WORD n_strings = 1;
  DWORD data_size = 0;
  const char *strings[] = {"File not found"};
  void *data = NULL;

  event_source = RegisterEventSource(NULL, event_source_name);
  ReportEvent(event_source, type, category, event_id, user_sid,
              n_strings, data_size,
              strings, data);
  DeregisterEventSource(event_source);

  return 0;
}

次のようにmingw-w64でクロスコンパイルします。

% x86_64-w64-mingw32-gcc -Wall -o log.exe log.c

このlog.exeをWindowsにコピーして動かすとWindowsのイベントログに出力できます。イベント ビューアーで確認すると次のようになります。

イベントビューアーで表示するとキレイに表示されない

「File not found」というメッセージの前に「ソース"MyApp"からの…」という説明が書いています。これが「キレイにログが表示されない」ということです。これの回避方法は次のセクションで説明します。

なお、WevtUtil.exeというツールを使うとコマンドラインからでも確認できます。(XMLは見やすいように整形していますが実際は1行です。)

> wevtutil.exe query-events Application /c:1 /rd:true /f:xml
<Event xmlns='http://schemas.microsoft.com/win/2004/08/events/event'>
  <System>
    <Provider Name='MyApp'/>
    <EventID Qualifiers='0'>0</EventID>
    <Level>2</Level>
    <Task>0</Task>
    <Keywords>0x80000000000000</Keywords>
    <TimeCreated SystemTime='2015-09-09T20:36:16.000000000Z'/>
    <EventRecordID>30162</EventRecordID>
    <Channel>Application</Channel>
    <Computer>gatows</Computer>
    <Security/>
  </System>
  <EventData>
    <Data>File not found</Data>
  </EventData>
</Event>

オプションの説明は省略します。「wevtutil.exe /?」で確認できるのでそちらを参照してください。

ポイントは「/f:xml」です。これはXMLで出力するという意味です。デフォルトではテキストで出力するのですが、そうするとメッセージ(今は「File not found」)を確認できません。これは「キレイにログが表示されない」と関係しているのですが、それの説明も次のセクションに回します。

ここで示したコードと引数で「イベントソース名」、「ログの種類」、「メッセージ」を出力できるコードをkou/windows-event-log-loggerに置いてあるので参考にしてください。

キレイにログを表示する方法

Windowsのイベントログはログを出力する側(たとえばアプリケーション)とログを読む側(たとえばイベントビューアー)で別になっています。ログを出力する側は必要な情報を出力します。ログを読む側は出力されたログをいい感じに表示します。いい感じとは、たとえば国際化して表示するということです。英語ユーザー向けには「File "logger.exe" doesn't exist.」と表示して、日本語ユーザー向けには「『logger.exe』というファイルがありません。」と表示するというようなことです。

イベントビューアーで見たときに「ソース"MyApp"からの…」という説明がでるのは、いい感じに表示できていないということを表しています。いい感じに表示するためには追加の情報が必要です。それがメッセージファイルです。

メッセージファイルには「このイベントのメッセージはこんな風に表示する」という情報が入っています。それが見つかるとイベントビューアーは「ソース"MyApp"からの…」という説明が消えます。

そのメッセージファイルを用意するにはMC.exeというツールを使いながらその情報が入ったDLLを作成する必要があります。

ただ、それだとクロスコンパイルできないので別の方法を2つ紹介します。

  • 他のマルチプラットフォーム対応のフリーソフトウェアが提供しているDLLを利用する。
  • Windows組み込みのDLLを利用する。
他のマルチプラットフォーム対応のフリーソフトウェアが提供しているDLLを利用

最初の方法は他のマルチプラットフォーム対応のフリーソフトウェアが提供しているメッセージファイルの情報が入ったDLLを利用する方法です。

Windowsのイベントログの作法に従うなら、アプリケーションはメッセージを組み立てるために必要な情報だけを出力し、メッセージファイルの方でそれを組み立ててメッセージにします。たとえば、アプリケーションからはファイル名だけを出力し、メッセージファイルには「『ここにファイル名を入れる』というファイルが見つかりませんでした。」というテンプレートを用意しておくということです。

しかし、その方法に従うと他のプラットフォームでうまくログを出力できません。他のプラットフォームではアプリケーションがメッセージを組み立てることが多いからです。そのため、マルチプラットフォーム対応のフリーソフトウェアはメッセージの内容をアプリケーション側で組み立てて、メッセージファイルには「『ここにそのままメッセージを入れる』」というテンプレートを用意し、すべてのメッセージをアプリケーション側で組み立てています。

たとえば、PostgreSQLもそのような実装になっていて、そのようなメッセージファイルの情報が入ったDLLを提供しています。

PostgreSQLが提供しているDLL(PostgreSQLのバイナリーのzipをダウンロードして展開した中にあるpgsql\lib\pgevent.dll)を利用する場合は次のようにします。管理者権限で実行しないと失敗するので注意してください。

> regsvr32 /n /i:MyApp pgsql\lib\pgevent.dll

イベントビューアーを見てください。「ソース"MyApp"からの…」という説明が消えています。

イベントビューアーで見るとキレイに表示される

注意点は一度登録したらDLL(この場合はpgevent.dll)の場所を変えてはいけないということです。レジストリーにこのDLLのパスが書き込まれているからです。

Windows組み込みのDLLを利用

他のマルチプラットフォーム対応のフリーソフトウェアのDLLを使えば自分のアプリケーションではDLLを作成する必要はありませんでした。しかし、いちいちDLLを登録するのは面倒です。ということで別の方法です。

別の方法も方針は前述の方法と一緒で、「『ここにそのままメッセージを入れる』」というテンプレートを利用するのですが、そのテンプレートを持ってくる方法が違います。Windowsに標準で入っているたくさんのテンプレートの中の、たまたまそのようなテンプレートになっているテンプレートを使います。

この方法を使うとメッセージ以外の情報(たとえばカテゴリーとか)がおかしなことになります。ただ、メッセージだけに興味があるのでその他は気にしないと割り切るならアリでしょう。

この方法を使っているのはApacheです。Apacheはnetmsg.dllを使っています。このDLLの中のイベントID3299のメッセージのテンプレートが「『ここにそのままメッセージを入れる1』『ここにそのままメッセージを入れる2』...『ここにそのままメッセージを入れる9』」というものなのです。

興味がある人はApacheのソースのserver/mpm/winnt/nt_eventlog.cをみてください。

まとめ

Visual Studioを使わずにmingw-w64だけでクロスコンパイルできるWindowsのイベントログ出力機能の実装方法について説明しました。

同じようなことをしようとして調べているフリーソフトウェア開発者の役に立つ情報になることを期待します。

なお、Groongaはこの間リリースされた5.0.7からWindowsのイベントログ出力機能が実装されています。WindowsでもGroongaを使ってみてください。

2015-09-10

クリアコードとフリーソフトウェアとビジネス

クリアコードの理念は、フリーソフトウェアとビジネスの両立です。

クリアコードは2015年7月1日から10期目に入っています。クリアコードが設立当初から大事にしていることの1つにフリーソフトウェア(ユーザーが自由に使えるソフトウェア)があります。ただし、それを大事にするにあたって「継続できること」という制約をつけています。言い方を変えると、「お金を稼ぐ」ということです。フリーソフトウェアを大事にしているという響きだけ聞くと、お金度外視でひたすらフリーソフトウェアを大事にしているイメージを持つかもしれません。しかし、お金を稼がないと継続できないので会社は潰れます。それでは大事にすることができなくなります。

ということで、クリアコードはどうやってフリーソフトウェアを大事にすることとお金を稼ぐことを両立させようとしているかを整理します。

お金を稼ぐ方法

クリアコードが使っているお金を稼ぐ方法は次の通りです。

  • 導入支援
  • カスタマイズ
  • サポート
  • 受託開発

それぞれ簡単に説明します。

導入支援

導入支援とはフリーソフトウェアのインストール・設定を支援することの対価としてお金をもらう方法です。

Mozilla製品(FirefoxとThunderbird)でよくある方法です。他のフリーソフトウェアの導入支援をすることもありますが、Mozilla製品が一番多いです。

この方法は大まかにいうと次のような流れになります。

  1. 依頼者が実現したいことをよく聞く
  2. 聞いた内容を理解し整理する
  3. どのようにしたら実現できるかを検討する
  4. 実現方法を提案する
  5. 導入する

導入支援の依頼者は対象ソフトウェア周辺に関する知識が不足しているためクリアコードに依頼します。そのため、クリアコードは自分たちが持っている知識を活かして、適切な実現方法を提案し、実際に依頼者が実現したい状態を達成することが大事になります。知識があっても、依頼者が実現したい状態と違う状態を達成してしまっては適切な支援ができたとは言えません。そのため、依頼者が実現したい内容を理解することは非常に重要です。

この流れの中でフリーソフトウェアを大事にする方法は次の通りです。できるだけこれらを実現するように依頼者と調整しながら進めます。

  • 調査や実際の作業で得た知見を誰でも利用できる情報として公開
  • 開発したツールをフリーソフトウェアとして公開
    • 例:↑のリンクに書いています。
  • 他の導入支援のために利用した・開発したフリーソフトウェアを利用
  • 導入支援中に見つけた問題を報告・修正

フリーソフトウェアを大事にするということは「開発に参加する・寄付する」というイメージがあるかもしれません。しかし、それら以外にも、関連情報を公開したり、ソフトウェアを広めたり、困っているユーザーを助けることなども大事にすることです。そうすることで開発者たちの作業が減ることにつながるからです。

そのため、これらはどれもフリーソフトウェアを大事にすることにつながります。

フリーソフトウェアを大事にするとき、依頼者のことを忘れてはいけません。クリアコードがフリーソフトウェアを大事にする行動をすると、依頼者にとってもよい状態になることを大事にします。そうしないと、依頼者の満足度が下がって継続して一緒に仕事をできないからです。仕事にならないとお金を稼げません。

ここに挙げた方法では次のような観点で依頼者にとってもよい状態になると説明して理解を求めます。

  • 調査や実際の作業で得た知見を誰でも利用できる情報として公開
    • 他の導入支援のときにすでに公開していた情報を更新することができ、依頼者は更新された情報を無償で利用できます。(たとえば、Firefox/Thunderbirdのバージョンアップがあった場合など。)
  • 開発したツールをフリーソフトウェアとして公開
    • 他の導入支援のときにも利用できるようになり、その際に機能追加・問題修正した場合は依頼者は無償でその恩恵を受けられます。(たとえば、Firefox/Thunderbirdのバージョンアップに追従する作業など。)
  • 他の導入支援のために利用した・開発したフリーソフトウェアを利用
    • 作業時間を短縮でき、費用を少なくできます。
  • 導入支援中に見つけた問題を報告・修正
    • クリアコードが独自で調査・修正してパッチをメンテナンスするよりも、メンテナンスコストが下がるので、長い期間でみると費用を少なくできます。(導入して終わりではないので、長い期間をみるのは妥当なことです。)
カスタマイズ

カスタマイズとはフリーソフトウェアの設定変更、本体変更、既存のプラグインの変更、プラグインの新規作成などの作業の対価としてお金をもらう方法です。他のお金を稼ぐ方法である、導入支援、サポート、受託開発の一環として実施することが多いです。

依頼者固有の内容については公開しませんが、一般的に使えそうな知識やプログラムは、依頼者に説明して理解を得た上でできるだけ公開します。このときの方針は前述の導入支援のときと同様です。つまり、「クリアコードはこういうポリシーなのでこうしていいですよね?」ではなく、「依頼者にとってもメリットがあることなのでこうしていいですか?」というところを大事にするということです。

サポート

サポートとは運用中に発生した問題の調査、開発時の技術的問題の解決支援などの対価としてお金をもらう方法です。Webで検索すると「OSSサポートサービス」を提供している会社がいくつか見つかりますが、それらと同様のお金の稼ぎ方です。

Mozilla製品は導入支援後にそのままサポートを提供するケースが多いです。受託開発でシステムを構築した場合はそのシステムのサポートを提供することがあります。

一般ユーザーから直接問い合わせる形でのサポート(ヘルプデスクと呼ぶのでしょうか)は提供しておらず、情報システム部門の方経由、SIerさん経由、既存のサポートサービスのエスカレーション先としてサポートを提供します。

サポートは基本的にリモートから提供しています。つまり、直接依頼者のところにいって調べることはせず、情報を提供してもらい、それらを使って問題を解決します。ただし、「行ったほうが早い」ときは行きます。

リモートでのサポート提供ではどういった情報を提供してもらいたいかを適切に伝えることが重要になります。そうしないと闇雲に時間が過ぎていくからです。そうなると問題が発生している期間が長くなりますし、依頼者の負担も増えます。結果として、依頼者の満足度が下がり、サポートサービスの利用を打ち切るでしょう。

リモートで調査するというのはフリーソフトウェアの開発では普通のことなので、フリーソフトウェア開発で培った知見を活かします。どういう知見かというと、どういうときにこのケースが発生する可能性があり、それを確かめるためにはどういう情報を知ればよいか、また、その情報をいかに少ない負担で取得してもらうか、というのを考えるということです。そのために、状況を整理する、ソースから問題が発生する可能性のあるケースを洗い出す技術などが必要になります。具体例としていくつか過去の記事を挙げます。

サポートのときのフリーソフトウェアを大事にする方法は導入支援と同じですが、「導入支援中に見つけた問題を報告・修正」が多くなります。多くの場合、なにか問題があって問い合わせがきています。そのため、それを開発元に報告する機会も多くなる、ということです。

受託開発

受託開発は依頼者が欲しいソフトウェアを開発する対価としてお金をもらう方法です。契約方法は次のどちらかのパターンが多いです。

  • 最初は請負契約で、お互いに信頼できることを確認できていて準委任契約の方が向いていそうな内容(たとえば、作業開始時にすべての作業内容が決まらないとき)なら準委任契約にする
  • 準委任契約が向いていそうな内容のときは最初から準委任契約にする

開発するソフトウェアは、すべてがフリーソフトウェアの場合もありますし、一部だけフリーソフトウェアの場合もありますし、すべてがプロプライエタリなソフトウェアの場合もあります。仕事を受けるときはフリーソフトウェアになる度合いが大きい仕事を優先します。

開発するソフトウェアがすべてプロプライエタリなソフトウェアの場合でも、フリーソフトウェアを活用します。たとえば、RubyでWebアプリケーションを開発する場合は、Ruby、Ruby on Rails、Sinatraなどのフリーソフトウェアを活用しますし、GNU/Linux上でマルチメディアを再生するアプリケーションを開発する場合はGStreamerを活用します。これはフリーソフトウェアを大事にするクリアコードとしての最低限譲れないラインです。

なお、単に活用するだけでなく、問題点や改良点が見つかった場合は開発元に報告する、知見を公開するなどでよりフリーソフトウェアを大事にする活動につなげようとします。

ただし、このときもクリアコードの都合だけを考えないことが大事なポイントです。依頼者にとってもメリットがある形、少なくともデメリットがない形での実現を目指します。

最低限のラインは依頼者が満足することです。たとえば、依頼者と約束した機能を約束した妥当な期間内に実現していること、依頼者に素早く適切な回答を返し続けることで良好な関係を築いていること、などが満足につながります。もし、前に汚いコードで作ってしまった・本来必要ない作業も(気づかずに)実施しているなどの理由で、機能の実現にかかる時間が「長い印象」を与えることが続けば依頼者の満足度は下がっていくでしょう。(妥当な理由を依頼者が理解できるように説明できれば「長い印象」はもたれないでしょう。)

依頼者にとってのメリットが見いだせない場合(たとえば、それをやっていると時間が無くなって欲しいものが実現できない場合)は、業務の空き時間や趣味のフリーソフトウェア開発のときに開発元に報告します。

業務の空き時間を作るためには効率よく仕事をします。空き時間ができたクリアコードのメンバーは空き時間がない他のメンバーが溜めている問題を代わりに開発元に報告します。

クリアコードのメンバーはもともと好きでフリーソフトウェアの開発をしてきた人たちです。そんな人たちが、クリアコードに入ったから趣味のフリーソフトウェア開発をやめた、となるのはクリアコードとしてはうれしくありません。好きでフリーソフトウェアの開発をしていた人が減るからです。

クリアコードがフリーソフトウェアを大事にする方法として、メンバーが趣味に使える時間を増やすという方法があります。クリアコードは基本的に残業をしない働き方にしています。また、1年ほど前から有給休暇の有効期限切れ日数が0になるような取り組みを進めていて、今ではほとんどのメンバーが有効期限が切れる前に使い切っています。業務時間の一部を好きなように使ってよいという会社もありますが、クリアコードは休みの日を増やし、メンバーがフリーソフトウェアの開発に使える時間を増やすことでフリーソフトウェアを大事にしています。

まとめ

クリアコードがどうやってフリーソフトウェアを大事にすることとお金を稼ぐことを両立させようとしているかを整理しました。

クリアコードは、依頼者のことを大事にし、これからも継続してフリーソフトウェアを大事にしていきます。

タグ: 会社
2015-09-17

PGroongaでのJSON検索の実装方法

PGroonga(ぴーじーるんが)はPostgreSQLから全文検索エンジンGroonga(ぐるんが)を使えるようにするためのPostgreSQLの拡張機能です。PGroongaを使うとPostgreSQLに格納したデータに対して高速な全文検索を実現できます。PostgreSQLは標準では日本語テキストを全文検索できません。LIKEでシーケンシャルサーチする必要があり、レコード数・テキストサイズが増えるほど性能が劣化します。PGroongaを導入することで大量の日本語テキストデータに対しても高速に全文検索できます。

さて、そんなPGroongaですが、2015年9月29日(肉の日)にリリースされた0.9.0からjsonb型をサポートしました。サポートしたというのはjsonb型のデータをインデックスを使って高速に検索できるようになったということです。

jsonb型というのはPostgreSQLでJSONを保存するための型の1つです。JSONを正規化する点が特徴で、生のJSONよりも扱いやすいです。

たとえば、JSONでは次の2つはどちらも正しいです。

{
  "a": 1,
  "a": "b"
}
{
  "a": "b"
}

RFC 7159の「4. Objects」には次のように名前は一意である「べき」としているだけだからです。

The names within an object SHOULD be unique.

その後の説明には、名前が重複している場合の挙動は実装依存だと書いてあります。

When the names within an object are not unique, the behavior of software that receives such an object is unpredictable. Many implementations report the last name/value pair only. Other implementations report an error or fail to parse the object, and some implementations report all of the name/value pairs, including duplicates.

jsonbは正規化処理の中で名前を一意にする(最後の名前と値のペアだけを使う)処理もあります。そうすると、扱うときは名前が重複しているかも!?と考えずに済むので扱いやすいのです。

この記事では、PGroongaでどうやってjsonb型をサポートしているかを説明します。まず、何ができるのかを説明し、その後、実現方法を説明します。

できること

PGroongaではjsonb型のデータに対してインデックスを使って次の検索をできます。

  • 指定したjsonb型のデータを含んでいるレコードを検索
  • 指定したキーの値に対する完全一致検索
  • 指定したキーの値に対する範囲検索(a)
  • 指定したキーの値に対する全文検索(a)(b)
  • 全部の値に対する完全一致検索
  • 全部の値に対する範囲検索(a)
  • 全部の値に対する全文検索(a)(b)

PostgreSQLが提供する機能(GINとjsonb_ops)を使ってjsonb用インデックスを作成した場合、「(a)」と書いている操作は実現できません。

JsQueryという拡張機能を使うとPostgreSQLが提供する機能を使う場合よりも高度な検索を実現できますが、それでも「(b)」と書いている操作(全文検索)は実現できません。

このように、PGroongaを使うとjsonb型のデータに対して、既存の機能よりもより高度な検索をインデックスを使って高速に実現できます。

なお、あらかじめ全文検索対象の値が入っているキーがわかっている場合は、式に対するインデックスを使うことでjsonb型のデータの値に対して全文検索できます。

参考までに、それぞれの検索がどのような検索になるか例を示します。

指定したjsonb型のデータを含んでいるレコードを検索

検索対象のJSONとして次の2つのJSONがあるとします。

{"a": "hello", "b": "world"}
{"a": "hello", "c": "world"}

クエリーが次の場合は両方ヒットします。どちらもトップのオブジェクトに「a」というキーがあり、その値が「"hello"」だからです。

{"a": "hello"}

クエリーが次の場合は「{"a": "hello", "b": "world"}」だけヒットします。もう片方はトップのオブジェクトに「b」というキーが存在しないからです。

{"b": "world"}
指定したキーの値に対する完全一致検索

この検索は「指定したjsonb型のデータを含んでいるレコードを検索」のサブセットなので省略します。

指定したキーの値に対する範囲検索(a)

検索対象のJSONとして次の2つのJSONがあるとします。

{"code": 200}
{"code": 300}

code」の値が200番台という条件で検索できるということです。「200 <= code < 300」という条件で検索できるイメージです。この場合は{"code": 200}だけがヒットします。

指定したキーの値に対する全文検索(a)(b)

検索対象のJSONとして次の2つのJSONがあるとします。

{"message": "サーバーが起動しました"}
{"message": "サーバーがダウンしました"}

message」の値に「起動」というテキストを含むかという条件で検索できるということです。この場合は{"message": "サーバーが起動しました"}だけがヒットします。

全部の値に対する完全一致検索

検索対象のJSONとして次の2つのJSONがあるとします。

{"code": 200}
{"codes": [200]}

JSONのどこかに「200」という数値があるかという条件で検索できるということです。この場合はどちらにもヒットします。「200」がどこにあるかは問わないからです。

全部の値に対する範囲検索(a)

検索対象のJSONとして次の2つのJSONがあるとします。

{"code": 200}
{"codes": [299]}

JSONのどこかに200以上300未満という数値があるかという条件で検索できるということです。この場合はどちらにもヒットします。最初のJSONには「200」があり、2つめのJSONには「299」があるからです。

全部の値に対する全文検索(a)(b)

検索対象のJSONとして次の2つのJSONがあるとします。

{"message": "サーバーが起動しました"}
{"tags": ["起動失敗"]}

JSONのどこかに「起動」というテキストを含む値があるかという条件で検索できるということです。この場合はどちらもヒットします。最初のJSONは「"起動しました"」に「起動」が含まれていて、2つめのJSONは「"起動失敗"」に「起動」が含まれているからです。

実現方法

だいぶ前置きが長くなりましたが、このような検索をインデックスを使って高速に実現するためにどのように実装しているかを説明します。

ポイントは次の2点です。

  • JSONの値をそれぞれ1つのデータとして分割する
  • 内部で複数のインデックスを組み合わせる

まず、それぞれ1つのデータとして分割するということについて説明します。

次のJSONを考えます。

{"a": "hello", "b": "world"}

このJSONには「"hello"」と「"world"」という2つの値があるので、次のように2つのデータに分割します。ABという書き方は「Aというパスの値はBだよ」ということを示したつもりです。

  • .a"hello"
  • .b"world"

Groongaにはストレージ機能があり、RDBMSのようにテーブルを作り、そこにレコードを保存することができます。PGroongaはそれぞれの値を1つのレコードとして保存します。このレコードを保存しているテーブルを「Valuesテーブル」と呼んでいます。

今回の例だと次のようなレコードを保存するということです。

id path string
1 .a "hello"
2 .b "world"

これだけだとJSONの値は見つけられてもその値を持っている(PostgreSQLの)レコードを見つけることができません。そのため、もう1つテーブルを作ります。「Sourcesテーブル」と呼んでいるもので、インデックス対象のデータを管理しています。このテーブルのレコードがインデックス対象のレコードに対応します。このSourcesテーブルの各レコードにValuesテーブルへの参照を格納します。元のJSONがどの値から成り立っていたかを示しています。

なお、Valuesテーブルへの参照にはGroongaのベクター型のカラムを使います。これは、PostgreSQLでいう配列型です。

今回の例では次のようなレコードを保存するということです。valuesカラムの中に入っている12も前述のValuesテーブルのレコードのIDです。(ctidというのはPostgreSQL特有のIDでデータの実体を識別するIDのようなものだと思ってください。)

id ctid values
100 [1,1] [1, 2]

あとは、次のような流れで検索できるようにするだけです。

  1. Valuesテーブルから検索(たとえば、「.aカラムの値が"hello"」という完全一致検索)
  2. 1.で見つかったレコードをvaluesカラムに持つレコードをSourcesテーブルから検索
  3. 2.で見つかったレコードからctidカラムの値を参照し、PostgreSQLのレコードを特定する

この、1.と2.を高速に実現するために「内部で複数のインデックスを組み合わせる」ということをしています。

まず、Valuesテーブルを考えます。

id path string
1 .a "hello"
2 .b "world"

.aカラムの値が"hello"」という完全一致検索をするためには「path == "." && string == "hello"」という条件を評価すればよいことになります。そして、これを高速に実現するためにpathstringにインデックスを用意します。

これで、「1. Valuesテーブルから検索」を高速に実現できるようになります。

次に、「2. 1.で見つかったレコードをvaluesカラムに持つレコードをSourcesテーブルから検索」を高速に実現するためのインデックスを用意します。

これは、Valuesテーブルにインデックスカラムindexを追加します。indexにはこのレコードを参照しているSourcesテーブルのレコードのIDのリストが入ります。(インデックスカラムは転置インデックスでレコードのIDのリストはポスティングリストだと言えばわかる人にはわかります。)

今の例ではどちらの値もSourcesテーブルのID100のレコードから参照されているので、どちらも[100]になります。

id path string index
1 .a "hello" [100]
2 .b "world" [100]

この情報があると、「2. 1.で見つかったレコードをvaluesカラムに持つレコードをSourcesテーブルから検索」を高速に実現できます。なぜなら、indexカラムに入っているレコードIDのリストが求めるものだからです。

今の例ではValuesテーブルの次のレコードがヒットしています。

id path string index
1 .a "hello" [100]

このレコードを含んでいるSourcesテーブルのレコードは100だけです。なぜなら、index100だけが入っているからです。

Sourcesテーブルのレコードが見つかったら「3. 2.で見つかったレコードからctidカラムの値を参照し、PostgreSQLのレコードを特定する」は簡単です。ctidカラムの値を返すだけだからです。

今の例だと[1,1]を返すだけです。

id ctid values
100 [1,1] [1, 2]

このように、次の2つを組み合わせてjsonb型の値を高速に実現できるようにしています。

  • JSONの値をそれぞれ1つのデータとして分割する
  • 内部で複数のインデックスを組み合わせる

この実現方法の利点

この実現方法の利点を説明します。

この実現方法ではJSONそのものをそのまま検索しません。JSON内のそれぞれの値を別のレコードに分割し、それぞれの値に絞って検索します。これが範囲検索や全文検索といった高度な検索を使える理由です。

たとえば、先ほどの例では次のように値を保存しています。

id path string
1 .a "hello"
2 .b "world"

JSONは構造がネストしているため、条件を指定することが困難ですが、これは表になっているので、path == ".a" && string == "hello"と条件を指定できます。演算子を変えてstring @ "hello"とすれば簡単に全文検索をすることができます。(Groongaでは@は全文検索の演算子です。)

数値ならnumber > 100 && number < 200とすれば範囲検索になります。

また、単なる表なので、いつも通りインデックスを用意できます。操作に合わせたインデックスを作ればその操作を高速に実現できるというわけです。たとえば、文字列の前方一致検索をできるインデックスを用意すれば高速に文字列を前方一致検索できます。

この「表である」ということには別の利点もあります。JSONのどこにある値か関係なく検索できるという点です。

.aにある"hello"という値」という条件は「path == ".a" && string == "hello"」となりますが、pathを条件に加えなければ「どこにあってもいいけど"hello"という値」という条件「string == "hello"」となります。

JSONで値を格納したいということは、データの構造がブレることがある場合が多いでしょう。そのとき、「どこのキーかは問わないけど○○というキーワードを含んでいるJSONを全文検索」とできたら便利ではないでしょうか?

これもpathを条件に含めずに「string @ "○○"」とすれば実現できます。

この実現方法の欠点

この実現方法の欠点はディスク使用量が多いことです。

値を分解してGroongaに保存しているので、PostgreSQLが持っているデータを2重に持っています。

また、1つのJSONには複数の値があるので、Valuesテーブルのレコード数が多くなります。これもデータが増える原因になります。

ただし、少しでもValuesテーブルのサイズを小さくする工夫はしています。違うJSONの中に、同じパスで同じ値がある場合は同じValuesレコードを共有します。多くの場合、ある程度同じ構造や同じ値がでてくると予想していて、この工夫は効果があると期待しています。

たとえば、Webサーバーのログを考えます。次のようなJSONがたくさん保存されることでしょう。

{
  "code": 200,
  "path": "/"
}
{
  "code": 200,
  "path": "/favicon.png"
}

.codeに注目してください。値がすべて200です。HTTPのステータスコードは200404500などよく使われる値に偏ります。同じパスで同じ値のときにValuesレコードを共有すると.code200という値は共有できます。このようにValuesテーブルの肥大化を防ぐ工夫をしています。

説明しなかったこと

説明を単純にするために文字列型の値だけを例にして説明しました。JSONでは文字列型以外に数値型と真偽値型があります。それらをどう扱っているかの説明は省略しました。

また、JSON内の配列をどう扱っているか、パスをより簡単に指定するための工夫も省略しました。

実はpathカラムにはもう少し違う形でパスが入っているということの説明も省略しました。

速度面についても説明していません。

使い方(SQLの書き方)についても説明していません。

まとめ

PGroongaは0.9.0でjsonb型の検索をサポートしました。そして、その実現方法を説明しました。

興味がでてきた方はドキュメントを参考にして使ってみてください。使い方(SQLの書き方)を説明しています。

わからないことがあったらGitHubのissueメーリングリストチャットで質問してください。

ある程度大きなデータで性能を測定し、それを公開してくれると大変励みになるのでよろしくおねがいします。

2015-09-30

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
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|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|