オープンソースこねこね

Webプログラミングなどについてあれこれ。

Composerがパッケージのstabilityを解決するしくみ

PHPとComposerで先日composer/composerdev-masterに依存したプログラムを作っていたら、composer installのときに以下のようなエラーがでてインストールできない問題にぶちあたりました。

Your requirements could not be resolved to an installable set of packages.

Problem 1
    - kohkimakimoto/altax v3.0.6 requires composer/composer dev-master -> no matching package found.
    ...

解決方法はcomposer.json"minimum-stability": "dev""prefer-stable": trueを指定するか、対象のパッケージに"composer/composer": "@dev"のようなstabilityフラグをつければOKでした。

さて、この件に関連する日本語情報があまりなかったのですが、 根本の仕組み(Composerがパッケージのstabilityを判断してダウンロードする仕組み)を丁寧に書いたブログをみつけたので、以下和訳してみました。

Composerのスタビリティフラグ

原題:Composer Stability Flags

https://igor.io/2013/02/07/composer-stability-flags.html

今のところcomposerのサポートにやってくる最もよくある問い合わせは、スタビリティ(パッケージの安定性、stability)がどう解決されるのかよくわからないというものだ。 よくこのケースは以下のような問い合わせになる。

パッケージB:dev-masterに依存するパッケージA:dev-masterを要求(require)したら、composerがパッケージBが見つからないというんだ。

ルートパッケージ

ルートパッケージはメインのcomposer.jsonファイルのことだ。これはcomposer installを実行するときにいるディレクトリと同じディレクトリ内にある。 多くのcomposer.jsonのフィールドはルートオンリーで、これはルートパッケージ内で指定されたときだけ影響をもつということだ。

ルートパッケージはコンテキストだ。 あなたが自分のパッケージのディレクトリ内でパッケージAに依存しているといった場合、あなたのパッケージがルートパッケージとなる。 パッケージAのディレクトリにcdしたらAがルートパッケージだ。

スタビリティはルートパッケージで決定される。そしてルートパッケージのみで決定される。 これを忘れないようにして、ちょっと考えてみよう。

Composerはユーザの手にわたる依存物がどのようなスタビリティか判断をする。 ユーザとしてあなたは開発版、ベータ、または安定版のリリースを使いたいか決める。

最低限のスタビリティ(minimum-stability)

このスタビリティの判断はルートパッケージのminimum-stabilityフィールドに基づいて行われる。これはルートオンリーだ。スタビリティフラグのデフォルト値を定義し、下限として振る舞う。

f:id:kohkimakimoto:20140403113456p:plain

これは引き下げることのできるルーラーで、デフォルトは"stable"をさしている。 しかし引き下げると、より低いスタビリティフラグを示すことができる。

minimum-stabilityはすべての制約のためのデフォルトの安定性を定義する。

スタビリティの解決

それでは、つぎのようなシナリオを考えてみよう。ルートパッケージがA:dev- masterを要求していて、 それがさらにB:dev-masterを要求している場合だ。

f:id:kohkimakimoto:20140403114304p:plain

ルートパッケージは以下のようになる

{
    "require": {
        "A": "dev-master"
    }
}

Composerは以下のステップを踏む:

  • minimum-stabilityを決定:このケースではフィールドが定義されていないのでデフォルト値が設定される。これは"stable"だ。
  • Adev-masterというバージョン制約をもっている。dev-プリフィクスがついているので、これはdevバージョンであることがわかる。そしてdevバージョンは"dev"スタビリティをもっている。ルートパッケージで定義されたこのdevバージョン制約のため、暗黙的に@devスタビリティフラグを得る。
  • Aは制約A:dev-master@devをもっているので、このバージョンはマッチしてcomposerはリンクする。ABdev-masterという制約つきで依存している。これはdev-プリフィクスを持っている、よって"dev"スタビリティをもっている。

    ところが、この制約はパッケージAの中で定義されていて、かつルートパッケージではないので、暗黙的に@devスタビリティフラグを得ることはできない。その代わりにminimum-stabilityを継承する。これは"stable"だ。よって、解決される制約はB:dev-master@stableとなる。

