Firefox ESR52以降のバージョンでは、隠し設定のsecurity.enterprise_roots.enabled
をtrue
に設定することで、Windowsの証明書ストアに登録されたルート証明書をFirefox側で認識して使えるようになりました。
ただ、この機能の背景や使い方についてユーザー側の期待と実際の挙動との間に若干の齟齬が見られるため、ここで改めて状況を整理してみます。
この機能を有効にすると、以下の事を実現できます。
それに対し、以下のことは依然としてできません。
何故こうなっているのかは、機能の背景を知ることで理解できます。
上記の設定が導入されたBugを見ると、これは「エンタープライズでFirefoxを使いやすくする」という文脈に基づく機能だということが読み取れます。
「エンタープライズ」とは、従業員規模が千人や万人といった単位に達するような大規模な組織での使用ということです。このような規模の組織では組織内専用のルート証明書が必要になる事がままあり、それをFirefoxで使うためには証明書のインポート機能を提供するアドオンを使うか、集中管理の仕組みの実装の裏をかいて強引にインポートさせるかしかありませんでした。ところが、現在Firefoxは古い基盤技術からの脱却を進めているため、これらの裏技的なやり方は早晩使えなくなる見込みが立っています。そこで、裏技ではなくきちんとした正当な機能としてルート証明書をインポートする方法を設ける必要があった、というのがこれらのBugの背景にある事情です。
「Windowsの証明書ストア」は、実際にはそれ専用のデータベースがあるわけではありません。Windowsのレジストリ内には以下のようなレジストリキーの配下に証明書の情報が分散して格納されており、それらをマージして一覧表示した物を「証明書ストア」として見せているということになります。
HKEY_CURRENT_USER\Software\Microsoft\SystemCertificates
HKEY_LOCAL_MACHINE\Software\Microsoft\SystemCertificates
HKEY_LOCAL_MACHINE\Software\Microsoft\EnterpriseCertificates
HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\SystemCertificates
エンタープライズ運用における「ルート証明書の追加」とは、Active Directoryなどを使って、これらの中で特に以下の位置に証明書を登録する事を指しています。
HKEY_LOCAL_MACHINE\Software\Microsoft\SystemCertificates\Root\Certificates
HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\SystemCertificates\Root\Certificates
HKEY_LOCAL_MACHINE\Software\Microsoft\EnterpriseCertificates\Root\Certificates
このことから、Firefoxは前述の隠し設定が有効な場合、起動時に上記のレジストリキー配下を走査して、追加された証明書があればそれらをインポートするという設計となっています。
ただし、この機能によってインポートされた証明書は、Firefox自身の証明書ストアに永続的に保存されるわけではありません。複数のレジストリキーをマージした結果がWindowsの証明書ストアとして扱われるのと同様に、Firefoxでも「Firefox自身の元々の証明書ストアの内容」+「この機能によって認識されたWindowsの証明書ストア内の証明書」が、実際の認証処理における証明書ストアとして使われるという形となります。(そのため、不要になった証明書はWindowsの証明書ストアから削除するだけで、Firefoxからも認識されなくなります。Firefoxの証明書マネージャで証明書を削除する、という事をする必要はありません。)
この機能でインポートされた証明書はFirefoxの証明書マネージャには表示されないため、期待通りに証明書がインポートされているかどうかは、デバッグ用の詳細なログを見て判断する必要があります。
MCD用設定ファイルを使う場合には、例えば以下のようにします。
// 証明書のインポート機能を有効化する設定
lockPref('security.enterprise_roots.enabled', true);
// NSS(Firefoxのセキュリティモジュール)のログを出力するための設定
lockPref("logging.pipnss", 5);
lockPref("logging.config.LOG_FILE", "C:\\Users\\Public\\certlog.txt");
lockPref("logging.config.add_timestamp", true);
lockPref("logging.config.clear_on_startup", false);
lockPref("logging.config.sync", true);
この設定を反映した上で、実験用のダミーの証明書をHKEY_LOCAL_MACHINE\Software\Microsoft\EnterpriseCertificates\Root\Certificates
配下に加えるレジストリファイルを使って「example.com」という名前の証明書をインポートした状態でFirefoxを起動すると、C:\Users\Public\certlog.txt
に出力されたログに以下のような箇所が含まれるようになります。
...
2017-05-09 10:21:23.017000 UTC - [Main Thread]: D/pipnss certificate is trust anchor for TLS server auth
2017-05-09 10:21:23.017000 UTC - [Main Thread]: D/pipnss Imported 'example.com'
2017-05-09 10:21:23.017000 UTC - [Main Thread]: D/pipnss imported 1 roots
...
このD/pipnss Imported '(証明書の一般名)'
というログが出ていれば、その証明書はFirefoxから見えています。逆に、そのようなログが現れていない場合、証明書は無視されているということになります。
Windowsでルート証明書のファイルをダウンロードしてダブルクリックすると、その証明書を証明書ストアにインポートすることができます。しかし、この方法でインポートされた証明書は上記のFirefoxの機能では認識されません。何故でしょうか。
これは、証明書が保存されるレジストリ上の位置に理由があります。この方法で手動でインポートした証明書は、HKEY_CURRENT_USER
配下の以下の位置に保存されます。
HKEY_CURRENT_USER\Software\Microsoft\SystemCertificates
それに対し、Firefoxの証明書インポート機能はHKEY_LOCAL_MACHINE
配下の以下のキーのみを走査します。エンタープライズ向けの機能としてはそれで正解で、個々のユーザーがWindowsの証明書ストアに追加した物まで認識するのはお門違いだからです。
HKEY_LOCAL_MACHINE\Software\Microsoft\SystemCertificates\Root\Certificates
HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\SystemCertificates\Root\Certificates
HKEY_LOCAL_MACHINE\Software\Microsoft\EnterpriseCertificates\Root\Certificates
2017年6月1日現在、政府認証基盤(GPKI)のルート証明書はFirefoxの証明書ストアには含まれていません。要望は上がっているのですが、政府側の対応がMozillaの定めるルート証明書の登録基準を満たしていないために作業が滞っているという状態です。
Windowsの証明書ストアには政府認証基盤の証明書も含まれているため、「Windowsの証明書ストアのルート証明書をFirefoxでインポートできるようになったのなら、政府認証基盤の証明書もインポートされて、証明書のエラーに悩まされることもなくなるのでは?」と期待される方もいらっしゃることでしょう。
ですが残念ながら、この機能では政府認証基盤のルート証明書はインポートされません。これは、Firefoxの証明書インポート機能の処理対象があくまで「管理者によってWindowsの証明書ストアに追加された証明書」に限られているからです。
Bugzilla上のコメント等で度々言及されていますが、Mozillaはユーザーの安全を守るために、どのルート証明書を登録するかを独自に判断しており、他社の判断を鵜呑みにはしないというポリシーで証明書ストアを管理しています。FirefoxがWindowsの全ての証明書を無条件で信頼してしまっていては、このポリシーが無意味になり、ユーザーの安全やプライバシーを保護するための判断を全てMicrosoftに委ねることになってしまいます。ですので、Mozillaとしてはあくまでユーザー(エンタープライズの文脈では、組織のシステム管理者)により追加された証明書のみをインポート対象にするというのが、この機能の趣旨となっています。
なお、以上のような趣旨のため、政府認証基盤(GPKI)のルート証明書を改めて「システム管理者が追加した証明書」としてWindowsの証明書ストアに登録すれば、これはFirefoxのインポート対象となります。
以上、Firefox 52以降で使えるWindowsの証明書ストアからのルート証明書のインポート機能について、詳細と検証手順をご案内しました。
弊社で取り扱うFirefoxの導入・サポート案件においても、ルート証明書のインポートについては度々ご相談を頂いています。ニーズの高い機能でありながら今まで対応が進んでこなかったのには、アドオンや裏技的な方法で強引に解決してしまえるからという言い訳が立っていたからという側面は否定できないでしょう。従来からあるアドオンが切り捨てられるという方針の転換には批判の声も多く挙がっていますが、本体で対応される事が望ましい機能が本来あるべき形で実装されるきっかけとなったという事で、ここは素直に喜んでおきたいところです。
ちなみに、Firefox 52の段階ではこの機能はWindows限定の物となっています。macOSやLinuxでは設定を有効化しても効果を得られませんのでご注意下さい。
須藤です。関西Ruby会議2017が終わってからRubyKaigi 2017の発表を応募しました。
RubyKaigi 2017のCFPがでています。CFPとはもともと(?学会の文脈で)はCall For Papersの略ですが、RubyKaigiの文脈ではCall For Proposalsの略で、「RubyKaigiでの発表を募集しています」という意味です。RubyKaigi 2017の発表の応募は6月17日まで受け付けています。
応募する人が増えるといいなぁと思うので、実際の応募例として私のRubyKaigi 2015からRubyKaigi 2017の分の応募内容を紹介します。どうしてRubyKaigi 2015の分からかというと、CFPアプリケーションができて応募の記録が残るようになったのがRubyKaigi 2015からだからです。
実際の応募内容を紹介する前に、どのような項目を書く必要があるかを紹介します。
TitleにはRubyKaigiに参加する人が「どの発表を聞くか」をパッと選ぶときに参考になりそうなものを書きます。
AbstractにはRubyKaigiに参加する人が「どの発表を聞くか」を検討するときに参考になりそうな説明を書きます。
Detailsには発表を選考する人が「どの発表を選ぶか」を検討するときに参考になりそうな発表の説明を書きます。発表を聞いた人が何を得られる発表なのかを書くとよいでしょう。
Pitchには発表を選考する人が「どの発表を選ぶか」を検討するときに参考になりそうな発表者の説明を書きます。たとえば、「私が実装した人なので私が一番詳しいです。なので、私が話すのが一番いいんです!」ということを書きます。
これらのことは応募フォームの説明にも書いているので、応募するときには説明をちゃんと読んで応募内容がずれていないか確認するとよいでしょう。
応募の書き方については以下の情報が参考になります。
明示的に書かれている情報をインターネット上に見つけられませんでしたが、RubyKaigiは「Ruby」の「Kaigi」なので、Rubyで作られたなにかの発表よりもRubyそのものの発表の方がRubyKaigiにあっていそうです。
RubyKaigi 2015での応募例です。この応募は採択されました。
発表内容:The history of testing framework in Ruby
Title:
The history of testing framework in Ruby
Abstract:
This talk describes about the history of testing framework in Ruby.
Ruby 2.2 bundles two testing frameworks. They are minitest and test-unit. Do you know why two testing frameworks are bundled? Do you know that test-unit was bundled and then removed from Ruby distribution? If you can't answer these questions and you're interested in testing framework, this talk will help you.
This talk describes about the history of bundled testing framework in Ruby and some major testing frameworks for Ruby in chronological order.
Details:
このトークは参加者が「自分にとって適切なテスティングフレームワークを選ぶための知識」を持ち帰れることを目標とします。
テスティングフレームワークはRubyで開発する上で重要な役割を持つソフトウェアです。Rubyを使うと非常に柔軟にプログラムを書くことができるため、意図せずに既存の挙動を変えてしまうことが起こしやすいです。自動化されたテストも一緒に開発すると、できるだけ低いコストでその問題の発生を抑えつつ、楽しくプログラムを書きやすくなります。それを支援するのがテスティングフレームワークです。
テスティングフレームワークはRubyで楽しくプログラムを書くために重要な役割にも関わらず、自分にとって適切なテスティングフレームワークを選ぶという活動はあまり行われていないように感じています。多くの人が使っているから、というのも選んだ理由としては妥当な理由ではあるのですが、流行が変わったとき・テスティングフレームワークのAPIが変わったときなど、今までと状況が変わったときに適切な対応を取りづらくなります。大きな流れが1つではなくなるからです。
自分で「どうしてこのテスティングフレームワークを選んでいるのか」を理解していれば、たとえ状況が変わったときでも適切な基準で適切なフレームワークを選びなおすことができるでしょう。
このトークではRubyにバンドルされたテスティングフレームワークの歴史といくつかの主要なRuby用のテスティングフレームワーク(RSpec, minitest, test-unitと関連ソフトウェアをいくつか)の歴史について時系列で紹介します。歴史を知ることで、それぞれのテスティングフレームワークが何を重視しているのか、そして、それが自分が大事にしていることと合致するのかを判断する材料になるはずです。これにより、参加者が「自分にとって適切なテスティングフレームワークを選ぶための知識」を持ち帰ることの実現を目指します。
Rubyにバンドルされたテスティングフレームワークの歴史については↓にまとめたものをベースとします。
http://www.clear-code.com/blog/2014/11/6.html
Pitch:
最新のRubyである2.2でtest-unitがバンドルされた背景(*)を知る人は少ないでしょう。RubyKaigiでのトークとして、RubyのユーザーがRubyの開発の歴史(の一部)を知る機会があるのは妥当だと考えます。
(*) Test::Unit互換APIを提供するため。そうしないと既存のテストが動かなくなってRubyのバージョンアップの障害になる人がでてバージョンアップの障害になるかもしれない。Python 3のように新しいバージョンがでても古いバージョンを使い続けるユーザーがたくさんいる状況は開発チームとしてはうれしくないので避けたい。
私はRubyのテスティングフレームワークの歴史に最初から関わっているわけではありません。Ruby 1.8と1.9の間くらいからだけです。しかし、その頃からRubyのテスティングフレームワークまわりについては観測してきましたし、このトークで登場するtest-unit gemテスティングフレームワークの現在のメンテナーです。そのため、私はこのトークをする人として適切だと考えます。
RubyKaigi 2016での応募例です。この応募は採択されました。
発表内容:How to create bindings 2016
Title:
How to create bindings 2016
Abstract:
This talk describes how to create Ruby bindings of a C library. I want to increase Ruby bindings developers. If you're interested in using C libraries from Ruby and/or using Ruby more cases, this talk will help you.
This talk includes the following topics:
- List of methods to create Ruby bindings of a C library.
- Small example of each method.
- Pros and cons of each method.
Details:
このトークの目標は参加者が「自分のユースケースにあったRubyバインディングの作り方を知ること」です。まったく知らないという参加者でもわかるようにします。「昔は知っていたけど最近のことはわからない」という参加者でも、2016年時点での最新情報を提供することで、得られるものがあるようにします。
多くのRubyistはRubyバインディングを作る機会はありませんし、作る必要もありません。しかし、作り方を知っていればいざというときにRubyバインディングを作るという選択肢が生まれ、よりRubyを活用できる可能性が高くなります。
Cライブラリーと連携できることはRubyのよいところの1つです。Cライブラリーと連携できると、Rubyスクリプトを高速化したり、既存のCライブラリーの機能を利用したりできます。速さが足りなくてRubyを使えない、機能が足りなくてRubyを使えない、そのようなときの解決策としてRubyバインディングの作り方が役に立ちます。
このトークではRubyバインディングの作り方として次の方法を紹介します。
- RubyのC APIを使ったRubyバインディングの作り方
- SWIGを使ったRubyバインディングの作り方
- libffiを使ったRubyバインディングの作り方
- GObject Introspectionを使ったRubyバインディングの作り方
それぞれについて具体的なコードとメリット・デメリットを紹介します。この情報をもって「自分のユースケースに適切なCライブラリーのRubyバインディングの作り方を知ること」の実現を目指します。
Pitch:
多くのRubyistはRubyバインディングを作る機会はありませんし、作る必要もありません。しかし、作り方を知っていればいざというときにRubyバインディングを作るという選択肢が生まれ、よりRubyを活用できる可能性が高くなります。
Cライブラリーと連携できることはRubyのよいところの1つです。Cライブラリーと連携できると、Rubyスクリプトを高速化したり、既存のCライブラリーの機能を利用したりできます。速さが足りなくてRubyを使えない、機能が足りなくてRubyを使えない、そのようなときの解決策としてRubyバインディングの作り方が役に立ちます。
以上より、Rubyを活用する機会を増やすため、RubyKaigiでRubyバインディングを作る方法を知る機会があるのは妥当だと考えます。
私は10個以上のRubyバインディングを作った経験があり、このトークで紹介するすべての作り方を使ってきました。そのため、私はこのトークをする人として適切だと考えます。
RubyKaigi 2017での応募例です。この応募が採択されるかどうかはわかりません。
Title:
Improve extension API: C++ as better language for extension
Abstract:
This talk proposes better extension API.
The current extension API is C API. In the past, some languages such as Rust (RubyKaigi 2015), Go (Oedo RubyKaigi 05), rubex (RubyKaigi 2016) were proposed as languages to write extension.
This talks proposes C++ as a better language for writing extension. Reasons:
- C++ API can provide simpler API than C API.
- C++ API doesn't need C bindings because C++ can use C API including macro natively. Other languages such as Rust and Go need C bindings.
- Less API maintenance cost. Other approaches need more works for Ruby evolution such as introduces new syntax and new API.
Details:
このトークの目標は参加者が「現在のC APIの課題を知ること」と「このトークで提案するC++ APIがよいかどうかを判断する材料を十分に得ること」です。前者に関しては拡張ライブラリーを書いたことがない人でもわかるようにします。後者に関しては拡張ライブラリーを書いたことががない人には判断できないでしょう。よって、このトークのメインターゲットは拡張ライブラリーを書いたことがある人です。この人たちはこのトークでよりよい拡張ライブラリーのAPIについての知見を得るはずです。拡張ライブラリーを書いたことがない人たちは拡張ライブラリーのAPIそのものについての知見が増えているはずです。
このトークではC APIの課題として次のことを説明します。
- 関数のシグネチャーを2回書かないといけず、それらがズレていてもコンパイル時に発見できないため、実行時にクラッシュしてしまう可能性が高まる。
- 冗長である。たとえば、メソッドを登録するときにCの関数を定義してそれを別途登録するケース、
each
やbegin rescue
を実現するためにCの関数を定義するケース、などである。- CのデータとRubyのデータ間の変換が面倒であり、入力チェックのコードが多くなってコードの見通しが悪くなり、メンテナンス性が下がる。
- Rubyは例外が発生するとlongjumpするので、例外発生前に動的に確保したメモリーの開放処理が煩雑になる。
それぞれについて具体的なコードを紹介します。たとえば、関数のシグネチャーを2回書かないといけないとは次のようなコードのことです。
static VALUE rb_sample_hello(VALUE self) { return rb_str_new_static("Hello"); } void Init_sample(void) { VALUE sample = rb_define_class("Sample", rb_cObject); rb_define_method(sample, "hello", rb_sample_hello, 0); }
このコードでは
hello
メソッドの引数の数が0であることを示すために、rb_sample_hello(VALUE self)
と関数を定義し、それをRubyに伝えるためにrb_define_method(..., 0)
を実行しています。引数の数が増えた時は両方更新する必要があります。片方の更新を忘れるとクラッシュします。このような情報をもって「現在のC APIの課題を知ること」の実現を目指します。
それぞれの課題について、課題を解決するC++ APIを提案します。APIだけでなくどうして解決できるのか、および、実装も説明します。このAPIとその解説および実装の説明により「このトークで提案するC++ APIがよいかどうかを判断する材料を十分に得ること」の実現を目指します。
たとえば、関数のシグネチャーを2回書かないといけないケースについては、次のようなC++ APIを提案します。
void Init_sample(void) { rb::Class("Sample", rb_cObject). define_method("hello", [](VALUE self) {return rb_str_new_static("Hello");}); }
このAPIではC++のラムダ式
[](VALUE self)
から引数が0であることがわかるため、その情報を自動的にRubyにも伝えます。これにより、シグネチャーの不整合によるクラッシュを防ぐことができます。[](const char *self)
のようにシグネチャーが不正な時はコンパイル時にエラーにすることもできます。このような情報をもって「このトークで提案するC++ APIがよいかどうかを判断する材料を十分に得ること」の実現を目指します。
また、RustやGoやrubexなど他のアプローチについても紹介し、それらのアプローチの課題も紹介します。
たとえば、RustやGoを使うアプローチではC APIのマクロの扱いに課題があります。これらの言語ではCのマクロを扱えないため、
RSTRING_LEN()
のようなC APIを使うためにはそれぞれの言語で同様の処理を実現する必要があります。rubexのアプローチではrubexという独自の言語の文法を覚える必要があるという学習コスト面と、rubex自体がRuby本体の進化(たとえば文法の追加)に追従しなければいけないというメンテナンス面の課題があります。このような情報も「このトークで提案するC++ APIがよいかどうかを判断する材料を十分に得ること」の実現で役立つはずです。
もしかしたら、参考情報としてRubyのC++ APIを提供するRiceも紹介します。RiceはBoost.PythonのようなAPIを提供します。単なる拡張ライブラリーではなくバインディングを書くときにより便利です。「このトークで提案するC++ APIがよいかどうかを判断する材料を十分に得ること」の実現に役立つ情報なはずです。
Pitch:
Rubyは3.0で高速化を目指しています。Rubyが高速になることで、より速度を期待するユーザーが増えると予想します。Rubyレベルの高速化で満足できるユーザーも多いでしょうが、さらなる高速化を期待するユーザーも増えるはずです。
そうなったとき、より簡単に拡張ライブラリーを書けるようにすることでより簡単にRubyスクリプトを高速に実行できるようになります。そうなることで、そのようなユーザーも楽しくRubyを使えます。楽しくプログラムを書ける人が増えることはRubyのポリシーとあっています。
このトークで紹介する既存のアプローチは私のアイディアではありませんが、提案するC++ APIの部分は私のアイディアです。
私は10個以上の拡張ライブラリーを作った経験があり、C APIのよいところも課題も知っています。そのため、私はこのトークをする人として適切だと考えます。
RubyKaigiの発表の応募例として私のここ3年の応募内容を紹介しました。RubyKaigi 2017の発表の応募は6月17日まで受け付けています。ぜひ応募してみてください。
須藤です。このあいだ試したらApache Arrowのリポジトリーにpushできたので私のコミット権関連の設定は完了しているようです。
東京近郊に住んでいる人向け情報:この大阪で開催したイベントと同じような内容のイベントを6月13日(明日!)に東京でも開催します!大阪だったので参加できなかったという方は、ぜひ東京でのイベントに参加してください。
2週間ほど前になりますが、2017年5月28日に大阪でApache Arrowの勉強会を開催しました。前日に関西Ruby会議2017で発表していたのですが、せっかく大阪に来たのでApache Arrowの普及活動をしようと思い、開催しました。会場はクラスメソッドさんに貸してもらいました。日曜日なのに会社を開けてもらってありがとうございます!
なお、クラスメソッドさんに会場提供をお願いしたのはOSS Gate大阪ワークショップでつながりがあったからです。思わぬところで意外なつながりが活きていて、ありがたいことです。
当日使った資料は次の通りです。中盤までは既存の情報を紹介したので、最初の方のページはそれらの情報へのリンク集になっています。終盤は今回用にまとめた情報を紹介したので、それらの情報がこのスライドのメインの内容になります。イベントの内容はクラスメソッドさんがまとめてくれたイベントレポートを参照してください。
関連リンク:
大阪で開催したApache Arrow勉強会を紹介しました。なんと、6月13日(明日!)に同様のイベントが東京でも開催されます!
会場提供はSpeeeさんです。SpeeeさんとはOSS開発支援やDataScience.rbなどでつながりがあり、会場を提供してもらえました。ありがとうございます!
Fluentdのプラグインの開発をする上で避けて通れないのが設定です。ユーザーに設定ファイルを書いてもらってプラグインの動作をカスタマイズできるようにすることは必須です。
config_param
とconfig_section
という2つのAPIを使いこなすとさまざまな設定を簡単にきれいに書くことができます。
config_param
は設定を定義するために使います。
次のように名前と型を指定するのが基本形です。
desc "description for name"
config_param :name, :string
この場合は:name
という名前の文字列型の設定を定義しています。型によって、使えるオプションが異なるので型ごとに説明します。なお、config_param
の直前にdesc
を書くと設定の説明を書くことができます。説明を書いておくとfluent-plugin-config-formatコマンドで説明付きで設定の定義をダンプすることができるようになります。
type
として以下の9つの型を指定可能です。
:string
:integer
:float
:size
:time
:bool
:enum
:array
:hash
それぞれの型に変換して、指定された名前のインスタンス変数に値をセットします。
config_param :name, :string, default: "", secret: true, alias: :full_name
default
: デフォルト値を指定します。省略するとこの設定は必須になり、Fluentd起動時に設定されているかどうかチェックされます。secret
: 真を指定すると、Fluentd起動時のログなどで値がマスクされます。alias
: この設定の別名を指定します。バイト単位のサイズを設定します。k,m,g,tというサフィックスを利用可能です。それぞれ、キロ、メガ、ギガ、テラを表します。大文字、小文字は区別しません。 サイズは以下のように整数に変換されます。
limit 10 # 10 byte
limit 10k # 10240 byte
limit 10m # 10485760 byte
limit 10g # 10737418240 byte
limit 10t # 10995116277760 byte
使えるオプションはstring等と同じです。
時間の長さを指定します。s,m,h,dというサフィックスを利用可能です。それぞれ、秒、分、時、日を表します。小文字のみ有効です。
単位を省略するとto_f
した値を使用します。秒に変換されます。
interval 0.5 # 0.5秒
interval 1s # 1秒
interval 1m # 1分 = 60秒
interval 1h # 1時間 = 3600秒
interval 1d # 1日 = 86400秒
使えるオプションはstring等と同じです。
列挙型です。ユーザーに特定の値のリスト内から設定値を選ばせたいときに使います。ユーザーがリスト内に存在しない値を設定した場合、Fluentd起動時にエラーが発生します。
config_param :backend_library, :enum, list: [:geoip, :geoip2_c, :geoip2_compat], default: :geoip
default
: デフォルト値を指定します。配列です。
config_param :users, :array, default: [], value_type: :string
このように書くと、以下のような設定で@users = ["user1", "user2", "user3"]
と設定されます。
users user1,user2,user3
default
: デフォルト値を指定します。value_type
: 値の型を指定します。:string, :integer, :float, :size, :bool, :time
のいずれかを指定できます。ハッシュです。設定ファイルの書き方は2通りあります。
config_param :key_values, :hash, default: {}, symbolize_keys: true, value_type: :string
設定例:
key_values {"key1": "value1", "key2": "value2"} # JSONで書く
key_values key1:value1,key2:value2
どちらも以下のようにパースされます。
{ key1: "value1", key2: "value2" }
default
: デフォルト値をハッシュリテラルで指定します。symbolize_keys
: 真を指定するとキーをシンボル化します。value_type
: 値の型を指定します。全ての値で同じ型を使用します。config_section
はconfig_param
をグループ化するために使用します。
具体例としては、組み込みのバッファーの設定、パーサーの設定などがあります。
例えば、fluent-plugin-s3では以下のように認証情報の設定、バッファーの設定、フォーマッターの設定をグループ化して記述することができます。fluent-plugin-s3では認証方法が複数あるので、認証方法ごとにセクションを作ることによって、利用している認証方法がわかりやすくなっています。このように設定をグループ化することによって設定ファイルの可読性が向上しています。
<match *.log>
@type s3
<assume_role_credentials>
role_arn xxxxx
role_session_name xxxxx
</assume_role_crecentials>
<buffer tag,time>
@type file
path /var/log/fluent/s3
timekey 3600 # 1 hour partition
timekey_wait 10m
timekey_use_utc true # use utc
</buffer>
<format>
@type json
</format>
</match>
config_section
のメソッドシグニチャーは以下の通りです。
config_section(name, root: false, param_name: nil, final: nil, init: nil, required: nil, multi: nil, alias: nil, &block)
name
: セクション名をシンボルで指定します。デフォルトではここで指定した名前と同じ名前のインスタンス変数をプラグインインスタンス内で参照することができます。root
: ルートセクションである場合に真を指定します。Fluentd内部で利用するもので、一般のプラグインでは利用しません。param_name
: プラグインのインスタンス内で利用する、インスタンス変数名を@
を除いたシンボルまたは文字列で指定します。final
: プラグインのサブクラスでこのセクションの上書きを禁止するならば真を指定します。一般のプラグインでは、あまり意識しなくてもよいです。init
: このセクションでは初期値が必須であるパラメータのみを定義します。一般のプラグインでは、あまり意識しなくてもよいです。required
: このセクションが必須であれば真を指定します。真を指定した場合、設定ファイルにこのセクションがないとFluentdの起動時にエラーが発生します。multi
: このセクションを複数指定可能であれば真を指定します。alias
: このセクションの別名を指定します。例: sample というプラグインに以下のようなセクションがある場合の設定ファイルの書き方を例示します
config_section :child, param_name: 'children', required: false, multi: true, alias: 'node' do
config_param :name, :string
config_param :power, :integer, default: nil
config_section :item, multi: true do
config_param :name, :string
end
end
<source>
@type sample
<child>
name gohan
power 100
<item>
name senzu
</item>
</child>
<child>
name goten
power 10
</child>
</source>
config_param
とconfig_section
を使うことで得られる利点をまとめます。
fluent-plugin-config-format
コマンドでMarkdownやJSONで定義を出力することができる
config_param
とconfig_section
を使うことで、デメリットはありません。
Fluentd組み込みのAPIを使いこなして、使いやすくメンテナンスしやすいプラグインを書きましょう。
※この記事の情報はFirefox ESR52を対象としています。これより新しいバージョンではこれらの情報は当てはまらない場合がありますので、ご注意下さい。
FirefoxにはWebサイトの閲覧時の体感的な快適さを向上するため、実際にリクエストが行われる前から接続やデータのダウンロードを行っておくという「先読み」の機能があります。ただし、無差別に全てのリンクを辿るのではなく、いくつかの条件が揃った時に初めて先読みが発動するようになっています。具体的には以下の要領です。
ユーザーから見れば快適さが増して嬉しい機能ですが、反面、ネットワークのトラフィックやセッション数は増大する事になります。弊社のMozillaサポート事業のお客様からも、契約回線の帯域が限られているなどの理由で、情報システム管理担当者の意向として「先読み機能を無効化したい」というご相談を頂く事があります。また、「既に担当者側でそのような設定を行ったつもりだが、実際にその指定が機能しているかどうかを確認したい」というご相談もあります。
前述の先読みに類する機能を全て無効化する場合、指定は以下のようになります。
network.predictor.enabled
=false
(真偽型)network.predictor.enable-hover-on-ssl
=false
(真偽型)network.predictor.enable-prefetch
=false
(真偽型)network.prefetch-next
=false
(真偽型)network.dns.disablePrefetch
=true
(真偽型)network.dns.disablePrefetchFromHTTPS
=true
(真偽型):TLSを使用したページでの挙動の制御。network.http.speculative-parallel-limit
=0
(整数型)では、これらの設定が期待通りに反映されている事をどのように確認すればよいのでしょうか。これがこの記事の本題です。
設定の書き間違いのようなケアレスミスや、それ以外にも何らかの理由から、設定したはずの値が反映されないままになっているという事は度々あります。そのため、この手のカスタマイズはなるべく、実際の挙動から設定の反映状況を確認する事が望ましいです。
HTTP接続からデータのダウンロードまでを行うフルスペックの「先読み」については、先読み対象となるHTTPリクエストが実際に処理されたかどうかを見るのが確実です。例えばこのブログの2016年5月10日の記事は同年5月18日の記事へのリンクを含んでいますが、このリンクにはrel="next"
という属性が指定され次のページへのリンクである事が示されているため、先読みの対象となります。
このような先読みは、ブラウザコンソールで動作を確認できます。
キーボードショートカットの「Ctrl-Shift-J」を使うかパネルメニューの「開発ツール」(またはメニューバーの「Web開発」)で「ブラウザコンソール」をクリックするとブラウザコンソールのウィンドウが開かれますので、そのウィンドウ上部の「ネットワーク」がハイライトされている状態でこのブログの2016年5月10日の記事を開いてみて下さい。先読みが機能していればGET http://www.clear-code.com/blog/2016/5/18.html
というメッセージがコンソールに出力され、機能していなければこのメッセージは出力されません。
コンソールに出力される出力が多すぎて見分けが難しいといった場合には、低レベルのログでも動作を確認できます。この方法については後述します。
先読み機能には、DNSでの名前解決のみを先行して行う機能(DNSプリフェッチ)もあります。例えば、ページ内に<link rel="dns-prefetch" href="http://dns-prefetch.example.com">
のような記述が含まれていると、FirefoxはHTTPでのリクエストではなく、dns-prefetch.example.com
の名前解決の問い合わせのみを単独で行います。
この機能が動作しているかどうかを確認するためは、DNSへの問い合わせを行うモジュールが内部的に発行するイベントを監視する必要があります。この確認にもブラウザコンソールを使います。
そのためには、about:config
で以下のように設定します。
devtools.chrome.enabled
=true
(真偽型):スクリプトの実行を可能にする。network.dns.notifyResolution
=true
(真偽型):DNSでの名前解決時に内部的なイベントを発行するようにする。上記の設定を反映した状態でブラウザコンソールを開くと、コンソール下部に入力欄が出現します。この入力欄に Services.obs.addObserver(function(aSubject, aTopic, aData) { console.log(aTopic+': '+aData); }, 'dns-resolution-request', false);
と入力してEnterキーを押すと、この内部的なイベントを捉えてコンソールにメッセージとして出力する事ができます。DNSプリフェッチのための指定が含まれているWebページ(テストケース)を開いた時にdns-resolution-request: dns-prefetch.example.com
のようなメッセージがブラウザコンソールに出力されれば、DNSプリフェッチのための指定が処理されたと分かりますし、逆に、そのようなWebページを開いてもこのメッセージがコンソールに出力されなければ、DNSプリフェッチは無効化されていると判断できます。
Firefoxの先読み機能の中には、データの読み込みは行わないまでも、ソケット接続のみ確立しておくという機能もあります。リンクの上にポインタが載った時点でまずソケット接続だけ先行して行っておき、クリックされた時にすぐにHTTPのリクエストを送出できるようにするというもので、この機能は投機的接続と呼ばれます。
ソケット接続を確立しただけの段階ではコンソールには何も出力されず、また、DNSプリフェッチのような内部的なイベントも発行されないため、投機的接続が行われているかどうかは、低レベルのログを解析して調べる必要があります。
Firefox 52ではログ収集対象のモジュールやログファイルの出力先は環境変数ではなく設定値で指定します。about:config
で以下の設定を作成して下さい。
logging.nsHttp
=5
(整数型)logging.NetworkPredictor
=5
(整数型)logging.config.sync
=true
(真偽型)logging.config.add_timestamp
=true
(真偽型)logging.config.clear_on_startup
=false
(真偽型)logging.config.LOG_FILE
=C:\Users\Public\predict.log
(文字列型)この状態でFirefoxを再起動すると、logging.config.LOG_FILE
で指定したパスの位置にpredict.log-main.123
のような名前でログファイルが出力され始めます。(main
は親プロセスのログであることを示します。ファイル名の末尾の数字はプロセスIDです。)投機的接続の機能は通常のWebブラウズの中で暗黙的に行われるため、この状態のまましばらくWebページを2〜3画面分ほど見て回り、十分な量のログを溜めて下さい。
ログが溜まったら、上記のログ出力用の設定をリセットしてFirefoxを再起動し、出力されたログファイルをブラウザで開きます。この時、ログは以下のような内容になっているはずです。
2017-06-02 09:51:57.565000 UTC - [Main Thread]: I/Logger Flushing old log files
2017-06-02 09:51:57.659000 UTC - [Main Thread]: D/nsHttp Creating nsHttpHandler [this=9fe1400].
2017-06-02 09:51:57.659000 UTC - [Main Thread]: D/nsHttp nsHttpHandler::Init
2017-06-02 09:51:57.659000 UTC - [Main Thread]: D/nsHttp nsHttpHandler::PrefsChanged [pref=(null)]
2017-06-02 09:51:57.659000 UTC - [Main Thread]: D/nsHttp nsHttpHandler::PrefsChanged Security Pref Changed (null)
2017-06-02 09:51:57.659000 UTC - [Main Thread]: D/nsHttp nsHttpHandler::MakeNewRequestTokenBucket this=9fe1400 child=0
2017-06-02 09:51:57.659000 UTC - [Main Thread]: D/nsHttp nsHttpAuthCache::Init
2017-06-02 09:51:57.659000 UTC - [Main Thread]: D/nsHttp nsHttpAuthCache::Init
2017-06-02 09:51:57.659000 UTC - [Main Thread]: V/nsHttp Creating nsHttpConnectionMgr @b5a1420
...
続けて、キーボードショートカットの「Ctrl-Shift-K」を使うかパネルメニューの「開発ツール」(またはメニューバーの「Web開発」)で「Webコンソール」をクリックし、ログファイルを読み込んだタブに対してコンソールを表示します。コンソール下部の入力欄に以下のログ解析用のスクリプトをコピー&ペーストし、Enterキーを押して実行します。
var body = document.body.textContent;
var requested = body.match(/nsHttpConnectionMgr::OnMsgSpeculativeConnect/g) || [];
var skipped = body.match(/Transport not created due to existing connection count/g) || [];
!requested.length ? 'not working' : requested.length == skipped.length ? 'all skipped' : 'preconnected';
すると、以下の3つのいずれかの解析結果がコンソールに出力されます。
preconnected
:投機的接続が行われた。all skipped
:投機的接続が無効化されている(もしくは、同時接続数が最大値に達しているため投機的接続が行われなかった)。not working
:ネットワーク環境の都合等で投機的接続が行われていない。3番目の「投機的接続が行われていない」というケースは、そのクライアントが接続しているネットワーク環境によって発生し得ます。
Firefoxは投機的接続を開始する前に接続先ホストがプライベートなネットワークに属しているかどうかを見ており、DNSで名前解決した結果が192.168.1.10
や10.21.0.100
といったプライベートネットワーク上のIPアドレスだった場合には投機的接続の処理を中断するようになっています。
これは、投機的接続は元々、遠隔地にあるホストとのソケット接続の確立に時間がかかるためにフライングで接続しておくという趣旨の機能なので、ネットワーク的に近いホストに対しては行う意義が薄いからです。
なお、前項で取得したログを解析すると、データの読み込みを伴う先読みが行われたかどうかも判別できます。今度は以下の解析用スクリプトをWebコンソールで実行します。
var body = document.body.textContent;
var success = body.match(/Predictor::Predict.*\n.*called on parent process.*\n.*not enabled/g) || [];
var all = body.match(/Predictor::Predict.*\n.*called on parent process/g) || [];
(success.length == all.length) ? 'all canceled' ? 'some canceled';
出力結果は以下の2つのうちいずれかです。
all canceled
:先読み処理が無効化されている。some canceled
:先読み処理が有効である。以上、Firefox ESR52において先読みに類する機能の無効化の方法と、その検証方法をご案内しました。
ここで解説している情報の中には、まとまった情報源が無く、お客様のご要望を受けて調査した結果判明したという情報も含まれています。クリアコードではOSSのドキュメント化されていない仕様を調査して明らかにする業務も行っておりますので、導入をお考えのOSSが想定通りの動作をするかどうかに不安がある場合や、既にお使いのOSSが期待通りに動作せずお困りの場合などには、お気軽にお問い合わせ下さい。
多くのFluentdのプラグインを見てきて知見が増えてきたのでまとめます。
Fluentdのプラグインでは、configure
メソッド内で設定を自力で解釈することによりconfig_section
やconfig_param
で定義していない設定値も扱えるようになっています。
以下のような設定ファイルを
<source>
@type sample
user hoge
pass secret_passowrd
<pattern>
name foo
</pattern>
<pattern>
name bar
</pattern>
</source>
次のコードで自力で解釈することができます。
def configure(conf)
super
@patterns = conf.select {|element| element.name == "pattern" }
@user = conf["user"]
@pass = conf["pass"]
end
このように自力で解釈すると、テストも書かなければならないし、間違いも発生しやすくなるので以下のように、Fluentdの本体でよくテストされているconfig_section
とconfig_param
を組み合わせて使いましょう。config_section
とconfig_param
を使うと設定を宣言的に書くことができて、とてもわかりやすくなります。その上、fluent-plugin-config-formatというコマンドでMarkdownやJSONで出力することができます。desc
を使って説明も加えておくと、fluent-plugin-config-formatでも説明が出力されて便利です。
config_section :pattern, param_name: "patterns", multi: true do
desc "name for pattern"
config_param :name, :string
end
desc "user name for login"
config_param :user, :string
desc "password for login"
config_params :pass, :string, secret: true
def configure(conf)
super
# 何か追加の処理
end
<source>
@type github-activities
users ashie,cosmo0920,kenhys,kou
</source>
上記のような設定を以下のようなコードで、配列化して使っているプラグインがありました。
desc "user names e.g. user1,user2,user3"
config_param :users, :string, default: ""
def configure(conf)
super
@users = @users.split(",").map(&:strip)
end
これは以下のように書き換えることができます。
desc "user names e.g. user1,user2,user3"
config_param :users, :array, default: [], value_type: :string
def configure(conf)
super
end
<source>
@type groonga
protocol http
# ...
</source>
上のような設定を次のようなコードでパースして使用しているプラグインがありました。
config_param :protocol, default: :http do |value|
case value
when "http", "gqtp", "command"
value.to_sym
else
rails Fluent::ConfigError, "must be http, gqtp or command: <#{value}>"
end
end
def configure(conf)
super
# @protocol を使う
end
これは以下のように書き換えることができます。
config_param :protocol, :enum, list: [:http, :gqtp, :command], default: :http
def configure(conf)
super
# @protocol を使う
end
書き換えることで、コードがすっきりしました。
<match dummy.log>
@type honeycomb
writekey very-secret-key
# ...
</match>
パスワードやAPIキーなどの秘密にしておきたい情報を設定ファイルに書かせるときは、secret
オプションを付けましょう。
secret
オプションをtrue
にするとFluentdの起動時に表示されるダンプなどで値がマスクされます。
config_param :writekey, :string, secret: true
以下のようにsecret
オプションを忘れてしまうと、Fluentd起動時のダンプなどにそのまま出力されてしまうので、バグ報告などのときにユーザーが自分でマスクしなければなりません。
config_param :writekey, :string
config_param :tag, :string, default: nil
def configure(conf)
super
raise Fluent::ConfigError, "tag is required" unless @tag
end
上のように必須の値のデフォルト値をnil
にして、自分でチェックしているコードをたまに見かけます。
下のようにデフォルト値を省略するとその設定は必須になり、Fluentd起動時にチェックが実行されます。
config_param :tag, :string
def configure(conf)
super
end
config_param :tag, :string
desc
を書くとfluent-plugin-config-formatというコマンドで、説明を見やすくフォーマットしたものを出力することができます。これはREADMEに設定の説明を書くときに使うととても便利です。
desc "tag for records"
config_param :tag, :string
Fluentdで用意されている機能を活用して、メンテナンスを続けやすいプラグインを開発しましょう。
※この記事は、Laravelを使った開発の経験がある人を対象としています。Laravelの基本的な使い方自体の説明は含まれていませんので、ご注意下さい。
Webアプリケーションを開発していると、「検索窓に入力された語句を含むレコードを一覧表示する」といった機能を付けたくなる場面がよくあります。この時よく使われる手軽な方法としてSQLのLIKE
がありますが、LIKE
には検索対象のレコードの数が増えれば増えるほど処理に時間がかかるという欠点があります。そこで登場するのが全文検索という手法です。全文検索では事前に用意しておいたインデックス情報を使うことにより、レコード数が増加しても安定して高速な検索を行うことができます。サービスの利用者が増加して数万・数十万といった数のレコードを取り扱うような必要が生じてきた場合、全文検索の導入を検討する価値は十分にあるでしょう。
以前、Ruby on Railsで作ったアプリケーションからPGroongaを使って日本語全文検索機能を実現する方法を紹介しました。今回はそのPHP版として、Laravelで作ったブログ風のアプリケーションにPGroongaを使った日本語全文検索機能を組み込む方法をご紹介します。
この記事では、適当なLaravelアプリケーションが手元に無い場合を想定し、解説用に用意したLaravelアプリケーションとPostgreSQLとの組み合わせに対して、全文検索機能を組み込む手順を解説しています。すでに開発中・運用中のPostgreSQLを使用したアプリケーションがある場合には、そのアプリケーションに組み込む手順としてテーブル名等を適宜読み替えて下さい。
また、タイトルにも書いてある通りですが、この記事で紹介しているPGroongaは、PostgreSQLに対して全文検索機能を提供するソフトウェアです。開発・運用中のアプリケーションがPostgreSQL以外のデータベースを使用している場合にはPGroongaを使えませんので、くれぐれもご注意下さい。(他のデータベースを使っている場合、例えばMySQLやMariaDBであれば、PGroongaの代わりにMroongaを使うことになります。その場合の手順はここでは解説していませんので、あしからずご了承ください。)
それでは、開発環境を用意していきます。まず、以下の2つを用意します。
ホストマシンとしてLinuxのデスクトップ環境を用意して、ホストマシン自身をクライアントとして使うのが最も簡単です。別々のマシンを使う場合には、両者は同じネットワーク上に存在するか、もしくはホストマシンのネットワーク上の任意のコンピュータにクライアントから自由に接続できるものとします。
Laravelには開発環境を簡単に構築するためのHomesteadという枠組みがあり、今回はそれを使ってみることにします。Vagrantが必要なので、事前にホストマシンにVagrant 1.9以上とVirtualBoxをインストールしておいて下さい。
開発環境のboxイメージが公開されているので、まずはそれを導入します。
% vagrant box add laravel/homestead
==> box: Loading metadata for box 'laravel/homestead'
box: URL: https://atlas.hashicorp.com/laravel/homestead
This box can work with multiple providers! The providers that it
can work with are listed below. Please review the list and choose
the provider you will be working with.
1) parallels
2) virtualbox
3) vmware_desktop
ここではVirtualboxのイメージを使うので2を選択します。
Enter your choice: 2
==> box: Adding box 'laravel/homestead' (v2.1.0) for provider: virtualbox
box: Downloading: https://atlas.hashicorp.com/laravel/boxes/homestead/versions/2.1.0/providers/virtualbox.box
==> box: Successfully added box 'laravel/homestead' (v2.1.0) for 'virtualbox'!
vagrant box add laravel/homestead 14.75s user 7.42s system 19% cpu 1:53.24 total
boxを正常にダウンロードできたので次に進みます。
Homesteadのリポジトリを以下のようにホストの作業ディレクトリにcloneします。
% mkdir ~/work
% cd ~/work
% git clone https://github.com/laravel/homestead.git
masterブランチは不安定な場合があるため、今回はリリースブランチを使います。
% cd homestead
% git checkout v5.4.0
...
HEAD is now at f54a9f0... Tagging 5.4.0 (#595)
(END):
ホストにて init.sh
を実行します。
% bash init.sh
Homestead initialized!
すると以下の3つのファイルが同じディレクトリに作成されます。
Homestead.yaml
はVMの設定ファイルです。
今回は以下の項目を変更します。
ip
: 異なるネットワークのIPアドレスを指定する。(ホストマシンが接続しているネットワークが192.168.10.0/24なら、192.168.20.0/24などのアドレスにする)authorize
: ホストマシンで作成したsshの公開鍵のパスを指定する。keys
: ホストマシンで作成したsshの秘密鍵のパスを指定する。folders
: ホストマシンのディレクトリーをゲスト上から見えるようにする指定だが、ホストマシン上にはLaravelアプリケーション用のファイルを設置しないため不要なので、コメントアウトする。sites
: ゲスト上にこれから作成するアプリケーションに合わせてパスを書き換える。networks
: ゲストがホストマシンが接続しているネットワークにブリッジ接続するための設定を追加する。上記変更を反映した Homestead.yaml
は次の通りです。
---
ip: "192.168.20.10" # ホストマシンと異なるネットワークになるようにする。
memory: 2048
cpus: 1
provider: virtualbox
authorize: ~/id_homestead.pub #公開鍵のパス
keys:
- ~/id_homestead #秘密鍵のパス
#folders:
# - map: ~/work/Blog
# to: /home/vagrant/Blog
sites:
- map: homestead.app
to: /home/vagrant/Blog/public # 後々、この位置にファイルが作られる。
databases:
- homestead
networks:
- type: "public_network"
bridge: "eth0" # "wlan0"など、ホストマシンの主なインターフェースに合わせる。
IPアドレスやブリッジのインタフェース名、鍵の情報は環境に応じて適宜読み替えてください。
設定ファイルとコマンドの準備ができたので、ホストマシンからVMを起動します。
% vagrant up
設定に問題なければVMが正常起動します。 続けて、クライアントからゲスト上のLaravelアプリケーションに接続するために、起動したVMのブリッジ接続でのIPアドレスを調べます。
% host_if=eth0
% host_nw="$(ip addr | grep -o "inet.*$host_if" | cut -d ' ' -f 2 | cut -d '.' -f -3)."
% vagrant ssh -- ip addr | grep "$host_nw"
inet 192.168.10.52/24 brd 192.168.10.255 scope global enp0s9
この例では192.168.10.52
が割り当てられているので、クライアントからは http://192.168.10.52/
をブラウザで開けば動作を確認できることになります。
http://192.168.10.52/
のようなIPアドレスの直接入力は煩雑なので、クライアントのhostsファイルを編集して、homestead.app
というホスト名で参照できるようにしておきます。クライアントのhostsに以下の内容を書き足しましょう。
192.168.10.52 homestead.app
クライアントがUbuntuのデスクトップ環境の場合、hostsは /etc/hosts
です。クライアントがWindowsの場合は、hostsは C:\windows\system32\drivers\etc\hosts
の位置にあります。
以上で準備完了です。クライアント上のWebブラウザで http://homestead.app/
を開いてみて下さい。以下のようなエラーページが表示されるはずです。
これはHomestead上のnginxが返しているエラーで、所定の位置にまだLaravelアプリケーションが存在していないという事を示しています。
それではLaravelによるブログ風のアプリケーションを用意していきます。といっても、Laravelでのブログ風アプリケーションの開発の仕方そのものはここでは重要ではないので、あらかじめこちらで用意したサンプルのブログ風アプリケーションを使うことにしましょう。
まず、ホストマシンからゲストマシンへsshでログインします。
% vagrant ssh
vagrant@homestead:~$
そうしたら、サンプルアプリケーションのGitリポジトリをcloneします。この時、clone先のディレクトリ名をBlog
と明示して、ファイルの設置先がホストマシン上のHomestead.yaml
の内容と一致するようにする必要があることに気をつけて下さい。
vagrant@homestead:~$ git clone https://github.com/clear-code/pgroonga-example-laravel.git Blog
cloneし終えたら、アプリケーションを初期化します。
vagrant@homestead:~$ cd Blog
vagrant@homestead:~/Blog$ composer install
vagrant@homestead:~/Blog$ php artisan migrate
vagrant@homestead:~/Blog$ php artisan db:seed
以上の手順をすべて実施したら、クライアントから http://homestead.app/posts
を開いてみて下さい。以下のような記事一覧ページが表示されるはずです。
Homestead.yaml
の設定(例えば、Laravelのアプリケーションの設置先パス)を間違えていると、先のエラーページと同じ物が表示されるかもしれません。その場合、Homestead.yaml
を修正してから vagrant provision
を実行し、ゲスト上のnginx等を再起動する必要があります。
記事一覧ページの右上にある検索窓に「Groonga」のようなキーワードを入力すると、そのキーワードの検索結果として、posts
テーブルのレコードのうちbody
カラムにキーワードを含むレコードの一覧が表示されます。ただ、コントローラの実装(app/Http/Controllers/PostController.php
)を見ると分かりますが、これは以下のような単純なSQLのLIKE
による絞り込みの結果です。Groonga OR Mroonga
のような凝った検索は行えません。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index(Request $request)
{
$query = $request->get('query');
if (!empty($query)) {
$posts = \App\Post::where('body', 'like', "%{$query}%")->orderBy('id', 'desc')->get();
}
else {
$posts = \App\Post::orderBy('id', 'desc')->get();
}
return \View::make('posts.index')
->with('posts', $posts)
->with('query', $query);
}
}
Homesteadの環境で普通にlaravel new Blog
すると、MySQLを使うように設定されたLaravelアプリケーションが作られます。しかし今回はPGroongaの使い方の解説なので、この例では以下のように.env
を編集して、データベースにはPostgreSQLを使うよう設定してあります。
commit 5f48f346ccf36e07aefdf062b2f374d83dc6151d
Author: YUKI Hiroshi <yuki@clear-code.com>
Date: Mon Jun 26 01:56:56 2017 +0000
Use PostgreSQL by default
diff --git a/.env b/.env
index f41ce19..ceaa34d 100644
--- a/.env
+++ b/.env
@@ -5,9 +5,9 @@ APP_DEBUG=true
APP_LOG_LEVEL=debug
APP_URL=http://localhost
-DB_CONNECTION=mysql
+DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
-DB_PORT=3306
+DB_PORT=5432
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret
diff --git a/config/database.php b/config/database.php
index cab5d06..abf3d43 100644
--- a/config/database.php
+++ b/config/database.php
@@ -13,7 +13,7 @@ return [
|
*/
- 'default' => env('DB_CONNECTION', 'mysql'),
+ 'default' => env('DB_CONNECTION', 'pgsql'),
/*
|--------------------------------------------------------------------------
冒頭にも述べていますが、PGroongaはPostgreSQLに対して全文検索機能を提供する物なので、それ以外のデータベースに対しては使えません。ご注意下さい。
お待たせしました! ようやくここからが本題です。
すでにあるLaravelアプリケーションでPGroongaを使って全文検索をするには、以下の4つのステップを踏みます。
それでは順番に見ていきましょう。
何はともあれPGroongaのインストールが必要です。Homesteadの環境はUbuntu 16.04ベースということで、今回はUbuntu用のインストール手順を参照しました。以下は実際に実行したコマンドです。
$ sudo add-apt-repository -y universe
$ sudo add-apt-repository -y ppa:groonga/ppa
$ sudo apt-get update
$ sudo apt-get install -y -V postgresql-9.5-pgroonga
PGroongaのインストール手順は実行環境によって異なります。Ubuntu 16.04以外の環境でのインストール手順については、PGroongaのプロジェクトサイトで公開されているインストール手順の説明を参照して下さい。
PGroongaは、サーバー上にパッケージをインストールしただけでは使えません。機能を利用するには、PostgreSQLのデータベースに対してCREATE EXTENSION pgroonga;
というSQL文を明示的に実行する必要があります。
ということで、このSQL文を実行するためのマイグレーションを作成します。
$ php artisan make:migration install_pgroonga
Created Migration: 2017_06_23_091529_install_pgroonga
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class InstallPgroonga extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// ここから追記
DB::statement('CREATE EXTENSION pgroonga');
// ここまで追記
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// ここから追記
DB::statement('DROP EXTENSION pgroonga CASCADE;');
DB::statement('DELETE FROM pg_catalog.pg_am WHERE amname = \'pgroonga\';');
// ここまで追記
}
}
up
に書かれている内容はPGroongaのインストール手順にあるもので、down
に書かれている内容はアンインストール手順にあるものです。
用意ができたら、このマイグレーションを実行します。
$ php artisan migrate
これでPGroongaを使う準備ができました。
次に、PGroongaで全文検索するためのインデックスを作ります。
Laravelのマイグレーションではindex
メソッドでインデックスを定義します。
$ php artisan make:migration add_posts_body_index
Created Migration: 2017_06_23_091530_add_posts_body_index
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddPostsBodyIndex extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// ここから追記
Schema::table('posts', function($table) {
$table->index(['id', 'body'], 'pgroonga_body_index', 'pgroonga');
});
// ここまで追記
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// ここから追記
Schema::table('posts', function($table) {
$table->dropIndex('pgroonga_body_index');
});
// ここまで追記
}
}
PGroongaを正しく使えているという事を説明するために、PGroongaのチュートリアルで説明されている内容との対応関係を見ていきます。 このチュートリアルでの説明に倣うと、インデックス作成用のSQL文は以下のようになります。
CREATE INDEX pgroonga_body_index ON posts USING pgroonga (id, body);
ここで、pgroonga_body_index
はPGroongaでの慣習に倣って付けた任意のインデックス名、posts
はテーブル名です。USING pgroonga (id, body)
は、インデックスの種類としてHashやB-treeなどと同列の物としてPGroongaを選択し、インデックスにはid
(主キー)とbody
の両方を含めるという意味になっています。
Schema::table('posts', function($table) {
$table->index(['id', 'body'], 'pgroonga_body_index', 'pgroonga');
});
Laravelのマイグレーションでは、テーブル名はSchema::table('posts'...
の部分で示されています。index
メソッドでインデックスを作成しており、この第1引数の['id', 'body']
がインデックスに含めるカラム名の配列、第2引数がインデックス名、第3引数がインデックスの種類を示しています。PGroongaのチュートリアルで説明されている情報が過不足無く指定できていることが分かるでしょう。
なお、インデックス名を省略してnull
にするとインデックス名を自動的に決定させることもできますが、down
のマイグレーションでインデックス名を指定してdropIndex
メソッドを呼ぶ都合上、ここではインデックス作成時にも明示的にインデックス名を指定しています。
そして、マイグレーションを実行します。
$ php artisan migrate
これによって、posts
テーブルのbody
カラムを対象とした全文検索用のインデックスが作成されます。既存のレコードに対するインデックスもこの時一緒に作成されますし、この後で作成されたレコードに対しても自動的にインデックスが作成されるようになります。
以上で、全文検索の準備が整いました。
最後に仕上げとして、全文検索にPGroongaを使うように検索処理を書き換えます。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class PostsController extends Controller
{
public function index(Request $request)
{
$query = $request->get('query');
if (!empty($query)) {
// 変更前
// $posts = \App\Models\Post::where('body', 'like', "%{$query}%")->orderBy('id', 'desc')->get();
// 変更後
$posts = \App\Models\Post::whereRaw('body @@ ?', $query)->orderBy('id', 'desc')->get();
}
else {
$posts = \App\Models\Post::orderBy('id', 'desc')->get();
}
return \View::make('posts.index')
->with('posts', $posts)
->with('query', $query);
}
}
PGroongaで全文検索をするためには、PGroonga専用の演算子を使います。where
メソッドではそれらの演算子を取り扱えないので、ここではwhereRaw
メソッドを使ってSQLの式を直接書いています。
今回使った@@
という演算子(この演算子はPGroongaの現在のドキュメントでは&?
で置き換えられていることになっていますが、&?
はPHPの実装上の都合で使えないため、非推奨のこちらを使用しています)は、与えられた検索クエリを一般的なWebの検索エンジンの検索クエリのように取り扱う物です。よって、Groonga リリース
(Groongaとリリースの両方の語を含む)やGroonga OR PGroonga
(GroongatoPGroongaのどちらか片方だけでも含む)や( Mroonga OR PGroonga ) リリース
(MroongaかPGroongaのどちらか片方に加えて、リリースという語句を含む)のような複雑なクエリも、検索窓に入力するだけでそのまま使うことができます。
以上で、PGroongaによる全文検索への乗り換えが完了しました。実際に検索を実行して、期待通りの結果が返ってくるか確かめてみて下さい。
以上、LaravelアプリケーションでPGroongaを使って全文検索を行う手順の語句最初のステップをご紹介しました。
この解説を見ると分かる通り、すでにPostgreSQLを使っている環境であれば、PGroongaを使い始めるのは非常に簡単です。また、アプリケーション内の変更はごく一部だけで済むため、LIKE
検索との性能比較もやりやすいでしょう。全文検索を使ったことがない人は、これを機にぜひ一度試してみて下さい。
less
を終了するには?何かのコマンドの実行結果が長くなって画面外に溢れてしまう場合でも、grep ... | less
という風にパイプライン経由でless
コマンドに結果を渡すと、結果を自由にスクロールしながら落ち着いてゆっくり読むことができます。
しかし、「q」でless
を終了すると、その時表示されていた内容は画面から消えてしまいます。コマンドの出力結果を見ながら次の作業をしようと思うと、以下のような工夫をしないといけません。
less
をバックグラウンドに切り替えて、また参照したくなったらその都度fg
コマンドで復帰させる。tmux
などのターミナルマルチプレクサを使い、画面を分割して片方の画面でless
の結果を表示しながら、もう片方の画面で操作を行う。less
に渡して表示するのではなく、一旦リダイレクトでファイルに保存して、改めて必要に応じless
でファイルを開く。ただ、毎度こういった工夫をするのは面倒です。
そこで便利なのが、less
の起動オプションの1つである--no-init
(または-X
)です。このオプションを指定してless
を起動すると、top
を終了したときのようにその時の表示内容が画面上に残ったままになるため、それを参照しながら次の操作を行うことができます。
--no-init
が指定された状態でless
を起動するには?このように便利な--no-init
オプションですが、毎回このオプションを指定するというのはあまり現実的ではないでしょう。そこで便利なのが環境変数LESS
です。less
はLESS
という名前の環境変数に指定されている内容を規定のオプションとして自動的に使用するため、~/.bashrc
や~/.profile
などに以下のように書いておけば、単にコマンド名をless
と入力するだけで、less
を常に--no-init
オプションありの状態で起動できるようになります。
export LESS='--no-init'
察しの良い方はもうお気づきかもしれませんが、ここには--no-init
意外にも様々なオプションを併せて指定しておけます。他にも便利なオプションとしてお薦めなのは--shift
(-#
)でしょう。これは左右カーソルによる横スクロールの速度を変えるもので、既定の状態では画面半分ずつ一気に横スクロールするのに対し、--shift 4
のように小さめの値を明示しておけば、スクロール速度がなだらかになるため、見ていた箇所を見失う事もなくなります。
export LESS='--no-init --shift 4'
また、--LONG-PROMPT
(または-M
)というオプションを使うと、現在表示されているのが何行目から何行目までの範囲なのかが画面の下端に表示されるようになります。何行目あたりを見ているかが分かると、less
で内容を大雑把に確認してからvim
などで編集するということもやりやすくなりますので、これもお薦めのオプションです。
export LESS='--no-init --shift 4 --LONG-PROMPT'
git diff
やgit log
で起動される時のless
の挙動を元に戻すここまでの話で終わらせても良いのですが、これをそのまま実施すると、ソフトウェア開発者の方などでgit
コマンドを多用する場合に困ったことが起こります。それは、git log
やgit diff
、git grep
といった操作で自動的に起動されるless
の挙動が変わってしまうということです。具体的には、以下のような問題が起こります。
git diff
の差分が小さかった場合や、git grep
で見つかった件数が少なかった場合など)でもless
が起動してしまって、いちいち手動操作で「q」キーでless
を終了しなくてはならなくなる。ESC[33m
のようなゴミが混ざるようになる。これは何故なのでしょうか。
git
がless
を起動する時の既定のオプション実は、git
コマンドには自動的にless
を起動する場面用の既定のオプションが埋め込まれています。これはMakefile
の中で以下のように定義されています。
ifndef PAGER_ENV
PAGER_ENV = LESS=FRX LV=-c
endif
ここでのLESS=FRX
というのがそうで、これがgit
コマンドの起動時に実行されるスクリプトの中の
for vardef in @@PAGER_ENV@@
do
var=${vardef%%=*}
eval ": \"\${$vardef}\" && export $var"
done
という箇所に埋め込まれて、最終的に: "${LESS=FRX}" && export LESS
というコマンドが実行されることで、「環境変数LESS
で何かオプションが指定されていればそれをそのまま使い、無ければ既定のオプションとしてFRX
を使う」という事が行われています。
FRX
は-F -R -X
の短縮表記で、それぞれロングオプションで書き表すと--quit-if-one-screen --RAW-CONTROL-CHARS --no-init
となります。
--quit-if-one-screen
(-F
)は、前述の1つ目の問題(結果が1画面内に収まるような場合でもless
が起動してしまう)を解消するための指定です。このオプションが指定されていると、less
は表示した内容が1画面内に収まる場合にそのまま自動終了するようになります。これと--no-init
(-X
)の併用により、あたかも「結果が長い時だけless
が起動され、結果が短い時はless
を使わずそのまま出力される」かのような挙動になっていたというわけです。
--RAW-CONTROL-CHARS
(-R
)は、2つ目の問題(ESC[33m
のようなゴミが混ざる)を解消する指定です。実はESC[33m
などのゴミのように見える文字列は文字の表示色を変えるための制御コードで、--RAW-CONTROL-CHARS
オプションが指定された場合、less
はこの制御コードに基づいて文字の色変えや強調表示を行うようになります。
git
から起動されるless
の挙動を戻す2つの方法以上の事を踏まえて、git diff
やgit grep
の時のless
の挙動を元に戻す方法は2通り考えられます。
1つは、不足していたオプションを環境変数LESS
で指定するようにする方法です。
export LESS='--no-init --shift 4 --LONG-PROMPT --RAW-CONTROL-CHARS --quit-if-one-screen'
less
の既定の挙動が全般的に変わってしまっても問題なければ、これが一番簡単でしょう。
全般的な既定の挙動を変えずにgit
から起動された時の挙動だけを変えたい場合は、git config
でこれらのオプションを加えた状態のless
をページャとして明示的に設定するという方法があります。
$ git config --global core.pager 'less --RAW-CONTROL-CHARS --quit-if-one-screen'
この場合、git
から起動されたless
はここで指定されたオプションと環境変数LESS
で指定されたオプションの効果がマージされた挙動となります。
以上、less
の既定のオプションの指定の仕方とお薦めの設定、そしてgit
との組み合わせでの注意点をご紹介しました。
less
にはここで登場した物以外にも様々なオプションがあります。man less
を見ながら必要なオプションを環境変数LESS
に設定して、普段の作業効率の向上に繋げてください。