Perlメインだった人からみたElixir

最近Elixirにふれる機会が有りましたので、理解を深めるためにも、見識を書いていきたいと思います。

なお、このエントリを書いている時点でElixir歴だいたい3日ほどです。いろいろ間違えているかもしれませんが、もし間違いに気づいた方、Twitterで教えてくれると嬉しいです。

私のメイン言語変遷

ここでいうメイン言語というのは、「業務で主に触っている(触っていた)言語」という定義でよろしくお願いします(他にもたくさん触ってますけど、かくの面倒なのでいろいろ端折ったりしてます)。

  • 1998: batch(MS-DOSのアレ)
  • 1999-2000: bash, batch
  • 2001-2002: bash, PHP, たまにPerl
  • 2003-2015: bash, Perl, たまにRuby
  • 2016-2018: JavaScript(Node.js), Swift, PHP, たまにPerl
  • 2019: JavaScript??

タイトルにPerlメインだった人って書いてるのは、多分自分で一番慣れている言語がPerlだからそうしている、という感じです。このエントリもPerl使ってる向けに書いているところがあるので、ご了承ください。

ちなみに、とってもお恥ずかしい話なんですけど、Elixirに触るまではElixirが動的型付け言語だということすら知りませんでした。

個人的感想「これPerlでは?」

少し触ってみて思ったのは、「これはRubyというよりPerlにだいぶ酷似してるな」ということでした。

Perlについては、比較的歴史のある動的型付け言語ということでよく知られているかと思います。そしてPerl自体はマルチパラダイムな言語なのですが、実はPerlのご先祖様はLispということもあって、関数型パラダイムの要素を多分に包含しているのです。

一方でElixirもまたマルチパラダイムな言語であり、関数型パラダイムの要素がふんだんに盛り込まれています。その上動的型付け言語だということになると、この時点でだいぶPerlに似ている感じがしますね。

Perlで言うところの「あれ」が「これ」

さて、ここからは具体的な例を挙げて、ElixirとPerlの機能ごとのマッピングを少しだけしていきたいと思います(並列性云々の話は、そこまでたどり着けていないので書きません)。

Cartonだとかcpmみたいなやつは「mix」

RubyでいうところのBundler、Node.jsでいうところのnpm、そしてPerlでいうところのCartonに相当するものとして、Elixirではmixというツールがあります。

このmixは、Perlでいうところの「Minilla」でもあります(パッケージ作成ツールです)。

例えばmy_modというパッケージを作成する場合は、mix new my_modを実行すれば、パッケージのひな形がmy_modフォルダに生成されます。このとき、assertionベースな検証手法が用いられたテストコードも同時に生成される点なんかは、minil newしたときとそっくりですね。

依存パッケージの指定は以下のように、mix.exsdefp deps doendの間につらつらと記入していくようです。

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
    ]
  end

そして依存パッケージの導入はmix deps.getだけでOKとなります。

metacpanみたいなやつは hex.pm

ドメインがまさかの.pmなのでElixirはPerl!!!」というのは冗談ですが、Perlで言うところのmetacpan.orgに相当するのがhex.pmというサイトになります。

hex.pmはもともとErlangとElixirのパッケージ検索サイトなのですが、ElixirがErlangのラッパー言語だからか、Erlangで使われているエコシステムをそのまま使ってるようです。

例えばhttpクライアントが欲しい場合、hex.pmでhttp clientを検索すれば、いくつも該当します。

ただ、hex.pmにおける各パッケージのドキュメンテーションは、そのスタイルが統一されていない(CSSが全く違うというレベル)ので、読む側としては正直なんとかしてほしい気持ちはあります。きっとルールが定められていないからなんでしょうかね。。。

REPL(PerlでいうところのReply)は「iex」

標準でついてくるREPLはiexというものでして、必要十分かつ標準的な機能を持ちます。Perlで言うならばReplyが似たようなものかと思います(コチラは標準ではついてこないですが)。

Sigilは意味が違うけど、Perlにあるアレと似てる

Perlでは、Sigilというと変数の最初につく$@といった「変数の性質を決める記号」という事になっていますが、Elixirでは異なる意味の物となっています。それは、以下のような文字列リテラルや正規表現、文字のリストなどを表すためのものです。

// 文字列リテラル(エスケープ・式埋め込みあり)
strX = ~c/1 + 2 = #{1 + 2}/

// 文字列リテラル(エスケープ・式埋め込みなし)
strY = ~C/1 + 2 = #{1 + 2}/

// 正規表現
regex = ~r/(Perl|Elixir)/i