ここが障害のポイントだ。なぜならB:dev-master@stableはどうやっても解決できないからだ。composerは与えられたスタビリティの範囲でパッケージBが見つからないとこたえるのだ。

この問題に対処する方法の一つはminimum-stabilityを"dev"に下げることだ。 しかしこれは通常とてもよくないアイデアだ。これはすべての制約に適用されてしまい、 その結果、すべてのパッケージを不安定なバージョンで取得してしまう。

だからお願いだ。それをしないでくれ。

スタビリティフラグ

代わりにスタビリティフラグを使おう。

フラグはバージョン制約の一部として定義される。スタビリティはルートパッケージでのみで決定されるので、フラグもまたルートオンリーだ。 依存パッケージ内で定義されたフラグは単純に無視される。

フラグは不安定版パッケージを指定するホワイトリストとして使うことができる。このケースにおいて、私はBホワイトリストに追加したい。このようにする:

{
    "require": {
        "A": "dev-master",
        "B": "@dev"
    }
}

注目すべきなのは、実際のバージョンをルートパッケージ内で定義していない点だ。 これはルートパッケージはインストールされるBのバージョンを扱わないということを意味する。 バージョンの決定は指定する制約をもっているAに委譲している。

これによって、もしABへの依存をdev-masterから~1.0またはそれ以外に変更したとしても、ルートパッケージはなにも変更する必要がなくなる。

Silexの例

この動作が実際にどのようなものか、よりアイデアを得るために、silexを例に見てみよう。

これを書いている現時点で、silexは安定バージョンがない(訳者注:今は1.2の安定バージョンがありますね)。インストールするために@devフラグを追加する必要がある:

{
    "require": {
        "silex/silex": "1.0.*@dev"
    }
}

Silexは1.0の開発バージョンである1.0.x-devバージョンのみがある。

Silexのすべての依存物は安定バージョンがある。これはデフォルトでv2.1.7の多数のsymfonyコンポーネントv1.0.1のpimpleを得るということだ。

もし数日前にリリースされたsymfonyコンポーネントv2.2.0-RC1バージョンを試したくなった場合、 それを以下のようにホワイトリストにすることができる:

{
    "require": {
        "silex/silex": "1.0.*@dev",
        "symfony/event-dispatcher": "@RC",
        "symfony/http-foundation": "@RC",
        "symfony/http-kernel": "@RC",
        "symfony/routing": "@RC"
    }
}

バージョンをすべて指定するのは面倒なので、minimum-stabilityを下げることもできる。 この場合それはOKだ。あなたが望まない不安定バージョンがインストールされることがないからだ。

{
    "minimum-stability": "RC",
    "require": {
        "silex/silex": "1.0.*@dev"
    }
}

prefer-stable

このポストを書いてしばらくした後、composerはprefer-stableという機能をリリースした。 もし、依存物のスタビリティを把握したくない場合、単にprefer-stableフィールドをルートパッケージで使うことができる。 Composerは可能なもので最も安定した依存物を導きだす。

これはとても便利でほとんどの場合それで十分だ。しかし、私はあなたが本当に必要としているスタビリティを考えることをお勧めする。明示的にそれを定義することもね。あなたは便利さと制御をトレードしてるかもしれない。

結論

composerがどのようにスタビリティを解決しているか。不安定バージョンを取得するためにスタビリティフラグどのように使うことができるか。この記事があなたのよりよい理解に役立てば、幸いだ。

でも覚えておいてくれ:スタビリティフラグを必要とするもっともよくある理由は、あなたの依存物のメンテナが安定板をタグ付けしない理由によるものだ。彼らにブランチエイリアスの追加とタグリリースをさせるため、いってやってくれ。彼らがそれを行ったらすぐに、スタビリティフラグを捨てて、またハッピーになれるよ。

元記事はこちら

Xcode5でxib(Storyboardを使わない)プロジェクトの作り方

