TECH PLAY

フォルシア

フォルシア の技術ブログ

246

本記事は Next.js Advent Calendar 2020 の 22 日目の記事です。 こんにちは。旅行プラットフォーム部エンジニアの東川です。 フォルシアではフロントエンドフレームワークとして Next.js を使用していますが、2020年は Next.js にとって激動の年であったといえます。 この 1 年間でバージョンは 9.1 から 10.0 に上がり、SSG(Static Site Generation), ISG(Incremental Static Generation)などの新機能が次々に追加されました。 10 月 27 日に Next.js のカンファレンス Next.js Conf の開催と同時に Next.js バージョン 10.0 が発表されました。 国際化に対応したルーティング、Next.js Analytics, Next.js Commerce, React17 対応など数多くの新機能とバージョンアップが発表されましたが、next/image はその リリースノート の中でも一番上で取り上げられています(Next.js Analytics については山門がこのアドベントカレンダーで 紹介記事 を書いているのでぜひご覧ください)。 簡単に言えば、next/image とは 画像サイズと拡張子をデバイスとブラウザに応じて最適な形で出し分けてくれる React コンポーネントのことです。 この記事では next/image の基礎的な使い方と仕組み、コンポーネントの引数の解説をしたいと思います。 画像の最適化は重要だが手間がかかる Next.js Conf の Keynote で指摘されているように、画像ファイルはウェブページ全体のバイト数の半分を占めます。 最適化されていない画像の送受信や描画はページ表示の遅れにつながり、UX(ユーザーエクスペリエンス)の悪化につながります。 フォルシアでは EC サイトの構築を多くやってきていますが、EC サイトのように多くの商品画像を表示する必要があるウェブサイトの場合、この問題は特に重要です。 Next.js Conf の講演 Why Images Hurt App Performance & How the New Next.js Image Component Can Help や next/image の RFC では、最適化されていない画像とは何かと、その UX への悪影響について以下の点が指摘されています。 画像サイズ: 通信されるの画像サイズと実際に表示される画像サイズがあっていない 例えば、スマホの画面に 100×100pixel の画像を <img> タグで表示したいとします。このとき、大きすぎるサイズの画像 500×500pixel の画像を送ってしまうと、無駄な通信が発生して画面描画が遅れます。 また、 <img> タグに width, height が設定されていない場合、画像の表示前後で DOM の配置が変化する可能性があります。 これらは、google が提唱する core web vitals の一部、LCP(Largest Contentful Paint, 簡単に言えばファーストビューが表示されるまでの時間です) や CLS(Cumulative Layout Shift, 簡単に言えば描画までに起こった画面レイアウトの変化量) の悪化につながり、全体的な UX の悪化につながります。 特にスマホの場合は、処理スペックに限りがある一方で viewport(画面の表示領域) が小さいため、最適な画像サイズを送ることは特に重要になるといえます。 拡張子: 軽量な拡張子の画像が使われていない モダンな拡張子、例えば webp は jpeg, png に比べて 30%程度軽量です。従って、jpeg, png 画像をこれらの拡張子に置き換えることで通信量の削減ができます。 タイミング: viewport 外の画像を読み込んでいる 初期描画でページ内のすべての画像を読み込むと、表示にかかる時間が不必要に伸びてしまいます。 速度と表示を両立させるためには、初期描画では viewport 内(ブラウザの表示領域)の画像のみを、それ以外の画像は viewport が近づいたタイミングで順次読み込みます(遅延ローディングと呼ばれます)。 上記の課題は有名な対応法が知られています。 例えば 画像サイズの最適化に関しては、 <img> タグで srcset を設定すれば、ブラウザが複数の画像から最適なサイズの画像を読み込ませるようにすることができますし、拡張子の最適化は jpeg, png を片っ端から webp に変換すれば対応できます。 遅延ローディングに関しても intersection observer を使った実装などがよく知られています。 Next.js に限っても next-optimized-images などの画像の最適化をしてくれるライブラリが知られていました。 しかしながら、画像の最適化はウェブ全体を見ると十分に浸透しているとは言えません(上のリリースノートによれば、99.7%の画像は webp のようなモダンな拡張子が使用されていないそうです)。 これには以下の理由が考えられます。 ブラウザ間の差異を考慮する必要がある 上述のモダンな拡張子 webp などは、一部のブラウザではサポートされていないため、これらのブラウザのサポートと画像拡張子の最適化を同時にしようとすると、ブラウザを見て返却する拡張子を変化させる必要があります。 また、遅延ローディングには様々な実装方法が知られていますが、実装方法によってはうまく機能しないブラウザなどもあり注意が必要です。 また、ブラウザによって画像サイズを出し分ける場合、素朴には各画像に対して srcset と各サイズの準備をする必要があり、実装の手間がかかります。 外部サーバーから画像を取得する場合、画像の最適化とキャッシュ機能を担う中継サーバーが必要になる 外部サーバーから取得した画像を最適化する場合、外部サーバーから画像を取得し、画像の最適化をしてブラウザに返却するような中間のサーバーが必要になります(往々にして、このようなサーバーは最適化された画像を保持するキャッシュとしての役割も持ちます)。 画像の最適化に真剣に取り組もうとすると、これらの課題をクリアしながら開発する必要があり、そのコストは決して少なくありませんでした。 これらの解決策として登場したのが next/image です。 next/image の概要と基本的な使い方 また、以下の例は 公式の example を下敷きにしています。動作確認は以下の条件で行いました。 Next.js: canary(2020/12/14時点), Chrome: 78, Firefox: 83, Internet Exploler: 11 例として、以下の画像ファイル( river.jpg )を考えます。 next/image の導入は非常に簡単であり、 タグを React コンポーネント に置き換えるだけで画像サイズと拡張子の適切な出し分けができるようになります。 付属ライブラリのインストールなども不要です。 // Case A. 通常の画像コンポーネント const UnoptimizedImage = () => ( <img src="{`/river.jpg`}" width="{360}" height="{240}" /> ); // Case B. next/iamgeを使った画像コンポーネント import Image from "next/image"; const OptimizedImage = () => ( <img // <img="" />を <img />に置き換えるだけ!! src={`/river.jpg`} width={360} height={240} /> ); では、これがどのように画像を最適化してくれるのかを見ていきましょう。 Case A, B の画像を横に並べて比較してみたものが下です。 当然期待されることですが、 <img> を使った場合も <Image> コンポーネントを使った場合も同じ画像が表示されます。 一方で、画像取得のリクエストや生成される DOM 要素は大きく異なり、 <Image> コンポーネントを使った場合 public/下には何の最適化もしていない画像を配置したにもかかわらず、最適な画像サイズの画像が最適な拡張子で遅延読み込みされるようになっています。 上の例だと、元々 resource size 2.7MB の jpeg ファイルが返却されていたのが 24kB の webp に変換され、resource size は元々の 1%程度まで小さくなっています。 <Image> コンポーネントから生成される DOM 要素は下のようであり、 <img> に加えて <img> をラップするような DOM 要素が生成されます。 // Case A. 通常の画像コンポーネントから生成されるDOM要素 <img src="/river.jpg" width="360" height="240" /> // Case B. Imageコンポーネントから生成されるDOM要素 <!-- レイアウトを整える用のラッパーDOM要素 --> <div style=" display: inline-block; max-width: 100%; overflow: hidden; position: relative; box-sizing: border-box; margin: 0; " > <div style="box-sizing: border-box; display: block; max-width: 100%"> <img style="max-width: 100%; display: block" alt="" aria-hidden="true" role="presentation" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYwIiBoZWlnaHQ9IjI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2ZXJzaW9uPSIxLjEiLz4=" /> </div> <!-- 画像のDOM要素 --> <!-- srcが/_next/image/下のパスに置き換わり、decoding, srcsetが設定されている ! --> <img src="/_next/image?url=%2Friver.jpg&w=1080&q=75" decoding="async" style=" visibility: visible; position: absolute; inset: 0px; box-sizing: border-box; padding: 0px; border: none; margin: auto; display: block; width: 0px; height: 0px; min-width: 100%; max-width: 100%; min-height: 100%; max-height: 100%; " srcset=" /_next/image?url=%2Friver.jpg&w=384&q=75 1x, /_next/image?url=%2Friver.jpg&w=750&q=75 2x, /_next/image?url=%2Friver.jpg&w=1080&q=75 3x " /> </div> ソース を見るとわかるように <Image> コンポーネントは以下のような DOM 要素を生成します。 // packages/next/client/image.tsx // <Image>コンポーネントで生成されるDOM要素 <div style={wrapperStyle}> {sizerStyle ? ( <div style={sizerStyle}> {sizerSvg ? ( <img style={{ maxWidth: "100%", display: "block" }} alt="" aria-hidden={true} role="presentation" src={`data:image/svg+xml;base64,${toBase64(sizerSvg)}`} /> ) : null} </div> ) : null} <img {...rest} {...imgAttributes} decoding="async" className={className} ref={setRef} style={imgStyle} /> </div> <Image> コンポーネントにおける画像取得の仕組みは大まかに以下のようです。 生成された DOM 要素で decoding=async が設定されているため、画像のデコード処理が非同期的にバックグラウンド処理されるようになります。 <Image> コンポーネントはデフォルトで遅延ローディングになっており、対象画像が viewport に近づくとローディングが始まります。内部的には intersectionObserver を使って対象画像との相対位置を監視しています( ソースの該当箇所 )。 上の生成された DOM 要素 を見ると分かるように、 <Image> コンポーネントから生成された <img> タグには srcset が設定されています。これにより、ブラウザは srcset で設定された、w(width)と x(ピクセル密度)が異なる 3 つの画像からブラウザ幅に応じて最適なものを選んでリクエストします。例えば、 PC 画面の場合だと <Image> コンポーネントに設定された width=360 に最も近い画像幅 w=384 をもつ x=1 のケースが選ばれ、リクエスト /_next/image?url=%2Friver.jpg&w=384&q=75 が送られます。 /_next/image は next のビルド時にできる画像サーバーです。リクエストを受けた画像サーバーは、リクエスト元のブラウザとクエリパラメターから画像サイズと拡張子を適切なものに変換し、ブラウザに返却します。上の例だと、Chrome は webp 対応しているため、webp で width=384, height=254 の画像ファイルをクライアントに返却します。一点注意するべき点は、 <Image> による画像の変換はビルド時ではなくランタイムで行われるということです。これにより画像のレスポンス時間は長くなるものの、画像数の増加によるビルド時間の増大を防ぐことができます。 <Image> を使うことにより、画像のサイズ・拡張子・読み込みタイミングの最適化という冒頭で上げた3つの問題が解消されていることがわかります。 また、この記事では詳しく説明しませんが /_next/image にできる画像サーバーにはキャッシュ機能があり、変換された webp 画像が .next/cache/images/ 下に保持され、同じリクエストに対しては webp への変換なしでクライアントに返却される、ブラウザに画像がある場合は /_next/image でバリデーションした後に 304 コードを返却することでブラウザのキャッシュを利用するようにする、などのことをしています。 <Image> はブラウザに応じて、適切に拡張子を選んでくれます。下は各ブラウザの devtool のネットワークを調べたものですが、 webp に対応している Chrome, Firefox に対しては webp が、webp 非対応の IE に対しては jpeg がそのまま返却されていることがわかります。 コンポーネントのオプション 公式ドキュメント で紹介されているように、 <Image> コンポーネントには豊富なオプション引数が存在します。 // <Image>コンポーネントの引数 <Image src={`/river.jpg`} // ソースファイル, string width={420} // 表示幅, number height={280} // 表示高さ, number quality={75} // 画質, number priority={false} // 表示の優先度, boolean loading={"lazy"} // 遅延ロードするかどうか, "lazy" | "eager" unoptimized={false} // 最適化するかどうか, boolean layout={"fixed"} // レイアウト, "fill" | "fixed" | "intrinsic" | "responsive" objectFit={"contain"} // layout='fill'の場合のobject-fit objectPosition={"50% 50%;"} // layout='fill'の場合のobject-position /> これらのほかに、例えば画像の alt 属性など <img> タグに設定できる属性は <Image> コンポーネントの props として設定することができます。 但し、 style, srcSet, decoding は例外で、設定したとしても <Image> コンポーネントの内の <img> タグの props を設定する際に上書きされてしまいます(上のソースコードを参照ください)。 必須引数 src : 画像のソースファイルです。 型: string public/下を参照するときは通常の <img> タグと同様に /path/to/image/below/publicDir/img.png のように設定します width : 画像の幅です 型: number 下で説明するように、 layout='fill' の時以外は必須です height : 画像の高さです 型: number 下で説明するように、 layout='fill' の時以外は必須です width や height は通常の <img> タグと異なり、必須の引数です。 width や height の設定されていない画像は CLS の悪化を引き起こしますが、 <Image> コンポーネントでは開発者が自然とそれを避けられるように設計されていることがわかります。 priority, loading: 表示タイミング・表示の優先度についての任意引数 priority : preload するかどうかのフラグです 型: boolean デフォルト値: false true の場合は、ページ遷移時に preload されます。 loading : 遅延ローディングをするかどうかのフラグです 型: "lazy" | "eager" デフォルト値: lazy loading=lazy の場合は viewport から計算された値でローディングを開始し、 loading=eager の場合は viewport の位置にかかわらず、ページ遷移した時にローディングを開始します。 上で説明したようにデフォルト設定では画像が遅延ロードされますが、遅延ロードが有効でないケースもあります。 例えば、サイズが大きくローディングに時間がかかる画像やトップページのヒーローイメージのようにファーストビューですぐに表示したい画像などです。 これらのケースでは preload=true や loading='eager' の設定が有効です。 unoptimized, quality: 最適化の有無と画質についての引数 unoptimized : 最適化するかどうかのフラグです。 型: boolean デフォルト値: false unoptimized=true の場合、生成される html では <img src='/river.jpg'> のようになり、srcset も設定されません。このため、 _next/image にリクエストはされず、最適化された画像がクライアントに返却されることもありません。 quality : 画質 型: number (1~100 の数値) デフォルト値: 75 quality を変化させると next の画像サーバー /_next/image/ へのリクエストのクエリパラメター q が変化します。下の例だと quality を 1(最低値), 75(デフォルト値), 100(最高値)とした時のスクリーンショットです。resource size はそれぞれ 3.6kB, 24kB, 73kB でした。この例だと q=1 の場合は画質の荒さが気になりますが、 q=75 は q=100 とほとんど遜色なく置き換えても問題ないように感じられます。 quality によって画像サイズが劇的に変化するため、このオプションは背景画像などサイズが大きくローディング時間を短縮したい状況で使えそうです。 unoptimized=true と設定するべき状況として、RFC では next/image に対応していない loader の画像を取得する場合などが挙げられています(対応している loader の一覧については 公式ドキュメント を参照してください)。 layout, objectFit, objectPosition: 画像の幅と高さなどのレイアウトについての引数 <Image> コンポーネントでは画像のレイアウトに対しても豊富なオプションが提供されています。 layout : viewport を変更した時のレイアウトを表します 型: "fill" | "fixed" | "intrinsic" | "responsive" デフォルト値: intrinsic layout='fixed' : viewport の幅によらず、設定された width, height の画像を表示します。 layout='intrinsic' : width が viewport 幅よりも小さい場合は viewport 幅に合わせて小さくなりますが、画像の幅が viewport 幅よりも大きい場合は width の値に設定されます。 layout='responsive' : viewport 幅に依存して画像幅が変化します。 layout='intrinsic' の場合と異なり、画像の幅が viewport 幅よりも大きい場合は viewport 幅に合わせて画像幅が増加します。 layout='fill' : 親の DOM 要素の height, width に合わせて画像の幅と高さが設定されます。 objectFit , objectPosition : layout='fill' と同時に使用され、親の DOM 要素内での相対値を表すオプション object-fit , object-position の値を設定します。 下の画像は layout を "fixed", "intrinsic", "responsive" の3つのケースに対して、画像幅 width が viewport 幅よりも小さい場合と大きい場合でどのように表示されるかを比較したものです。 デフォルトでは "intrinsic" が適用されており、画像が viewport からはみ出ないようになります。 layout="fixed" は企業ロゴなど常に一定の大きさを保ちたいものに対して使用するのが良さそうです。 背景画像など viewport 幅に合わせて表示したい画像に対しては layout="responsive" が有効です(css の background-image で画像を指定することも可能ですが、next/image の RFC でも指摘されているようにパフォーマンスの悪化が懸念されます)。 <h2>layout: 'fixed'</h2> <Image src={`/river.png`} width={360} height={240} // viewportの幅によらず一定の画像幅を保つ layout={"fixed"} /> <h2>layout: 'intrinsic'</h2> <Image src={`/river.png`} width={360} height={240} // widthより小さいviewport幅の場合はviewport幅に合わせてスケール // widthより大きいviewport幅の場合はwidthに設定 layout={"intrinsic"} /> <h2>layout: 'responsive'</h2> <Image src={`/river.png`} width={360} height={240} // viewport幅に合わせてスケール layout={"responsive"} /> まとめ next/image で行われている画像の最適化は、拡張子とサイズの最適化、遅延ローディングと一つ一つを見るとシンプルです。 しかし、これらをブラウザやデバイスの差異を吸収しながら自前ですべて実装しようとするとコストもかかり、バグも生じやすくなります。 next/image を使用することでこの強力な最適化をほとんど zero config で実装でき、通常の <img> からの置き換えがしやすく非常に開発者にやさしい設計となっています。 また、width や height が必須の引数になっていたり、 layout="responsive" のオプションが提供されていたりと、パフォーマンスの悪化を招くような実装を自然に避けることができるように、注意深く設計されていることがわかります。 上で解説したように豊富なオプション引数があり、対象画像と要件に合わせて画像の表示タイミングやレイアウトを柔軟に設定できることも魅力の一つです。 新しく Next.js のアプリケーションを作るのであれば next/image を使わない手はないといってよいでしょう。 この記事の執筆中に Vercel が 40 億円の資金調達をした というニュースが入ってきました。 2021 年も Next.js の進化から目が離せませんね。
FORCIAアドベントカレンダー2020 21日目の記事です。 PostgreSQLのユーザー定義関数をRustで実装する話です。 こんにちは、エンジニアの松本です。主な業務としてインメモリデータベースをRustで実装しています。 フォルシアではPostgreSQLを使っており、C言語で 拡張 も書いていますが、Rustを使って書けるようになると環境構築やテストがしやすくなって嬉しいです。本記事ではRustで関数を実装するとPostgreSQLから使えるようにラップしてくれる zombodb/pgx というクレートを紹介します。 C言語実装との比較実験を行い、遜色ない速度で実行できることを確認しました。 環境構築 環境はUbuntu 20.04.1 LTS (Focal Fossa)で行います。 PostgreSQL13.1を 公式の手順 でインストールしました。加えて sudo ln -s /usr/local/pgsql-13.1 /usr/local/pgsql とシンボリックリンクを張り、 export PATH=/usr/local/pgsql/bin:$PATH としてパスを通している状態です。 何をやるか SQLでは扱いにくい処理を行うときにユーザー定義関数を書くことが多いです。 ループを含むような処理の例として、コラッツ予想で知られている「整数nについて偶数ならば n = n/2 、奇数ならば n = 3*n+1 とする」という手順を 「 n == 1 となるまで繰り返すときの回数」を返すような関数を作ります。 C言語で書く場合 まずはC言語での実装を示します。詳細は ドキュメント を参照して下さい。 # collatz.c #include "fmgr.h" #include "postgres.h" // `int32`, `int64` は `postgres.h` 内で定義されている。 PG_MODULE_MAGIC; PG_FUNCTION_INFO_V1(collatz_c); Datum collatz_c(PG_FUNCTION_ARGS); Datum collatz_c(PG_FUNCTION_ARGS) { int32 arg = PG_GETARG_INT32(0); // 第1引数をint32として取得する int64 n = arg; int32 count = 0; while (n > 1) { if (n % 2 == 0) { n /= 2; } else { n = 3 * n + 1; } count += 1; } PG_RETURN_INT32(count); // countをint32として返却する } コンパイルを行い、 $ gcc -shared -O2 -Wall -fpic -I/usr/local/pgsql/include/server collatz.c -o collatz_c.so $ sudo mv collatz_c.so /usr/local/pgsql/lib/ # CREATE or REPLACE FUNCTION collatz_c(int4) RETURNS int4 AS 'collatz_c.so', 'collatz_c' LANGUAGE C IMMUTABLE STRICT; CREATE FUNCTION # select collatz_c(12); collatz_c ----------- 9 12, 6, 3, 10, 5, 16, 8, 4, 2, 1 と遷移するので出力 9 が正しいことが確認できます。 pgxを使ってRustで書く場合 本題である zombodb/pgx を紹介します。PostgreSQL 10~13に対応しています。 cargo install cargo-pgx でサブコマンドをインストールします。 cargo pgx init を実行すると、pgxの検証用にPostgreSQL 10~13の各バージョンがインストールされます。ご飯が食べられるくらいには時間がかかります。 cargo pgx collatz として、ボイラーテンプレートからプロジェクトを作成します。 src/lib.rs に処理を実装します。 # lib.rs use pgx::*; pg_module_magic!(); #[pg_extern(immutable)] fn collatz_strict(arg: i32) -> i32 { if arg 1 { n = if n % 2 == 0 { n / 2 } else { 3 * n + 1 }; debug_assert!(n >= 1); count += 1; } count } #[pg_extern(immutable)] fn collatz(arg: Option ) -> i32 { match arg { Some(arg) => { if arg 1 { n = if n % 2 == 0 { n / 2 } else { 3 * n + 1 }; debug_assert!(n >= 1); count += 1; } count } None => { panic!("The function 'collatz' got a null, expected the arg is a positive integer.") } } } #[test] fn test_collatz() { assert_eq!(0, collatz_strict(1)); assert_eq!(1, collatz_strict(2)); // 2,1 assert_eq!(7, collatz_strict(3)); // 3,10,5,16,8,4,2,1 assert_eq!(2, collatz_strict(4)); // 4,2,1 assert_eq!(5, collatz_strict(5)); // 5,16,8,4,2,1 assert_eq!(8, collatz_strict(6)); // 6,3,10,5,16,8,4,2,1 assert_eq!(16, collatz_strict(7)); // 7,22,11,34,17,52,26,13,40,20,10,5,...,1 } #[test] #[should_panic] fn test_collatz_panic() { collatz_strict(0); } cargo test で動作確認をすることができます。同じファイルに手軽にテストを書き、標準のパッケージマネージャから実行できる点はRustの長所の一つだと感じます。 cargo pgx package でリリースビルドを行うと、 target/release 以下に必要なファイル群が作成されます。 $ tree target/release/collatz-pg13/usr/local/pgsql-13.1/ target/release/collatz-pg13/usr/local/pgsql-13.1/ ├── lib │ └── collatz.so └── share └── extension ├── collatz--1.0.sql └── collatz.control collatz--1.0.sql を確認すると、 collatz_strict には STRICT をつけて宣言していることが確認できます。引数に Option 型が含まれない場合は自動で STRICT をつけた宣言が作成されるようになっています。 -- collatz--1.0.sql CREATE OR REPLACE FUNCTION "collatz_strict"("arg" integer) RETURNS integer STRICT IMMUTABLE LANGUAGE c AS 'MODULE_PATHNAME', 'collatz_strict_wrapper'; CREATE OR REPLACE FUNCTION "collatz"("arg" integer) RETURNS integer IMMUTABLE LANGUAGE c AS 'MODULE_PATHNAME', 'collatz_wrapper'; 必要なファイルを移動し、extensionとして登録します。 $ sudo mv target/release/collatz-pg13/usr/local/pgsql-13.1/lib/collatz.so /usr/local/pgsql/lib/ $ sudo mv target/release/collatz-pg13/usr/local/pgsql-13.1/share/extension/collatz* /usr/local/pgsql/share/extension/ -- create extension collatz; CREATE EXTENSION select collatz(12); collatz --------- 9 速度比較 作成したそれぞれの関数について100万回実行時の速度を検証します。 -- テスト用テーブルを作成 # create table numbers as (select generate_series(1,1000000) as num); SELECT 1000000 Time: 1210.538 ms -- 結果が等しいことを確認 # select * from (select num, collatz(num) as rust, collatz_c(num) as c from numbers)s where rust!=c ; num | rust | c -----+------+--- (0 rows) -- 速度検証用のコマンド # select sum(collatz_strict(num)) from numbers ; sum ----------- 131434424 (1 row) # select sum(collatz_c(num)) from numbers ; sum ----------- 131434424 (1 row) 各関数について3回実行したところ下記の結果となりました。 関数 1回目[ms] 2回目[ms] 3回目[ms] C( collatz_c ) 464.390 475.266 465.974 Rust( collatz_strict ) 455.088 449.032 461.443 Rust( collatz ) 446.275 442.574 451.141 あくまで私の環境での実測値になりますが、Rust実装の方がC実装よりも高速に処理されることが確認できました。環境構築やテストの利便性を考えればRustに移行したほうがよいと考えられます。 まとめ 本記事ではPostgreSQLのユーザー定義関数をpgxを使ってRustで実装しました。C言語実装の関数と比べて遅くないことを確認しました。 本記事では紹介しきれませんでしたが、pgxには一通り必要な機能が揃っているように感じました。新しい関数は当然Rustで書くよねという時代が来るかもしれません。 フォルシアではPostgreSQLのパフォーマンス改善に強いエンジニアを募集しています。
本記事はNext.js Advent Calendar 2020の 22 日目の記事です。 ! 2020年12月時点の情報です Next.js 13(2022年10月リリース)で next/image は大幅に刷新され、本記事で解説している layout, objectFit, objectPosition などのプロパティは廃止されました。また、バージョンの違いにより本記事の記載内容と公式ドキュメントの内容が一部異なっている場合があります。 こんにちは。旅行プラットフォーム部エンジニアの東川です。 フォルシアではフロントエンドフレームワークとして Next.js を使用しています
FORCIAアドベントカレンダー2020 20日目の記事です。 こんにちは、旅行プラットフォーム部の島本です。現在B2C向けの新サービス立ち上げを企てています。 新規事業立ち上げのプロセスの一つにプロトタイプ作成があります。 フォルシアには社内製のWEBアプリケーションフレームワーク(2019年に 新フレームワークを開発 しています)があるのですが、プロトタイプ作成のような信頼性よりスピード性重視の場合には、オーバースペック感があります。 一方で、世の中にはコマンドをいくつか実行するだけでWEBアプリを立ち上げられる便利なツールもあります。普段私が扱っているNode.jsベースのものだとこれらが挙げられます。 create-next-app create-react-app しかし、商用化を見据えると、今後すべて作り変えるであろうプロトタイプといえど、なるべく自社フレームワークに近い構成で開発したい気持ちもあります。 そこで下記の要素を取り入れたプロトタイプ用WEBアプリのベースをさくっと作ってみることにしました。 Next.js + Expressのカスタムサーバの構成 TypeScriptで開発 Backends For Frontends(BFF)構成っぽくする DB参照も有り(フォルシアではPostgreSQLを利用することが多いため pg-promise を利用) このような構成になります。 プロジェクトの作成 npx create-next-app [project-name] cd [project-name] ※ [project-name]をnext_prototypeとして作成する TypeScriptで開発するための設定 npm install -D typescript @types/react @types/react-dom @types/node mv pages/index.js pages/index.tsx mv pages/_app.js pages/_app.tsx pages/_app.tsxを編集しTypeScript化。 import { AppProps } from 'next/app' const MyApp = ({ Component, pageProps }: AppProps) => { return ; } export default MyApp 起動できることを確認。 npm run dev 起動後、下記のtsconfig.jsonが作成される。 { "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "skipLibCheck": true, "strict": false, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve" }, "include": [ "next-env.d.ts", "**/*.ts", "**/*.tsx" ], "exclude": [ "node_modules" ] } Expressのカスタムサーバを導入 Next.jsはデフォルトではパスと一致するpagesディレクトリ配下の各ファイルにルーティングされます。 このルーティングに独自実装を組み込みたい場合にカスタムサーバを利用します。 例えば、特定のパスの場合のCookie操作やリダイレクト処理の実装などが挙げられます。 参考: https://nextjs-ja-translation-docs.vercel.app/docs/advanced-features/custom-server npm install express npm install -D @types/express mkdir server touch server/index.ts touch tsconfig.server.json server/index.tsを編集。 import express, { Request, Response } from "express"; import next from "next"; const dev = process.env.NODE_ENV !== "production"; const app = next({ dev }); const handle = app.getRequestHandler(); const port = process.env.PORT || 3000; app.prepare().then(() => { const server = express(); server.all("*", (req: Request, res: Response) => { return handle(req, res); }); server.listen(port, (err?: any) => { if (err) throw err; console.log( `> Ready on localhost:${port} - env ${process.env.NODE_ENV}` ); }); }); tsconfig.server.jsonを編集。 // for Next custom-server { "extends": "./tsconfig.json", "compilerOptions": { "module": "commonjs", // Next.jsとExpressの両方を連携させるために、commmonjsを利用 "outDir": "./dist", "noEmit": false, // Next.jsはBebelを使用してTypeScriptをコンパイルするので、TSコンパイラはjsを出力しない。設定を上書きする。 }, "include": ["./server"] } package.jsonのscriptを書き換える。 "scripts": { "dev": "tsc -p tsconfig.server.json && node ./dist/index.js", "build:next": "next build", "build:server": "tsc -p tsconfig.server.json", "start": "NODE_ENV=production node dist/index.js" }, 起動できることを確認。 npm run dev pg-promiseを用いてDataBaseを参照できるようにする PostgreSQLのインストールは割愛します。 npm install pg-promise @types/pg-promise touch modules/database.ts modules/database.tsを編集。 import pgPromise from "pg-promise"; const pgp = pgPromise({}); const config = { db: { // 設定項目: https://github.com/vitaly-t/pg-promise/wiki/Connection-Syntax host: "127.0.0.1", port: 5432, database: "mydb", user: "user", password: "password", max: 10, // size of the connection pool query_timeout: 60000 // 60sec } }; export const sqlExecuter = pgp(config.db); Next.jsのdevelopment modeでの起動中にソースを修正してrebuildが走るとpg-promiseの下記のWarningが発生します。 WARNING: Creating a duplicate database object for the same connection. production mode では問題ありませんが、気になる場合は Singleton pattern でコネクションを再利用するよう実装することで回避できます。 詳しくはこちら: https://github.com/vercel/next.js/discussions/18008 DBから取得したデータを返すAPIのエンドポイントを追加 Next.jsでは pages/api 配下のファイルが、 /api/* でアクセス可能なAPIエンドポイントとして扱われるため、ファイルを配置するだけでAPIエンドポイントを作成できます。 参考: https://nextjs.org/docs/api-routes/introduction touch pages/api/data.ts pages/api/data.ts を編集。 import { sqlExecuter } from "../../modules/database"; export default async (req: any, res: any) => { const data = await sqlExecuter.any( "select 'DB参照したデータ' as any_column" ); res.status(200).json({ data }); }; APIをfetchして取得したデータを画面に書き出す npm install axios touch modules/request.ts modules/request.tsを下記の通りに編集。 import axios from "axios"; const serverSideBaseURL = "http://localhost:3000/api"; const clientSideBaseURL = "http://localhost:3000/api"; const requestInstance = axios.create({ baseURL: serverSideBaseURL }); const clientRequestInstance = axios.create({ baseURL: clientSideBaseURL }); export const getRequestInstance = (isServerSide: boolean) => { if (isServerSide) { return requestInstance; } return clientRequestInstance; }; ※Next.jsのgetInitialPropsの処理はサーバ側・クライアント側のいずれでも実行されるため、APIのパスが変わる場合の考慮が必要です。 pages/index.tsxを編集。 import { NextPage } from 'next' import { getRequestInstance } from "../modules/request"; const Page: NextPage = ({ data }) => { return data.map( (d: any, index:number) => <div>{index}番目のデータ: {d.any_column}</div> ) } Page.getInitialProps = async (ctx: any) => { const request = getRequestInstance(Boolean(ctx.req)); const res = await request.get("data").then(res => res); return res.data; } export default Page ブラウザで http://localhost:3000 にアクセスし画面にDB参照して得られたデータが表示されることを確認。 おしまい。 終わりに いかがだったでしょうか?APIやDBのクライアントもインストールするだけですぐに使えて便利な世の中ですね。今回はAPIのレスポンスをそのまま書き出すまでで終わりましたが、 Material UI などのコンポーネントライブラリを使うことで画面の作成も簡単にできます。 私が開発中の新サービスはいずれ公開できればと思います。
FORCIAアドベントカレンダー2020 20日目の記事です。 こんにちは、旅行プラットフォーム部の島本です。現在B2C向けの新サービス立ち上げを企てています。 新規事業立ち上げのプロセスの一つにプロトタイプ作成があります。 フォルシアには社内製のWEBアプリケーションフレームワーク(2019年に新フレームワークを開発しています)があるのですが、プロトタイプ作成のような信頼性よりスピード性重視の場合には、オーバースペック感があります。 一方で、世の中にはコマンドをいくつか実行するだけでWEBアプリを立ち上げられる便利なツールもあります。普段私が扱っているNode.jsベースのものだとこれ
FORCIAアドベントカレンダー2020 19日目の記事です。 検索プラットフォーム事業部の小海です。技術部教育チームにも所属し、新入社員研修に関わっています。 今年の新入社員研修はコロナの影響を受け、すべての研修をオンラインで行うこととなりました。 今回は、オンライン研修を行うにあたって技術部教育チームが実践したことを紹介します。 新入社員研修 フォルシアの新入社員研修では通常、入社1ヵ月は基礎研修を実施し、社会人としての土台をしっかりと学びます。その後1ヵ月はジョブローテーションを行い、配属予定の部署以外の仕事を経験し、社内全体を理解していきます。 最初の2ヵ月の研修については こちらの記事 をご覧ください。 基礎研修・ジョブローテーションが終わった後、技術基礎研修としてHTML・CSS・JavaScript・SQLなどをWEB教材や先輩社員による技術講義で学んだのち、名簿アプリ研修で検索アプリの全体像を掴みます。その後、仮配属で実際の業務を先輩社員と一緒に1ヵ月ほど経験してから本配属となります。 技術部新入社員研修については こちらの記事 、名簿アプリ研修については こちらの記事 をご覧ください。 今年起こったこと 通常は新入社員研修を2ヵ月実施したのちに技術部新入社員研修となるため、技術部教育チームは6月からの研修に向けて準備を進めていました。しかし、コロナの影響で新入社員研修はすべてオンライン研修となりました。 基礎研修は外部研修なども多く、ジョブローテーションもオンライン研修の準備が整っていなかったため、6月から予定していた技術教育を急遽4月から行うこととなりました。 今年の新入社員のインタビュー記事もあります。新入社員目線からの声は こちらの記事 をご覧ください。 スケジュールが大きく変わり、試行錯誤の中で取り組んだオンライン研修でフォルシアの技術部教育チームが実践したことは以下の4点です。 1) 積極的にコミュニケーションを フォルシアは新卒採用時に人柄を重視しており、プログラミングの経験が浅くても採用する方針を採っています。 新入社員歓迎会や基礎研修等の実施が難しい中での研修となったため、新入社員のプログラミング経験や人柄を理解してから研修に臨むために下記を実践し、積極的にコミュニケーションを取るよう心掛けました。 新入社員研修前ヒアリング 研修初日に技術部教育チームと新入社員と1対1でのヒアリングを設定しました。研修に取り組む前に、プログラミング経験だけではなく社会人生活や在宅環境などの懸念を聞くことで、研修全体の満足度向上に繋げることができました。 コミュニケーションセッション 研修開始3週間は新入社員・技術部教育チーム・技術部2年目の先輩社員で話す場を用意しました。週に3回30分間、5名程度のグループに分け、毎回組み合わせを変えて設定しました。 小さな疑問から抱えている課題など、実際に研修を受けている新入社員の声を聴くことで、研修内容の再検討や追加講義など臨機応変に対応することができました。 KPTふりかえり会 基礎研修が終わり仮配属となると新入社員1人1人がそれぞれ違う業務に取り組みます。仮配属期間中は週に1回1時間を使って、技術部教育チーム2名と新入社員とでKPT法を用いた振り返り会を設定しました。 それぞれの学びや課題を共有することで、学びをより深め、他者の学びを吸収し、課題の解決策の道標とすることを目的としています。オンラインでは Google WorkspaceのJamboard を使用しました。 2) 余った時間の有効活用 新入社員はプログラミング経験に差があるため、技術課題では進捗に大きな差が生まれることがあります。 オンライン研修では進捗が見えにくいためフォローが難しく、新入社員が手持ち無沙汰になってしまうことがあります。課題が早めに終わってしまった新入社員のために、下記の内容を事前にまとめておくことで時間を有意義に使ってもらうことができました。 過去の全体会議資料 先輩社員の自己紹介 情報管理ツール「esa」 にある先輩社員が書いたおすすめ記事 業務に役立つ書籍 3) オンライン研修のメリットを活かす オンライン研修のメリットを活かすため、下記のことに取り組みました。 先輩社員による技術講義の聴講 フォルシアは独自技術も多いため、社内での技術講義も多くあります。オンラインでは会議室の定員に縛られずに行えるため、新入社員以外の聴講を可としました。今年は技術講義の見直しも行われ、新設された講義の中には20名近くが参加する講義もありました。 画面共有 講義や技術的な説明をする時には先輩社員の画面を共有をすることで、エディタやツールなど実際の作業環境を見る機会にもなりました。 チャットの有効活用 オンライン講義では、オンライン会議ツールのチャット機能を自由開放し、講師も都度見るようにすることで、感想や質問など発言しやすい雰囲気を醸成することができました。 オンライン輪読会 技術部教育チームが選定した書籍でオンライン輪読会を行いました。同期や先輩社員と一緒に読み、発表や質問等を行うことで、1人で読むよりも効率的に学べるだけではなく、発表や質問の練習にもなりました。 4) オンライン研修を楽しむ 技術部教育チームのメンバーも在宅研修に慣れていない中で研修に臨みましたが、『新入社員にオンライン研修を楽しんでもらう』ことを最優先に考え、そのために『技術部教育チームもオンライン研修を楽しむ』ことを心掛けました。 私が特に印象に残っているのは3月末に行われた技術部教育チームのミーティングです。 技術部教育チームとしては2ヵ月後に予定していた研修を急遽来週から行う事になり、オンライン用に研修内容を再検討しなければならない状況でした。しかし、そのミーティングでは「困ったね」「どうしよう」ではなく、その状況を前向きに捉え「オンラインだからこそできる研修」のブレストを行いました。 この前向きに楽しむ姿勢は研修を通じて新入社員にも伝わっていたと考えています。何事にも前向きな技術部教育チームのメンバーにはとても感謝しています。 さいごに オンライン研修にあたって技術部教育チームが心掛けたことを紹介してきましたが、成功した要因としては、若手社員が積極的に協力してくれたこと、そして何より、新入社員が楽しく前向きに取り組んでくれたことが大きいと実感しています。 次年度がどのような状況での研修になるかはまだわかりませんが、今年度実践したことを活かして、さらにパワーアップした技術部新入社員研修にしたいと思います。
FORCIAアドベントカレンダー2020 18日目の記事です。 検索プラットフォーム部 エンジニアの石川です。 2019年にキャリア入社し、それ以前はカーナビゲーションのアルゴリズム開発や自動運転技術の開発に従事していました。 ここ数年、AI(人工知能)を搭載した製品やサービスがとても増えており、エンジニアでなくともAIに興味を持つ方が増えているように感じています。 また、AIエンジニアに対する需要も増しており、AIエンジニアを目指す方も増えているようです。 今日はAIがどういうものかをあまり知らない方や、AIエンジニアを目指そうとしている方へ向けた記事を書きたいと思います。とはいっても自分もまだまだ勉強中なので、あくまで私個人の解釈と意見になります。 近年のAIというと機械学習を指すことが多いのですが、これは「コンピュータが一定のルールに基づいて大量のデータを分析して、特徴やパターンを学習する」というもので、代表的なものではスパムメールの判定やECサイトでの商品のレコメンドなどに使われています。 特にこの「一定のルールに基づいて」の部分に人間の脳の構造を模した仕組みを応用したものは深層学習(ディープラーニング)と呼ばれ、画像認識や音声認識、自然言語処理などに使われており、この数年で飛躍的に精度が向上して、人間に近い判断ができるようになってきたように思います。 その一方で、学習した結果コンピュータが内部でどのように判断したか、ということが非常にわかりにくくなってしまったという側面もあります。 ちなみにそれ以前のAIはどういったものかというと、人間が大量のデータを分析して特徴やパターンを見出してそのパターン通りに動くようにプログラムしていました。これはこれで開発者が意図した通りに動くため管理がしやすく、きめ細やかな設計をしていればむしろ高性能な面もあり、日本製品のクオリティの高さの一因でもあるので、決して古いから使えない、というものではないのです。特に乗り物の制御などの安全に関わる分野においては、動作する状況を限定的にして間違いのない判断をさせることの方が重視されるように思います。 ディープラーニングとは ここ数年AIが注目を集めている最大の理由の一つは、上記の深層学習(ディープラーニング)の発達にあります。 先ほど説明したとおり「人間の脳の構造をコンピュータで疑似的に作り出して学習させる」ということなのですが、これだけではよくわからないと思うので、ここでは具体的に、子供に勉強させる、という例で解説してみようと思います。 さて、親が子供(幼稚園児か小学校低学年くらいの子としましょう)に色々な動物を見分けられるように教えようとする場面を想像してみてください(動物のことは知らないけど、計算はめちゃめちゃ早くて記憶力も抜群な子供です)。 親は子供のために子供部屋や学習机などの学習環境を用意して、カリキュラム(どんな教材をどのくらい、どんなふうに勉強するか)を考えて、教材として図鑑を用意します。このあとどのように勉強させるでしょうか? ひとりで図鑑が読める子なので逐一教えなくても自分で学んでいきます。親は定期的に「これ、なーんだ」と問題を出してテストしてあげるだけです(教えるのではなく、自分で学習していくやり方は公文式に似ていますね)。 こうしてめでたく子供は動物を見分けられるようになり、正面だけでなく後ろ姿だったり体の一部しか見えなくても正解するレベルになりましたが、「なんでそう判断したの?」と尋ねても、「そう思ったからだよ」としか答えてくれず、うまく言葉で表現はできません。なので、これ以上カリキュラムやテキストをどう直したらもっとできるようになるのか親にはさっぱりわかりませんし、その子に合わせてカスタマイズしていかないとこれ以上のレベルに到達するのは無理そうです。 また、最初は楽しんで学習していた子供もあんまり「お勉強」をさせすぎると、テストでいい点とれればいいや、という考えになり丸暗記に走ってしまって応用が効かなくなることもあります(機械学習ではこれは過学習と呼ばれます)。 逐一教えなくていいのは良いのですが、この無限の可能性を秘めた子供に何をどうどこまで学習させたら良いのかは親にもわかりません。ちなみに私も一児の親ですが、ウチの子にどんな才能があるのか誰か教えて欲しいです! AI開発とは さて、今度は上記の話を専門用語を交えつつAI開発について説明してみます(必ずしも1対1で専門用語と結びつくわけではないので正確性を欠いているところもありますが、イメージで捉えていただければと思います)。 まず親(AIエンジニア)は子供(モデル)に何ができるようになって欲しいのかを考えます。 次に学習環境(フレームワーク)や筆記用具(ライブラリ)を用意し、その子どもの脳の特性(ニューラルネットワーク)に応じたカリキュラム(ハイパーパラメータ)を設定し、テキスト(学習データ)を用意し勉強させ、定期的にテスト(検証データ)を実施することを何度も繰り返すことで学習を進めます。 これを開発フローにすると、まずAIで解決したい課題をきちんと設定することから始まります。 次にその課題を解決するために必要な環境や道具を選定し、手に入れます。 幸い、AI開発に必要なフレームワークやライブラリの多くは無料で提供されています。 ただし、多くの種類がありますし、モデルや目的に合わせたものを選ぶ必要があります。 そして課題解決のための要となる学習・検証用のデータが必要になりますが、実際にこれを入手、作成するのが非常に大変です。 研究用に使われている学習データはたくさんありますが、あくまで研究用なので商用利用ができなかったり、それで学習したモデルに現実の問題を解かせても十分な精度がでないこともあります。 それだけでなく、自分で作成したデータはあらかじめ手作業で分類したり、学習に適した形に加工したりする必要があるので非常に手間がかかる作業です。 こうして必要なものが一通り揃ったら、実際に学習させ精度を確認します。 一度で十分な精度が出ることはほぼないので、何度何度もモデルを見直したりパラメータのチューニングをして学習を繰り返して精度を向上させていきます。 簡単にですが機械学習を用いない場合のアルゴリズム開発フローと機械学習によるモデル開発フローを図にしてみました。 大きな流れは変わらないのですが、機械学習ではデータの加工(学習データの作成)とモデルの学習が必要となることが特徴です(図のピンク枠の部分)。 もちろん、機械学習でない場合でもデータの加工をすることはありますが、多少データが粗かったり不足したりしていてもエンジニアのKKD(経験、勘、度胸)でロジックを直接調整できるのに対して、機械学習では学習データを介してモデルを学習させるためデータの質がモデルの精度に直結するというところが大きな違いになります(学習データの作成、加工の部分ではエンジニアのKKDを発揮することは大いにあるようです)。 エンジニアの在り方 さて、冒頭にお話したようにAIエンジニアへの需要は高まっているらしいのですが、AIエンジニアには何が求められているのでしょうか。 AIエンジニアになるためには、と調べると概ね以下の3つを学びましょうと出てきます。 プログラミング言語(Pythonなど) 数学(統計学、微分積分、行列など) 機械学習(フレームワークやライブラリ、ニューラルネットワークなど) たしかに技術スキルとしてはこれらになるのですが、個人的には AIで解決できる課題を発見すること AIが解決できるレベルに課題を単純化すること が重要だと思っており、これらはAIでは代替できないものです。 現在AIはあらゆる業界で使える道具になってきているので、趣味でも、他職種でも良いので様々な経験やバックグラウンドを併せ持つことで色々な視点から課題を解決していけるエンジニアが求められるのではないかと思います。 あくまで私見となりますが、製品やサービスの開発においてはAIについて論理的に精通していることよりも技術をうまく使いこなせることの方が大事だと思っています。 ディープラーニングが流行し始めた頃であれば、そのロジックに精通していることは重要だったかもしれませんが、今はフレームワークやライブラリもかなり使いやすくなっていますし、AmazonやGoogleのクラウド上でモデルの学習やAIによる判断も提供されていますので技術としてAIを使う敷居はかなり低くなってきたのではないかと思います。 どのような技術も、導入時と発展時ではエンジニアに求められるスキルは異なるものです。 どうしてもAIというワードが独り歩きしてしまいがちですが、AIはあくまでビジネス上の課題を解決するためのツールであるという意識で捉えてもらうと良いと思います。 最後に AI開発について、エンジニアではない方にも理解していただけるように書いてみましたが、いかがでしたでしょうか。 今後もAIの活用は続いていくと思いますがその開発の背景には、子を想う親の気持ちが詰まっている(!?)と理解していただければ幸いです。
FORCIAアドベントカレンダー2020 17日目の記事です。 新卒2年目エンジニアの平岡です。 2年前の今頃は有機合成化学の研究室で、試薬を混ぜてひたすら実験をしていました。 この記事ではTwitter APIを題材に、普段業務で触れる機会の少ないインフラ周りや複数の言語を触るなどして遊んだ話を書きます。 Twitter API 何かの情報を集める際、google検索だけでなくTwitter検索を使うこともあり、タイムリーな情報や口コミ等の情報はTwitter検索が適していると思います。 TwitterではAPIを提供 しており、タイムラインの取得・投稿や検索結果の取得ができるようです。 search APIを使ったキーワード検索を題材に、アプリの実装からAWSでの公開まで広く浅く触れてみました。 使用申請 TwitterAPIを利用するための各種トークンを発行してもらうために申請が必要です。 こちら から申請できます(要ログイン)。使用目的などを英作文し、申請から約3時間後には使用可能な状態になりました。 認証 Twitter公式のドキュメント にOAuth 1.0とOAuth 2.0、Basic認証について載っています。OAuth 1.0では個人アカウントでログインしたときと同じ機能が使えるのに対し、OAuth 2.0では公開されている情報までしか扱えないようです。 今回触ってみたsearch APIでは特にどれを用いても問題ないですが、例えば一般に公開されていない鍵アカウントのタイムラインの取得はOAuth 2.0の認証ではできないと思われます。 OAuthとは何ぞや、という方は、以下の記事が参考になります。 一番分かりやすい OAuth の説明 大まかな概念はこの記事でつかめます。 OAuth 2.0 の仕組みと認証方法 OAuth 1.0とOAuth 2.0の違いと仕組みについて書かれています。 やりたいこと TwitterAPIをたたいて結果をブラウザで見られるようにしたい ローカルからだけでなく外からでも見れるようにしたい 普段触っていない言語を触ってみたい ということを漠然と思いながら、以下のステップで進めていきました。 コマンドライン上で結果を取得する(Python) 取得した結果をブラウザに表示する 公開する(AWS) Rustでも実装してみる コマンドライン上で結果を取得する(Python) まずは、日本語の記事が多くて書きやすいPythonで書くことにしました。 環境 Ubuntu20.04(WSL2) Python3.8.5 実装 参考記事 の通りの実装でコマンドライン上での動作を確認できました(自分が書いたコードは後述します)。 今回は無料で使うことのできるStandard searchを触っています。こちらでは7日間のツイートを検索することができます。 search APIは複数種類用意され ており、30日間のツイートを検索できるものや2006年以降のツイートを検索できるものもあるようです。 Standard searchの使い方や用いることができるパラメータについては こちら に載っています。 また、各種トークンについては 開発者用管理画面 から、自分のプロジェクトのKeys and tokensタブにて確認することができます。申請時に発行されたトークンを忘れてしまった場合も、ここで再生成することができます。 取得した結果をブラウザに表示する(CGI) 参考記事:PythonでCGIを用いたWebアプリケーションを作る CGIサーバーを起動する import http.server http.server.test(HandlerClass=http.server.CGIHTTPRequestHandler) このファイルを実行すればCGIサーバーが起動します。 $ python cgiserver.py Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ... 同階層にindex.htmlを配置して http://localhost:8000 にアクセスすると、そのhtmlが表示されます。 index.html formにキーワードを入力し、Twitter APIを叩くPythonファイルを実行させます。 出力先を target="result" で指定してiframe内に検索結果を表示させることで、検索時に画面遷移をさせないようにしました。 <!DOCTYPE html> <html> <head> <title>CGI Sample</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </head> <body> <form action="./cgi-bin/search.py" method="POST" target="result"> <input type="text" name="text" value="test" /> <input type="submit" name="submit" /> </form> <iframe name="result" style="top: 200px;height:500px;width:100%;border:0px;margin:100px"></iframe> </body> </html> search.py 各種キー・トークンはconfigファイルに記載しました。 CONSUMER_KEY = "-----------------" CONSUMER_SECRET = "-----------------" ACCESS_TOKEN = "-----------------" ACCESS_SECRET = "-----------------" #!/usr/bin/env python3 # -*- coding: utf-8 -*- import config import urllib from requests_oauthlib import OAuth1 import requests import cgi def main(): # configの値を使う CK = config.CONSUMER_KEY CKS = config.CONSUMER_SECRET AT = config.ACCESS_TOKEN ATS = config.ACCESS_SECRET # フォームからキーワードを受け取る word = cgi.FieldStorage().getvalue('text', '') count = 10 # 一回あたりの検索数(最大100/デフォルトは15) range = 10 # 検索回数の上限値(最大180/15分でリセット) # iframeに埋め込むHTML html_body = """ <!DOCTYPE html> <html> <head> <title>検索結果</title> <style> h1 { font-size: 3em; } </style> </head> <body> <h1>TwitterAPI Result</h1> <div>%s</div> </body> </html> """ if not word: print("キーワードを指定してください") else: tweets = search_tweets(CK, CKS, AT, ATS, word, count, range) print(html_body % (''.join(tweets))) def search_tweets(CK, CKS, AT, ATS, word, count, range): # 文字列設定 word += ' exclude:retweets' # RTは除く word = urllib.parse.quote_plus(word) # リクエスト url = "https://api.twitter.com/1.1/search/tweets.json?lang=ja&q=" + \ word+"&count="+str(count) auth = OAuth1(CK, CKS, AT, ATS) response = requests.get(url, auth=auth) data = response.json()['statuses'] # 2回目以降のリクエスト cnt = 0 tweetsCount = 0 tweets = [] while True: if len(data) == 0: break cnt += 1 if cnt > range: break for tweet in data: tweetsCount += 1 user = tweet["user"] tweets.append(str(tweetsCount) + "件目 ") tweets.append("name:" + user["name"] + "\n" + " ") # tweets.append(user["statuses_count"]) # 投稿数 # tweets.append(user["friends_count"]) # フォロー数 # tweets.append(user["followers_count"]) # フォロワー数 tweets.append("投稿日時:" + tweet["created_at"] + "\n" + " ") tweets.append( "いいね数:" + str(tweet["favorite_count"]) + "\n" + " ") tweets.append( "リツイート数:" + str(tweet["retweet_count"]) + "\n" + " ") tweets.append(tweet['text'] + "\n" + " ") maxid = int(tweet["id"]) - 1 url = "https://api.twitter.com/1.1/search/tweets.json?lang=ja&q=" + \ word+"&count="+str(count)+"&max_id="+str(maxid) response = requests.get(url, auth=auth) try: data = response.json()['statuses'] except KeyError: # リクエスト回数が上限に達した場合のデータのエラー処理 print('上限まで検索しました') break return tweets if __name__ == '__main__': main() search.pyには実行権限を付けておきます。 $ chmod 755 search.py 検索する 「http://localhost:8000」 にアクセスし、フォームにテキストを入力して送信するとTwitterでの検索結果が表示されました。 公開する(AWS) フォルシア入社後の研修でAWSを触る機会があり、EC2を立てるのはとても簡単だったので AWSのEC2 での公開を試みました。 アカウント作成 公式のフロー に従ってプロフィールを入力します。お支払方法(クレジットカードなど)の登録が必要なのですが、無料枠で遊ぶこともできます。 EC2を立ててsshする サインイン後EC2で検索し、 ダッシュボード に移ります。 ダッシュボード内やインスタンスタブから 「インスタンスを起動」をクリック します。 今回は無料枠で使うことのできるAmazon Linux 2 AMI (HVM)を使います。 「インスタンスタイプの選択」では、無料枠で使えるものがt2.microのみです。 「セキュリティグループの設定」に移り、必要なポートを設定します。 「確認と作成」→「起動」をするとキーペアについて確認されます。 初回はキーを作成してダウンロードします。 .ssh以下など適切な場所に配置し、 chmod 600 で権限を変更します。 起動しているインスタンス一覧が見れるページ にて、インスタンスの状態が実行中になればssh接続できます。「パブリック IPv4 DNS」(ec2-xx-xx-xx-xx.us-east-2.compute.amazonaws.com)を確認します。 ssh -i ~/.ssh/${key} ec2-user@${パブリック IPv4 DNS} あとは好きに遊ぶことができます。 Docker インスタンスを終了すると環境やソースが消えますが、インスタンスを起動する都度環境を作り直すのはとても手間がかかります。 そこで、Dockerfileを作りコマンド1回の実行で環境構築を済ませるモチベーションが生まれました。Python3.8.5とpipを入れて、必要なライブラリをpip installすれば環境は完成です。 FROM python:3.8.5 # ユーザ作成 RUN groupadd web RUN useradd -d /home/python -m python WORKDIR /home/python # pip RUN wget https://bootstrap.pypa.io/get-pip.py | python RUN apt-get update && apt-get install -y urllib3 requests_oauthlib requests # サーバ設置ファイル ADD cgiserver.py /home/python ADD index.html /home/python # cgi-binフォルダを作成 RUN mkdir cgi-bin ADD search.py /home/python/cgi-bin ADD config.py /home/python/cgi-bin RUN chmod 755 /home/python/cgi-bin/search.py # ポート番号を指定して、CGIサーバを起動 EXPOSE 8000 ENTRYPOINT ["/usr/local/bin/python", "/home/python/cgiserver.py"] USER python AWS上でアプリ起動 必要なファイルが少ないので手動でscpしました。 $ tree . ├── aws_init.sh ├── cgi-bin │ ├── config.py │ └── search.py ├── cgiserver.py ├── docker │ └── python_cgi │ └── Dockerfile ├── index.html 権限に問題のないホームに置きます。 scp -i ${key} index.html cgiserver.py docker/python_cgi/Dockerfile cgi-bin/search.py cgi-bin/config.py aws_init.sh ec2-user@ecxx-xx-xx-xx-xx.us-east-2.compute.amazonaws.com:~ インスタンスにssh後、Dockerを入れてbuild, runすれば環境構築・CGIサーバーの起動が完了です。以下のシェルスクリプトを用意してscpし、実行しました。 sudo yum update sudo yum install -y docker sudo service docker start sudo usermod -a -G docker ec2-user mkdir cgi-bin mv search.py cgi-bin mv config.py cgi-bin sudo docker build -t python_cgi . sudo docker run -d -p 8000:8000 python_cgi ssh -i ${key} ec2-user@ecxx-xx-xx-xx-xx.us-east-2.compute.amazonaws.com:~ [ec2-user@ip-xx-xx-xx-xx-xx ~]$ chmod +x aws_init.sh [ec2-user@ip-xx-xx-xx-xx-xx ~]$ ./aws_init.sh 「http://ec2-xx-xx-xx-xx.us-east-2.compute.amazonaws.com:8000」 にアクセスすればローカルと同じCGIアプリを見ることができます。 このURLで外部からも参照することができます。 注意 データの保存 EC2単体だとインスタンスを終了させると、インスタンス内で行った変更は破棄されます。データの保存には EBS を併用すると良いです。 IP IPを固定しないとインスタンス再起動時にはパブリック IPが変わります(現状ではIPを固定する必要が特に無いためそのままにしています)。 課金額 インスタンスを立てたままにするなどして、無料枠を超えた利用が発生すると登録しているクレジットカードから課金が発生します。 最初の12か月間は月750時間までは無料枠で使うことができるらしいです。今回の設定でEC2インスタンスを1台だけ起動する分には立てたままでも課金は発生しない(1台だと最大で月に24×31=744時間)と思われますが、何に課金が発生するかはしっかり確認した上で使いましょう。 Rustでの実装 下準備 rustlings を一通り解いて、ある程度Rustのソースが読めるようになりました。最初は、 非同期処理runtime: tokio サーバー: warp HTTP request: reqwest として、Twitter APIを叩いた結果のjsonを返すものを作ろうとしました。 warpで受け取ったパラメータを使ってreqwestでTwitter APIを叩く、というような実装を書いたのですが、warpでjsonを返す処理の中でTwitter APIを叩く処理が上手く書けなかったため、別のcrateを検討しました。RustのWebフレームワークで主要なものに、 Rocket と Actix Web がありますが、Rocketは非同期処理に対応していないためActix Webを使うことにしました。 各crateにはexampleが用意されており、実装の際に参考になりました。 https://github.com/seanmonstar/warp/tree/master/examples https://github.com/seanmonstar/reqwest/tree/master/examples https://github.com/actix/actix-web/tree/master/examples 環境 rustc 1.48.0 cargo 1.48.0 [package] authors = ["hiraoka"] edition = "2018" name = "twitter" version = "0.1.0" [dependencies] actix-web = "3.3.2" actix-rt = "1.1.1" reqwest = { version = "0.10.9", features = ["json"] } serde = { version = "1.0.117", features = ["derive"] } serde_json = "1.0.59" dotenv = "0.15.0" qstring = "0.7.2" コード bearer_tokenを用いるOAuth2.0認証を使いました。.envファイルにbearer_tokenを書きました。 bearer_token = AAAAAAAAAAAAAAAAAAAAA-------- use actix_web::{middleware, web, App, HttpRequest, HttpResponse, HttpServer}; use dotenv::dotenv; use qstring::QString; use reqwest::header::{HeaderMap, AUTHORIZATION}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::env; #[derive(Debug, Serialize, Deserialize)] struct SearchResult { search_metadata: Value, statuses: Vec , } #[derive(Deserialize, Serialize)] struct QueryObject { q: String, count: u32, } struct Twitter {} impl Twitter { pub fn new() -> Self { Twitter {} } pub async fn search( &self, _req: HttpRequest, ) -> Result<SearchResult, Box > { let endpoint = "https://api.twitter.com/1.1/search/tweets.json"; let mut headers = HeaderMap::new(); // .envファイルのトークンの値を読み込む dotenv().ok(); let bearer_token = env::var("bearer_token").expect("bearer_token is not found"); headers.insert( AUTHORIZATION, format!("Bearer {}", bearer_token).parse().unwrap(), ); let query_str = _req.query_string(); let qs = QString::from(query_str); let q = qs.get("q").unwrap(); let count = qs.get("count").unwrap(); let client = reqwest::Client::new() .get(endpoint) .query(&[("q", q), ("count", count)]) .headers(headers); let res: SearchResult = client.send().await?.json().await?; Ok(res) } } async fn twitter_search(req: HttpRequest) -> HttpResponse { let result = Twitter::new().search(req).await; match result { Ok(res) => HttpResponse::Ok().json(res), Err(err) => HttpResponse::InternalServerError().body(err.to_string()), } } #[actix_rt::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .wrap(middleware::Compress::default()) .service(web::resource("/twitter_search").route(web::get().to(twitter_search))) }) .bind("0.0.0.0:8000") .expect("Can not bind to port 8000") .run() .await } cargo run して 「http://localhost:8000/twitter_search?q=keyword&count=10」 など、検索ワード(q)とカウント(count)をパラメータを指定するとTwitter検索結果のjsonが返ってきます。 おわりに 初めからRustでの実装を試みていたのですが、Rustのエラー処理や型の理解が浅く、コンパイルに苦戦したため、Pythonで動くものを作ってからRustで再実装しました。それぞれの領域でベストプラクティスを試せたわけではありませんが、広く触ってみるのは楽しかったです。 Pythonは調べると日本語の記事が沢山出て来る上、普段触っていないにしても動くものを作るまでのコストはRustと比較して低かったです。 一方で、Rustはこれまで自分が触れたことの無い概念が多くあり、学習コストが高かったです。また、 Rustの安定版がasync/await構文をサポートしたのは2019年11月 と日が浅く、非同期処理・同期処理のサンプルが混在して見つかり混乱することもありました。困ったらgithubのソースやcrateのドキュメントを見るのが一番ですね。もっとRustのコンパイラーと仲良くなりたいです。
FORCIAアドベントカレンダー2020 16日目の記事です。 旅行プラットフォーム事業部の山門です。 Versel社が現地時間の10/27に開催したNext.js CONFでは、開催当日にver10の発表もあり、なかなかに盛り上がりを見せたのが記憶に新しいですね。 CONFでは画像周りのアップデートが大きく取り上げらていた印象ですが、自分の中では同タイミングで発表されたNext.js Analyticsの方に興味が湧いたので、これを機にNext.js Analyticsについて調べてみました! Next.js Analytics is 何者? 本家のサイト をざっと見てみると、 実際のユーザーが操作したデータをもとに、サイトの本当のパフォーマンスを提供することができると紹介されています。 こういったパフォーマンスはリクエストをシミュレーションして計測することが多いですが、 Next.js Analyticsは実際にユーザーがページを見たときのデバイスをもとに情報を収集しているようで、 この実際の生きた情報、というのが推しポイントのようです。 始め方 公式の Analyticsページ を参考に詳細を見ていきましょう。 ※前提として、Next.jsのバージョンが10以降である必要があり、9以前のバージョンの場合はアップグレードが必要となります。 始め方は大きく分けて二つあり、Next.jsを開発しているVercel社が提供する、Vercelというサービス上で動いているアプリか、そうでないアプリかで分かれます。 Vercel上で動いているアプリの場合、Vercel上で適当なアプリを立ち上げ、Analyticsを有効にすると使えてしまうため、 それとは少し違うパターンで動くかどうかを試してみました。 フォルシアの一部サービスでは、AWS上にNext.jsを使ってアプリ構築をしており、全く同じではないのですが、 その構成を意識して、AWS Amplify上にNext.jsで構築したアプリに対して、Next.js Analytics導入を試してみました。 ということで、早速やっていきましょう。 公式の Self-Hosted の項目をみると以下の手順を踏むことで始めることができます。 templateからVercelのnew Projectを作成 作成後、プロジェクトのDomainsタブから、残っているドメインを削除 AnalyticsタブからNext.js Analyticsを有効化 有効化して表示されるconfig設定を、アプリの適切な箇所に配置 データが一定量貯まるまで待っていると、測定結果が見られるようになる キャプチャも交えて順を追ってみましょう。 ※注意書きで、ProプランまたはEnterpriseプランの場合のみ、Vercel以外でホストされているアプリケーションで使用できると書いているのですが、1日分であればお試しでも見られるようでした。 下準備 1. templateからVercelのnew Projectを作成 プロジェクトのtemplate作成画面 からNext.jsを選択します。 選択すると、下記のようにGitHub,GitLab,Bitbucketのいずれかでログインするように求められるので、 今回は社内の利用ツールに合わせてGitLabを選択します。 初回ログイン時だと、下図のようなアクセス権限を求められますが、Authorizeします。 その後、アカウント選択、プロジェクト名、インポートプロジェクト名を設定し、Deployを押します。 すると、自動でDeployが始まります(便利)。 そして、Deployが無事終わると、templateから作られたサンプルプロジェクトがすでにVercel環境上で立ち上がっています。 ここまでは、Vercel上でNext.js Analyticsを利用する場合と同じようですね。 ここで、Analyticsを有効化して計測してみる、ということもできるのですが、もう一手間かけていきましょう。 2. 作成後、プロジェクトのDomainsタブから、残っているドメインを削除 Settingタブに移動し、Domainsを見つけます。 そして、少々心苦しいですが、生まれたての <umaretate>.vercel.app のEditからのDomain削除を敢行しましょう。 消すよ?ええんか?と聞かれますが強い気持ちでREMOVEします。 3. AnalyticsタブからNext.js Analyticsを有効化 お次はAnalyticsタブに移動し、AnalyticsをEnable状態にし、next.config.jsに書くためのanalyticsIdを取得します。 このIDはあとでまた使います。 ここまででざっくり準備は完了です。 肝心のAWS Amplify上にNext.jsアプリ構築をしていないので、そちらを行ってから4,5についてまた見ていこうと思います。 Amplifyを使ってAWS上にNext.js環境を立ち上げる 今回Next.jsを立ち上げる環境としてAWS Amplifyを使っています。 Amplifyとは 公式 によると AWS Amplify は、モバイルとウェブのフロントエンドデベロッパーが、安全でスケーラブルなフルスタックアプリケーションを構築しデプロイできるようにする、AWS による製品およびツールのセットです。Amplify を使用すれば、アプリケーションのバックエンドを数分で設定し、わずか数行のコードでそれをアプリケーションに接続できます。そして、3 ステップで静的なウェブアプリケーションをデプロイできます。AWS Amplify で迅速な市場投入を実現しましょう。 とのことで、「バックエンド構築からフロントエンド開発までこれひとつでできちゃう便利ツールセット」のことのようです。 今回のような、ちょっと動く環境を用意して試したいことがある時に最適ですね! 構築はAmplify公式が提供している Tutorial に沿って行いました。 ※構築にはAWSアカウントが必要なため、持っていない方は作っていただく必要があります。 基本的にTutorialがしっかりしているため、説明はそちらに任せますが、構築時に引っかかった点についてはこの章の最後に備忘録的に残しておきます。 Tutorialを無事に完了し、Amplifyでdeployまで行くと下図のようなメモアプリが立ち上がるので(画面はすでにCreate Postを実行しているので、若干異なるとは思います)、ここまでできたらAnalyticsの設定に再び戻りましょう。 参考までに、Tutorialで構築した際のサービス構成図としては下図のようになっています。 (参照元と同じAmplify DocsのGetting startedに従って構築したため、構成図はお借りしています。) 参照元: Developers.IOより抜粋 備忘録: Tutorial構築時のエラー対処 Testing SSG 項目でファイルを書き換えた後 npm run dev をした際に、 GraphQLError: FormData is not Defined というエラーが発生しアプリが立ち上がりませんでした。 調べてみると AWSのフォーラム でも質問が上がっており、amplify packageをupdateする事で解消するとありましたが、 それに従い、amplify package で最新にupdateを行うと、今度は以下のエラーが発生します。 Error: Amplify has not been configured correctly. このエラーは aws-amplifyのissue で近い指摘が上がっており、 issue記載の通りにやれば上記エラーは解消できるのですが、お次はGraphQLのエラーが出ます(もぐらたたきゲーム感がありますね)。 Error: No credentials at GraphQLAPIClass 結局初めのエラーはaws-amplifyのバージョンが3.3.9で発生しており、この時点で最新の3.3.11にしたところGraphQLのエラーが出たということで、その間の3.3.10にする事で解決しました。 何か起きたら、packageのバージョン周りは疑ってみるといいかもしれません。 アプリ構築後の残作業 4. 有効化して表示されるconfig設定を、アプリの適切な箇所に配置 アプリが構築できたら、そのプロジェクトに next.config.js ファイルを用意し、Analyticsタブで表示されていたconfigをcopyして貼り付けます。 module.exports = { analyticsId: } 無事用意できたら、 npx serverless コマンドで再deployをしましょう。 5. データが一定量貯まるまで待っていると、測定結果が見られるようになる deployが完了したら、ほぼ完了なのですが、最後に一定のアクセス数を貯める必要があります(自分は立ち上がった環境に対して、家のPCとスマホで数十回程度アクセスしていました)。ぽちぽちアクセスして、一定時間おきに(Vercelは30分程度と書いていましたが、自分は1,2時間程度でした)Analyticsページを見に行くと、無事スコアが表示されました!!! スコアの遷移はデバイスごと(PC,タブレット,モバイル)に横軸時間軸で、全体・page名単位・URL単位で確認できます。 (ただし無料版でトラックできる期間は1日のため、継続してモニタリングしたい場合はProもしくはEnterprise版への加入が必要です。) ちなみに、Analyticsでは以下4項目を計測しています。 First Contentful Paint( FCP ) ページ読み込みが開始されてから、ページのコンテンツの一部が画面にレンダリングされるまでにかかった時間 テキストやイメージ(背景画像含む)、 <svg> 要素、白以外の <canvas> 要素などが該当 1秒以内に描画され始めるのが望ましい 測定に適した閾値は75パーセンタイル Largest Contentful Paint( LCP ) ビューポート内で最大サイズの画像やテキストボックスが表示されるまでにかかる時間 <img> 要素、 <image> 要素内の <svg> 要素、 <video> 要素、url()関数を介して読み込まれた背景画像を持つ要素、テキスト要素などを含むブロックレベルの要素が該当 2.5秒以内に表示されるのが望ましい 測定に適した閾値は75パーセンタイル 描画されるにつれて最大サイズのコンテンツは変わり、更新される 最終的に、ビューポート内で最大サイズのコンテンツが描画されるまでにかかった時間 ユーザーが何かしら操作した場合は、それ以降に最大サイズのコンテンツが登場しても更新しない Comulative Layout Shift( CLS ) ユーザーが予期しないレイアウトシフトが発生していないかどうかを数値化した値 ざっくり(レイアウトシフトが発生したフレームがビューポート内に占める割合)×(ビューポート内で動いた割合)をスコア化したもの スコアが0.1未満が望ましい 測定に適した閾値は75パーセンタイル First Input Delay( FID ) ユーザーが最初にページを操作してから(リンク押下やボタンのタップなど)ブラウザが実際に処理を開始し始めるまでの時間 初めに限定しているのは、サイトの品質と信頼性に関する、全体の印象に大きく関わるのが初めの応答のため 100ミリ秒未満が望ましい 測定に適した閾値は75パーセンタイル 上記のうち、LCP,FID,CLSの3項目はCore Web Vitalsとして、Googleによって導入されたUXの指標の中でも重要とされている項目で、SEO的にも重要とされています。 こうしたwebページで重要とされる項目が、リアルに即したデータで計測することができるのは魅力的ですね! 終わりに 最後まで読んでいただきありがとうございました! 今回新登場したNext.js Analyticsを、サービスで使っているのに近い環境で動かすことができるのかを試してみましたが、 無事に計測するところまで行くことができました! 使ってみた感想としては、 導入が手軽 シミュレーションではなく、実際に訪れたユーザーの情報をもとにスコアを計測して、リアルな状況を明らかにできる といったメリットがある一方で、 実際にサイトが重い場合に、どのコンテンツがボトルネックで、どう改善していくと良いか、といったフローが見えにくい ProまたはEnterpriseのプランでコストをかけてモニタリングしていくとなった時に、もう少し詳細な情報が欲しい といったこれからの改善を期待したい点もいくつか見受けられました。 とはいえ、まずは入れてみて実際自分たちのサイトはどれくらいのスコアで早いのか、遅いのか、といったことを知るツールとしては、 導入もconfig追加してdeployするだけでお手軽なので、試してみる価値はあると思います。 何かを改善したいとなった時には、まずは計測することが重要です。 今回触ってみたNext.js Analyticsもその改善の一助になると期待して、 私自身もより良いアプリを作るために、改善を続けていきたいと思います。
FORCIAアドベントカレンダー2020 15日目の記事です。 フォルシアはAtCoderJobsにてエンジニアを絶賛募集中! 新卒採用はこちら 中途採用はこちら この記事はなんでしょう? 新卒エンジニア1年目の吉田です。冒頭でも紹介した通り、フォルシアではAtCoderJobs経由での採用を行っていますが、私も就活ではこちらを活用して入社しました。 私を含めた20年新卒には、初めてAtCoderJobs経由で採用された社員が複数名いるということになるわけですが、ここはひとつJobs一期生を代表して、競技プログラミングとフォルシアでの業務に関して、就活や入社後の経験も踏まえた実感を書き出してみよう!というのが、本稿の趣旨となります。 さらりとした私のこれまで お控えなすって! 私は経済系の大学院出身です。在学中は知人の紹介でwebアプリを開発するアルバイトをしていました。 といっても、エンジニアとしてはfor文とif文が書ける程度の実力で、フロントサイドのちょっとした修正をするにも時間がかかるような状況でした。 少年は競プロを通しエンジニアの道を知る そんな中、ふとしたきっかけでAtCoderの存在を知り、趣味の一環として継続的に取り組むようになりました。 ちょうど私が就活を始めた2018年からAtCoderJobsがサービスを開始したこともあり、就活の時期には活用してみることにしました。掲載企業の多くが要求ランクに水色を指定していたこともあり、水色になることを目標にそこそこ頑張った記憶があります(まだBeginners Contestがレート1199までを対象としていた時期ですね)。 そして縁あってフォルシアに内定が決まり、残りの学生生活は修士論文を書きつつ、競技プログラミングをやりつつ・・・といった日々となりました。 入社後も競技プログラミングはやりつつ、しれっと青色になることができ、今に至るといった感じでしょうか。 競技プログラミングは業務の役に立っているか? 私にとってはyesです。 問題解決能力は問題解決で身に着けるものなり 競技プログラミングは、一言でいえば「プログラミングという土俵での問題解決能力を競う競技」であると解釈しています。 業務においても、「プログラミングという土俵での問題」を解決するという点では同じです。 競技プログラミングでは問題解決のためにアルゴリズムやデータ構造の知識を駆使しますが、業務ではこれがフレームワークや扱うデータ等の知識に置き換わるという感じでしょうか。 また、競技プログラミングでは具体的な問題があらかじめ与えられていて、解決策を考える部分が大半を占めますが、業務では抽象的な現象から具体的な問題点を見つける部分、またどのような方法でその解決を行うか考える部分の比重が大きくなります。 そのため完全にオーバーラップするまでは言えないのですが、競技プログラミングにおける問題解決能力の向上は、業務における問題解決能力の向上に直結すると考えています。 ああ やっぱり過去問が一番だわ 私が競技プログラミングに取り組み始めた時期は、前述の通りエンジニアとしての力はほとんどない状況でした。 力がなかった原因は、あきらかに圧倒的な知識と経験の不足によるものでした。きちんと情報科学の勉強をしたわけでも、趣味で開発をしていたわけでもなかったので、作業の中で生まれる課題は初めて見るものばかり、考えつく対応策も不適当なものが多かったように思います。 しかし、競技プログラミングに取り組む過程で、私は上達のため過去に出題された問題をたくさん解く必要に迫られます。 この作業は、 問題点を見つけ 解決策を考え それを実現する という一連のサイクルを繰り返し訓練する事に他ならず、私にとっては不足していた知識と経験を非常に効率よく補強するものとなりました。 実際AtCoderのレートが伸び始めた頃には、webアプリの開発をするアルバイトの作業中に「昔苦しんだ挙句放置していた問題が簡単に解決できた」「これまで理解できなかったソースコードが容易に理解できた」といった体験を何度もしました。 実は競技プログラミングで使っている言語は開発と一切関連がないのですが、それでもエンジニアとしての根本的な能力が劇的に改善されたと実感しています。 入らないとわからないこともある 一方で、フォルシアに入社する前に想像していた「役に立ち方」と異なっていた部分もあります。 例えば、「競技プログラミングを通じて学んだアルゴリズムやデータ構造の知識が、直接業務で問われるわけではない」という点です。 AtCoderJobs上で フォルシアの新卒求人 は水色以上を要求ランクとしています。 競技プログラミングで良いパフォーマンスを得るには、問題解決のスキルだけでなく、出題されうるアルゴリズムやデータ構造の知識が必要不可欠です。 フォルシアでは、AtCoderの水色を「標準ライブラリのデータ構造に乗せれば解ける問題」を解くだけの知識と能力が担保されている水準、という観点で要求しています。 水色を目指そうとすると、結構な時間を知識の習得に割く必要があるかと思います。特に私は情報科学の教育をほぼ受けていなかったこともあり、かなり苦戦した記憶があります。 この苦労して培った競技プログラミングにおける知識の部分は、一切役に立たないというわけでもないのですが、業務内容と直接は関係しないものです。少なくとも、競技プログラミングの知見を生かして計算量改善をしていこう!というわけにはいきません(オーダーレベルで改善を行う業務はかなり稀だと思います)。 費やした労力を考えると、少しもったいない気持ちもあります。 しかしフォルシアに入って、これまでの学習がまったくの無駄だった、あるいは要求ランクと実務の間にギャップがある、といったことは感じません。これは実際に業務をするまで理解できませんでしたが、フォルシアの顧客が属する旅行業界が扱っているデータは本当に膨大で複雑です。 私が担当しているアプリではダイナミックパッケージと呼ばれる商品を扱っていますが、これは交通手段と宿泊施設を自由に組み合わせられ、その上価格も動的に変動するというもので、データもアプリも非常に(非常に!)複雑だと実感しています。 膨大かつ複雑なデータに対する高速な検索を実現するため、フォルシアではデータの持ち方にとても気を遣っています。これを理解し適切に運用するための学習は、扱うものは違えど競技プログラミングにおけるデータ構造の学習に通ずるものがあります。 そのため、「AtCoderで水色になれる水準の知識を習得した」という保証は新卒採用の基準として適切だと考えています。 (もちろんこれは「AtCoderのレート」をアピールポイントにする場合の話であって、AtCoderのレートが緑以下ならば適性がない、という意味ではまったくありません!フォルシアがエンジニアに対して求める理想を、採用段階ですべて満たしている必要はありません。フォルシアのビジネスや志向に興味を持った方は、ぜひ公式のフォーム( こちら! )からご応募いただければと思います。) 補足として(これは重要なことなので強調したいのですが)フォルシアは、AtCoderJobs経由での入社だからといって競技プログラミングの継続を強要しませんし、競技プログラマ向けに特別な業務を強いることもありません。 無論、計算機科学の深い知識が必要になる仕事もありますし、希望する方のそういった分野での活躍も強く期待しています(今はインメモリデータベースが熱い!詳しくは こちら )が、あくまで意思決定はエンジニア個人に委ねられています。 業務と競技プログラミングの相乗効果 ここまでの主張は「競技プログラミングをやっていると業務にとってプラスになるよ!」というものでしたが、今になって思うのは、逆に「業務を行っていると競技プログラミングにとってプラスになるよ!」という面すらあるのだ、ということです。 変数名はナメたら命取り 一つ目は、「型」を学べるという点です。これはデータ型という意味ではなく、コーディングの流儀という意味での型です。 アルバイトコーダー時代の私はそもそも流儀云々以前の実力だった上に、基本的に一人での開発、内容も「動けば何でもよし!」というものでした。 それゆえに命名は適当、表記の統一性もバラバラ、可読性なんて言葉聞いたことありませんが?という有様でした。 競技プログラミングを始めてからも、読むのは自分だけ、正答すれば何でもよし!という世界の中で、コードの可読性を上げる努力をしたことはありませんでした。 一方で、フォルシアでコードを書く上では、フォルシアのコード規約を守る必要があります。 また、甘えた命名をしたり可読性の低い処理をコミットしてしまった場合、コードレビューで矢のように指摘が飛んできます(コードレビューに関しては同期が こちら で記事を書いています)。 これは複数人での開発において、プロジェクトの開発効率や保守性を高いものにする上で必要不可欠な過程ではありますが、同時に私にとっては、業務の中で自分のコーディングの「型」を育てる過程にもなりました。 私は入社後にしばらく競技プログラミングを一切しない時期があったのですが、久しぶりにやや複雑な問題を解こうとした際にとてつもない違和感を感じたことを覚えています。 これまで通りのやり方で問題を解くコードを書いていると、心の中のリトルヨシダが「そこは関数に切りだせ」「もっとわかりやすい名前をつけろ」と話しかけてくるのです。いつの間にやら、私はすっかり可読性の低いコードを受け付けない体質になってしまっていたようでした。 特に開発経験のあまりない、プログラミングはAtCoderでしかしていません!というような方にとっては、(フォルシアに限らず)業務を通じて一定の流儀がある環境に身を置くことで、プラスになる面があると考えています。 チャランポランなコード程バグると恐い 二つ目は、デバッグが速くなるという点です。 先程述べた通り、業務では競技プログラミングと比べて問題点を見つける部分の比重が大きくなります。 例えば運用・保守の業務であれば「問題が発生した。解決してほしい」という依頼を出発点として、沢山あるコードの中から原因を特定して修正することになります。 また、開発業務であっても、自分の実現したいことと既存の実装に矛盾がある場合は、その原因を特定して改善する必要があります。 競技プログラミングでは基本的に自分のコード、または正解のコードを見る機会がほとんどだと思いますが、業務の中では誤っている(かもしれない)コードを見る機会が非常に多いです。 また、前項の通り、業務の中で「型」を身に着け、可読性の高いコードを書くようになることで、自分のコードがそもそもバグを生みにくい、かつバグを発見しやすいものになっていきました。誤りを見つける訓練と、誤りを見つけやすくする訓練の二つが、業務の中で自然と行われていたことで、競技プログラミングにおいても自分のデバッグ力の向上を実感することができました。 フォルシアでの競技プログラミング さて、業務におけるシナジーという観点で話を進めてきましたが、入社してからも純粋に競技プログラミングを楽しむ機会は沢山あります。 せっかくの機会なので、過去約一年以内にフォルシア内で行われた、またはフォルシアとして参加した関連イベントについて紹介していきます。 AtCoder やはり一番盛んなのはAtCoderです。20卒の入社で社内のユーザーがかなり増えましたが、それ以前から取り組んでいる先輩も多数います。 最近は社内向けの仮想コンテストを定期的に開催しており、夏のインターン時には学生も交えて盛り上がりました(インターンについて詳しくは こちら )。 また、アルゴリズム実技検定(PAST)も毎回多くの社員が受験しています。前回は満点を出した同期が社内向けの解説会を開催し、普段競技プログラミングに触れない層も含めたイベントになりました。 ゆるふわオンサイト フォルシア主催のプロコンです。 詳しくはこちら 前回は20卒の同期たちが作問をしていました。私はオンラインでこっそり参加しましたが、あまり成績が良くなかったのでこのことは誰にも明かしていません。 次回の開催予定はまだ未定ではありますが、必ず再開されると思います。フォルシア競技プログラミング部では作問のできるエンジニアを絶賛募集中! ICFPC 72時間で難問を解くタフなプロコンです。私は軟弱なので参加しませんでしたが、タフな同期と先輩が参加していました。フォルシア競技プログラミング部はタフなエンジニアを絶賛募集中! ISUCON チームでwebサービスの高速化を競うプロコンです。私は参加しましたが、普段やっている競技プログラミングと比べるとかなり実務よりの知識が要求されるため新鮮に感じました。 案の定というか結果は散々だったので来年こそは!という気持ちがあります。フォルシア競技プログラミング部は自走力のあるエンジニアを絶賛募集中! PG BATTLE 3人1組でそれぞれが問題を解いて合計点を競うプロコンです。 私は20卒の同期と組んで出場しましたが、二人が満点だったにも関わらずせんべい役の私が大失速してしまいランクインを逃してしまいました。フォルシア競技プログラミング部は強靭な顎を持ったエンジニアを絶賛募集中! CodinGame ゲームAIを作って他のAIと戦わせ、上位を目指すプロコンです。 かなりカジュアルですが、企業ごとに上位プレイヤーのスコアの集計でランキングが競われます。 私はカジュアルに参加してカジュアルな戦績しか得られませんでしたが、フォルシアとしては全世界62位でした。フォルシア競技プログラミング部は拡大再生産に強いエンジニアを絶賛募集中! おわりに 節目節目に宣伝を入れ直せ 上でも少し触れましたが、特に情報系以外の出身の方や、競技プログラミングがコーディング生活の中心という方ほど、競技プログラミングとフォルシアでの業務は相互にシナジーを生む、ということが実感できるかと思います。この記事を読んで「もしかして私?」と思ったあなたには、この後ぜひ見ていただきたいページがあるので、それを紹介してこの記事はおしまいということにします。 フォルシアはAtCoder Jobsにてエンジニアを絶賛募集中! 新卒採用はこちら 中途採用はこちら
FORCIAアドベントカレンダー2020 15日目の記事です。 この記事はなんでしょう? 新卒エンジニア1年目の吉田です。冒頭でも紹介した通り、フォルシアではAtCoderJobs経由での採用を行っていますが、私も就活ではこちらを活用して入社しました。 私を含めた20年新卒には、初めてAtCoderJobs経由で採用された社員が複数名いるということになるわけですが、ここはひとつJobs一期生を代表して、競技プログラミングとフォルシアでの業務に関して、就活や入社後の経験も踏まえた実感を書き出してみよう!というのが、本稿の趣旨となります。 さらりとした私のこれまで お控えなすって
FORCIAアドベントカレンダー2020 14日目の記事です。 事業開発部の龍島です。皆さんschema firstな開発してますか? フォルシアではwebアプリケーション開発にサーバサイドはExpress.js + TypeScript、クライアントサイドはNext.js(React.js) + TypeScriptを用いており、間をつなぐAPIインターフェースの定義にOpenAPI Specification(swagger)を用いてschema firstな開発を行っています。 ここ2年ほどschema firstな開発を行ってきましたがAPIのインターフェース定義を初めに設計する
FORCIAアドベントカレンダー2020 14日目の記事です。 事業開発部の龍島です。皆さんschema firstな開発してますか? フォルシアではwebアプリケーション開発にサーバサイドはExpress.js + TypeScript、クライアントサイドはNext.js(React.js) + TypeScriptを用いており、間をつなぐAPIインターフェースの定義にOpenAPI Specification(swagger)を用いてschema firstな開発を行っています。 ここ2年ほどschema firstな開発を行ってきましたがAPIのインターフェース定義を初めに設計することでサーバサイドとクライアントサイドの開発を複数人で同時に進めることができ、近年のフォルシアでも大規模化してきているwebアプリケーション開発に適した手法だと感じています。 web上にはschema firstで開発していこう!という記事は多くありますが、実際にOpenAPIの仕様を満たすAPIを実装する方法を解説した記事は少ないです。 社内でもOpenAPI周りの仕組みは約2年前に情報のない中手探りで整備したものですが、苦労した部分もありつつ、ある程度上手くワークしていると思うので、これから導入を検討している方に向けて紹介したいと思います。 schema first開発が世の中に浸透してきた今ではライブラリも充実してきているため、現状の仕組みに加えて新しいライブラリの導入で今ならこうしたほうが良さそうという点も補足していきます。 今回紹介する内容の全体像を図にしてみました。①~③の箇所で利用している内容について、詳細をみていきましょう。 1. リクエスト、レスポンスのTypeScriptの型を自動生成 一番メリットを享受しやすいのはTypeScriptの型生成の部分かと思います。 フォルシアでは sw2dts を用いてOpenAPIの定義からリクエスト、レスポンスの型定義を自動生成しています。sw2dtsはOpenAPI定義のJSONからdtsファイルを出力する dtsgenerator のラッパーですが、dtsgeneratorでできなかったGETのクエリパラメータも型定義として出力できるようになっています。 /post/:id などのようにpathにパラメータが含まれる場合は定義が出力できないので注意が必要です。 現在では openapi-typescript というライブラリが出てきており、クエリパラメータもOpenAPI3系であれば上手く出力できるようなので、置き換えが可能そうです。 2. リクエストパラメータのバリデーション機能を自動生成 型定義を生成しても、リクエストパラメータに関しては定義通りに叩いてもらえるとは限らず、パラメータをバリデーションする必要があります。バリデーションを手作業で実装すると、定義と乖離してしまう可能性があります。二重管理を防ぐためOpenAPIの定義からバリデーションを生成したいです。 整備当時はこの要求を満たす良いライブラリを見つけられず、expressの標準的なバリデーションライブラリのexpress-validatorを利用して実現しました。OpenAPI定義のJSONからexpress-validatorの schema validation 機能の形式のオブジェクトを生成するスクリプトを内製しています。基本的なバリデーションは成功し、内製のため自由度もあり、ある程度上手く運用できているとは言えますが、default値の設定など対応できていない部分もあり、可能なら外部のライブラリで巻き取ってしまいたいとも感じています。 現在では express-openapi-validator という目的に完全合致したライブラリがあり、Express.jsのmiddlewareとして導入することで定義通りにバリデーションをしてくれるようです。現在の内製スクリプトの仕組みを置き換えるものとしてフォルシアでも導入を検討しています。 3. 実際のレスポンスが定義通りかチェックするテストを自動生成 前述のsw2dtsを用いてレスポンスの型定義は作れているものの、DBから取得してきた値などでTypeScriptの型定義が100%信用できない場面もあり、実際のレスポンスが定義通りであることを保証するテストも導入しています。 openapi-response-validator はオブジェクトがOpenAPIの定義を満たしているかチェックすることができるので、frisby.jsで実際にAPIを叩き、レスポンスが仕様を満たしているかをテストしています。 番外 OpenAPIの定義をいい感じに書く API開発とは少しずれますが、以前 OpenAPI(Swagger)の定義をいい感じに書くTips を記事にしています。よろしければご覧ください。 まとめ 以上がフォルシアでのOpenAPIの定義に準拠するAPI開発で導入している仕組みです。まとめると下記のようになります。 リクエスト、レスポンスのTypeScriptの型をopenapi-typescriptなどを用いて自動生成 実際のリクエストパラメータの値をexpress-openapi-validatorなどを用いてバリデーション 実際のレスポンスの値をopenapi-response-validatorなどを用いてテスト TypeScriptの世界と実際のリクエスト、レスポンスの両方がOpenAPIの定義と一致していることを保証する仕組みを作ることで、定義と実装の乖離を防げます。 OpenAPIを用いた開発は一度環境を整備してしまえば、特にクライアント側とサーバ側を同時に開発するようなプロジェクトでかなり有効です。今回改めて周辺ライブラリを調査することで改善できそうな点も見えてきたので、フォルシアのschema first開発環境もUPGRADEしていきたいと思います。
FORCIAアドベントカレンダー2020 13日目の記事です。 こんにちは検索プラットフォーム部エンジニアの伊藤(亜紀)です。 今日は、社内の意見交換の活性化を目的として始めたオンラインディスカッション(以下Jamboardディスカッション)について、その内容や始めた経緯、工夫をご紹介します。 Jamboardディスカッションとは フォルシアでは、週に1度、エンジニアのほぼ全員40人程度が参加して、情報共有ミーティング(WEBミーティング)を行っています。 今年の10月より、そのミーティングのなかで、フォルシアのビジネスや日々の仕事を見直し、改善していくためのディスカッションを始めました。 進め方 大勢が参加するWEBミーティングであっても各人が意見を表明しやすいように、 Google Jamboad のブラウザアプリを利用しています。 ※Jamboardについて検索すると、専用ハードウエアを使って云々という説明がたくさん出てきますが、ブラウザからもJamboardを閲覧・編集することができます。 最初に、ファシリテーターがその月のディスカッションテーマの背景について説明します。その後、全体に向かって問いを投げかけ、それに対する答えを参加者がその場でJamboard上の付箋に書いていきます。 ある程度答えが出揃ってきたら、ファシリテーターがいくつかの意見をピックアップして、「詳しく教えてください」「理由を教えてください」といった深堀りをしていきます。 ファシリテーター以外のメンバーも、気になることがあれば適宜「その点はこう考えた方が良いのでは?」「これはどういう意図ですか?」といった質問や指摘をしていきます。 30分弱ほどで1~2つの問いについての付箋記載→深堀りを繰り返し、次の週は続きの問いからスタートします。 11月のディスカッションのJamboard。付箋に参加者の様々な考えが記載されています。 ディスカッションを始めたきっかけ フォルシアにおけるコミュニケーション 元来フォルシアは「同じ場所で時間を共有して互いの人となりを理解すること」や「社員が部署や役職を超えて話をすること」が仕事にも良い影響を与えるとの考えのもと、 福利厚生 や社員発案の シャッフルランチ などにおいて、社員間のコミュニケーションを活性化させる取り組みを積極的・継続的に行ってきた会社でした。 それが、2020年度はコロナ禍により、こういった対面の活動は大幅に縮小せざるを得なくなりました。 業務そのものについても、在宅勤務で業務を遂行することはできますが、他社員の考え方や置かれている状況・取り組んでいる課題について、さりげない雑談やそれとなしに耳に入る会話から知る機会は大幅に減少しました。そういった情報を「些細な情報」「余計な情報」とする見方もあると思いますが、フォルシアではそういった情報に価値を見出してそれまで社員全員のデスクをひとつの執務室に集めていたこともあり、私個人としてはこの変化はフォルシアにとって大きな変化に感じられました。 特に、他のチームで起こっている問題や困りごとに触れる機会が減ると、自分たちのビジネスや業務プロセスを"全社視点に立って"改善していこうという気持ちにもなりにくい、というのは私自身在宅勤務をしながら痛感しており、課題意識を感じていました。 各人が意見を表明し、他者が見ている景色を知る場としてのディスカッション そんな課題感について上長と相談したところ、始めることになったのが、このJamboardディスカッションです。 大勢が集まる場で込み入った議論はできないまでも、共通のテーマに対して各人が意見や経験を話すことで、 誰もが意見を表明し 他者の多様な意見から、自分には見えていない現実の景色や新しい視点を知り その新たな視点をふまえて、現在のビジネスや業務プロセスを見直す そんな機会を作りたいと考え、Jamboardディスカッションを企画しました。 ディスカッションテーマ これまでの開催テーマをご紹介します。 10月:ロイヤリティビジネスの今後 フォルシアのメインプロダクトSpookは、一般的な受託開発ではなく、長らくロイヤリティビジネスの形態をとってきた点に特徴があります。昨今の「サブスク」浸透や、今年はフォルシア創業から20年目の節目であることもふまえ、フォルシアのロイヤリティビジネスの今後を考えるために、まずは「今」を再確認する機会を設けました。 お題 問1:ロイヤリティビジネスの形態によって、私たちはどのような恩恵を受けているでしょうか? 問2:ロイヤリティビジネスの形態によって、私たちはどのような制約を受けているでしょうか? 問3:ロイヤリティビジネスの形態によって、私たちが提供できている価値はどのようなものでしょうか? 11月:どうやっている!? 運用保守 フォルシアでは開発と運用保守で担当エンジニアを分けず、1人のエンジニアが両方を担当します。両方を担当することで広い視野に立ったエンジニアリングが可能な一方で、性質の異なる両タスクを並行して進めていくのには特有の難しさもあります。そのなかでも運用保守について、各人の取り組みを問いかけてみました。 お題 問1:運用保守として、どのような対応をしていますか? 問2:運用保守対応で、難しいと感じること・悩むことは何ですか? 問3:運用保守対応で、心がけていること・工夫していることを教えてください。 投げかける問いは、あえて「現実の確認」にとどめる 「より良くするためには」「これから何をやっていくべきか」といった直球の議論をしたい思いは強くありましたが、そういった課題感は問いの背景として触れるにとどめ、問いはあくまでも「各人が見ている現実」を書いてもらうものとしました。 具体的には、投げかけを「ほぼ誰でも少し考えれば答えることができて、人によって違った答え(または同じ答えでも違った理由)が返ってくるであろう」問いに絞りました。 これは、もともとは とにもかくにも参加者に付箋を書いてもらわないと始まらない。そのためにアウトプットを出すハードルをできる限り下げたい でもせっかく大勢で話すからには意味のある場にしたい。だから最低でも「他者の視点」は参加者が持ち帰ることができるようにしよう との考えでやっていたことでした。改善案までたどりつけたら文句なしに素晴らしいですが、限られた時間のなかでは欲ばりすぎず「背景課題の存在や、それを取り巻く状況を、ディスカッション終了時点で理解してもらえたらそれでOK!」と考えることにしました。 しかし何度かディスカッションを重ねているうちに、これはアウトプットのハードルを下げるだけでなく、改善や何か新しい取り組みを始めようというときに、とても大事なステップなのだということに気づきました。 いきなり課題の絞り込みや解決に取り掛からずに、「背景課題を取り巻くあれこれを各人がどう見ているか」にとことん焦点を当てたことによって、「ぱっと想像できる以上に、周囲の人は皆違った景色を見ているし、違った現実認識をしている」ということを改めて認識させられるとともに、背景にあった課題がより手ざわり感のあるものになったように感じられました。 どんな課題も他者と協力して解決していく必要がありますが、協力のためには、そのよすがとなる共通の認識や感覚・経験が必要です。 頭ではそうとわかっていても、そのことを忘れて(自分が興味がある)課題にいきなり取り掛かろうとしてしまうことが個人的には多かったのですが、「背景課題を取り巻くあれこれを各人がどう見ているか」についての問いと回答は、そのよすがのようなものを作る役割を果たしてくれるものになるように感じました。 開催してみて 正直なところ、やってみる前は「リモートかつ大勢で活発な議論ができるものだろうか」という心配とともにスタートしたのですが、これまでのところ、実際には予想を大きく上回るたくさんの意見・経験談をもって開催してきています。 ファシリテーションは難しい(!)ですが、周囲のフォローにも助けられ、私自身非常に学びのある時間になっています。 Jamboardディスカッションに参加した社員からは、 「会社としてなぜこのようなやり方をしているのか」など在籍年数によって知識量に差が出がちだが、経緯となった事例がわかってよかった 難しい問いもあったが、自分が普段苦労していることなどはたくさん付箋を書くことができた 様々なバックグラウンドの人がいるなかで、前職の経験をふまえた話をきけて興味深かった といった声をもらいました。 最後に いかがでしたでしょうか。リモートワーク下でのコミュニケーションについては今後も様々な組織で模索が続くと思いますが、この事例がひとつの参考になれば幸いです。 12月分からは議題提起者を変えて、また新たな切り口からディスカッションを行っていきます。
FORCIAアドベントカレンダー2020 12日目の記事です。 弊社はこれまで PostgreSQL を利用した高速なスペック検索をコアコンピタンスとしてきましたが、今後はドキュメント検索にも注力していく予定です。OSS のドキュメント検索エンジンといえばまず思いつくのが Elasticsearch  です。PostgreSQL と比較されることの多い Elasticsearch ですが、今回は特に日本語処理の周りを技術的にやや深めに比較してみたいと思います。 本記事はPostgreSQL についてある程度知っているがElasticsearch はあまり知らない、という方を対象としています。 Elasticsearch とは 簡単にElasticsearch の特徴を挙げると下記のとおりです。 全文検索に特化した検索エンジン。コア部分は java 製の全文検索エンジン Apache lucene を利用 REST APIですべてのオペレーションを実行可能。POSTするデータはJSONで記述する スキーマレス。DB設計をせず文書を登録・検索できる。スキーマを設定することも可能 スケーラブル Elasticsearch v.s. PostgreSQL 一般的な比較記事は多いので、よくまとまっていると思われる記事を紹介します。 Elasticsearch vs. MongoDB vs. PostgreSQL Comparison 上記はデータベースとしての比較記事です。ドキュメントストアとしてよく使われるMongoDB との比較も含まれていて参考になります。 Elasticsearch は MongoDB 同様 "Schema-free"、すなわちDBの設計をせずにとりあえず文書を放り込んで検索することができます。この手軽さが人気のある理由と思われます。 SQLとElasticsearchとのクエリの比較 - Qiita データベースとしてのElasticsearch - Qiita 上記は検索言語(query)の比較を行っています。Elasticsearch の検索はSQLではなく Elasticsearch 独自のシンタックス(Domain Specific Language) で行います。 SQL的な厳密で複雑な検索は苦手ですが、テキスト検索については様々な指定ができて便利です。検索処理をコントロールするスクリプトや並び順を調整するオプションを query に含めることもできます。 また aggregation といって検索結果をランキングして出力するだけでなく、ファセット毎の検索数をあわせて出力するよう指定もできます(SQL でいうgroup byに相当) 。 GUI tool  の比較 PostgreSQL には pgAdmin4,   Elasticsearch には head と呼ばれるGUIツールがあります。 pgAdmin はスタンドアロンのアプリケーションですが head は node.js の server版と chrome extension 版があります。[ 1 ] pgAdmin pgAdmin - PostgreSQL Tools Elasticsearch  head GitHub - mobz/elasticsearch-head: A web front end for an elastic search cluster Google Trend の比較 Google Trendでの比較 Google Trend 的にはPostgreSQL が Elasticsearch を圧倒しています。 例外的に中国では Elasticsearch が強いようです。 全文検索速度の比較 PostgreSQL と Elasticsearch は設計思想が違うので一概に比較できないのですが、 Full-Text Search Battle: PostgreSQL vs Elasticsearch | sudo README によれば、かなり乱暴にまとめると下記のようになっています。 普通はElasticsearchが 5倍位速い PostgreSQL でチューニングした場合、速度は大体同等 上記は様々な条件付きの結果です。詳細はオリジナルの記事をご覧ください。 機能拡張の比較(extension v.s. plugin ) PostgreSQL にはextension と呼ばれる機能拡張用のモジュール( pg _bulkload のように pg ** という名前が多い)が多数あります。 Elasticsearch にも同様の機能拡張用のモジュールがあり pluginと呼ばれています。どちらもユーザが作ることができますが、PostgreSQL のextension はC言語で記述、plugin は Java で書くことになります。そのせいかplugin の方が個数は多いように見えます。 Elasticsearch の面白いプラグインとして ingest-attachment plugin があります。これは pdf や docx, xlsx, pptx といったオフィス文書ファイルをpostすると、テキストや author 属性、CreateDate 情報を抽出して自動的にDBに登録してくれるものです。このようにElasticsearch のプラグインはより応用的・ユーザ志向である一方、PosgreSQL の extension はシステム開発者向けという位置づけになるかと思います。 Elasticsearch では言語(英語、ドイツ語、日本語など)に固有な処理もプラグインの形で提供されています。日本語形態素解析もプラグインとして Elasticsearch に組み込みます。 Elasticsearch の日本語形態素解析 以下では Elasticsearch の日本語形態素解析周辺について述べていきます。 Elasticsearch の言語解析はAnalyzer と呼ばれる言語毎のプラグインの形になっていますが、基本的な機能や仕様は言語共通になっています。すなわち、 文字単位の前処理を行う Char_filter 単語分割を行う Tokenizer 不要単語の削除など後処理を行う Token_filter の3つから構成されています。 詳細は下記をご覧ください。 ElasticsearchのAnalyzer, Tokenizer, Token Filters, Char Filtersの一覧 - Qiita Elasticsearchを日本語で使う設定のまとめ - Qiita 日本語解析で使える Analyzer プラグインは下記の5つです。 analysis-icu GitHub - elastic/elasticsearch-analysis-icu: ICU Analysis plugin for Elasticsearch analysis-kuromoji Japanese (kuromoji) Analysis Plugin | Elasticsearch Plugins and Integrations [7.10] | Elastic analysis-kuromoji-ipadic-neologd GitHub - codelibs/elasticsearch-analysis-kuromoji-ipadic-neologd: Elasticsearch's Analyzer for Kuromoji with Neologd analysis-mecab GitHub - animalmatsuzawa/elasticsearch-analysis-mecab: elasticsearchのanalysis plugin 形態素にmecabを使用 sudachi GitHub - WorksApplications/elasticsearch-sudachi: The Japanese analysis plugin for elasticsearch analysis-icu プラグインは日本語だけでなく中国語、韓国語などアジア言語に対応しています。 ユニコード処理に強いので全角半角統一などユニコード関連の文字の正規化には便利ですが、単語分割は弱そうです。また、ユーザが辞書を追加することができません。[ 2 ] Kuromoji と Sudachi はJavaで書かれた日本語形態素解析モジュールです。 上記はその Elasticsearch プラグイン版です。analysis-kuromoji は辞書のソースコードは MeCab と共通なので解析結果は MeCab と似ていますが、デフォルトで入っている辞書がIPADICと最低限のものになっています。 そこで NEologd 辞書 を追加したものが analysis-kuromoji-ipadic-neologd です。 analysis-mecab は Java からMeCab を呼び出す形で形態素解析するものです。[ 3 ] Java製でないためElasticsearch ではマイナーなプラグインのようです。 弊社では日本語形態素解析としてMeCab を使うことが多く、辞書が MeCab 互換だと嬉しいです。そこで以下では analysis-kuromoji,  analysis-kuromoji-ipadic-neologd, analysis-mecab の3つについて調査します。 日本語処理のカスタマイズ ICU プラグインを利用すると下記処理ができます。 ICU Normalization Character Filter は NKFC正規化 を行うもので、英数字やカタカナの全角半角統一等ができます。 Kuromoji プラグインは下記の処理ができます。 踊り字(々、ゞ)の正規化とは、「常々」⇒「常常」といった処理を行います。 これら以外にも言語非依存のフィルターとしてHTMLのタグを除去する HTML Strip Character Filter や電話番号等を正規表現で正規化する Pattern Replace Character Filter があります。 また、これらのフィルターをアップロードする文書のどの部分(フィールド)に適用するか、柔軟に指定することができます。これにより文書の日付欄には日付の正規化を行う、電話番号欄には電話番号の正規化を適用する、名前欄は形態素解析しない等の設定をすることができます。 Kuromojiによる形態素解析 analysis-kuromoji には単語分割に3つのモード(normal, search, extended) があります[ 4 ]。 normal  通常の単語分割(最長一致) search  通常の形態素解析に短い単語分割を追加したもの extended 未知語(辞書に登録されていない単語)を1文字単位にばらしたものを更に追加 search モードは検索のrecall を上げるために使います。extended は n-gram で部分一致する単語を検索することを想定していると思われます。解析例は下記のようになります。 Untokenized Normal mode Search mode Extended mode 関西国際空港 関西国際空港 関西 国際 空港 関西 国際 空港 日本経済新聞 日本経済新聞 日本 経済 新聞 日本 経済 新聞 シニアソフトウェアエンジニア シニアソフトウェアエンジニア シニア/ソフトウェア/ エンジニア シニア/ソフトウェア/エンジニア ディジカメを買った ディジカメ/ を/ 買っ/ た ディジカメ/ を/ 買っ/ た デ/ ィ/ ジ/ カ/ メ/ を/ 買っ/ た 下記によれば、Searchモードは漢字のみで構成される4文字以上の単語、もしくは7文字以上の単語に対してコストを重くすることで、分割を促しているようです。 Java製形態素解析器「Kuromoji」を試してみる 以下に   search モードでの解析例を示します。言語解析結果を表示する API である _analyze を上述した head ツールで利用しています。 ユーザ辞書は使えるか analysis-kuromoji、およびanalysis-kuromoji-ipadic-neologd は CSV 形式のテキストファイルをユーザ辞書として組み込むことができます。プラグインのカスタマイズパラメータに "user_dictionary" というフィールドがあり、そこにテキストファイルを指定します。 kuromoji_tokenizer | Elasticsearch Plugins and Integrations [7.10] | Elastic しかし、テキストファイルで指定することからわかるようにユーザ辞書としてはせいぜい数百語程度のサイズを想定しているようです。数万~十数万語レベルのユーザ辞書は使えません。換言すると(検索用にバイナリコンパイルされた)ユーザ辞書を実行時に指定して利用する方法はありません。 実行時でなければ analysis-kuromoji-ipadic-neologd プラグインのソースコードをダウンロードして、NEologd 辞書のソースコードにユーザ辞書をCSV で追加してプラグインをビルドすることにより、大量の単語の追加をすることができます。 一方analysis-mecab は "user_dictionary" カスタマイズパラメータにMeCab用にコンパイルしたユーザ辞書を指定することで、その辞書を利用することができます。 長い単語を辞書登録して形態素解析できるか analysis-kuromoji は内部的に Lucene Kuromoji を使っていますが、Lucene Kuromoji には「見出しは16文字未満」という制約があります[ 5 ]。 一方、NEologd には16文字以上の長い単語が多数登録されています。analysis-kuromoji-ipadic-neologdでこの制約が改善されているか確認しました。 念の為 analysis-mecab でも、辞書登録された長い単語が正しく形態素解析できるか確認しました。[ 6 ] 原文 (NEologd 辞書ソースより採取) 原文文字数 kuromoji-ipadic-neologd analysis-mecab あいしてると言ってよかった 13 あいしてると言ってよかった あいしてると言ってよかった あさぎり町立上小学校皆越分校 14 あさぎり町立上小学校皆越分校 あさぎり町立上小学校皆越分校 あけましておめでとうございます 15 あけましておめでとうございます あけましておめでとうございます あなたの夢の中そっと忍び込みたい 16 あなた/の/夢の中/そっと/忍び込みたい あなたの夢の中そっと忍び込みたい うわうみ漁業協同組合日振島女性部 16 うわ/うみ/漁業協同組合/日振島/女性/部 うわうみ漁業協同組合日振島女性部 分割された箇所に"/" を入れています。残念ながらkuromoji-ipadic-neologd では16文字以上の辞書登録単語は分割されてしまいました。この他にも記号で始まる単語で解析されないものがあり(形態素解析結果が空になる)、kuromoji-ipadic-neologd は若干不安定な印象です。 PostgreSQL の日本語形態素解析 PostgreSQL で単語分割を導入して検索精度を上げるのには textsearch ja モジュールを用います。ja wakati 関数により MeCab を呼び出して単語分割を行い、tsvector 型のフィールドに分割結果を入れます。検索するときは検索文字列を tsquery 型に変換して比較を行います。詳細は下記を参照ください。 MeCab と textsearch_ja を使って高速な全文検索を実現しよう - PowerGres 体験記 第 4 回 形態素解析の前の正規化は to_tsvector() 関数内部で実行されますが、ビルトインでカスタマイズはできません。 tsvector 型は 制約が多く 検索が遅くなりがちなので、以下のような回避策がとられる場合もあります。 検索対象テキストを単語分割する。単語間にセパレータ文字を挟んでテキストとして登録する(a)。 検索文字列を単語分割する。単語間にセパレータ文字を挿入する(b)。 (a) に対して(b) をフルテキスト検索する。 まとめ Elasticsearch の日本語解析の周辺をやや細かく眺めてみました。 文字の正規化やストップワードの除去など、テキスト検索に必要な各種機能をElasticsearch はワンストップで提供しています。文書のフィールド毎に解析方法を替えることができ柔軟性が高いです。したがって通常の用途(それなりに単語切りできれば良い)には十分ですが長い専門用語を正確に検索したいといった用途には向きません。 Elasticsearch の設計思想は、長い単語を正確に形態素解析してprecision を上げるより短く切って recall 重視というように感じます。そのほうが解析速度も確保できます。 また、Kuromoji と その Elasticsearch プラグインである analysis-kuromoji (あるいは Lucene Kuromoji)はソースコードが違うというのも意外でした。様々なバージョンがあり辞書や解析結果が若干異なるというのは面倒です。またユーザ辞書の扱いがMeCab に比べると弱いと感じました。後継(Sudachi) が登場しているのはその辺りが理由かもしれません。 PostgreSQLは MeCab を呼ぶ機能はありますが、それ以外の正規化や後処理は基本的に自作する必要があります。 ただ、どれも実装は容易です。MeCabは登場してから10年以上経ちますが、基本設計と性能・実装が良いため日本語形態素解析のスタンダードとして確立しています。ユーザ辞書に辞書登録すれば原文にその単語が出てきたときに正しく分割されます。その意味で安心して使うことができます。 今回は日本語形態素解析周辺の話で終わってしましました。Elasticsearch と PostgreSQL に於ける同義語・類義語の扱いや、Elasticsearch 7.* で導入されたドキュメントベクトル検索について触れられませんでした。いずれ機会があれば紹介したいと思っています。 注釈 [1] 以前は plugin 版や docker 版もありましたが、最新版 Elasticsearch 7.* にはないようです。 Elasticsearch は上述したように全てのオペレーションをREST API で実行できるので、開発や動作確認は curl や wget で済むといえば済むのですが、head には JSON のvalidate やprettyPrint 機能もあり便利です。クラスタの統計情報を確認することもできます。 [2] なので char_filter には analysis-icu を使い tokenizer には 後述する analysis-kurmoji を使うのがベストプラクティスとされているようです。 [3] URL版は Elasticsearch 5.* 用で最新版では動作しませんが、簡単な修正で 6.* ↑ 用にビルドすることができます。 [4] これは Kuromoji 自体の機能です。 [5] Kuromoji(Atilika)0.9-SNAPSHOTに、NEologd(ipadic、unidic)を適用してみた話 - CLOVER 🍀 [6] mode = normal で計測しました。analysis-mecab ではシステム辞書として mecab-ipadic-neologd を指定しています。
FORCIAアドベントカレンダー2020 12日目の記事です。 弊社はこれまで PostgreSQL を利用した高速なスペック検索をコアコンピタンスとしてきましたが、今後はドキュメント検索にも注力していく予定です。OSS のドキュメント検索エンジンといえばまず思いつくのが Elasticsearch です。PostgreSQL と比較されることの多い Elasticsearch ですが、今回は特に日本語処理の周りを技術的にやや深めに比較してみたいと思います。 本記事はPostgreSQL についてある程度知っているがElasticsearch はあまり知らない、という方を対象としていま
FORCIAアドベントカレンダー2020 11日目の記事です。 こんにちは。旅行プラットフォーム部の新卒1年目エンジニアの三浦です。 業務では大きな旅行サイトのプロジェクトに携わっており、技術面・仕事面ともにキャッチアップに追われる日々を過ごしています。 さて、今日は日々の業務で行っている、チーム開発に欠かせないコードレビューについての記事です。 先日チーム内でコードレビューについて話し合う時間があり、その中で勉強になる意見が多かったので自分なりにまとめて皆さんにお伝えしたいと思います。 この記事を読んでくださった方が少しでも「コードレビューやっていくか~」という気持ちになっていただければ幸いです。 記事に出てくる用語の整理 MR:Merge Requestの略。社内のコード管理に使用しているGitLab上で、改修を行ったブランチをMasterブランチに取り込む際に出すもの。これを見てコードレビューをし、大丈夫そうなら改修が取り込まれる。GitHubのPull Request レビュワー:コードレビューをする人。MRを見る人 レビュイー:コードレビューを受ける人。MRの作成者 そもそもコードレビューってなに? 私は未経験から入社したエンジニアなので、この状態から始まりました。 読んで字のごとく、人の書いたソースコードを見てレビューすることですが、より具体的に言うと、たとえば間違いがないか、もっと良い書き方はないかなどを実装者のコードを確認して指摘したりすることですよね。チーム開発ではそのようにコードの質を担保していきます。 しかしながら、自分でコーディングすることすら不慣れな私には、人のコードを読むことは難題でした。レビューなんて「ベテランの先輩が新人のコードが正しいかどうかチェックする」くらいのものだろうと思い、今のチームに参加した当時は敬遠していました。 ですが実際は様々なメリットが望めるもので、むしろ新人でプロジェクトに途中から参加した私のような立場の人こそ積極的にやっていくべきことなのです。 そう思えるようになったのは、「何のためにコードレビューをするのか」という目的意識を持てるようになったからだと思います。 「バグを減らす」ことももちろん大事な目的ではありますが、それだけではなく様々な目的があったのです。 では、そもそもどうしてコードレビューをするのでしょうか? コードレビューの目的とは 様々あるコードレビューの目的を整理してみます。 目的が意識できればやる気にもつながりますし、とるべき手段や考えるべきことも見えやすくなります。 バグを減らす 仕様の誤解、実装(ロジック)レベルのバグ、改修によるデグレなど細分化もできますが、とにかく予期しない挙動をなくすことです。一番わかりすい目的ですね。 ある先輩曰く「無くす、ではない(重要)」らしいです。レビューばかりにコストは避けないのでバランスが難しいですね。プロダクトによっても異なりそうです。 メンテナビリティの向上 メンテナンスのため、可読性向上や実装方針の統一も大事ですね。実装者以外の人が手を入れることが容易になります。 テストを書いておいて欲しいとなることもありますよね。少し入り組んだロジックでも、テストに通っていればひとまずその単位では動作が保証され、後の調査などの助けになります。 レビュイーの成長 この部分は特に先輩の方が意識されているところかと思います。実装者にはなかった発想や知識を提示することで、レビュイーの成長となります。 レビュワーの成長 先輩曰く「コードを読むことが最大のコーディング力&仕様理解の向上」。この頃実感できるようになりました。 私の場合、理解できないときはまず自分でいろいろ調べてみます。それでも理解できないときはその改修の方針や意図を理解できていないのか、または実装がおかしい可能性もあるので質問してみることに。とても勉強になります。 その他の人の共通理解の助長 私のチームではMRへのコメントを全てSlackに流すように設定しています。誰かが気になったり、良いと思ったりした部分が切り取られて流れるので、ここからの学びは相当大きいです。 まったく触ったことがない部分でも案外、「これ前にMRのコメントでチラッと見たな」となることもあります。 slackにコメントが流れるイメージ 諸説あるとは思いますが、私たちは以上のようなことをレビューの目的として意識しています。 これだけ意義深いとなれば「やってやろう」という気持ちにもなりますよね。 しかし、実際やるとなるとこれがまた大変で難しいのです。私も、自身の成長になるんだ!とは思いつつ、どうコメントすればいいんだ・・・と悩んでしまいがちです。適切なレビューをするにはどのようにしたら良いのでしょうか? どのような手順でレビューすると良いか 実際にレビューをするには、ただ漠然とコードを眺めていてもだめですよね。レビューをする際に意識すると良さそうな点を順序立てて整理してみます。 これを逆に考えれば、「レビュイーがどんなMRを書いていくと良いか」という話にもつながります。 まず改修の目的(before-afterで何を変えようとしているか)を確認 「何のための」「どう考えての」「どういう改修か」を確認する。 MRの概要欄や、リンクされている事前のやり取りなどを見ても不明瞭なら遠慮なく質問する。 そのMRでは(まだ)やらないことも確認する。不必要な指摘を避けられる。 [中上級者向け]レビュイーの仕様認識が妥当そうか検討してみる。 プロジェクト特有の知識、コモンセンスなどを頼りにする。 どのようにその仕様を確認したか(顧客と直接やり取りした、社内の〇〇さんに聞いた、定義書を確認した、自己判断、など)。 目的を実現する実装方針を自分なりに想像してみる どこで、どんな処理をすることになりそうか。考えた結果見当がつかないとしても考えないより良いはず。 複数案あって良い。 複雑なロジックがありそうかどうか。 [中上級者向け]見落としがちな例外ケースに思いを馳せてみる。 diffがあるファイルリストをみる diffがあるファイルリストを軽く眺め、ガッツリ見るべきファイルとそうでないファイルにあたりをつける。 複雑な処理がありそうな箇所を見る。 テストがあればそこから見た方が楽に実装を追える可能性がある。 2.の自分の想像と乖離があれば注意力をアップする。 実装を追う 感想ベースで、良いと思ったところにスタンプを挟むだけでも良い。 ここまで見てもらえているという、コードをレビューしてもらっている感が出てレビュイーも安心できる。 前述のようにslackに流れるので、レビュワー以外への共有にもなる。 MRへコメントすることへのハードルが下がって、レビューが活発になる。 読んでもよく分からないところは(調べつつ)率直に指摘する。折角だから教わる機会とするぐらいの気持ちでやる。 1.で確認した目的が過不足なく達成されているか自信がなければ気軽に聞いてみる。 1.で確認した目的と別の目的と思われる改修が含まれていたら指摘する。 2.で想像した方針と実際の実装方針が異なっていたら 自分・レビュイーいずれかに考慮漏れがないか、注意力を上げて見る。 自分の方針の方が良いかもしれないと思ったら、提案してみる。 方針ごとの優劣を考えてみる。どんな基準で方針を決めたか聞いてみるのも良い。 複雑なロジックほど注意してみる。テストが欲しい、コメントが欲しい、などは指摘しやすい。 テストケースの過不足やテストコード自体の可読性にも注意してみる。 [中上級者向け]類似の仕様や処理、定義値、命名などに心当たりがあれば軽く探して見比べてみる。 どうでしょうか。これだけ順序立てて考えるべき項目を整理しておけば、レビューの質も上がりそうな気がしてきませんか。もちろん、実際にこんなに細かく考えるのは難しいです。それでも、私はこれに即して考えることで以前より楽にレビューできるようになりました。 レビュワーが心がけたいこと 最後におまけとして、チーム全体でより良いコードレビューをしていくために、レビュワーが心がけるべきことをまとめておきます。これはチームや状況ごとに様々だと思います。あくまで私のチームで心がけていることです。 ある程度時間と労力をかける覚悟をもつ。 かけた労力以上の見返りはあるはず。 レビューをいっぱいすればチームも活発に、自身もそのアプリのマスターに。 レビュイーを待たせすぎない。 アサインされたら遅くとも1日以内、理想は通知され次第即見るくらいの気持ちで(gitlabの機能で、MRごとに自動で1人のレビュワーがアサインされます)。 レビュイーが指摘に対応する時間、も計算に入れる。 コメントしたらレスポンスが帰ってきてないか気にかける。 本当に見る人を選びそうなら見れそうな人の手動再アサインも視野に入れる。 レビュイーの労力を減らす。 可能なら改善方法のsuggestを出す。 簡単なコンフリクトなら指摘だけでなく解消してあげる。 とは言えレビュイーの労力を理由に指摘や質問を遠慮するべきではない。 ある程度はレビュイーを信頼する。 流石に動作確認はしてるはず、手元で自動テストは通してるはず、など。 もちろんレビュイーのレベルにもよる。 最悪大事に至らない段階で(CIの自動テスト等で)バグ検出が可能な面については多少注意力を下げても良いかもしれない。 MRへのコメントはレビュイーだけに向けたコメントではなく、全体への共有のためのコメントでもある。 最後に 先日チーム内でコードレビューについて話し合い、上記の内容が共有されたのですが、以前よりもレビューやコメントが活発になりました。また、実装者もよりレビューしてもらいやすいMR作りを心がけるようになりました。やはり、共通認識は大事ですね。 何より私自身がこれらを意識するようになり、人のMRをよく見るようになりました(とは言えレビューするのは難しいと感じる毎日ですが(笑))。 MRのコメント欄で議論が繰り広げられている図 チーム開発をする上で避けては通れないコードレビューですが、どうせやるなら楽しく、より成長できるものにしたいですね。それでは、皆さんも良いコードレビューライフを。
FORCIAアドベントカレンダー2020 10日目の記事です。 こんにちは。旅行プラットフォーム部エンジニアの乙村です。 フォルシアでは JavaScript を利用して開発することが多いのですが、最近は JavaScript の世界にも TypeScript という形で「型」の概念が広まり始めています。私が社会人エンジニアとして初めて触った言語は C++ という型付けがキッチリしている言語でしたが、学び始めた当初「インターフェース(抽象型)って何の役に立つのだろう?」と、ずっと疑問に思っていました。 インターフェース(抽象型)は何がうれしいのか、どういう場面で役に立つのかについて TypeScript を使って説明してみたいと思います。 はじめに オブジェクト指向(Object Oriented, 以下OO)の世界では当たり前のように使われるインターフェースですが、メソッドのシグニチャ定義がされているだけで実装がないため、最初は何が嬉しいのかよくわからないことが多いです。実際には依存関係の制御や実装の隠蔽などで、これがないと OO は成り立たないと言って良いくらい重要な概念です。 pure JavaScript には構文としての interface はありませんが、TypeScript は interface 構文をサポートしており、同じような恩恵が受けられます。本文中のサンプルコードは TypeScript で、Node.js 13.0.1、TypeScript 4.1.2 で動作確認をしています。 以降の話は、クラス(Class)による設計を中心とする言語(C++, C#, Java 等) におけるインターフェースの使い方の話ですので、TypeScript の文法としての interface を知りたい方は 公式ドキュメント を読むことをお勧めします。 オブジェクト指向言語における「インターフェース」とは 実装のないクラスだと思って下さい。メソッドの型だけ定義してありますが、そのメソッドを呼んだときの処理は何も定義されていません。プロパティを持つ場合もあります。 処理が定義されていないので、インスタンス化(new)できません。 C++、C#、Java などのオブジェクト指向言語ではインターフェースを作るための構文として "interface" が存在します。 クラスの多重継承は禁止されている言語が多いですが、インターフェースは多重継承が可能です。 インターフェースについて覚えるべきことは 2 つ 1. インターフェースを実装(継承)するクラスは、そのメソッドの処理を必ず実装する必要がある 以下のクラス図のように ClassA が InterfaceA を実装しているとします。 ClassA は method1() を実装していないと(コンパイル時やトランスパイル時の)型チェックでエラーになります。method2() のように ClassA 独自のメソッドがあっても問題ありません。InterfaceA で定義されている method1() が実装されてさえいれば、インターフェースを継承するクラスとしてのルールは守っています。 2. インターフェースを実装(継承)したクラスのインスタンスはインターフェース型の変数に代入できる ClassAのインスタンス(クラスを new した実体)を、InterfaceA の型の変数に代入できます。ただし、インターフェース型の変数からアクセスできるメソッドはインターフェースに定義されたもののみになります。 // インターフェース InterfaceA // 実装内容はなんでもよいが、このインターフェースを継承するクラスは // method1 を実装しないといけない interface InterfaceA { method1() : void; }; // InterfaceA を継承(実装) したクラス ClassA // method1 の実装があるので型チェックは通る class ClassA implements InterfaceA { // method1() をコメントアウトするとエラー method1() : void{ console.log("ClassA - method1()"); } method2() : void { console.log("ClassA - method2()") } }; let dummy = new InterfaceA(); // エラー(TS2693). インターフェイスは new できない let classTypedVar : ClassA = new ClassA(); // OK. クラスのインスタンスをクラスの型の変数に代入可能 let interfaceTypedVar : InterfaceA = new ClassA(); // OK. クラスのインスタンスをインターフェースの型の変数に代入可能 classTypedVar.method1(); // OK classTypedVar.method2(); // OK interfaceTypedVar.method1(); // OK interfaceTypedVar.method2(); // エラー(TS2551). インターフェースに定義のあるメソッドしか呼べない この「エラー」は TypeScript であればトランスパイル時に検知されます。コンパイル型の静的言語であればコンパイル時に指摘されます。エディタやIDEによっては、コーディング中に即時に指摘されます。 引数で渡す場合も代入と同様です。引数の型に InterfaceA が指定されている場合に、ClassA のインスタンスを渡すことができます。 // 関数の引数でも話は同じ. InterfaceA が要求されている箇所に ClassA のインスタンスを渡せる const someFunction = (interfaceTypedVar : InterfaceA) => { interfaceTypedVar.method1(); // OK interfaceTypedVar.method2(); // エラー(TS2551). インターフェースに定義のあるメソッドしか呼べない } someFunction(new ClassA()); // OK 注目すべきポイント ここで注目すべきなのは、someFunction の内部では ClassA の存在を知らなくてもそのメソッド(処理)を呼べるということです。つまり、InterfaceA を実装していさえすれば、ClassB でも ClassCでもそのインスタンスを渡すことができ、someFucntion はそれが実際は何のクラスのインスタンスかを知ることなく、そのメソッドを呼ぶことができます。 OOでよく聞くプラクティスの一つに「実装ではなく抽象に依存すべき」というものがあります。「抽象」を実現したものがインターフェース、「実装」はそのインターフェースを継承(実装)したクラスと考えてください。このプラクティスに従えば、someFunction の引数の型としては、ClassA ではなく InterfaceA のほうが好ましいということになります。 const someFunction = (classTypedVar: ClassA) => {...} // こうするとClassAに依存してしまう! const someFunction = (interfaceTypedVar : InterfaceA) => {...} // こうしたほうがいい で、一体何が嬉しいんでしょうか・・・?この例だとよくわかりません。 インターフェース活用の例 インターフェースが有用なケースを、Key-Value ストアクラス の作成を例に考えてみます。 1.よくあるパッケージ構成 アプリケーション用のクラスは app 、共通で使うクラスを infra というパッケージに置くことにします。共通して使うためのクラスとして、 KeyValueStore クラスを用意します。 共通部分を抜き出して複数のファイルから参照して利用するという、よく見る構成です。 KeyValueStore クラスはKey-Value 型のデータを管理するクラスですが、今回は永続化が不要という要件だったとして、メモリ上に Key-Value の情報を保持することにします。TypeScript での実装例は以下の通りです。 // infra/ keyValueStore.ts class KeyValueStore { // 属性 (データの保存先.ただのオブジェクトを辞書として使う) dictionary: { [key: string]: string; // TypeScript 記法. key も value も string 型の意味 }; // コンストラクタ constructor() { this.dictionary = {}; } // Key-Value の組み合わせで保存する save(key: string, val: string): void { this.dictionary[key] = val; } // key に結びついた Value を取得する load(key: string): string { return this.dictionary[key]; } // 保存されているデータを表示する showAll(): void { Object.keys(this.dictionary).map((key) => { console.log(`${key} : ${this.dictionary[key]}`); }); } } export default new KeyValueStore(); // インスタンスをエクスポートする KeyValueStore クラスのインスタンスをどう取得するかですが、ここではお手軽に、クラスを定義しているモジュール keyValueStore で、クラスではなくインスタンス自体を export しています。Node のように一度しかロードされないことが保証されているモジュールシステムを想定すると、このインスタンスはシングルトンになります。 ClientA と ClientB が それぞれ違うキーと値を保存します。違いは保存する Key-Valueだけです。 // app/ClientA.ts import keyValueStore from "../infra/keyValueStore"; export default class ClientA { public someMethod() : void { keyValueStore.save("A", "A desuyo."); } } // app/ClientB.ts import keyValueStore from "../infra/keyValueStore"; export default class ClientB { public someMethod() : void { keyValueStore.save("B", "B desuyo."); } } ちゃんと Key-Value が保存されているか main で確認します。 // main.ts import ClientA from "./app/ClientA" import ClientB from "./app/ClientB" import keyValueStore from "./infra/keyValueStore" new ClientA().someMethod(); new ClientB().someMethod(); keyValueStore.showAll(); // output // A : A desuyo. // B : B desuyo. このようにクラスを直接参照して使用する方法は、非常によくあるやり方です。サードパーティ製のライブラリをこのように使うことも多いはずです。 特に問題ないように見えますし、実際問題ないことも多いのですが、OO的にはこの構成では以下のような要求に応えにくいことが問題点とされています。 自作の Key-Value ストアではなくて世の中のイケてる Key-Value DBを使いたくなった 単体テスト時は決まった初期データでテストしたいから、ローカルファイルのデータを参照するようにしたい そもそも Key-Value ストアの実体はクライアントにとってはどうでもいい。実体の決定をなるべく遅らせたい また、言語によりますが、 keyValueStore のソースを少しでも変更すると、クライアントをすべてビルドしなおす必要がある という開発上のデメリット(ビルド時間の増大)が発生します。 これをインターフェースを使って解決します。 2.Key-Value ストアへの依存を切り離し、実装を切り替えやすくした構成 図にすると一気に複雑に見えますが・・・ IKeyValueStore という名前のインターフェースが登場しています。 この IKeyValueStore インターフェース を実装(継承)するクラスが、DBStore, OnMemoryStore, TextFileStore といった具象クラスです。これらはあくまで例ですが、それぞれ、DBへの保存、メモリ上への保存、テキストファイルへの保存をする Key-Value ストアを表しています。 ポイントは、クライアント(ClientA, ClientB)からは、インターフェース(IKeyValueStore) に依存があるだけで、Key-Valueストアの実処理が定義してあるクラス(DBStore, OnMemoryStore, TextFileStore)に依存(--->)がないということです。 クライアントは InfraFactory クラスの getStore() というメソッドを呼んでストアの実体を取得しますが、その返り値の型はインタフェースであり、実際にそれが何クラスのインスタンスなのかはわかりません。 実装を一部見ていきます。まず、インターフェース(IKeyValueStore ) には Key-Value ストアとして備えていてほしい機能(メソッド)を定義しておきます。 // infra/IKeyValueStore .ts export default interface IKeyValueStore { save(key: string, val: string): void; // Key-Value の保存 load(key: string): string; // Key-Value の読み出し showAll() : void; // 保存している値の出力(これは無くてもいい) } インターフェースを実装するクラスで、その実処理を定義します。 // infra/OnMemoryStore .ts import IKeyValueStore from "./IKeyValueStore"; // TypeScript の'implements' キーワードを使って、IKeyValueStore を // 継承したクラス OnMemoryStore を作る // IKeyValueStore で定義したメソッドを実装していないとエラーになる class OnMemoryStore implements IKeyValueStore { // (略) 「1.よくあるパッケージ構成」の KeyValueStore の実装と全く同じ } export default OnMemoryStore; // インスタンスではなく、クラスをエクスポート DBStore クラス、TextFileStore クラスも同様に IKeyValueStore インターフェースで定義されているメソッドを実装します。DBStore であれば Key-Value DB への値の読み書きがされるように実装し、TextFileStore であれば File への値の読み書きがされるように実装します。 クライアントから直接 OnMemoryStore などのクラスを new してしまうと具体的なクラスへの依存が発生することになり、後から切り替える場合にクライアント側の修正が必要になってしまいます。それを避けるため、インスタンスを作って返してくれるファクトリ(工場)の役割を持つクラスを用意します。 // infra/ InfraFactory.ts // key-value ストアインターフェース import IKeyValueStore from "./IKeyValueStore"; // インターフェースを実装(継承)したクラス群 import OnMemoryStore from "./OnMemoryStore"; import DBStore from "./DBStore"; import TextFileStore from "./TextFileStore"; // ストアの実体を返してくれるクラス class InfraFactory { private store : IKeyValueStore; // インターフェース型の属性 constructor(config : {storeType:String}) { if (config.storeType === "OnMemory") { // メモリ上にデータを保存するストア this.store = new OnMemoryStore(); } else if (config.storeType === "DB") { // DB上にデータを保存するストア this.store = new DBStore(); } else if (config.storeType === "Text"){ // テキストファイルにデータを保存するストア this.store = new TextFileStore(); } else { // default console.error("Wrong Configuration :", config) this.store = new OnMemoryStore(); } } getKeyValueStore() : IKeyValueStore { return this.store; } } // プログラム全体で、どの Key-Value ストアを使うのか、ここだけで変更が可能 // 環境変数や設定ファイルで変更することもできる const infraFactory = new InfraFactory({storeType:"OnMemory"}); export default infraFactory; このファクトリにより、あとで Key-Value ストアを変更したくなった場合に、このファクトリだけ修正すればよいことになります。 1.の問題点への回答 自作の Key-Value ストアではなくて世の中のイケてる Key-Value DBを使いたくなった 「イケてる Key-Value DB 」のAPIを save, load で呼び出すクラスを作成して getKeyValueStore() で返すようにする 単体テスト時は決まった初期データでテストしたいから、ローカルファイルのデータを参照するようにしたい テキストファイルを読み出して初期データとして持つクラスを作成して getKeyValueStore() で返すようにする そもそも Key-Value ストアの実体はクライアントにとってはどうでもいい。実体の決定をなるべく遅らせたい とりあえずデフォルトの OnMemoryStore を返すようにしておいて、決定次第返すクラスを差し替えればよい となります。 TypeScript のインターフェースはもっと柔軟 ここまでに説明した方法は、処理をクラスのメソッドとして表現する言語(JavaやC++)の場合によく行われるやり方ですが、JavaScript は関数そのものを引数や返り値として受け渡すことができる性質(第一級関数)をもつため、必ずしもクラスのインスタンスを返り値で返す必要はありません。TypeScript の場合はクラスだけでなく、データや関数にもインターフェースを設定することができるため、より柔軟でお手軽に実装への直接の依存を切り離すことができます。 https://www.typescriptlang.org/docs/handbook/interfaces.html (今回の例はリンク先の「Class Types」としてのインターフェースの使い方です) 実際には、関数やデータの取得箇所を一か所に集めることで、実装を一気に切り替えるという方法は pure JavaScipt でも可能です。ただし、interface がないために、クライアント側は返されるデータの型(プレーンなデータオブジェクト?クラスのインスタンス?関数?)や持っているプロパティについては何も保証されていない状態で使うことになります。 残る問題点は、パッケージ間の依存関係 これまではクラス間の依存関係に注目してきましたが、app パッケージと infra パッケージというパッケージ間で依存関係を見てみると、infra パッケージにあるインターフェース(IKeyValueStore )やファクトリ(InfraFactory )を app で使用しているため、app --> infra という依存関係ができています。これが問題になるケースがあります。 infra パッケージがないと、app の開発ができない(IKeyValueStore や InfraFactory の import 箇所でエラーになり、単独でビルドできない) infra でインターフェースを変更されると、app の修正が必要になる 呼び出し先ができていないと呼び出し元の開発ができないのは当たり前で、特に問題点ではないように思えます。 app パッケージと infra パッケージが別のチームにより開発されていると考えると、少し問題点がわかるかもしれません。infra チームがころころ interface のメソッドを変えてきたらどうでしょうか? infra 開発チーム 「Key-Value DB の中身を XXXDB から YYYDB に変更することにしたわ!」 「IKeyValueStore も変わるんで呼び出し元の修正よろしく!」 app 開発チーム 「(・・・なんのためのインターフェースやねん)」 ということが起きないように、 Interface を使って app が主導権を握れるようにします。依存関係を制御することで、app が infra に依存するのではなく、infra が app に依存するようにできます。 3.Key-Value ストアクラスだけでなく、それを含むパッケージへの依存を切り離した構成 これで infra パッケージが app パッケージに依存するようになりました。インターフェースとクラスの依存関係について補足すると、inferfaceA を classA が実装するとき、classA は intefaceA を知っていなければならない(importの必要がある)ため、依存性の向きは classA --> interfaceA となります。 まず、infra にあった IKeyValueStore インターフェースが app に移動しています。これで app はパッケージ内のインターフェースを参照すればよいことになり、infra のファイルを参照する必要がなくなります。これまでは、使われる側の infra が「うちはこういうAPIなのでそれに従って使ってください」という、変更の主導権が infra 側にある形(infra でAPIを変えられると app は従わざるをえない)でしたが、変更後は、使う側の app が「こういうインターフェースがほしい」と宣言して、infra がそれに従って実装するという主従関係の逆転が起きています。 ただし、InfraFactory クラスも infra にあったので、これを放置すると app と infra の相互依存になってしまい問題です。もう一段階抽象化して、Factory の実体を直接参照するのではなく、FactoryRepository から取得するようにします。 どれだけ抽象化を進めてもどこかでその実体を決定しなければなりません。それは通常、プログラムの開始位置 main に近い場所になります。プログラムの開始直後に、インターフェースから返る実体クラスを決定しておく必要があります。これを依存性の注入といいます。 新しく登場したインターフェース・クラスの実装をみてみます。 まずは、ファクトリのインターフェース IInfraFactory です。ストアを返すメソッド getStore() を実装する必要がある、ということを表現しています。 // app/IInfraFactory import IKeyValueStore from "./IKeyValueStore"; export default interface IInfraFactory { getStore() : IKeyValueStore; } ファクトリの実体クラス InfraFactory は IInfraFactory インターフェースを implements する以外は、前の例と変わっていません。すでに getStore() は実装済だからです。 // infra/InfraFactory // key-value ストアインターフェース // ★ インターフェースの場所が infra から app に代わっている import IKeyValueStore from "../app/IKeyValueStore"; import IInfraFactory from "../app/IInfraFactory"; // インターフェースを実装(継承)したクラス群 import OnMemoryStore from "./OnMemoryStore"; import DBStore from "./DBStore"; import TextFileStore from "./TextFileStore"; // ストアの実体を返してくれるクラス class InfraFactory implements IInfraFactory{ // ★ implements キーワードで「実装」を表す // 実装は前と同じ } export default InfraFactory; FactoryRepository はファクトリを登録するだけの場所です。 // app/factoryRepository.ts import IInfraFactory from "./IInfraFactory"; class FactoryRepository { private infraFactory : IInfraFactory; // ファクトリの実体は外部から注入する setInfraFactory(infraFactory : IInfraFactory) { this.infraFactory = infraFactory; } getInfraFactory() : IInfraFactory { return this.infraFactory; } } export default new FactoryRepository(); // インスタンスを返す. シングルトン. main でファクトリの実体を決定します。 // main.ts import InfraFactory from "./infra/InfraFactory"; import factoryRepository from "./app/factoryRepository" import ClientA from "./app/ClientA"; import ClientB from "./app/ClientB"; // 依存性の注入 // infra/InfraFactory を app の FactoryRepository に設定 factoryRepository.setInfraFactory(new InfraFactory({storeType:"OnMemory"})); new ClientA().someMethod(); new ClientB().someMethod(); factoryRepository.getInfraFactory().getStore().showAll(); // output // A : A desuyo. // B : B desuyo. クライアントは FactoryRepository からファクトリを取得して使用します。 // app/ClientA import factoryRepository from "./factoryRepository"; export default class ClientA { public someMethod() : void { factoryRepository.getInfraFactory().getStore().save("A", "A desuyo."); } } この状態で infra パッケージをまるごと削除しても、コンパイル/トランスパイルの型チェックでエラーが発生するのは main だけです。infra が開発を完了するまで、ダミーの infra パッケージを用意して main で依存性注入しておけば、app は infra を気にすることなく開発を進めることができます。 依存関係逆転の原則とクリーンアーキテクチャ 呼び出し元と呼び出し先の依存関係は「呼び出し元 → 呼び出し先」になるのが普通ですが、この例のように interface を使って、その方向を逆にすることができます。このテクニックは「依存関係逆転の原則」と呼ばれており、 「Clean Architecture 達人に学ぶソフトウェアの構造と設計」 に詳しく載っています。 この依存関係逆転の原則を徹底し、その中心点(依存の行きつく先)に Domain Driven Development (DDD) でいうドメインモデルを置いたものが 「クリーンアーキテクチャ」 になります。 まとめ インターフェースを使ってクライアントから実装を隠蔽することで、後からの変更をしやすくしたり、また、パッケージ間の依存関係のコントロールができるようになります。ただ、この例を見てもわかるとおり、同じ処理をするにもクラスやインターフェースの数が増えています。抽象化に伴う開発コストを払ってでも、変更容易性を確保したいかどうかは見極める必要があります。
FORCIAアドベントカレンダー2020 10日目の記事です。 こんにちは。旅行プラットフォーム部エンジニアの乙村です。 フォルシアでは JavaScript を利用して開発することが多いのですが、最近は JavaScript の世界にも TypeScript という形で「型」の概念が広まり始めています。私が社会人エンジニアとして初めて触った言語は C++ という型付けがキッチリしている言語でしたが、学び始めた当初「インターフェース(抽象型)って何の役に立つのだろう?」と、ずっと疑問に思っていました。 インターフェース(抽象型)は何がうれしいのか、どういう場面で役に立つのかについて Ty
FORCIAアドベントカレンダー2020 9日目の記事です。 事業開発部所属エンジニアの籏野です。 フォルシアではデータの取り込み・DBの構築といったバッチ処理についてフォルシア独自のツールを開発し、管理・実行していました。この独自ツールは、あらかじめ決められたフローを、設定を変えて実行するような作りになっており、タスク実行順の組み換えやアプリ独自の処理を追加するにはツール自体をアプリごとにカスタマイズする必要がありました。 このアプリごとのカスタマイズをより簡単に行えるよう、最近フォルシアでは「ecflow」というワークフローエンジンを導入し始めました。本記事では簡単なワークフローを作りながら、ecflowについて紹介したいと思います。 ecflowとは? ecflowは欧州中期予報センター(ECMWF)が開発したワークフローエンジンであり、天気予報のためのプログラム実行を担っています。 こちらの記事 でも紹介していますが、複雑な依存関係を持った大量のタスクを処理できるだけでなく、タスク間の待ち時間が短いことが特徴です。 フォルシアはフロントでの検索速度だけでなくバッチの速さにも重きを置いているため、このオーバーヘッドが短縮されることは大きなメリットでした。 ecflowでは独自形式のファイルを組み合わせることで、一つのワークフローを構築していきます。どのようなファイルを用意する必要があるのか、具体的に紹介していきたいと思います。 ※ecflowのインストールは 本家のドキュメント を参考にしてください。 タスクの定義 今回は単純に「Hello!!」と出力するだけのワークフローを作ってみます。 ファイル構成は以下のようになります。 ├── ecf_files │ └── echo.ecf ├── ecf_include │ ├── head.h │ └── tail.h └── test.py .ecfファイル ecflowで実行される処理は.ecfファイルに記載します。 例えば、今回作成した echo.ecf は以下のようになっており、任意の単語を出力できるようになっています。 %include <head.h> echo "%WORD%" %include <tail.h> 変数の埋め込み .ecfファイルでは変数名を % で囲うことで任意の文字列を埋め込むことができます。例に挙げた echo.ecf では %WORD% 部分に任意の文字列を埋め込むことで、出力する文字列を設定できるようになっています。 include %include <{{ file_name }}> と記載することで、任意の処理を各.ecfファイルに追加することができます。各タスクで共通に実行されるべき処理は別のファイルに切り出すことができるのです。 今回はecflowに対して、実行開始/終了を知らせる処理を head.h と tail.h に切り出しています。 【head.h】 #!/bin/bash set -eux set -o pipefail # ecflowとのやり取りに必要な変数 export ECF_PORT=%ECF_PORT% export ECF_HOST=%ECF_HOST% export ECF_NAME=%ECF_NAME% export ECF_PASS=%ECF_PASS% export ECF_TRYNO=%ECF_TRYNO% export ECF_RID=$$ export PATH=/usr/local/ecflow-%ECF_VERSION%/bin:$PATH # ecflowにタスク開始を知らせる。 ecflow_client --init=$$ # タスク中でエラーが発生した場合に実行する。 ERROR() { set +e wait ecflow_client --abort=trap trap 0 exit 0 } trap ERROR 0 trap '{ echo "Killed by a signal"; ERROR ; }' 1 2 3 4 5 6 7 8 10 12 13 15 【tail.h】 wait # ecflowにタスク終了を知らせる。 ecflow_client --complete trap 0 exit 0 ワークフローの構築 用意した.ecfファイルを組み合わせて、一つのワークフローを構成する必要があります。そのためには「それぞれの.ecfファイルをどのような順番で実行するか」を記載したファイルを用意し、ecflowに読み込ませる必要があります。 しかし、フローが複雑になってくると、この設定を0から用意するのがかなり難しくなってきます。そこでecflowが用意しているPythonライブラリを利用します。 【test.py】 import os from ecflow import Defs, Suite, Family, Task, Edit THIS_DIR = os.path.dirname(os.path.abspath(__file__)) ECF_DIR = os.path.join(THIS_DIR, "ecf_files") INCLUDE_DIR = os.path.join(THIS_DIR, "ecf_include") print("Creating suite definition") # Suite: 一つのワークフローを示す suite = Suite( "test", Edit( ECF_HOME=THIS_DIR, ECF_FILES=ECF_DIR, # ecfファイルを置いたディレクトリ ECF_INCLUDE=INCLUDE_DIR #includeするファイルを置いたディレクトリ ) ) # Family: 複数のタスクやFamilyをまとめたもの # 任意の名前を付ける(今回は"hello") hello = Family("hello") # Task: ecfファイルを読み込み処理を実行する hello.add_task( Task( "echo", # ecfファイルを指定 Edit( WORD="Hello!!" # %WORD%に埋め込む文字列 ) ) ) suite.add_family(hello) defs = Defs() defs.add_suite(suite) print("Checking job creation: .ecf -> .job0") print(defs.check_job_creation()) print("Saving definition to file 'test.def'") defs.save_as_defs("test.def") 上記を実行すると以下のように新しいファイルが生成されます。 ├── ecf_files │ └── echo.ecf ├── ecf_include │ ├── head.h │ └── tail.h ├── test │ └── hello │ └── echo.job0 ★NEW ├── test.def ★NEW └── test.py .defファイル 新しく生成された test.def が、先に紹介した「それぞれの.ecfファイルをどのような順番で実行するか」を設定したファイルになります。このファイルをecflowが読み込むことでワークフローが構築されます。 #5.1.0 suite test edit ECF_HOME '/home/forcia/ecflow_test' edit ECF_FILES '/home/forcia/ecflow_test/ecf_files' edit ECF_INCLUDE '/home/forcia/ecflow_test/ecf_include' family hello task echo edit WORD 'Hello!!' endfamily endsuite # enddef .jobファイル では、 echo.job0 とは何なのでしょうか?ファイルの中身は以下のようになっています。 #!/bin/bash set -eux set -o pipefail # ecflowとのやり取りに必要な変数 export ECF_PORT=3141 export ECF_HOST=localhost export ECF_NAME=/test/hello/echo export ECF_PASS=XXXXXX export ECF_TRYNO=0 export ECF_RID=$$ export PATH=/usr/local/ecflow-5.1.0/bin:$PATH # ecflowにタスク開始を知らせる。 ecflow_client --init=$$ # タスク中でエラーが発生した場合に実行する。 ERROR() { set +e wait ecflow_client --abort=trap trap 0 exit 0 } trap ERROR 0 trap '{ echo "Killed by a signal"; ERROR ; }' 1 2 3 4 5 6 7 8 10 12 13 15 echo "Hello!!" wait # ecflowにタスク終了を知らせる。 ecflow_client --complete trap 0 exit 0 こちらを見てわかるように、 echo.ecf では %include や %WORD% で記載されていた部分が展開されて通常のbashファイルが生成されています。 ecflowでは各変数を展開して生成されたファイルを、defファイルで指定した順番で実行することでワークフローを実行しているのです。 ワークフローの実行 生成されたdefファイルをecflowに読み込ませて実行してみます。 $ ecflow_client --load=test.def # 設定の読み込み $ ecflow_client --begin=test 今回の実行ログは ./test/hello/echo.1 に出力されます。 このファイルを確認すると Hello!! と出力されており、無事タスクが実行されたことがわかります。 ...略... + echo 'Hello!!' Hello!! ...略... 最後に 今回紹介したように、ecflowではワークフロー内のタスクが一つの実行ファイルとして生成されます。そのため、jobファイルを見ればタスク実行時に何が起きているのかが一発でわかり、デバッグ等もやりやすいです。 また、スクリプトに落とし込める処理は何でも実行できるので、タスク生成の自由度も高いのではないかと感じています。 今回の内容以外にもGUIによるワークフローの管理、トリガー設定、タスク失敗時の後処理・・・など、ecflowでできることはたくさんあります。それらについても、今後機会があれば紹介したいと思います。