これらをiexで出力してみると、以下のような結果になります(IO.putsはperlで言うところのsayみたいなやつ)。

iex> strX |> IO.puts
1 + 2 = 3
:ok

iex> strY |> IO.puts
1 + 2 = #{1 + 2}
:ok

iex> "I like Elixir" =~ regex  
true

iex> "I like PHP" =~ regex   
false

iex> "I like Perl" =~ regex
true

正規表現の適用が=~なのもPerlと全く同じで、親しみすら感じます。

ところで、ElixirのSigilに相当するものが「演算子」という形でPerlにもあって、以下のように利用できます。

# 文字列リテラル(Elixirの~cに近いが、変数展開はするけど式展開はだいぶ特殊)
my $strX = qq/1 + 2 = @{[1 + 2]}/;

# 文字列リテラル(Elixirの~Cに同じ)
my $strY = q/1 + 2 = @{[1 + 2]}/;

# 正規表現
my $regex = qr/(Perl|Elixir)/i;

これらをsayしてみると、以下のような結果になります。

# 実行内容
use feature qw/say/;
my $strX = qq/1 + 2 = @{[1 + 2]}/;
my $strY = q/1 + 2 = @{[1 + 2]}/;
my $regex = qr/(Perl|Elixir)/i;
say $strX;
say $strY;
say 'I like Perl' =~ $regex;
say 'I like PHP' =~ $regex;
say 'I like Elixir' =~ $regex;
# 実行結果
1 + 2 = 3
1 + 2 = @{[1 + 2]}
Perl

Elixir

まとめ

ElixirとPerlの似たところをほんの少しだけ取り上げてみました。これをきっかけに、ElixirもPerlもちょっとでいいから利用者増えてほしいなぁ・・・

実は他にも似ている箇所が結構ある(構造体とパッケージの紐づけとか)のですが、このエントリはここまでにしたいと思います。この2つの言語、だいぶ似てませんか?私はかなり似ているなと思うんですが・・・。

フリーランスになりました

副業から本業に

もともと副業といいますか個人事業を一昨年からやっていて、これまでもいくつかチームマネジメントや技術相談などをご用命いただいていましたが、本日より完全にフリーランスとなりました。

ともあれ、これまで私を正社員として雇ってくださった会社とは、引き続き業務委託という形で(稼働日数を減らして)一緒にお仕事を続けることになっています。

動機

ちょっとした野望を内に秘めているのですが(いまはまだ明かせません・・・)、これを実現するためには、若干自分の時間が足りないな、と半年ほど前から実感していました。しかし、副業をしていたこともあり、稼ぎについて大きな不安なしにこの決断をすることができました。

この結果を得るまでにはそこそこの紆余曲折を経ましたが、とにかく、時間の使い方をもうすこし自分で選びたい、という小さな目標はこれで達成できるのかなと思っています。

どんなお仕事をやっていくのか

冒頭でリンクしたスイ・ソリューションズ(屋号)では、主に以下のようなお仕事を承っております。

  • 各種システムの現状調査
  • webシステム及びクラウドシステムに関するアドバイス・提案

ちなみに今年でITエンジニアとしてのキャリアは20年目に突入するのですが、私は下記の事項に実績があります。

  • Microsoft Azureにおける各種SaaS/PaaSを使ったシステム設計・構築・運用(特にAzure Functions)
  • 20年にわたるLinuxに関する知見
  • Perl, PHP, bash, JavaScript(Node.js), Swiftなど、10種以上の言語による開発実績(とくにAPI)
  • TDDの導入
  • MySQLにおけるクエリチューニング(あくまで開発者として。)
  • エンジニアチームの立ち上げ、マネジメント、メンタリング
  • プロジェクトのマネジメント
  • 事業におけるシステムのポジション定義と予算策定

もし興味がありましたら、メールやTwitterでご連絡ください。無論、対面でも構いません。

まとめ

装甲騎兵ボトムズ的にまとめるなら、以下のようになるところでしょうか。

変わる、変わる、変わる。この世の舞台をまわす巨獣が、クラウドの底でまた動きはじめた。

天が軋み、人々は呻く。舞台が回れば吹く風も変わる。
昨日も、今日も、明日も、時の流れに閉ざされて見えない。

だからこそ、時を操る術を求めて、褪せぬ技術を信じて求めて。

次回「急変」。

変わらぬ技術などあるのか。

WEB+DB PRESS Vol.109 に寄稿させていただきました

WEB+DB PRESS Vol.109

紹介

WEB+DB PRESSVol.109(技術評論社:gihyoより2月23日発売予定)のPerl Hackers HubコーナーにサーバレスでもPerlというテーマで寄稿させていただきました。