ちょっと前に出版されたiOSアプリ入門書とか読んでるんですけど、最初のほうのサンプルアプリの作成手順で説明されているのがstoryboardを使わないでxibを使う構成だったりしてこまってました。 プロジェクトの作成画面で[Single View Application]を選択して[Use Storyboards]のチェックを外しましょう、って手順が書いてあって、[Use Storyboards]のチェックボックスねーよ! Xcode5からなくなってるよ! デフォルトで有効で外せねーってことかよ! うきー!

Storyboardを使うのが主流なんですか? でも今はまだiOSアプリ初心者だから、とにかく動くものをつくって、手順を繰り返して、コードを写経して、ツールの使い方とかObjective-Cのコードの感じとかAPIとかをちょっとずつ頭に入れたいんですよ。でもいきなりつまずいたーああああーーー。のおおおーーー!

さて、いろいろググって探した末、以下の手順でxibのプロジェクトを始めるのが良さげだったのでメモしておきます。参考のサイトはこちら、詳細は元の記事を見てください。

http://www.appcoda.com/hello-world-app-using-xcode-5-xib/

手順

  1. Xcodeを起動
  2. [Create a new Xcode project]を選択
  3. [iOS]の[Application]を選択[Empty Application]を選択
  4. 適当に[ProjectName]とかを埋めて、[Next]をクリックしてプロジェクトを作成
  5. Xcode上でAppDeletege.hとかのソースファイルが入っているフォルダを右クリックしてメニューから[New File...]を選択
  6. [iOS]の[Cocoa Touch]の[Objective-C class]を選択、[Next]をクリック
  7. [Class]に**ViewControllerのような名前のクラス名を入力、[Subclass of]にUIViewControllerを指定。[With XIB for user interface]にチェック。[Next]をクリック

これで. xibと対応するViewControllerが作成される。最後にこのViewControllerを読み込むようにするためにAppDeletege.mにコードをちょっと追加する。#importdidFinishLaunchingWithOptionsメソッドを以下のようにする。

#import "AppDelegate.h"

// ここを追加
#import "ViewController.h"
// ここを追加

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    
    // ここを追加
    ViewController *viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];
    self.window.rootViewController = viewController;
    // ここを追加

    return YES;
}

ViewControllerのところは上記の手順7で実際に作成したViewControllerのクラス名に読み替えてください。 以上っす。

PHPデプロイツールのAltaxのバージョン3をリリースしました。

去年10月位にバージョン2に書き直して、今回さらにまるっと書き直しました。 タスクの記述方法などもごっそり変えてしまったので、すでに使っている人は今までの設定ファイルは使えないので注意してください。

https://github.com/kohkimakimoto/altax

ドキュメントはこちら

http://kohkimakimoto.github.io/altax/ja/

一応英語圏のひとにも使ってもらえるようにドキュメントは英語でも書いています。正直、英語苦手なので変な英語だったら誰かツッコミいれてください。

概要

PHPでタスクが記述できるCapistranoみたいなものです。 SSHでリモートサーバに対してコマンドを並列実行します。 またpharでソースを全部まとめて配布しているので、動作に必要なのはPHPaltax.pharだけなので手軽につかってもらえると思います。

使い方

チュートリアルを書きました。

http://kohkimakimoto.github.io/altax/ja/tutorial/

タスク定義の例をあげると、Gitからデプロイする簡単なタスクは以下のように定義します。

Server::node("web1.example.com", "web");
Server::node("web2.example.com", "web");

Task::register("deploy", function($task){

    $appDir = "/path/to/app";

    // Execute parallel processes for each nodes.
    $task->exec(function($process) use ($appDir){

        // Run a command remotely and get a return code.
        if ($process->run("test -d $appDir")->isFailed()) {
            $process->run("git clone git@github.com:path/to/app.git $appDir");
        } else {
            $process->run(array(
                "cd $appDir",
                "git pull",
                ));
        }

    }, array("web"));

});

バージョンアップにあたって

タスクをLaravelのRouting定義ぽい記述方法にかえました。

実行コマンドのリターンコードや、出力内容を取得できるようにしました。

リモートホストに対してのファイルダウンロードやアップロードに対応しました。

プラグインでタスクを拡張できるようになりました。

プラグイン

プラグインで機能拡張をできるようにしました。 サンプルにAdminerというMySQL管理ツールをさくっと使えるようになるプラグインも同時に作って公開したので、 ご興味のあるかたはどうぞ。

http://kohkimakimoto.github.io/altax/ja/docs/plugins-introduction.html

このプラグインのしくみなんですが、元ネタというか影響をうけたのがGruntのプラグインの仕組みだったりします。 Gruntはプラグインのインストールをnpmをつかってやって、Gruntfile.jsに設定を書くという構成ですが、 AltaxはPHPのツールなのでComposerでプラグインをインストール、PHPの設定ファイルに設定を書く、という構成にしています。

個人的に今はリモートサーバにたいしてchefリポジトリをgitからcloneしてchef-soloをするという、 自動化タスクをプラグインで書いてみたりしています。

https://github.com/kohkimakimoto/altax-chef

開発とかテストの環境

最近メインPCをWindowsからMacにかえました。 だだし開発自体はVirtualBoxで構成したCentOS6上でやってます。そういうわけで、 プログラム動作は主にOSXCentOSで検証しています。あとCIにTravisを使っています。

なおMacに入っているデフォルトのPHPだとpcntlモジュールが入っていないので、 並列処理の際にプロセスをforkする処理が動かないので、この場合は並列動作させずに、 順次実行するようになっています。

SymfonyとLaravelを比べてみての考察

SymfonyとLaravelはPHPのWebアプリケーションフレームワークで 2014年現在、モダンなPHPフレームワークとして人気があるようです。 両方ともComposerによるパッケージ管理、MVCアーキテクチャ、 開発時のPHP組み込みサーバのサポート、 バンドル(Laravelではパッケージ)などでの機能拡張性を持っています。

ここ最近、個人的な興味でこのふたつのフレームワークを触ってみていたので、 感想と比較を述べてみます。

なお、実際に触ったバージョンはSymfony2.4とLaravel4.1です。 また実際のプロダクトの開発に使用したわけではないのと、 パフォーマンス比較などは行っていないことを断っておきます。

ざっくりとした比較イメージ

細かい感想と比較を書く前に、ざっくりとイメージを述べておきます。

  • Symfonyは構造がしっかりしていて重厚
  • Laravelは構造がフラットで軽量

ここでの重厚とか軽量とかは、プログラミングをしているときの感触であって、 アプリケーションの動作のことではありません。 また、開発するアプリケーションの規模にそれを合わせて、 サービスが小規模だからSymfonyは適していないとか、 大規模だからLaravelはよくないという話でもありません。

基本的にアプリケーションの規模が小規模でも大規模でもどっちのフレームワークを使ってもよいと思いました。

アプリケーションの規模にフレームワークの向き不向きが全くないとは思いませんが、 それよりも開発するプログラマのやり方、好みにマッチしているかどうか が一番のポイントなんじゃないかと思っています。以下、個別にピックアップします。

ネームスペース

PHPでは5.3からネームスペースによるコードの構造化がサポートされました。

Symfonyで開発するコードはこのネームスペースで厳格に構造化する必要があります。 たとえば、Pakagistのトップページ のコントローラはPackagist\WebBundle\Controller\WebController というネームスペースに置かれています。

Laravelではコントローラをはじめ多くのクラスがグローバルなネームスペースを使用します。Laravelの典型的なコントローラは以下のようにnamespaceなしで定義されます。

<?php
class HomeController extends BaseController {

    public function index()
    {
        return View::make('home.index');
    }
}

ネームスペースによる構造化はクラス名の衝突を避けられ、構造の見通しをよくします。 一方でそれに合わせてディレクトリ構造が深く複雑になりがちです。

Symfonyは構造が健全であることに重きをおいて設計され、Laravelは記述のシンプルさに重きをおいて設計されている感じです。

デフォルトのテンプレートエンジン

SymfonyはTwig、LaravelはBladeというテンプレートエンジンを標準で使います。

ところで、一世代まえのPHPフレームワーク(CakePHP,Symfony1,ZF1)だと、素のPHPをテンプレートエンジンに使用するのがスタンダードでした。 でも最近はそうでもなく、Symfonyの開発者であるFabien Potencierの ブログなどで言及 されているように、素のPHPでは「最適」なテンプレートの記述が行えないということから、 専用のテンプレートエンジンを利用するようになってきています。