ぜひお買い求めください!

Perl Hackers Hub – Perl Hacker達による旬の技術の紹介

Perl Hackers HubとはWEB+DB PRESS誌内の連載のひとつであり、Perl Hacker自身が筆を執り旬の技術を紹介するというものです。

gihyoさんのPerl Hackers Hubのページには、Perl Hackers Hubについて以下のような説明がついています。

本連載は,第一線のPerlハッカーが回替わりで,Perlの旬な技術について解説していきます。

記事概要

Microsoft Azure Functionsを使ってPerlスクリプトを実行します。スクリプトの内容は、「画像URLを与えると、画像の顔認識結果(年齢と性別)をJSON形式で返すWeb API」です。

サンプルコードとして、Azure上に環境とプログラムを全自動でデプロイしてくれるスクリプトを収録してあります。Azureアカウントがあれば、最初の無料枠で試せる範囲の内容となりますので、ぜひともお試しください。

謝辞

この度機会を与えてくださった@songmuさん、Perl Hackers Hubコアメンバーの皆さん、そして一緒に原稿の仕上げをしていただいた@inaoさん、本当にありがとうございました。

特に@inaoさんには度々ご迷惑を掛けてしまったにもかかわらず、辛抱強く対応してくださいました。感謝しきりです。

また、Azureについて深く言及した内容の記事はWEB+DB PRESS誌上では初めてだそうです(本当ですか?)。私も驚きました。正直な話、流石にAzureについては過去に誰か書いているだろうと思っていたのですが…意外なところで「誌上初」だったんですね。

ともかく私としては、今回の記事がPerl Hackers Hubというコーナーを通じて、様々な技術に触れるきっかけを読者の皆様に作ることができたらいいな、と思っています。

YAPC::tokyo 2019で登壇しました

YAPC::Japan 2019にて「実演サーバレスPerl – 顔認識データを扱おう」というタイトルで登壇させていただきました。見に来てくれた方、本当にありがとうございます。

そして、結局ライブコーディングするための時間が足りず、見せ場を作れなかった点、本当にごめんなさい。あの後、見に来てくれていた同僚や@note103さんに励まされたのが本当にありがたかったです。

資料

敗因

20分で、受け手側に前知識が必要な内容をライブコーディングするのは、あまりにも無謀でした。

前知識のインストールが終わった時点で7分ほど経過しておりました。さらにクラウド側(Azure)の処理待ち時間が思った以上にかかる。

これではまずいということで、事前に用意しておいた出来上がり環境を使って説明していきましたが、それでも時間が足りず、コードを書くところをお見せできませんでした(コードそのものをお見せすることはできたのですが・・・)。

補足(というか宣伝)

今回のセッションをおさらいする内容で、Web+DB Press誌 2019年2月号のPerl Hackers Hubに寄稿させていただいております。まだ発売まで時間がありますが、より詳しく解説されているものとなりますので、興味のある方はぜひお買い求めください!

また、Web+DB Press誌ではサンプルコードも提供します。サンプルコードは、今回の顔認識システムを自動でAzure上に構築するスクリプトとなっておりますので、構築する手間が惜しいが試すだけ試してみたい、という方にもおすすめです。

前夜祭でもLTしました

本編のみ参加の方はご存じないかもしれませんが、前夜祭でもLTで発表しました。Webappにおける@INC Hooks絶対許せないマンとしてネタに振り切った内容です。

LT資料

Acme::AtIncPoliceのその後

実は昨日Acme::AtIncPoliceissueが立ちまして。なんだろうと思ってみてみると、「Testで使ってるTest::Exceptionが入ってないからTest失敗するんですが」という内容でした。

それを受け、今朝慌ててTest::Exceptionをdependencyとして登録し、shipit。なんと0.01をshipitしてから、たった2日で0.02をshipitすることとなってしまいました・・・

感想など

@songmuさんベストトークおめでとうございます!感極まって感情が溢れ出たときに、周りで何名かもらい泣きした人がいたんですが、songmuさんの情熱があるからこそなんだろうなと。

個人的には@risouさんのPerl6トークと@hitode909さんのWebVRトークがかなり良い話だと思いました。hitode909さんからは豆本の夢日記を頒布していただきました。家宝にします。

Azure Functions v1 + Node.jsなアプリケーションをAzure Functions v2に移行してみる話

このエントリはServerless2 Advent Calendar 2018の17日目のエントリとなります。

半月ほど前業務で、稼働中のAzure Functions v1 + Node.jsなアプリケーションをAzure Functions v2に移行する作業を行いました。