素のPHPがスマートに対応できない領域は、テンプレート継承や、エスケープ処理、イテレータ処理などです。 もちろん、全くできないわけではないが、インラインのphpタグでテンプレートがごちゃごちゃしてしまうのが避けられない。 このへんの問題は、前述のFabienさんのブログでも参照してください。

で、SymfonyとLaravelのテンプレートエンジンのはなし。

SymfonyのTiwgは前述のPHPテンプレートのダメな部分を完全に取り除いて 理想的なテンプレートエンジンを目指して設計されている感じです。 テンプレート継承もできるし、エスケープも自動。またfor文は以下のように書ける

{% for user in users %}
    * {{ user.name }}
{% else %}
    No users have been found.
{% endfor %}

このテンプレートはPHPとは基本的に別ものであり、テンプレートにPHPを書くことはできません。

LaravelのBladeも同様にテンプレート継承やエスケープ機能を提供してくれますが、 設計としてPHPと別ものではなく素のPHPテンプレートに正規表現による薄い変換処理をかぶせて、ちょっと文法を拡張した感じのものです。 なので、bladeのテンプレートにはPHPのコードも書けるのですが、Twigのfor文みたいなPHPの書式から大きく異るような機能は用意されていません。

テンプレートに素のPHPを記述できることを、害悪ととるか柔軟性ととるかは、プログラマの好みによる話だと思います。 私はPHPを記述できたほうがいいと感じるタイプです。

Symfonyはその領域において最適なコンポーネントを提供するのに対して、 Laravelは最適ではないが、PHPプログラマが慣れ親しみやすいコンポーネントを提供する。 そういう設計になっています。

設定ファイル

設定ファイルの記述に関しても設計思想の違いが感じられます。 たとえば、URLとアクションをひもづけるルーティングの定義をSymfonyyamlアノテーションを使います。 (PHPでも記述できるが、かなり冗長なのでこれを使うひとは多分いない)

一方、Laravelは素のPHPを使います。ただしSymfonyPHP設定ファイルのような冗長さはなく、 グローバルネームスペースとスタティッククラスでRubyDSLっぽい簡潔な記述を提供します。

テンプレートエンジンの話と同様なのですが、 Symfonyは設定ファイルの記述においても最適な方法(PHPは最適ではないのでyamlなどの別の手段)を提供するのに対して、 LaravelはあくまでPHPを使い(PHPプログラマのためのフレームワークだからか)、その上で記述しやすい施策をとっている印象です。

まとめ

最後に抽象的にまとめると

といったところでしょうか。

LaravelのRouteクラスがグローバル空間で呼び出せる仕組み

最近Laravelを触っています。

で、LaravelだとURLと実行するアクションを関連付けるルーティング部分を

Route::get('/', function()
{
    return 'Hello World';
});

こんなふうに、RubySinatraっぽくかけるんですよね。ところがこのRouteというクラス、事前にuseで使う名前空間の指定もしていないし、実際にどのPHPクラスが使われているかよくわからない。そこでソースを追ってみました。

エイリアス

フロントコントローラのpubic/index.phpを起点に読み始めていくと、

pubic/index.php->bootstrap/start.php->vendor/laravel/framework/src/Illuminate/Foundation/start.phpとファイルが読み込まれて、この中で、

$aliases = $config['aliases'];

AliasLoader::getInstance($aliases)->register();

ということをやっている。でregisterメソッドで$aliasesのデータつかってspl_autoload_register関数を呼び出してオートローディングを設定している。$aliasesの内容はメインのコンフィグファイルconfig/app.phpにかかれていて、デフォルトは以下のようになっている。

'aliases' => array(

                'App'             => 'Illuminate\Support\Facades\App',
                'Artisan'         => 'Illuminate\Support\Facades\Artisan',
                'Auth'            => 'Illuminate\Support\Facades\Auth',
                'Blade'           => 'Illuminate\Support\Facades\Blade',
                'Cache'           => 'Illuminate\Support\Facades\Cache',
                'ClassLoader'     => 'Illuminate\Support\ClassLoader',
                'Config'          => 'Illuminate\Support\Facades\Config',
                'Controller'      => 'Illuminate\Routing\Controller',
                'Cookie'          => 'Illuminate\Support\Facades\Cookie',
                'Crypt'           => 'Illuminate\Support\Facades\Crypt',
                'DB'              => 'Illuminate\Support\Facades\DB',
                'Eloquent'        => 'Illuminate\Database\Eloquent\Model',
                'Event'           => 'Illuminate\Support\Facades\Event',
                'File'            => 'Illuminate\Support\Facades\File',
                'Form'            => 'Illuminate\Support\Facades\Form',
                'Hash'            => 'Illuminate\Support\Facades\Hash',
                'HTML'            => 'Illuminate\Support\Facades\HTML',
                'Input'           => 'Illuminate\Support\Facades\Input',
                'Lang'            => 'Illuminate\Support\Facades\Lang',
                'Log'             => 'Illuminate\Support\Facades\Log',
                'Mail'            => 'Illuminate\Support\Facades\Mail',
                'Paginator'       => 'Illuminate\Support\Facades\Paginator',
                'Password'        => 'Illuminate\Support\Facades\Password',
                'Queue'           => 'Illuminate\Support\Facades\Queue',
                'Redirect'        => 'Illuminate\Support\Facades\Redirect',
                'Redis'           => 'Illuminate\Support\Facades\Redis',
                'Request'         => 'Illuminate\Support\Facades\Request',
                'Response'        => 'Illuminate\Support\Facades\Response',
                'Route'           => 'Illuminate\Support\Facades\Route',
                'Schema'          => 'Illuminate\Support\Facades\Schema',
                'Seeder'          => 'Illuminate\Database\Seeder',
                'Session'         => 'Illuminate\Support\Facades\Session',
                'SSH'             => 'Illuminate\Support\Facades\SSH',
                'Str'             => 'Illuminate\Support\Str',
                'URL'             => 'Illuminate\Support\Facades\URL',
                'Validator'       => 'Illuminate\Support\Facades\Validator',
                'View'            => 'Illuminate\Support\Facades\View',

        ),

つまりRouteクラスの実体はIlluminate\Support\Facades\Routeということがわかる。

Laravelは自前で設定ファイルからオートローディングの登録を行うロジックをもっていて、それをつかってグローバル空間にクラスのエイリアスを作るという機能を実装していたわけだった。

※じつはIlluminate\Support\Facades\Routeにはルーティングに使用していた上記のgetメソッドは直接定義されておらず、ここからさらにFacadeという仕組みをつかって、別のクラスに実装を移しているのだけど割愛。

PHPDSLっぽいこと

調べてみると、どうってことはない作りでした。でもこの仕組みは、クラスの実装はネームスペースで構造化された空間に定義でき、インターフェースのみグローバル空間に公開するというのが、うまく出来てると思う。これに無名関数を組み合わせることで、LaravelのRoute定義のような、Rubyの内部DSL的な記述をPHP上に表現しているのだな、とちょっと感心したのでした。

おしまい。

epelにdockerがキタ━(゚∀゚)━!ので、CentOSにインストールした。

ローカルのVirtualBox上のCentOS6で動かしました。epelリポジトリは事前に設定してあるものとします。

インストール

# yum install --enablerepo=epel docker-io

サービスの起動

# /etc/init.d/docker start

doceker runする!

# docker run -t -i centos /bin/bash

コンテナ内でシェルが起動します。echoとか打ってみる。

bash-4.1# echo hello world!
hello world!
bash-4.1# exit