その際、今後も似たような移行作業があり得そうだと考え、この作業をざっくりこなしてくれるスクリプトを作りました。それがytnobody/azure-func-migrate-nodeです。ちなみにPerl製ではなくNode.js製です。

如何せんやっつけで作ったスクリプトですので、漏れ・抜けなどはあるかもしれませんが、お手すきの際にでもpull-reqしてくださると幸甚です。

移行のために最低限必要な処理

大きく分けて、以下の作業が必要となります

  • bindingsに対応したextensionsをインストールする
  • ランタイムバージョンを2.0に設定する

bindingsに対応したextensionsをインストールする

実際のextensionsインストール作業はfunc extensions installに委譲しているのですが、これまでv1で使っていたbindingsに相対するv2 extensionsを検出するロジックは作成しました。

なお、v1では"type": "documntDB"と指定していた箇所は"type": "cosmosDB"に変更する必要がありますので、その対応も行っています。

ランタイムバージョンを2.0に設定する

これもロジックを作りました。

まとめ

ざっくりと移行スクリプトを作りましたので、公開しました。が、だいぶやっつけで作ってありますので、適宜pull-reqをしてくださると幸甚です。

昔書いたGithub止まりのモジュールをほじくり返す – Test::Proc 篇

※このエントリはPerlアドベントカレンダー2018の5日目のエントリとなります。

CPAN(MetaCPAN)でモジュールを公開するということ

Perlでプログラムを書くことに慣れてくると、CPANモジュールを使うようになったり、もっと慣れてくると自分でPerlモジュールを作るようになり、最終的にはCPAN(今はMetaCPANですね)で公開するようになるかもしれません。私は過去にそのような道を歩んできました。

では私は過去にどんなモジュールを書いたのか。ひとまずMetaCPANに上がっているものを一部挙げると、 Otogiri(これはほとんど@tsucchiさんの力です), XML::Diver, Net::Azure::CognitiveService::Faceなどがあります。

PerlモジュールをMetaCPANで公開する前に

しかしながら、MetaCPANでモジュールを公開する前に気にかけておいて欲しいこととして、個人的には以下のような項目を挙げたいと思います。

  • そのモジュールを公開することで不快感を感じる人々が生じないか
  • そのモジュールはセキュリティ的に致命的な問題を抱えていないか
  • そのモジュールは新しい機能を提供してくれるか
  • そのモジュールはプログラミング体験をより幸せにしてくれるか
  • そのモジュールは従来の類似モジュールよりもパフォーマンスに優れるか
  • そのモジュールは従来の類似モジュールよりも依存性が削減されているか

もし上記について該当するかわからない場合は、ひとまずgithubに上げておきつつ、ブログやTwitter等で言及してみましょう。運が良ければ誰かが注目してくれて、レビューをしてくれるかもしれません。これらのことは、Perlモジュールの公開に及び腰になってしまわない程度に、気をつけるとよいのではないでしょうか。

CPANの歴史を少しだけ垣間見る

では、なぜこのように自律する必要があるのでしょうか。それを知るためには過去に目を向けることが重要です。

歴史を振り返ると、charsbarさんによるエントリでCPANは幼稚園児の砂場じゃないよねという、2008年当時、大変考えさせられたものがあります。若い方の中にはこのような歴史を知らない方もいると思いますが、是非とも同じ轍を踏まぬための知識として、知っておいていただければ幸いです。

また、まかまかさんによるPerl同人誌「Acme大全」には、「Acme大全2012」から毎回、巻末付録として「とあるAcmeモジュールの削除について」という章が設けられています(ブログにも同様のエントリがある)。

このように、MetaCPANへモジュールを公開する上では、ある程度その性質を律せられる側面があることは間違いないでしょうし、誰かがつらい思いをしないためにも必要なことであると私は思います。

それでも日の目を見ないモジュールもある

そんな事もあって、実際にはたくさんのPerlモジュールを作っていたとしても、実際にMetaCPANにて公開されるものはその一部でしかありません。ほかにもMetaCPANに公開するのが怖いとか、作者がそこまでの有用性を感じていないだとか、謙遜している、などの理由でMetaCPANで公開されていないPerlモジュールがGithubにはあります。

このようにあえてGithubでの公開にとどめられているPerlモジュールを、私は親しみを込めてGithub止まりモジュールと呼んでいます。

そして今回紹介するGithub止まりモジュールは、拙作のTest::Procです。

Test::Proc – プロセスを起動し、その起動状態をテストする

Test::Procは5年前にGithubにpushされています。今となっては当時の記憶を頼りに解説する他ないという点がいささか不安です。

さて、Test::Procが目指したところは、テストスクリプトでプロセスの起動状態をテストしたいというものでした。

使い方はSYNOPSISにある通りです。(done_testingが抜けていたので、補足してあります。)

use Test::More;
use Test::Proc;


my $proc = test_proc 'my_task' => sub {
    print 'test';
    warn 'dummy';
    sleep 20;
};


$proc->is_work;


$proc->stdout_like( qr/\Atest/ );
$proc->stderr_like( qr/\Adummy/ );


$proc->exit_ok;
done_testing;

あら捜し

レポジトリを見てみると、テストの少なさが気になります。github止まりモジュールにはよくある話かもしれないですが、だいぶ控えめなテストの数だなと感じますね。

そしてインターフェースが片手落ちです。というのも、このTest::Procはtest_procというDSLを提供しており、Test::Proc::Objectインスタンスを返しているのですが、test_proc側にはクロージャ以外でプログラムを起動する方法が提供されていません。Test::Proc::ObjectはProc::Simpleのサブクラスであり、24行目で引数の$codeを実行しているので、Proc::Simpleのインターフェースを見る限り、文字列で外部コマンドとオプションを渡すことができるはずです。

実験

実際に以下のようなテストを作って実験してみました。

$ cat hoge.sh 
sleep 3
echo "hoge"

$ cat t/12_shell.t 
use strict;
use Test::More;
use Test::Proc::Object;

my $proc = Test::Proc::Object->new('sleep 3 sec. Then print "done"', 'bash ./hoge.sh');

$proc->is_work;
$proc->exit_ok;
$proc->isnt_work;

$proc->stdout_like(qr/\Adone\z/);
$proc->stderr_like(qr/\A\z/);

done_testing;

これを実行すると、以下のようになり、テストは成功します。

$ perl -Ilib t/12_shell.t 
ok 1 - process sleep 3 sec. Then print "done" is work
ok 2 - process sleep 3 sec. Then print "done" exit code is 0
ok 3 - process sleep 3 sec. Then print "done" is not work
ok 4
ok 5 - process sleep 3 sec. Then print "done" STDERR like (?^:\A\z)
1..5

深掘りしていく

上記のことから、test_procにコマンドを文字列渡しして起動させることができそうですが、実際に以下のようなテストを試してみると、失敗してしまいます。

$ cat t/13_shell-with-test_proc.t 
use strict;
use Test::More;
use Test::Proc;

my $proc = test_proc 'my_work' => 'bash ./hoge.sh';

$proc->is_work;
$proc->exit_ok;
$proc->isnt_work;

$proc->stdout_like(qr/\Adone\z/);
$proc->stderr_like(qr/\A\z/);

done_testing;

$ perl -Ilib t/13_shell-with-test_proc.t 
Type of arg 2 to Test::Proc::test_proc must be sub {} (not constant item) at t/13_shell-with-test_proc.t line 5, near "'bash ./hoge.sh';"
Execution of t/13_shell-with-test_proc.t aborted due to compilation errors.

これはtest_procのインターフェースが($&)となっているために起こっている問題となります。ですのでこれを($$)としてあげると、以下のように成功します。

$ perl -Ilib t/13_shell-with-test_proc.t 
ok 1 - process my_work is work
ok 2 - process my_work exit code is 0
ok 3 - process my_work is not work
ok 4
ok 5 - process my_work STDERR like (?^:\A\z)
1..5

なぜgithub止まりなのか

このように、完成度の低さゆえにgithub止まりとなっているTest::Procですが、その他に大きな理由として、私個人が出くわす状況にそもそもテストスクリプトでプロセスの起動状態をテストしたいケースがあまりにも少ないという点が挙げられます。

そういうわけで、結局あまり洗練もされないまま5年もgithub止まりとして塩漬けになっていたというわけです。

Test::Procの今後

一応、ユースケース的に「いいじゃん?」って思ってくれる人がいらっしゃるのであれば、forkしてshipitするまでやってくだされば良いとおもいますが、今の所私自身がなにかをするということは無いでしょう。

コンセプトモデルということでここはひとつ、ご理解いただければ幸いです。

まとめ

ざっくりまとめると以下のようになります。

  • MetaCPANでPerlモジュールを公開する前に、いま一度そのモジュールの性質を見つめ直そう
  • Perlコミュニティの歴史から、誰かがつらい思いをしないための行いについて知ろう
  • Test::ProcというPerlモジュールを作ったけど、ユースケースに遭遇しなさすぎて結局Github止まりになった
  • Github止まりモジュールは「コンセプトモデル」として見ると良さそうかも