うごく~( ´∀`)♪

Dockerfileからimageつくってみる

Dockerfile

FROM centos
RUN yum clean all
RUN yum install -y openssh-server
RUN yum install -y passwd
RUN echo d0cker | passwd --stdin root

## https://github.com/dotcloud/docker/issues/1240#issuecomment-21807183
RUN echo "NETWORKING=yes" > /etc/sysconfig/network

## http://gaijin-nippon.blogspot.com/2013/07/audit-on-lxc-host.html
RUN sed -i -e '/pam_loginuid\.so/ d' /etc/pam.d/sshd

EXPOSE 22
CMD /sbin/init

docker buildする!

# docker build -t test .
Uploading context 10240 bytes
Step 1 : FROM centos
 ---> 539c0211cd76
Step 2 : RUN yum clean all
 ---> Using cache
 ---> f738a5199bab
Step 3 : RUN yum install -y openssh-server
 ---> Running in 033f90a21823
Loaded plugins: fastestmirror
Determining fastest mirrors
Error: Cannot find a valid baseurl for repo: base
Could not retrieve mirrorlist http://mirrorlist.centos.org/?release=6&arch=x86_64&repo=os error was
14: PYCURL ERROR 6 - "Couldn't resolve host 'mirrorlist.centos.org'"
Error build: The command [/bin/sh -c yum install -y openssh-server] returned a non-zero code: 1

おちた~(T_T)!!なんでかコンテナからインターネットに接続できん。のおおお!

2013-12-07追記:とおもったらVirtulaBoxのVMから作りなおしたら出来た。何がわるかったかわからんす。

2013-12-10追記:ネット接続できない問題は/etc/sysctl.confnet.ipv4.ip_forward = 1にしたら治ったのかもしれない。でもCMD /sbin/initは結局うまく動いてくれない。Dockerで/sbin/initを動かすのはGithubのissuseにもあがっていて、みんな試行錯誤してるようだけど、まだ解決していないっぽいですな。

ElasticsearchでCentOS上にNgram全文検索サーバを構築する - (その2)Ngramアナライザを設定する

前回に引き続きElasticsearchの設定を行います。

elasticsearch-headプラグインをインストールする

いろいろ設定を試していたりすると、設定内容やデータの確認のためにコンソールからcurlを実行してREST APIを実行するのが面倒になります。そこでElasticsearchにはWebUIからデータや設定内容を参照するための機能がプラグインで用意されているので、これを導入します。

インストールは以下のコマンドを実行するだけでOKです。

$ sudo /usr/share/elasticsearch/bin/plugin --install mobz/elasticsearch-head

あとはブラウザから

http://localhost:9200/_plugin/head/

にアクセスすればWebUIからデータの操作ができるようになります。

アナライザ

アナライザは、文字列タイプのフィールドをElasticsearchにインデックスする(データを保存する)ときや検索クエリを投げるときに行われる処理で、データを品詞分解したり、大文字小文字の入力を変換したりするテキストの解析処理のことです。インデックスするときと検索時に別々のアナライザを使うこともできます。

デフォルトでいくつかアナライザが用意されていますが、自分で定義することもできます。 今回は日本語をNgram検索をしたいのでカスタムアナライザを定義しました。なおNgramについてはググる

http://gihyo.jp/dev/serial/01/make-findspot/0005

などの参照してください。

/etc/elasticsearch/elasticsearch.ymlに以下の定義を追加します

# default analyzer (1-gram and 2-gram)
index.analysis.analyzer.default.tokenizer: custom_ngram_tokenizer
index.analysis.analyzer.default.filter.0: lowercase

index.analysis.tokenizer.custom_ngram_tokenizer.type: nGram
index.analysis.tokenizer.custom_ngram_tokenizer.min_gram: 1
index.analysis.tokenizer.custom_ngram_tokenizer.max_gram: 2
index.analysis.tokenizer.custom_ngram_tokenizer.token_chars.0: letter
index.analysis.tokenizer.custom_ngram_tokenizer.token_chars.1: digit

# default_search analayzer(2-gram)
index.analysis.analyzer.default_search.tokenizer: custom_bigram_tokenizer
index.analysis.analyzer.default_search.filter.0: lowercase

index.analysis.tokenizer.custom_bigram_tokenizer.type: nGram
index.analysis.tokenizer.custom_bigram_tokenizer.min_gram: 2
index.analysis.tokenizer.custom_bigram_tokenizer.max_gram: 2
index.analysis.tokenizer.custom_bigram_tokenizer.token_chars.0: letter
index.analysis.tokenizer.custom_bigram_tokenizer.token_chars.1: digit

設定を反映させるため、Elasticsearchを再起動し、データを再投入します。

さて、上記のアナライザの定義ですが、2つのカスタムアナライザを定義しています。まず最初の

# default analyzer (1-gram and 2-gram)
index.analysis.analyzer.default.tokenizer: custom_ngram_tokenizer
index.analysis.analyzer.default.filter.0: lowercase

部分ですが、index.analysis.analyzer.defaultというキーでデフォルトのアナライザを定義しています。index.analysis.analyzer.default.tokenizer: custom_ngram_tokenizerはトークナイザ(品詞分解する処理)にcustom_ngram_tokenizerを使うことを設定しています。で、このcustom_ngram_tokenizerはその下に設定内容が書いてあります。

index.analysis.tokenizer.custom_ngram_tokenizer.type: nGram
index.analysis.tokenizer.custom_ngram_tokenizer.min_gram: 1
index.analysis.tokenizer.custom_ngram_tokenizer.max_gram: 2
index.analysis.tokenizer.custom_ngram_tokenizer.token_chars.0: letter
index.analysis.tokenizer.custom_ngram_tokenizer.token_chars.1: digit

文字を1-gramおよび2-gramで分解する設定となっています。つまり、

こんにちは

という文字は

こん, んに, にち, ちは
こ,ん,に,ち,は

というように分解されインデックスされます。さて、もう一つのアナライザですが、

# default_search analayzer(2-gram)
index.analysis.analyzer.default_search.tokenizer: custom_bigram_tokenizer
index.analysis.analyzer.default_search.filter.0: lowercase

という定義になっています。このindex.analysis.analyzer.default_searchという設定は、検索時のみに使われるアナライザのデフォルトになります。こちらのトークナイザの設定はcustom_bigram_tokenizerで、これは

index.analysis.tokenizer.custom_bigram_tokenizer.type: nGram
index.analysis.tokenizer.custom_bigram_tokenizer.min_gram: 2
index.analysis.tokenizer.custom_bigram_tokenizer.max_gram: 2
index.analysis.tokenizer.custom_bigram_tokenizer.token_chars.0: letter
index.analysis.tokenizer.custom_bigram_tokenizer.token_chars.1: digit

となっており、2-gramで分解する設定です。こんにちはの例ですと

こん, んに, にち, ちは

という分解を行います。 で、なんでインデックス時と検索時のアナライザを別々に定義しているのかというと、1文字による検索でも何らかの検索結果を返したいと考えて設計したからです。

2-gramのみでインデックスしてしまうと、1文字による検索に一切マッチしません。 一方、検索時は2-gramのみをおこなっていますが、このアナライザに1文字の検索クエリをなげると、

# 以下はアナライザの動作確認をおこなうリクエスト
$ curl -XGET 'http://localhost:9200/blog/_analyze?analyzer=default_search&pretty=true' -d 'a'
{
  "tokens" : [ ]
}

# 検索
$ curl -XGET http://localhost:9200/blog/article/_search?pretty=true -d '{"query": {"match":{"_all":"a"}}}'
{
  "took" : 10,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 0,
    "max_score" : null,
    "hits" : [ ]
  }

のように、トークンが空になってしまい、そのままですとやはり検索に引っかかりません。 そこで一文字の時は、検索にアナライザを利用しないようにqueryをmatchからtermに変更して、以下のようなリクエストをなげるようにしました。

$ curl -XGET http://localhost:9200/blog/article/_search?pretty=true -d '{"query": {"term":{"_all":"a"}}}'

これで一文字のときも検索に引っかかるようになります。

マッピング

今回はまず導入ということで、フィールドに対してマッピングは行いませんでした。 マッピングは個々のフィールドに詳細な設定ができる、RDBMSでいうところのスキーマ定義に当たります。 マッピングを使えば特定のフィールド(たとえばブログのタイトル部分だけ)に特定のアナライザを適用したりとか、もっと高度で効率的な検索もできると思います。 しかし上記のデフォルトアナライザの設定だけでも、ドキュメントのテキスト部分全体に対してNgram検索がかけられて、いい感じの検索結果を得ることができています。