明日のPerlアドベントカレンダーは6日目。@yoku0825さんによる「マイエスキューエルにはPerl Mongerが必要かもしれないはなし」です。

Twitterアカウントを凍結されてしまいました。

様子です

しばらくはこちらのサブアカウント にて細々とつぶやいております。

ちなみに本日は私の誕生日でした。最高のプレゼントをありがとう、Twitter!

状況

Azure関連でちょっとした疑問があったので、 @AzureSupport に質問をしたところ、

と言われました。

言われたとおりに質問内容と、サポートに必要なID情報をDirect Messeageにて伝えたところ、suspendedとなってしまいました。

見解

今回suspendedとなってしまった原因は “Violating our rules against evading permanent suspension.” だそうです。

原因については「なるほど?」という感じですが、そもそも今回の経緯から、Twitter社はDirect Messageを覗き見しているという事がわかりました(利用規約にはその旨が書かれている)。

おそらくコンテキスト等を無視して、固有情報らしきものが流れてきたら簡単なチェックを経て、必要に応じてsuspendしているのではないでしょうか。

今後

不服申立てはすぐに行いました。が、仮にアカウントが復活しても、あまりアクティブな利用を行わず、そのままmastodonあたりに流れるかもしれません。

翌日

アカウントの解凍が行われました。Twitter社の担当部門の皆様、大変お手数をおかけしました。

Twitter社においては、認証済みアカウントとのやり取りにおける秘匿情報の取扱いについて、検閲・凍結のルールを見直していただくことを願う次第です。

200ミリ秒の検索APIをMicrosoft Azureでデザインする

200ミリ秒の検索APIとは

ここでは、httpクライアントからリクエストを受けてから全文検索を含む何らかの検索処理を行い、httpクライアントにレスポンスを返すまでの処理を、おおむね200ミリ秒(200ms、0.2秒)前後の時間でこなすWeb APIを指します。

検索という機能を考えたとき、200msという数字はまずまず軽快な応答速度ではないかと私は考えます。

今回はAzureの各種サービスを使って検索APIを作った時のノウハウを共有します。このAPIはデータサイズによって多少のばらつきはあるものの、ほぼ200ms前後の応答速度を実現することができております。

以下は、実際の処理履歴となります。おおむね200ms前後で処理が完了し、応答していることが分かると思います。

ログ

Microsoft Azureで作る

Microsoft Azure(以下Azure)で検索APIを作るにあたり、以下のようなシステムデザインを行いました。

システムデザイン

ある程度Azureに詳しい方であれば、上記の図を見ただけで概ねの構成がご理解いただけると思います。

Azure CDN

いわゆるCDN(Contents Delivery Network)サービスです。今年から動的コンテンツの高速化にも利用できるようになりました。

今回のケースでも「動的コンテンツの高速化」がその利用目的となります。

Azure Functions

実際にhttpリクエストを受け付け、httpレスポンスを返すためのアプリケーション・ロジックをデプロイし、運用するためのFaaSとなります。最近v2がリリースされましたが、私のケースではv1を利用しました。

C#やF#、PHPなどの言語に対応していますが、今回はNode.jsをつかって作ってみました。実際の実装・はまりどころについては前回のエントリにまとめてありますので、そちらも併せてご参照ください。

Azure Search

Apache LuceneクエリやODataフィルタを利用可能な検索エンジンサービスです。

全文検索や緯度経度をもとにした距離検索、ファセットなどにも対応しているため、複雑な検索機能を実装するにはほぼ必須となります。

また、Azure CosmosDBやAzure Table Storageなどから定期的(最短で5分おき)にデータを取り込むIndexerという仕組みがついてきますので、検索結果にリアルタイム性を求めない限り、データのインポートをする仕組みを自分で作る必要がありません。

Azure CosmosDB (または Azure Table Storage)

どちらもスキーマレスなデータストアとして利用できるサービスです。

今回はAzure CosmosDBを利用し、Azure Search向けの一次データをストックしておくデータストアとします。

まとめ

200ms前後の応答速度の検索APIをAzureで実現するシステムデザインを紹介しました。

「えっ、実際の作り方は書かないの?」という声が聞こえてきそうですが、その辺は公式ドキュメントや有志のブログに書いてあることしかやっていません。

強いてあげるとすれば、前回のエントリで書いたようなはまりどころがあるくらいですので、そちらを見ていただきたいと思います。

参考にしたドキュメント・ブログ

[Azure + javascript]Functionsのhttp triggerな関数でSearchの検索結果を返す

暖簾に腕押し

私がAzure Functionsに提供してほしいと切に願っている機能の一つに、Search Bindings(仮称)があります。なお、私がこの機能実装のリクエストを出しました。

しかし、現時点ではこのような機能が提供される気配は全くなく、付帯するロジックを記述するしかありません。暖簾に腕押し、というやつです。

実際、このようなロジックを書くことすら億劫なのですが、嘆いていても仕方がないですので、javascriptからAzure Searchへ問い合わせを行うnpmライブラリazure-searchを利用し、レスポンスを返すところまで実装することにしました。

役割

  • クライアント : webAPIを利用し、HTTP Responseに含まれた検索結果を受け取ります。

  • Azure Functions : クライアントからHTTP Requestを受け取り、Azure Searchに問い合わせをし、結果をクライアントに返します。

  • Azure Search : クエリをFunctionsから受け取り、検索結果を返します。また、Indexerという機能を利用し、定期的にTable Storageからデータを同期します。

  • Table Storage : 検索対象となるデータがストックされています。Indexerによって、定期的にデータ参照されます。

データ構造

検索対象となるデータ構造は以下のようなものです。

[
  {
    "PartitionKey": "2018-08-29:myroom",
    "RowKey": "ecd53616-e756-41fb-98d2-fe2b387e0c8a",
    "id": "ecd53616-e756-41fb-98d2-fe2b387e0c8a",
    "channel": "myroom",
    "body": "5000兆円 欲しい!!!",
    "author": "ytnobody",
    "visible": true,
    "timestamp": 1535508299
  },
  ...
  ...
]

PartitionKeyおよびRowKeyはいずれもTable Storageで必須の項目です。(参照:Azure ストレージ テーブルの設計ガイド: スケーラブルな設計とハイパフォーマンスなテーブル

Search側のスキーマ構造

インデックスmessageには、Table Storageに格納されているデータ構造を、ほぼそのまま持ってきています。Table Storageで利用していたPartitionKeyおよびRowKeyはここでは使いません。

  • id Edm.String (key, retrievable, searchable)
  • channel Edm.String (retrievable, filterable)
  • body Edm.String (retrievable, searchable)
  • author Edm,String (retrievable, filterable)
  • visible Edm.Boolean (retrievable, filterable)
  • timestamp Edm.Int64 (retrievable, filterable, sortable)

普通に実装

最初、以下のように実装しました。

// function.json
{
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ],
  "disabled": false
}
// index.js

module.exports = function (context, req) {
    // Searchクライアント初期化
    const AzureSearch = require("azure-search");
    const client = AzureSearch({
        url: process.env.SEARCH_URL, 
        key: process.env.SEARCH_KEY
    });

    // 検索ワードをスペースで切って配列にする
    const words = req.query.word ? req.query.word.split(' ') : [];

    // ページング指定
    const page = req.query.page ? parseInt(req.query.page) : 1;
    const top  = req.query.size ? parseInt(req.query.size) : 10;
    const skip = top * (page - 1);

    // 検索オプション
    const search = words.map(w => `body:${w}`).join(' AND ');
    const filter = 'visible eq 1';
    const searchOptions = {
        queryType:  "full",
        searchMode: "all",
        top:        top,
        skip:       skip,
        search:     search,
        filter:     filter
    };

    // 問い合わせ
    client.search('message', searchOptions, (err, results) => {
        context.res = err ? {
            status:  500,
            headers: {"Content-type": "application/json"},
            body:    {"message": `Internal Server Error: ${err}`}
        } :
        {
            status:  200,
            headers: {"Content-type": "application/json"},
            body:    results
        };
        context.done();
    });
};

察しの良い方なら気づいたかもしれませんが、このロジックは期待通りには動かず、502エラーを返してしまいます。

何がダメなのか

期待通りに動かない原因は、client.search(...)の結果を受け取る前にmodule.exports自体が処理を終えてしまうからです。

レスポンスらしいレスポンスを設定しないまま処理が終わってしまうので、502エラーを返す、ということです。

対応方法

結論から書くと、以下の2点を直すと良いです。

  1. module.exportsを、Promisereturnするように変更する。
  2. function.jsonにて、http output bindingsのname$returnにする。(portalの場合、応答パラメータ名のところにある「関数の戻り値を使用する」をチェックする)

なおしてみる

なおした後の実装がこちら。

// function.json
{
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "$return"
    }
  ],
  "disabled": false
}
// index.js

module.exports = function (context, req) {
    // Searchクライアント
    const AzureSearch = require("azure-search");

    // 検索ワードをスペースで切って配列にする
    const words = req.query.word ? req.query.word.split(' ') : [];

    // ページング指定
    const page = req.query.page ? parseInt(req.query.page) : 1;
    const top  = req.query.size ? parseInt(req.query.size) : 10;
    const skip = top * (page - 1);

    // 検索オプション
    const search = words.map(w => `body:${w}`).join(' AND ');
    const filter = 'visible eq 1';
    const searchOptions = {
        queryType:  "full",
        searchMode: "all",
        top:        top,
        skip:       skip,
        search:     search,
        filter:     filter
    };

    // 問い合わせ
    const promise = Promise.resolve(AzureSearch({
        url: process.env.SEARCH_URL, 
        key: process.env.SEARCH_KEY
    }))
    .then(client => client.search('message', searchOptions))
    .then(results => {
        return {
            status:  200,
            headers: {"Content-type": "application/json"},
            body:    results
        };
    })
    .catch(err => {
        return {
            status:  500,
            headers: {"Content-type": "application/json"},
            body:    {"message": `Internal Server Error: ${err}`}
        };
    });

    return promise;
};

function.jsonでは、http output bindingsのname$returnとなっており、functionの戻り値をレスポンスに使う設定となっています。

そしてindex.jsではPromise.resolve(...).then(result => ...).catch(err => ...)の形式でSearchに問い合わせを行った後の処理をハンドリングするよう定義し、promiseそのものをreturnするロジックへと書き換えられました。

本当はドキュメントに書いておいて欲しかった、もしくは・・・

実はPromiseをreturnすることで解決できるという事について、公式ドキュメントには書かれていないようです(ソース。openになっているし、書こうとはしてる模様)。

今回の解決法は、上記issueを辿ってようやく見つけることができたものでした。

この手の手間をなくすためにも、Search Bindingsが欲しいな、と思うのでした。

まとめ

  • Search Bindings欲しいですね。

2018-08-30 追記

node-azure-searchでPromise/thenを使った書き方では、検索結果のヒット件数を取得することができないという問題がありました。

const AzureSearch = require('azure-search');
const word = '5000兆円';
AzureSearch(...)
    .then(client => search({
        queryType:  "full",
        searchMode: "all",
        top:        20,
        skip:       0,
        search:     `message:${word}`,
        filter:     'visible eq 1',
        orderby:    'timestamp desc',
        count:      true // <--- @odata.countをレスポンスに含めるための指定
    }))
    .then(rows => {
        // rowsは検索結果(オブジェクト)が入った配列。
        // ここで検索結果のヒット件数である@odata.countを利用したいができない!!
    })
    .catch(err => { ... });

これはnode-azure-searchのindex.jsを修正することで、取得できるようになります。(ただしインターフェイスを破壊する変更です)

@@ -492,7 +492,7 @@ module.exports = function (options) {
            return new Promise(function (resolve, reject) {
              args.push(function (err, value, data) {
                if (err) reject(err)
-               else resolve(value)
+               else resolve(data) // resolve(value) not contains '@odata.count'
              })
              fn.apply(self, args)
            })

利用する側は以下のようになります。

const AzureSearch = require('azure-search');
const word = '5000兆円';
AzureSearch(...)
    .then(client => search({
        queryType:  "full",
        searchMode: "all",
        top:        20,
        skip:       0,
        search:     `message:${word}`,
        filter:     'visible eq 1',
        orderby:    'timestamp desc',
        count:      true // <--- @odata.countをレスポンスに含めるための指定
    }))
    .then(result => {
        const count = result['@odata.count'];  // 検索結果のヒット件数。
        const rows  = result['value'];         // rowsは検索結果(オブジェクト)が入った配列。 
        ...
    })
    .catch(err => { ... });

破壊的な変更であるため、forkして利用しています。

Hokkaido.pm #14で 予想の話 というか 予想を支えるシステム設計の話 をしてきました

予想の話予想を支えるシステム設計の話

札幌で開催されたHokkaido.pm #14というイベントに参加し、予想の話(というか予想を支えるシステム設計の話)をしてきました。

内容としては「おとなの自由研究」の取り組みの中で作った南関競馬予想システム「うまミる」のシステム構成と予想ロジック、運用などについてを話したのですが、30分は予想以上に短く、あらかじめ40分で発表枠を押さえておけばよかったというのが正直な感想です。

うまミるについてより詳しく聞きたい方は、アルコールチャンスを用意してくださればお話しますので、是非ともよろしくお願いします。

Hokkaido.pmの皆さん、本当に楽しかったです。ありがとうございました。出来たら11月くらいにもう一発くらい開催してくれたらいいのにな、って思いました!

あと当然ながら、初夏の札幌を満喫したのは言うまでもありません。