TECH PLAY

フォルシア

フォルシア の技術ブログ

241

こんにちは、経営企画室 広報の伊藤です。 7月もあっという間に過ぎ去り、いよいよ8月。2024年4月入社を考える学生の皆さんはそろそろ就職情報サイトへの登録などを行う時期でしょうか。 そんな本日は、フォルシアの採用面接において最もよく聞かれる「Spook(スプーク)って何ですか?」という質問を、採用担当者と広報担当の私とで、ITコンサルとエンジニアの社員に投げかけてみました。 コーポレートサイト でもフォルシアならではのテクノロジーとして紹介しているSpookですが、 そもそもSpookとはいったい何なのか? Spookの強みとは何なのか? どんな企業で導入されていて、今後どんな未来を見据えているのか? といった疑問を、社員の生の声で解説していきます! まずは第一弾、 「そもそもSpookとはいったい何のか?」 についてです。 Spookについて教えてくれた先輩社員プロフィール (右)ITコンサル:DXプラットフォーム部 営業部長 諏訪 (左)エンジニア:旅行プラットフォーム部 グループ長 西山 ITコンサルタント職 DXプラットフォーム部 営業部長 諏訪 俊(すわ・しゅん) /写真右 2017年 キャリア入社 旅行会社での海外渡航手配や法人営業を経て、2017年にフォルシアへ入社 前職での経験を活かし、旅行・観光業向け営業活動に5年間携わり、今年からはDXプラットフォーム部で非旅行業界向け営業・コンサル活動に従事 エンジニア職 旅行プラットフォーム部 グループ長 西山 諒平(にしやま・りょうへい) /写真左 2015年 新卒入社 新卒での入社後 、 大手旅行サイト開発を経て、現在は 旅行・観光業界向け商品販売プラットフォームサービス「webコネクト」 のプロダクト開発に従事 ※所属は2022年7月現在のものとなります それではさっそく聞いていきます! そもそも、Spookとは... フォルシアエンジニアで代々受け継いでいる開発の手法+その手法でつくられた機能群 サイトでは「技術基盤」という記載がありますが、Spookとはいったい何なのでしょうか? 西山 /エンジニア目線で一言で言うと、膨大・複雑なデータを高速に検索させるためのソフトウェアです。Spookを用いることで、顧客のDB(データベース)からデータを取り込み、検索に最適化した形に組み換え、ブラウザ表示を最適化させています。また、バックエンド~フロントエンドまで、高速検索の工夫が詰められています。 諏訪 /営業的には、フォルシア独自の検索エンジンを中心とした機能やノウハウの集合体を指していると捉えています。Spookは膨大・複雑なデータの高速処理に強みがあり、and/orを組み合わせた複雑な検索クエリでも高い検索性を維持できることが特長です。 これまでの様々な企業における検索システムの開発知見が集約されており、ゼロから開発するよりも短い期間で高度なシステムを構築できます。 「ソフトウェア」や「ノウハウの集合体」といった表現が出てきましたが、目に見える実態があるものなのでしょうか? 西山 /エンジニア用語で言うとフレームワークというものがありまして、例えば、「Webアプリケーションを使いたかったらこのお作法に則って作れば簡単にセットアップできるよ」というのをフレームワークと言うのですが、Spookはそれに近いイメージだと思っています。 Spookというお作法(フレームワーク)がフォルシアのエンジニアに提供されていて、それに則ってつくれば、ちゃんと、かつ速く動くシステムを素早く簡単につくれるというようなものです。 レシピ本のようなイメージでしょうか? 西山 /そうですね。レシピ本+包丁もフライパンもだいたい同じものがセットになって提供されているようなイメージです。 先ほどフレームワークと表現しましたが、実際には世の中一般にあるフレームワークよりも少し緩く、手順にまるまる従うか否か、エンジニア側で取捨選択の余地があります。なので、人によってはフレームワークと表現しない場合もあるかと思いますが、わかりやすく説明するとしたらフレームワークと表現して差し支えないように感じています。 「フレームワーク」という言葉はエンジニア用語かと思いますが、営業の現場ではどのように表現されているのでしょうか? 諏訪 /フレームワークというのは枠組みであったり土台だと解釈しているのですが、西山さんの説明にもあった通り、土台というほど固まってはいない、「機能の集合体」のように捉えています。 A社で実装したaaa、B社で実装したbbb...といった機能が大量にライブラリに入っていて、そのライブラリの中から今回自分がつくりたい機能に近しいものを取捨選択して新しいものをつくっているといったイメージです。例えば、今回自分が担当するC社はA社とB社とでいえばB社に近しい仕様でありつつも、一部分はA社に似ているという場合は、B社をベースにA社の要素も取り入れてC社用にカスタムして機能をつくっていく。そうやって生まれたものの集合体でSpookは成り立っていると捉えています。 西山 /Spookを用いての開発は受託開発なので、お客さんごとにアプリケーションがあり、アプリケーションごとの実装が必要なのですが、開発の土台としてSpookを用いることで、だいたいの作り方も、作り方のお作法も同じ。なのでそれをライブラリ化することで横展開しやすくなっているという意味では「機能の集合体」という風にも言えるように思います。 なるほど。Spookという商品(システム)を提供しているのですか?といった声もあったのですが、Spookはフォルシアエンジニアで代々受け継いでいる開発の手法+その手法でつくられた機能群ということですね。 成長し続けるSpook × 自分のアイディア = 新しさの創出 技術職志望の学生にライブラリ化されていることを話すと、それを使う・組み合わせるだけなんですか?自分たちで新しいものを考えてつくりだしていかないんですか?といった声を聞くことも多いのですが、自分たちで新しいものを作っていくわけではないということでしょうか? 諏訪 /「新しいものをつくる」ということには、①ゼロベースでこの世にない新しいものをつくる ②世の中にある便利なものを使って、それに自分のアイディアを加えて新しいものをつくる の二種類があると思うのですが、①ができる人はなかなかいないと思います。なので、先人のノウハウを学び、良いと思った部分を採用し、そこに自分のアイディアを加えていく②がいわゆる「新しいものをつくる」ということだと思っていて、Spookはまさにそのかたまりという風に思っていただければと思います。 西山 /この言語がイケてる、こういうフレームワークがイケてるといったエンジニアのなかでの流行り廃りがあるのですが、そういった部分におけるアップデートはSpookにもどんどん適用させていて、Spook自体はどんどん新しいものへとアップデートされてますし、お客さんへ提供する価値という意味でも、これまでの功績ゆえに新しい相談をいただけて、その相談がSpookのさらなる機能追加、提供価値の向上につながっていったりというケースもあります。 なので、事業会社のように0→1という新しさはないかもしれませんが、1→10であったり、10→100という新しさをつくりだしていく場面はまだまだたくさんありますし、なくなることはないと思っています。 つまり、Spookはいまだ完成されたものではない...ということでしょうか? 西山 /成熟はしていますが、完成はしていません。各エンジニアの工夫が共通ライブラリに還元されていったり、開発効率化のためにライブラリがアップデートされたりしていますが、完成はしないものです。   そもそもSpookとはいったい何なのか? フォルシアエンジニアで代々受け継いでいる開発の手法+その手法でつくられた機能群であり、今後も柔軟に成長していくSpook。今後の成長のさせ方も案件次第、創り手次第だと思うと非常に可能性を秘めているように感じます。 続いて第二弾では「Spookの強みとは何のか?」について伺います。お楽しみに!
アバター
タイトル : PostgreSQLで簡単なif文関数を作るなら キーワード: エンジニア、テクノロジー、PostgreSQL こんにちは、エンジニアの羽間です。 フォルシアではDBにPostgreSQLを利用しており、業務でSQLを書く機会がよくあります。 SQLを書く上では ビジネスロジックはできるだけ単体テストを書く SQLの見通しを良くする 同じ処理は共通化する といったことを心がけ、ユーザー定義関数を積極的に作成しています。 商品の検索といった速度を重視する処理においては、処理速度に優れるC言語関数を作成することが多いのですが、 C言語はちょっとした処理を書きたいときに不便で
アバター
概要 こんにちは、エンジニアの籏野です。 フォルシアのフロント開発ではReduxを利用して状態管理をしていることが多いです。 その中で、selectorsの書き方について少々気になったことがあったので紹介したいと思います。 背景 現在開発を進めているプロジェクトではRedux周りのディレクトリ構成にre-ducksパターンを採用しています。 ※re-ducksパターンについては詳しい記事がたくさんあるので詳しい説明は省きます。 re-ducksパターンに沿ってコードを書いているとselectorsを定義すると以下のようにしている方が多いのではないでしょうか。 // selecto
アバター
初めに こんにちは、エンジニアの籏野です。 フォルシアのアプリ開発ではアプリの状態管理にReduxを用いることが多いです。 Reduxから状態を取得するときに「reselect」という言葉が出てきますが、どのような点が嬉しいのかがいまいちわかっていなかったので調べました。 前準備 さくっとReduxを利用したアプリケーションを用意しましょう。 $ npx create-react-app redux-selector-experience --template redux-typescript $ cd redux-selector-experience $ npm run st
アバター
こんにちは。エンジニアの籏野です。 フォルシアでは OpenAPI でAPI定義を書いてから、APIを実装するのが一般的になってきました。 APIの実装についてはTypeScriptとexpressを利用することが増えてきている状況です。 今回はexpressとOpenAPIをより強固に結びつけるためのモジュールとして express-openapi を見つけたので試してみました。 ざっくりまとめ エンドポイントをOpenAPI定義に沿って作成してくれる リクエストパラメータのバリデーションもお手の物 レスポンス項目もチェックしてくれる 成果物 環境 Node.js: 16.13.2 TypeScript: 4.6.2 express: 4.17.3 express-openapi: 10.1.0 準備 npmでプロジェクトを作成し以下のモジュールをインストールします。 linter等はお好みで。 express express-openapi TypeScript @types/express ts-node expressの起動 まずはシンプルにexpressサーバーを起動してみます。 import express, { Request, Response } from "express"; const app = express(); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.listen(3000, () => { console.log("Start on port 3000"); }); app.get("/user", (req: Request, res: Response) => { res.send({ name: "hatano" }); }); ts-nodeを利用すれば、tsファイルのままで実行できるので便利ですね。 npx ts-node index.ts http://localhost:3000/user にアクセスすることで以下の結果が取得できます。 { name: "hatano" } OpenAPI定義を作成 OpenAPI が何なのかという説明は長くなるので省略します。 定義書の作成については別の記事でも紹介していますので、よければご確認ください。 npmパッケージを組み合わせてSwaggerの定義ファイルをいい感じに書く 今回作成したAPIを定義書に落とすと以下のようになります。 { "openapi" : "3.0.2" , "info" : { "title" : "API定義書" , "description" : "express-openapiを試すための定義書" , "version" : "0.1.0" }, "servers" : [ { "url" : "http://localhost:3000" } ], "paths" : { "/user" : { "get" : { "summary" : "ユーザー取得API" , "description" : "ユーザー情報を取得するAPI" , "operationId" : "getUser" , "parameters" : [], "responses" : { "200" : { "description" : "取得に成功した場合" , "content" : { "application/json" : { "schema" : { "type" : "object" , "required" : [ "name" ], "properties" : { "name" : { "type" : "string" } } } } } } } } } } } express-openapiの利用 ここからが本番です。 express-openapiを利用することでindex.tsは以下のように書き換わります。 import express , { Request , Response , NextFunction } from " express " ; import { initialize } from " express-openapi " ; import path from " path " ; const app = express (); app . use ( express . json ()); app . use ( express . urlencoded ({ extended : true })); app . listen ( 3000 , () => { console . log ( " Start on port 3000 " ); }); initialize ({ app : app , apiDoc : path . resolve ( __dirname , " openapi.json " ), validateApiDoc : true , operations : { getUser : [ function ( req : Request , res : Response , next : NextFunction ) { next (); }, function ( req : Request , res : Response ) { res . send ({ name : " hatano " }); } ] } }); initializeにオプションを与えるだけなので利用はとても簡単ですね。 それぞれのオプションについて解説します。 args.app (required) express-openapiを適用したいexpressアプリケーションを指定します。 args.apiDoc (required) 読み込ませるAPI定義書を指定するためのオプションです。 渡せる値としては以下のいずれかになりますが、基本的には定義書のパスを渡せばよい気がします。 OpenAPI定義のjsonパス fsなどを使ってjsonファイルを読み込んだ文字列 OpenAPI定義に沿ったオブジェクト 読み込んだ定義に何かしらの不備があるとエラーを出してくれるのも便利です。 OpenAPI定義のチェックを行わない場合は args.validateApiDoc をfalseにします。 express-openapi: validation errors [ { "instancePath": "/servers", "schemaPath": "#/properties/servers/uniqueItems", "keyword": "uniqueItems", "params": { "i": 1, "j": 0 }, "message": "must NOT have duplicate items (items ## 0 and 1 are identical)" } ] Error: express-openapi: args.apiDoc was invalid. See the output. args.operations API実装の本体になります。 OpenAPI定義書に記載の operationId をキーとして、APIを実装します。 値の部分には関数もしくは関数の配列を定義できます。 配列にした場合はミドルウェアとして定義され、NextFunctionを実行することで次の処理に伝播していきます。 既にお気づきだと思いますが、operationsを利用するとAPIエンドポイントパスを記載する必要がなく、OpenAPI定義に記載の通りのエンドポイントを勝手に生成してくれます。 リクエストパラメータのバリデーション express-openapiを利用する大きなメリットとして、リクエストパラメータのバリデーションを自動で行ってくれます。 先ほどのAPI定義にパラメータ定義を追加してみましょう。 "parameters": [ { "name": "user", "in": "query", "required": true, "schema": { "type": "string", "enum": ["hatano"] } } ], さらにinitializeに渡すオプションにerrorMiddlewareを追加します。 こちらはAPIアクセス時に何かしらのエラーが発生した時に実行されるミドルウェアになります。 エラーハンドリングが1箇所にまとまるのはいい感じです。 { errorMiddleware : ( err , req : Request , res : Response , _next : NextFunction ) => { res . status ( err . status || 500 ). json ( err ); } } この状態で http://localhost:3000/user にアクセスすると以下のエラーが返ります。 { status: 400, errors: [ { path: "user", errorCode: "required.openapi.requestValidation", message: "must have required property 'user'", location: "query" } ] } 必須のパラメータが付与されていないためのエラーになります。 今回はuserパラメータにenumを指定しているので、user=hogeのような値を指定した場合にもエラーとなります。 これまではリクエストパラメータのバリデーションを社内で独自に実装していましたが、express-openapiに大部分を任せられるようになるのはとてもありがたいです。 レスポンス項目のチェック express-openapiではレスポンス項目のチェックにも対応しています。 以下のように実装することでエラーを取得できます。 リクエスト項目と異なり、エラーがthrowされることはないのでエラー検知時にどのようにハンドリングするかはアプリで検討する必要があります。 // express-openapiがResponseの型を提供していないようなので、エラーを回避するために項目追加 function ( req : Request , res : Response & { validateResponse : any }) { const response = { name : 200 // string型想定のところにnumber型を入れるとエラーになる。 }; const validationError = res . validateResponse ( 200 , response ); if ( validationError ) { throw validationError ; } res . status ( 200 ). send ( response ); } { message: 'The response was not valid.', errors: [ { path: 'name', errorCode: 'type.openapi.responseValidation', message: 'must be string' } ] } OpenAPI定義にpathsを定義しない方法 ここまで試した方法は、すべての定義をOpenAPI定義書に記載するというものでした。 しかしexpress-openapiを利用していると、pathsの定義を記載せずにディレクトリ構成に従ってAPIを自動生成させることも可能です。 以下のようにファイルを作成し、APIを実装します。 paths └── user └── {id}.ts import { Operation } from "express-openapi"; export const GET: Operation = [ (req, res, _next) => { res.status(200).json({ id: req.params.id }); } ]; GET.apiDoc = { summary: "ユーザーID API", description: "ユーザーIDを取得する関数", operationId: "getUserId", parameters: [ { in: "path", name: "id", required: true, schema: { type: "integer" } } ], responses: { 200: { description: "取得に成功した場合", content: { "application/json": { schema: { type: "object", required: ["id"], properties: { id: { type: "integer" } } } } } } } }; また、initializeに渡すオプションに以下を追加します。 { paths : " ./paths " , // 以下はtsファイルでAPIを実装する場合に必要 routesGlob : " **/*.{ts,js} " , routesIndexFileRegExp : / (?: index )? \ .[ tj ] s$ / , } 上記を作成することで、 http://localhost:3000/user/{id} にAPIが作成されます。 ※{id}には任意のintegerが入ります。 operationsでAPIを作成した時と同じくリクエストパラメータのバリデーション、レスポンス項目のチェックを行ってくれます。 pathsもしくはoperationsのどちらかが必須設定項目になっています。 どちらを利用するかはチームの要件に従えばよさそうです。 OpenAPI定義の取得 ここまでpaths/operationsを利用してAPIの実装をしてきました。 最後に作成したOpenAPI定義を Swagger UI で確認してみましょう。 ここまで作成したOpenAPI定義は http://localhost:3000/api-docs にアクセスすることで取得可能です。 関連するオプションについても簡単に記載します。 args.docsPath default: /api-docs API定義を取得するためにアクセスするエンドポイント args.exposeApiDocs default: true APIドキュメントにアクセス可能とするかどうかのフラグ 何かしらの理由でAPI定義を外部公開したくない場合にはfalseにする SwaggerUIについてはDockerで簡単に立ち上げられます。 APIを試しに実行してみるというのがかなり楽にできます。 最後に express-openapiを利用することで、expressとOpenAPI定義がより密接につながることが確認できました。 OpenAPI定義は単なる定義書には収まらないことが今回の記事からもわかると思います。 フォルシアではほかにもAPIの型定義をOpenAPI定義から生成したり、スクリプトの一部を自動作成するようなスクリプトを内製しています。 エンジニアがよりコアなロジック部分の実装に注力できるような環境を今後も整えていきたいと思います。
アバター
こんにちは、エンジニアの山門です。 日々仕事をしていると、ちらほら手動で行っている定型作業というものが出てきてしまいます。それを自動化出来れば、これまでその作業に費やしていた時間が他のことに使え、業務効率の向上に繋がります。 今回はその定型作業を解消するためにBotを作ってみた話をしたいと思います。 経緯 あるときPMから1件のコメントがチャンネルに寄せられる 手動で毎回アラートするのが大変 そうだBacklog アラートBotを作成しよう 目的 フォルシアの一部では、プロジェクト管理ツールとして、 Backlog を利用しています。私の参加していたプロジェクトでも利用していたのですが、大規模プロジェクトだったこともあり、大量の課題の起票・更新が行われ、反応が遅れてしまうケースがありました。 これに対して、Backlogを開き、該当チケットを抽出することは出来るのですが、 課題を抽出するために画面を開く 条件を指定して検索する 該当チケットを取得 Slack上に通知 と手間がかかり、これを手動で時間を取って行うのは無駄なコストで且つ、ストレスのかかる作業だと感じました。当時私はチームの1エンジニアではありましたが、今後のプロジェクト進行を考えると、今の段階でBotを作り、こちらから情報を取りに行かずともSlackに通知してくれればHappyになれるのでは?と考え、Bot作成に乗り出しました。 使っている技術 今回技術要素としては以下を利用しました。 API : Baklog API, Slack API HTTP client: axios webApp: TypeScript Notification: GitLab CI API BacklogにはNulab社が提供するAPIがあり、公式の丁寧に書かれた 定義書 があったので、そちらを採用しました。 また、Slackへの通知に関しては、Incoming webhookを使うと手軽なのですが、message形式が比較的柔軟に作れる、スレッドに返信といったことができる、message送るときは今後は chat.postMessage の利用が推奨されている、などの理由で chat.postMessage を選択しています。 (個人的に使ってみたかったから、という理由もあります。) HTTP client 私の参加していたプロジェクトで既に axios を利用しており、勝手がわかっていたため axios を採用しましたが、fetch API とかでもいいと思います。 webApp こちらも同様に、現プロジェクトで採用していたため TypeScript にしました。 Notification 自分のPC上でcronを仕込んで動かすでも良かったのですが、折角なら別のところで動いている方が個人の事情に左右されないので GitLab CI を採用しました。 (フォルシアではGitHubではなく、GitLabを利用しています。) 機能(2022/03/15現在) 予め定義した人たちが担当者になっている、反応が遅れたチケットを通知 期間によって段階を区切って、3パターンにグルーピングしてalert CIのスケジュール実行で1日1回Slackに通知 構成 root ├── README.md ├── node_modules ├── package-lock.json ├── package.json ├── src └── tsconfig.json src以下 ├── definitions │ ├── apiDefinition.ts │ ├── searchConditionDefinition.ts │ └── userDefinition.ts ├── fetchData.ts ├── index.ts ├── sendMessage.ts ├── types │ ├── comments.d.ts │ └── issues.d.ts ├── util │ ├── dateUtil.ts │ └── typeUtil.ts └── view-data ├── comments.ts ├── issues.ts └── sendMessage.ts 構成はざっくり以下の構成になっています。 root: 設定回り src: API リクエスト周りのコード definitions: userIDやAPIのurl定義等 types: 型定義 util: module系 view-data: データ加工処理系 事前準備 Botの利用にあたって、事前にBacklogとSlackで準備が必要です。 Backlog API のAPIキーの発行 プロジェクトの個人設定に進みAPIの項目から新しいAPIキーを登録すれば完了です Slack Botの登録 Slack API のAPIキーを発行するには、Slack Appを作成する必要があります。詳細は割愛しますが、 https://api.slack.com/apps に進みCreate New App Add features and functionality でBotsを選択し、アプリの名前を決める Add features and functionality でPermissionsを選択し、 Scopesを chat:write , users:read にする xoxb- から始まるキー情報が上の方に表示されているはずなので、これを控えておく(これがAPIキーです) Install your app to your workspace でforciaのワークスペースにinstall 通知を入れたいチャンネルに対して、アプリをinviteする 自分はこれを忘れていて少してこずりました ポイント Backlog API Backlog API のメインで使っているAPIは /api/v2/issues です。 updatedSince , updatedUntil を更新日の範囲が絞れるので、これを使って特定の期間内のチケットを取得しています DoS攻撃対策なのか、短時間に数十リクエストを投げるとアクセス制限がかけられてしまうので、リクエスト数は注意して作っています issues の1回のレスポンス上限が100件なので、担当者のチケットが100件以上ある場合はリクエストを分けるなど注意が必要です (イケてないですが、)現状は担当チケットが多い特定の人の場合に個別で条件分岐させています Slackに送るメッセージ Slackが提供しているBlock Kitというものを利用してメッセージを組み立てています 記法に従ってjsonを組み立てて chat.postMessage に投げると、それに応じた形式でslack上に表示してくれます BlockKit Builder を使うとプレビューを見ながら必要な形式を確認できるのでお勧め メッセージは大きく分けて3ブロックで構成しています ヘッダー部分 メンション部分 スレッド部分 基本的には必要なパラメータを用意してAPIを投げればSlackに通知できますが、スレッドに返信するには一工夫必要です。 chat.postMessage でリクエストを投げると、下記のようなレスポンスが返って来るのですが、その中の ts という値がリクエストしたメッセージに紐づいているタイムスタンプです。このタイムスタンプを thread_ts パラメータに指定して再度 chat.postMessage にリクエストすることで、該当のメッセージのスレッドにリクエストすることができます。 ※ID情報等は保護のため仮の値で表現しています。 { ok: true, channel: '<channel名>', ts: '1593849552.024700', message: { bot_id: '<bot_id>', type: 'message', text: 'このコンテンツは表示できません。', user: '<user_id>', ts: '1593849552.024700', team: '<team_id>', bot_profile: { id: '<id>', deleted: false, name: 'backlog-watcher', updated: 1592644323, app_id: '<app_id>', icons: [Object], team_id: '<team_id>' }, blocks: [ [Object] ] } } 実際にスレッドに飛ばすと以下のように期間ごとに分けたメッセージが通知されます。(表示センスはご容赦ください、、 ) slackへのmention mentionを飛ばすためのslackIDの取得には https://api.slack.com/methods/users.list を利用しました users:read の権限があれば、ワークスペース内の人のuser情報を取得できます 純粋に @yamakado などとしてもmentionされないので、IDを持ってくる必要があります 今回はメンバーがある程度固定されているので、実装時にIDを取得してそれを定義で持つようにしています 定期実行 日次で1回実行しているのですが、GitLab CIのschedule機能を使って実行しています API キーなどは基本的にcommitしないほうが良いため、GitLab上のVariablesに登録して、スクリプト実行時に環境変数で渡すようにしています 最後に 日常の不便を見つけて自主的に取り組んだBot作成ですが、個々人に対して通知する仕組みが作れたため、結果的にチーム全体で活用することが出来ています。 実際に導入されてから1年以上が経ちますが、こちらから主体的に情報を取りに行かずとも、コミュニケーションの主軸であるSlackに自動で通知して くれるので、とても快適に感じています。 副次的な効果として、通知で可視化されたことでチームメンバーの目にも留まり、チケットの相談やヘルプがしやすくなった気がします。 これからも日々の定型作業を自動化して、業務効率をどんどんあげていきたいです!
アバター
これは、 FORCIA Advent Calendar 2021 の24日目の記事です。 こんにちは、DXプラットフォーム事業部の小海です。在宅勤務やオンライン会議が一般的になりつつある世の中ですが、皆様いかがお過ごしでしょうか。 フォルシアでもGoogleMeetによるオンライン会議を普段から使用しています。今回はオンライン会議をより楽しいものにするための工夫を紹介します。 はじめに フォルシアでは定期的に社内ハッカソンを実施しており、エンジニアやエンジニア以外の社員も開発したいもの・勉強したいことを持ち寄り定期的に報告をしながら共に学ぶ機会を設けています。 数年前までは参加者で合宿所に行っていましたが、最近の情勢ではオンラインで開催をしています。参加者それぞれ自宅で開発・勉強し、定期的にオンライン集まり報告などをしつつ、最終日に発表会を行っています。 合宿所で行われたハッカソンについては こちらの記事 をご覧ください。 今回は2021年のGWに行われたオンラインハッカソンで私が実施した「GoogleMeetのチャット欄を読み上げてみた」という内容について紹介します。 なお、記事は技術的な内容でソースコードの記載もありますが、設計の考え方や課題解決の仕方に主軸を置いているため、プログラミング苦手な方ももう少しブラウザバックは我慢して頂けますと幸いです。 なぜそのテーマを選んだのか ハッカソンに参加するにあたり「最近不便に感じたことはないか」「最近面白いと思ったことはないか」などを考えていました。 私は技術部の教育チームにも所属しているため、4月からGoogleMeetを使った社内でのオンライン講義の企画・講師・参加をしていました。 その中で参加者がチャット欄に感想や質問を書くことが多々あったのですが、講師が説明に集中しているとき、チャット欄を見ることができなくなったり、講義の内容によってはチャット欄を見ながら講義をすることが難しいということがありました。 また、会議や講義は講師の腕だけじゃなく参加者が能動的に参加することが大事だと思っていたため、オンラインならではの仕組みでより活発で面白い講義にしたいと思っていました。 普段からゲーム配信や実況を楽しんでいた私は、コメント欄の読み上げ機能には馴染みがあり、GoogleMeetのチャット欄を読み上げさせることができたら楽しいのではと思ったのがきっかけです。 使用したツールや言語と選定理由 GoogleMeet 普段からFORCIAでは社内オンライン会議にGoogleMeetを使用しています GoogleChromeの拡張機能 GoogleMeetと読み上げツールを使用するため 棒読みちゃん( Ver0.1.11.0 Beta21 ) 筆者が普段から使用しているツールのため 棒読みちゃん開発者様サイトはこちら 仮想サウンドドライバ 入力や出力をライン指定できる仮想ドライバです 筆者は YAMAHA SYNCROOM に同梱されているドライバを使用しました YAMAHA SYNCROOM 公式サイトはこちら 設計 まず考えるべき壁は3点あります。 GoogleMeetのチャット欄の情報をどのように取得するか 取得したチャット欄の情報をどのように読み上げツールに渡すか 読み上げツールで読み上げた音声をどのようにGoogleMeetで再生させるか GoogleMeetのチャット欄の情報をどのように取得するか これは検討段階から答えを持っており、自作のGoogleChrome拡張を作成することで、特定のWebページを開いているときにWebページの情報をJavaScriptで取得できます。 自作GoogleChromeの拡張のソースコードは後述します。 GoogleMeetのチャット欄の情報をどのように読み上げツールに渡すか ここは非常に悩みました。 基本線で考えていたのは「GoogleChrome拡張から読み上げツールのAPIを呼ぶ」という方式です。ただし、GoogleChrome拡張から読み上げツールのAPIを叩くことの難易度が高く、想定よりも時間がかかってしまいそうと感じていました。 GW期間という限られた時間だったこともあり、あまり時間をかけたくなかったため、もっと楽な方法がないか調べていたところ、読み上げツールには『クリップボード監視』という機能がありました。読み上げツール側で『クリップボード監視』をONにしておくと、クリップボードの変更を検知して内容を自動で読み上げるというものです。 この『クリップボード監視』をONにしておき、GoogleChrome拡張からクリップボードに文字列を保存するようにすれば、読み上げが可能となるので、この方向で進めることとしました。 読み上げツールで読み上げた音声をどのようにGoogleMeetで再生させるか GoogleMeetではGoogleChromeのタブ共有で音声を共有できますが、なるべく画面共有などは使わずに、会議の運用に影響しない形で読み上げさせる方法を模索していました。 最終的には、仮想サウンドドライバを使用し、読み上げツールの出力先を仮想サウンドドライバに設定し、GoogleMeetの入力を仮想サウンドドライバに設定することで連携を可能にしました。 ただし、そのように設定した場合、読み上げツールがマイクを使用してしまうため、同じPCで私がマイクを使用できなくなります。仮想ミキサーなどを使用することで解消できますが、時間短縮のため、私と読み上げツールは別のPCで会議に参加することにしました。 実装 ここまで来れば、後はGoogleChrome拡張を作成して、チャット欄をクリップボードに保存すれば実現ができます。 少々記事が長くなりつつあるので、細かい説明は省きますが興味がある方向けにソースコードを共有します。 ファイル構造 拡張を作成するときのファイル構造は下記のようになります。 今回は gmeetClipborder という名前で作成することにしました。 gmeetClipborder  ├ content  │ ├ content.js  │ └ jquery-3.6.0.min.js  ├ images  │ └ icon.png  ├ js  │ └ background.js  ├ popup  │ ├ jquery-3.6.0.min.js  │ ├ popup.html  │ └ popup.js  └ manifest.json manifest.json GoogleChrome拡張作成の構成ファイルです。 { "name" : "GoogleMeet ClipBoarder" , "version" : "1.0" , "description" : "GoogleMeet ClipBoarder" , "permissions" : [ "declarativeContent" ], "background" : { "scripts" : [ "js/background.js" ], "persistent" : false }, "content_scripts" : [ { "matches" : [ "https://meet.google.com/*" ], "js" : [ "content/jquery-3.6.0.min.js" , "content/content.js" ] } ], "page_action" : { "default_popup" : "popup/popup.html" , "default_icon" : { "32" : "images/icon.png" } }, "icons" : { "48" : "images/icon.png" }, "manifest_version" : 2 } background.js GoogleChrome拡張の本体といえるJavaScriptです。 // 基本的なルール chrome . runtime . onInstalled . addListener (() => { chrome . declarativeContent . onPageChanged . removeRules ( undefined , () => { chrome . declarativeContent . onPageChanged . addRules ([{ conditions : [ new chrome . declarativeContent . PageStateMatcher ({ pageUrl : { hostEquals : ' meet.google.com ' }, }) ], actions : [ new chrome . declarativeContent . ShowPageAction ()] }]); }); }); // 有効化・無効化のStatus管理 const Enable = (() => { let isEnabled = false ; const that = this ; this . get = () => { return isEnabled ; }; this . set = ( bool ) => { isEnabled = Boolean ( bool ); return that ; // for method chaining }; this . toggle = () => { isEnabled = ! isEnabled ; return that ; // for method chaining }; return this ; })(); // popupやcontentの世界との連携 const CommandHandler = { GetEnabled : () => Enable . get (), ToggleEnabled : () => Enable . toggle (). get () }; chrome . runtime . onMessage . addListener ( ( request , _sender , sendResponse ) => sendResponse ( CommandHandler [ request . cmd ]()) ); popup.html 拡張アイコンのクリック時に表示されるHTMLです。 <!DOCTYPE html> <html> <head> <style> button { height : 30px ; width : 30px ; outline : none ; font-weight : bold ; } button .is-active { background-color : red ; color : white ; } </style> </head> <body> <button id= "toggleButton" ></button> <script src= "jquery-3.6.0.min.js" ></script> <script src= "popup.js" ></script> </body> </html> popup.js 上記popup.htmlで読み込まれるJavaScriptです。 (() => { // static const SELECTOR_TOGGLE_BUTTON = " #toggleButton " ; const CLASS_ACTIVE = " is-active " ; // functions const renderStatus = ( isEnabled , $button ) => { if ( isEnabled ){ $button . addClass ( CLASS_ACTIVE ); $button . html ( " on " ); } else { $button . removeClass ( CLASS_ACTIVE ); $button . html ( " off " ); } }; // on loaded $ (() => { const $button = $ ( SELECTOR_TOGGLE_BUTTON ); // initial setting chrome . runtime . sendMessage ( { cmd : " GetEnabled " }, ( isEnabled ) => renderStatus ( isEnabled , $button ) ); // attach event $ ( " #toggleButton " ). on ( " click " , () => { chrome . runtime . sendMessage ( { cmd : " ToggleEnabled " }, ( isEnabled ) => renderStatus ( isEnabled , $button ) ); }); }); })(); content.js 拡張によってページ内で読み込まれるクライアント側のJavaScriptです。 ' use strict ' ; (() => { // static const CLASS_READED = " gmc-readed " ; const SELECTOR_ALL_MESSAGES = " .GDhqjd div div " ; const SELECTOR_NEW_MESSAGES = SELECTOR_ALL_MESSAGES + " :not(. " + CLASS_READED + " ) " ; const MAX_TEXTS_AT_ONCE = 5 ; // on loaded $ (() => { initialize (); setInterval ( MainLoop , 1000 ); }); // initialize const initialize = () => { const $elm = $ ( SELECTOR_ALL_MESSAGES ); $elm . addClass ( CLASS_READED ); }; // get new text from HTML const getMessage = () => { const $elm = $ ( SELECTOR_NEW_MESSAGES ); $elm . addClass ( CLASS_READED ); const messages = []; $elm . each (( _i , e ) => { messages . push ( $ ( e ). data ( " message-text " )); }); return messages ; } // save text to clipboard const saveClipboard = ( str ) => { if ( ! str ) return ; const listener = ( e ) => { e . clipboardData . setData ( " text/plain " , str ); e . preventDefault (); document . removeEventListener ( " copy " , listener ); } document . addEventListener ( " copy " , listener ); document . execCommand ( " copy " ); } // main const MainLoop = (() => { // closure let prevEnabled = false ; const callback = ( isEnabled ) => { if ( isEnabled ) { if ( ! prevEnabled ){ initialize (); } else { const messages = getMessage (); if ( messages && messages . length > 0 && messages . length <= MAX_TEXTS_AT_ONCE ){ saveClipboard ( messages . join ( " \n " )); } } } prevEnabled = isEnabled ; }; return () => { chrome . runtime . sendMessage ({ cmd : " GetEnabled " }, callback ); }; })(); })(); GoogleChromeに自作の拡張機能を読み込ませる 下記の手順で、作成した拡張機能を読み込ませることができます。 chrome://extensions/ にアクセス デベロッパーモードをONにする 『パッケージ化されていない拡張を読み込む』を選択 開発したフォルダ「gmeetClipborder」を選択 これで、GoogleMeetのチャット欄の読み上げができるようになりました(拍手) おわりに 今回はオンライン会議を面白くするアイデアを技術で実現した例をお話しさせていただきました。いかがだったでしょうか。ブラウザバックを我慢して読み進めて頂いたプログラミングが苦手な方、ここまで読んで頂いてありがとうございます。おそらく後半は存分にスクロールして頂いたかと思います。 日常でふと思いついたアイデアを形にすることは、とても楽しいしワクワクします。今回作成したGoogleMeetの読み上げ機能はハッカソンでの発表会でもとても好評で、参加者みんな読み上げツールに好きな言葉を読み上げさせて楽しんでくれていました。チャット欄で大喜利が発生し、読み上げツールの発言に皆さん気を取られ、私の発表はあまり聞いてもらえなかったのが良い思い出です。アイデアって難しいですね。オンライン飲み会専用ツールとなりそうです。 この記事の内容に興味をもった方は是非弊社採用ページもご確認ください!
アバター
こんにちは、DXプラットフォーム事業部の小海です。在宅勤務やオンライン会議が一般的になりつつある世の中ですが、皆様いかがお過ごしでしょうか。 フォルシアでもGoogleMeetによるオンライン会議を普段から使用しています。今回はオンライン会議をより楽しいものにするための工夫を紹介します。 はじめに フォルシアでは定期的に社内ハッカソンを実施しており、エンジニアやエンジニア以外の社員も開発したいもの・勉強したいことを持ち寄り定期的に報告をしながら共に学ぶ機会を設けています。 数年前までは参加者で合宿所に行っていましたが、最近の情勢ではオンラインで開催をしています。参加者それぞれ自宅で開
アバター
これは、 FORCIA Advent Calendar 2021 の22日目の記事です。 こんにちは。アドベントカレンダー22日目の記事を担当させて頂きます、エンジニアの澤田です。 普段の業務でJavaScriptでプログラムを書くことが多いですが、その際によく使用するJavaScriptのライブラリにUnderscore.jsがあります。 なぜUnderscore.jsを多用するのかというと、フォルシアで開発している主な機能の1つに検索機能があることが関係しています。 検索機能では、データベースに対してSQLを実行し、その結果をHTMLのテンプレートに当てはめていく、ということをよく行います。 SQLの実行結果は、構造化されていないデータの配列であることが多いため、HTMLテンプレートに適用しやすくするためにデータを組み替えて親子関係を持たせたり、複数のSQL実行結果を組み合わせたりします。 その際のデータ処理にUnderscore.jsを使うことが多いのです。 (ただ、最近のブラウザなどのJavaScript実行環境では、Underscore.jsの様々な関数がネイティブでサポートされているので、Underscore.jsを使用する範囲は徐々に少なくなりそうです) Underscore.jsの特徴として、関数型プログラミングのような書き方ができる、という点があります。 関数型プログラミングでは、ある関数に値を渡して得られた値を別の関数に渡す、ということを繰り返していく書き方をしますが、そうした処理を行うための関数型言語にあるような便利な関数がUnderscore.jsにはたくさん用意されています。 例えば以下のような関数があります。 map: 配列(※)と関数を渡して、配列の各要素に対して関数を適用した配列を返す filter: 配列と真偽値を返す関数を渡して、配列の各要素の内、関数に適用した結果trueになるものの配列を返す reduce(foldl): 配列と関数と初期値を渡して、以下の順に処理を繰り返し、最終的に得られた値を返す(「折り畳み」と言うこともあります) 初期値と配列の最初の要素を関数に渡して得られた値 上記の値と配列の次の要素を関数に渡して得られた値 上記の値と配列の次の要素を関数に渡して得られた値 ... 上記の値と配列の最後の要素を関数に渡して得られた値 chain: 上記のmapやfilter等、Underscore.jsがサポートしている関数を複数組み合わせて適用した結果を返す ※配列ではなく連想配列オブジェクトを渡すこともできます。 Underscore.jsを使うと関数型プログラミングのような書き方ができるとして、Underscore.jsの内部ではどのように関数が定義されているのでしょうか? それが気になって、今回Underscore.jsのソースコードを読んでみることにしました。 それでは早速見ていきましょう! なお、Underscore.jsのバージョンは1.3.1を使用し、ソースコードはUMDのDevelopment版( GitHubリポジトリ上のソースコードはこちら )を使用しています。 map関数 まず、自分がよく使うmap関数を見ていきたいと思います! ソースコード には以下のように定義されています。 // Return the results of applying the iteratee to each element. function map(obj, iteratee, context) { iteratee = cb(iteratee, context); var _keys = !isArrayLike(obj) && keys(obj), length = (_keys || obj).length, results = Array(length); for (var index = 0; index < length; index++) { var currentKey = _keys ? _keys[index] : index; results[index] = iteratee(obj[currentKey], currentKey, obj); } return results; } ここでmap関数が受け取る引数は、それぞれ以下のような意味になっています。 obj: 関数を適用する配列(または連想配列オブジェクト) iteratee: obj の各要素に適用する関数 context: this の値を置き換えたい場合に渡す(自分はあまり使いません) 最初に iteratee = cb(iteratee, context); と書かれていて、 cb というWrapper関数を通していますが、基本的には渡した iteratee がそのまま返ります。 _keys には、引数の obj が連想配列オブジェクトの場合に、キーの配列が入りますが、通常の配列の場合は値が設定されません。 var currentKey = _keys ? _keys[index] : index; のところで、 _keys に値が設定されているかどうかで、 _keys の要素をキーに使うのか、配列のインデックスをキーに使うのかが分かれています。 引数の obj が、配列と連想配列オブジェクトのどちらでも受け取れるようになっているのはこの工夫があるからですね! あとは、 _keys または配列の要素数だけループを回して、一時変数として定義されている配列 results に関数を適用して得られた値を順番に格納していき、最後にその results を返す形になっています。かなりシンプルな定義ですね! reduce関数 次にreduce関数を見てみましょう! ソースコード には以下のように定義されています。 // **Reduce** builds up a single result from a list of values, aka `inject`, // or `foldl`. var reduce = createReduce(1); おっと、、実際の中身はcreateReduce関数に定義されているようなので、そちらを見ていきます。 // Internal helper to create a reducing function, iterating left or right. function createReduce(dir) { // Wrap code that reassigns argument variables in a separate function than // the one that accesses `arguments.length` to avoid a perf hit. (#1991) var reducer = function(obj, iteratee, memo, initial) { var _keys = !isArrayLike(obj) && keys(obj), length = (_keys || obj).length, index = dir > 0 ? 0 : length - 1; if (!initial) { memo = obj[_keys ? _keys[index] : index]; index += dir; } for (; index >= 0 && index < length; index += dir) { var currentKey = _keys ? _keys[index] : index; memo = iteratee(memo, obj[currentKey], currentKey, obj); } return memo; }; return function(obj, iteratee, memo, context) { var initial = arguments.length >= 3; return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial); }; } ここでcreateReduce関数は以下の関数を返しているので、reduce関数が受け取る引数は obj 、 iteratee 、 memo 、 context の4つであることが分かります。 return function(obj, iteratee, memo, context) { var initial = arguments.length >= 3; return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial); }; 引数の obj 、 iteratee 、 context は先ほどのmap関数と同じですが、新しく出てきた引数 memo で初期値を受け取っています。 memo を渡せば、 initial は true になり、 context を渡さなければ optimizeCb(iteratee, context , 4) は iteratee をそのまま返すので、 この場合、createReduce関数内で定義されているreducer関数は以下と同等になります。(ここで、 createReduce(1) で dir の値には 1 を渡しているので、 index の初期値は 0 と定義できます) var reducer = function(obj, iteratee, memo) { var _keys = !isArrayLike(obj) && keys(obj), length = (_keys || obj).length; for (var index = 0; index < length; index++) { var currentKey = _keys ? _keys[index] : index; memo = iteratee(memo, obj[currentKey], currentKey, obj); } return memo; }; 先ほどのmap関数と似ていますね! 違いはと言えば、 results の部分が memo に置き換わっている点です。 map関数の場合は results に関数を適用した結果を格納していきますが、reduce関数の場合は、要素を順番に辿りながら、 memo とその要素を iteratee 関数に渡して得られた結果を memo に再代入し、最後にその memo を返す形になっています。 こちらもシンプルな定義ですね! また、 dir を受け取るcreateReduce関数を定義することで、要素を最後から前に向かって逆向きに折り畳む、reduceRight関数も以下のように簡単に定義できています。 // The right-associative version of reduce, also known as `foldr`. var reduceRight = createReduce(-1); リファクタリングの観点からも、素晴らしい抽象化ですね! chain関数 最後に、Underscore.jsの関数を複数組み合わせて適用できる、chain関数を見てみたいと思います! ソースコード には以下のように定義されています。 // Start chaining a wrapped Underscore object. function chain(obj) { var instance = _$1(obj); instance._chain = true; return instance; } おや、、chain関数は _$1(obj) というインスタンスを取得し、インタンス変数 _chain に true を設定して返しているようです。 _$1 という謎めいたクラスが出てきました。これは何でしょう? ソースコードを見ると以下のように定義されています。 // If Underscore is called as a function, it returns a wrapped object that can // be used OO-style. This wrapper holds altered versions of all functions added // through `_.mixin`. Wrapped objects may be chained. function _$1(obj) { if (obj instanceof _$1) return obj; if (!(this instanceof _$1)) return new _$1(obj); this._wrapped = obj; } 引数の obj が _$1 クラスのインスタンスであることはあまり無いと思うので、以下の2行を見ればよさそうです。 if (!(this instanceof _$1)) return new _$1(obj); this._wrapped = obj; ここで、 this は new キーワードを付けて _$1 関数が呼ばれた場合に _$1 クラスのインスタンスになるので、逆に new を付けずに呼ばれた場合は this instanceof _$1 は false になります。 すると new を付けずに呼ばれた場合は !(this instanceof _$1) は true になり、 new を付けて再度呼ばれます。 そのため、 new を付けても付けなくても _$1 クラスのインスタンスが生成され、メンバ変数 _wrapped に引数の obj が格納されて返されます。 そうするとおそらく、Underscore.jsの関数を複数適用した場合に、それぞれの関数が _wrapped に値が入っているかどうかを見ているのではないか、と推測できますね! _wrapped でソースコードを探していくと、mixin関数の定義で以下のように書かれています。 // Add your own custom functions to the Underscore object. function mixin(obj) { each(functions(obj), function(name) { var func = _$1[name] = obj[name]; _$1.prototype[name] = function() { var args = [this._wrapped]; push.apply(args, arguments); return chainResult(this, func.apply(_$1, args)); }; }); return _$1; } そして mixin関数はソースコードの最後の方で以下のように呼ばれています。 var _ = mixin(allExports); まさにここのようです! allExports にはUnderscore.jsで使える全ての関数が入っているので、 これら全ての関数を _$1 オブジェクトのプロパティに設定しつつprototypeプロパティにも設定しています。 そのため、 _$1 クラスのインスタンスとして関数が実行された場合は、そのインスタンスと _wrapped の値を関数に適用した結果を、chainResult関数に渡していることが分かります。 そしてchainResult関数は以下のように定義されています。 // Helper function to continue chaining intermediate results. function chainResult(instance, obj) { return instance._chain ? _$1(obj).chain() : obj; } ここで少し紛らわしいのが、 _$1(obj).chain() のchain関数は _.chain() とは異なる、というところです。 _$1(obj) で _$1 クラスのインスタンスが生成されてメンバ変数 _wrapped に obj が入り、 _$1(obj).chain() は、 chainResult(this, chain.apply(_$1, [obj]) を返す形になります。 ここで、 _$1 クラスのインスタンスが再生成されているので、メンバ変数 _chain の値は何も設定されていない状態( undefined )になっています。 そのため、 chainResult(this, chain.apply(_$1, [obj]) は chain.apply(_$1, [obj]) を返し、それは _$1 クラスの新しいインスタンスで、そのメンバ変数 _wrapped には obj が、 _chain には true が設定された状態になっています。 ...と、少々ややこしくなってきたので、 以上をふまえつつ、以下のコードを実行した場合の動作を見てみましょう! _.chain([1, 2, 3]) .map(function(num) { return (num * 2); }); _.chain([1, 2, 3]) が実行される _$1 クラスのインスタンス _a が生成される ※分かりやすくするため、インスタンス _a 、インスタンス _b ... と順番に名前を付けていきます! _a のメンバ変数 _wrapped に [1, 2, 3] が入る _a のメンバ変数 _chain に true が入る _a が返る _a のメンバ関数のmap関数が実行される chainResult(this, map.apply(_$1, [[1, 2, 3], function(num) { ... }])) が実行される。 ここで、 map.apply(_$1, [[1, 2, 3], function(num) { ... }]) は [2, 4, 6] になるので、 chainResult(this, [2, 4, 6]) が実行される this._chain は上記で true が入っているので、 _$1([2, 4, 6]).chain() が実行され、 まず _$1([2, 4, 6]) が実行される _$1 クラスのインスタンス _b が生成される _b のメンバ変数 _wrapped に [2, 4, 6] が入る _b が返る _b のメンバ関数のchain関数が実行される chainResult(this, chain.apply(_$1, [2, 4, 6])) が実行される。 ここで、 chain.apply(_$1, [2, 4, 6]) は _$1 クラスの新しいインスタンス _c を生成して返し、 _c のメンバ変数 _wrapped には [2, 4, 6] が、メンバ変数 _chain には true が入っている chainResult(this, chain.apply(_$1, [2, 4, 6])) の this._chain は設定されていない( undefined になっている)ので、そのまま _c が返る 最後の _c は _$1 クラスのインスタンスになっており、 メンバ変数 _wrapped には [2, 4, 6] が、 _chain には true が入った状態になっています。 _c は _$1 クラスのインスタンスなので、Underscore.jsで使える全ての関数を続けて実行できるようになっています。 なるほど!! chain関数を使って、Underscore.jsの関数を複数実行した結果は、最後にvalue関数を実行すると取得できますが、 value関数は以下のように定義されていて、メンバ変数 _wrapped の値を返すだけになっています。 // Extracts the result from a wrapped and chained object. _$1.prototype.value = function() { return this._wrapped; }; シンプルですね!! さいごに 普段の業務で、既にある機能を改修する際にまずドキュメントを読みますが、モジュールの役割や依存関係を正確に理解・把握するために、実際に書かれているコードを読むことが多いです。 その中で、「こんな書き方があるのか」と勉強になり、自分でもそれを使うようになることがたくさんあります。 今回、Underscore.jsのソースコードを読んでみて、今まで使ったことがないような書き方を知ることができて、とても面白かったです! (例えば func.length でfunc関数が受け取る引数の数が分かったり、 push.apply(arrayA, arrayB)) で配列 arrayA に配列 arrayB の要素を追加できる、など) 皆さんも有名なライブラリのソースコードを読んでみて、新しい書き方を見つけてみてはいかがでしょうか :)
アバター
こんにちは。アドベントカレンダー22日目の記事を担当させて頂きます、エンジニアの澤田です。 普段の業務でJavaScriptでプログラムを書くことが多いですが、その際によく使用するJavaScriptのライブラリにUnderscore.jsがあります。 なぜUnderscore.jsを多用するのかというと、フォルシアで開発している主な機能の1つに検索機能があることが関係しています。 検索機能では、データベースに対してSQLを実行し、その結果をHTMLのテンプレートに当てはめていく、ということをよく行います。 SQLの実行結果は、構造化されていないデータの配列であることが多いため、HTML
アバター
これは、 FORCIA Advent Calendar 2021 の21日目の記事です。 こんにちは。第二旅行プラットフォーム部エンジニアの浦上です。アドベントカレンダーの枠を取ってみたはいいものの特にネタが思いつかずフォルシアの過去のアドベントカレンダーを遡っていたところこのような記事を見つけました。 プログラミング言語ではなく、フォルシアの高速検索の鍵を握るSQLで数独を解く なるほど、アドベントカレンダーというのはCやPythonのような『普通の』プログラミング言語以外で数独を解けばよいのですね。 ということでこの記事ではTypeScriptの『型』だけを用いて数独を解いていこうと思います。 盤面の表し方 まずは、数独の問題を表現する型を作りましょう。 扱いやすいように、以下のルールで扱うことにしました。 埋まっているマスは 1 から 9 の数値リテラル型で表す。 空きマスは数値リテラル型 0 で表す。 盤面全体は、左上から順に長さ81のタプル型で表す。 以上をコードで表すと下記のようになります。 Problem1 も変数ではなく「初項から順に数値リテラル 0 , 2 , 4 , ......である長さ81のタプル型」であることにご注意ください。 // 使用可能な数字は1から9まで type Digit = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 ; // 空きマスは0で表す type Empty = 0 ; // 長さ9のタプル型 type Tuple9 < T > = [ T , T , T , T , T , T , T , T , T ]; // 長さ81のタプル型 type Tuple81 < T > = [ ... Tuple9 < T > , ... Tuple9 < T > , ... Tuple9 < T > , ... Tuple9 < T > , ... Tuple9 < T > , ... Tuple9 < T > , ... Tuple9 < T > , ... Tuple9 < T > , ... Tuple9 < T > ]; // 数独の問題を表す型 type Problem = Tuple81 < Digit | Empty > ; // 問題例 type Problem1 = [ 0 , 2 , 4 , 0 , 8 , 7 , 5 , 0 , 9 , 8 , 9 , 1 , 4 , 5 , 6 , 3 , 7 , 2 , 5 , 6 , 7 , 0 , 9 , 3 , 0 , 0 , 1 , 7 , 8 , 6 , 5 , 2 , 9 , 0 , 3 , 4 , 2 , 0 , 0 , 3 , 1 , 0 , 0 , 0 , 0 , 1 , 4 , 0 , 6 , 7 , 8 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 6 , 0 , 0 , 0 , 3 , 0 , 0 , 0 , 0 , 0 , 1 , 0 , 9 , 0 , 0 , 0 , 0 , 5 , 0 , 0 , 0 , ]; 準備 数独を解くために必要となる型をいくつか準備しておきましょう。 // タプルのindexを行番号に変換するための型 type ToRow = [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 2 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 4 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 6 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 7 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 , 8 ]; type PopValue < T extends Digit [], K extends number , V extends Digit | Empty > = { [ key in keyof T ]: key extends ` ${ K } ` ? Exclude < T [ key ], V > : T [ key ]; }; type RowLimit < Board extends Problem , Ret extends Tuple9 < Digit > = Tuple9 < Digit > , I extends unknown [] = [] > = I [ " length " ] extends 81 ? Ret : RowLimit < Board , PopValue < Ret , ToRow [ I [ " length " ]], Board [ I [ " length " ]] > , [... I , unknown ] > ; // 以下は使用例 type Ex1 = PopValue < [ 1 | 2 | 3 , 3 ], 0 , 2 > // -> [1 | 3, 3]; type Ex2 = PopValue < [ 1 | 2 | 3 , 3 ], 1 , 3 > // -> [1 | 2 | 3, never]; type RowLimitProblem1 = RowLimit < Problem1 > ; /* -> [ 1 | 3 | 6, never, 2 | 4 | 8, 1, 9 | 4 | 5 | 6 | 7 | 8, 9 | 2 | 3 | 5, 9 | 1 | 2 | 3 | 4 | 5 | 7 | 8, 9 | 2 | 4 | 5 | 6 | 7 | 8, 1 | 2 | 3 | 4 | 6 | 7 | 8 ] */ PopValue はタプル型の第 K 項から型 V を削除するための型です。使用例は Ex1 、 Ex2 をご覧ください。 RowLimit は「0〜8行目(TypeScriptの配列は0から数えるのでそれに合わせて1〜9行目でなく0〜8行目と表しています)のそれぞれについて、まだ使われていない数字はどれか」というのを表現する型です。上記の例だと、1番上の行には1,3,6がまだ使われていないこと、その下の行はもう完成していて何も使えないこと、などを表しています。 これがどのように実現されているかというと Ret の初期値を Tuple9<Digit> 、つまりすべての行に1から9すべてが使える状態にしておく 左上のマスから順に、 Ret の「そのマスの行番号」から「そのマスに書かれている数字」を削除する 右下のマスまで見終わったら終了 ということをしています。タプル型 I の長さを1ずつ増やしながら再帰することで型でもfor文のようなことを実現できます。 「行」だけでなく「列」「グループ(3×3の正方形)」の条件もつくるために、 ToColumn ・ ToSquare 、 ColumnLimit ・ SquareLimit も同様に定義しておきます。 // タプルのindexを列番号に変換するための型 type ToColumn = [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ]; // タプルのindexを3×3のグループの番号に変換するための型 type ToSquare = [ 0 , 0 , 0 , 1 , 1 , 1 , 2 , 2 , 2 , 0 , 0 , 0 , 1 , 1 , 1 , 2 , 2 , 2 , 0 , 0 , 0 , 1 , 1 , 1 , 2 , 2 , 2 , 3 , 3 , 3 , 4 , 4 , 4 , 5 , 5 , 5 , 3 , 3 , 3 , 4 , 4 , 4 , 5 , 5 , 5 , 3 , 3 , 3 , 4 , 4 , 4 , 5 , 5 , 5 , 6 , 6 , 6 , 7 , 7 , 7 , 8 , 8 , 8 , 6 , 6 , 6 , 7 , 7 , 7 , 8 , 8 , 8 , 6 , 6 , 6 , 7 , 7 , 7 , 8 , 8 , 8 ]; type ColumnLimit < Board extends Problem , Ret extends Tuple9 < Digit > = Tuple9 < Digit > , I extends unknown [] = [] > = I [ " length " ] extends 81 ? Ret : ColumnLimit < Board , PopValue < Ret , ToColumn [ I [ " length " ]], Board [ I [ " length " ]] > , [... I , unknown ] > ; type SquareLimit < Board extends Problem , Ret extends Tuple9 < Digit > = Tuple9 < Digit > , I extends unknown [] = [] > = I [ " length " ] extends 81 ? Ret : SquareLimit < Board , PopValue < Ret , ToSquare [ I [ " length " ]], Board [ I [ " length " ]] > , [... I , unknown ] > ; なお、このコードはTypeScript4.4以前だと再帰回数の上限を超えてしまいエラーとなってしまいます。TypeScript4.5で型レベルでの末尾再帰最適化が行われるようになったので動くようになりました。この機能が追加されていなければ今回の記事は実現しなかったでしょう。 数独を解く それではいよいよ数独を解いていきましょう。結論から書いてしまうと、以下の型で数独を解くことができます。 type Solve < Board extends Problem , Row extends Tuple9 < Digit > = RowLimit < Board > , Column extends Tuple9 < Digit > = ColumnLimit < Board > , Square extends Tuple9 < Digit > = SquareLimit < Board > , Answer extends Digit [] = [], L extends number = Answer [ " length " ], T = Board [ L ] extends Empty ? Row [ ToRow [ L ]] & Column [ ToColumn [ L ]] & Square [ ToSquare [ L ]] : Board [ L ] > = L extends 81 ? Answer : T extends Digit ? Solve < Board , PopValue < Row , ToRow [ L ], T > , PopValue < Column , ToColumn [ L ], T > , PopValue < Square , ToSquare [ L ], T > , [... Answer , T ] > : never ; 上のコードは「左上のマスから順番に『そのマスに使える数字を1つ選んで埋めて、次のマスに進む』を繰り返す。最後(右下)のマスまで詰まずに埋めることができたらそれが答え」という方針を実現するコードです。 まずは各型変数の説明です。 Board :問題の盤面を表す Row :(現時点で)各行でまだ使われていない数字はどれかを表す長さ9のタプル型。 Column 、 Square : Row の「行」を「列」、「グループ」に置き換えたもの。 Answer :左上から順に埋めた数字を記録していくタプル型。これの長さが81まで達するとそのときの Answer が解。 L : Answer の長さは色々なところで使うので見やすさのために新たに文字で置いたもの。「ここまでいくつ埋めたか」を表すと同時に、「今から埋めようとしているマスのindex」も表している。 T :今から埋めようとしているマス(= L 番目のマス)で使うことのできる数字を表す型。 Board の L マス目に応じて以下のように決まる。 問題で埋まっているマスならその数字しか使えないので Board[L] 空きマスなら行、列、グループの条件をすべて守る必要があるので Row[ToRow[L]] 、 Column[ToColumn[L]] 、 Square[ToSquare[L]] の交差型。ここで、例えば Row[ToRow[L]] は「 L 番目のマス目の属している行で使える数字」を表すことに注意。 T extends Digit ? ... によって T にunion distributionが適用される(union distributionについては こちらの記事 の解説がとても分かりやすいです)ので、例えば T が 1|3 であった場合、 Solve < Board , PopValue < Row , ToRow [ L ], 1 > , PopValue < Column , ToColumn [ L ], 1 > , PopValue < Square , ToSquare [ L ], 1 > , [... Answer , 1 ] > | Solve < Board , PopValue < Row , ToRow [ L ], 3 > , PopValue < Column , ToColumn [ L ], 3 > , PopValue < Square , ToSquare [ L ], 3 > , [... Answer , 3 ] > と展開されます。要は Answer に埋めようとしている数字を追加 L 番目のマスが所属する行、列、グループのそれぞれから L 番目のマスに使った数字を削除 した状態で次の再帰に進みます。 途中で T がnever型になってしまった、つまり使える数字が1つもなくなってしまった場合にはその分岐はneverを返却して終了するので、無事81マスすべて埋めることができたものだけが最終的な答えとなるわけです。 実際に問題を解いてみる 早速何問か解いてみましょう。 type Answer1 = Solve < Problem1 > ; /* -> [ 3, 2, 4, 1, 8, 7, 5, 6, 9, 8, 9, 1, 4, 5, 6, 3, 7, 2, 5, 6, 7, 2, 9, 3, 8, 4, 1, 7, 8, 6, 5, 2, 9, 1, 3, 4, 2, 5, 9, 3, 1, 4, 7, 8, 6, 1, 4, 3, 6, 7, 8, 2, 9, 5, 4, 7, 2, 9, 3, 1, 6, 5, 8, 6, 3, 5, 8, 4, 2, 9, 1, 7, 9, 1, 8, 7, 6, 5, 4, 2, 3 ] */ type Problem2 = [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 3 , 0 , 0 , 0 , 0 , 0 , 0 , 6 , 0 , 9 , 1 , 5 , 2 , 0 , 0 , 0 , 0 , 0 , 0 , 2 , 3 , 0 , 4 , 8 , 0 , 0 , 0 , 0 , 4 , 1 , 8 , 0 , 0 , 6 , 5 , 0 , 0 , 7 , 0 , 9 , 3 , 2 , 5 , 0 , 0 , 0 , 8 , 0 , 6 , 0 , 9 , 3 , 4 , 1 , 0 , 9 , 2 , 1 , 5 , 0 , 0 , 3 , 0 , 0 , 5 , 0 , 0 , 0 , 1 , 8 , 2 , 9 , 6 ]; type Answer2 = Solve < Problem2 > ; /* -> [ 6, 9, 4, 8, 5, 7, 1, 2, 3, 3, 8, 7, 1, 4, 2, 6, 5, 9, 1, 5, 2, 6, 3, 9, 7, 8, 4, 2, 3, 5, 4, 8, 1, 9, 6, 7, 4, 1, 8, 9, 7, 6, 5, 3, 2, 7, 6, 9, 3, 2, 5, 8, 4, 1, 8, 7, 6, 2, 9, 3, 4, 1, 5, 9, 2, 1, 5, 6, 4, 3, 7, 8, 5, 4, 3, 7, 1, 8, 2, 9, 6 ] | [ 6, 9, 7, 8, 4, 2, 1, 5, 3, 3, 8, 4, 1, 5, 7, 6, 2, 9, 1, 5, 2, 6, 3, 9, 7, 8, 4, 2, 3, 5, 4, 8, 1, 9, 6, 7, 4, 1, 8, 9, 7, 6, 5, 3, 2, 7, 6, 9, 3, 2, 5, 8, 4, 1, 8, 7, 6, 2, 9, 3, 4, 1, 5, 9, 2, 1, 5, 6, 4, 3, 7, 8, 5, 4, 3, 7, 1, 8, 2, 9, 6 ] */ type Problem3 = [ 0 , 0 , 0 , 3 , 0 , 0 , 0 , 1 , 9 , 6 , 7 , 0 , 0 , 0 , 4 , 0 , 0 , 0 , 0 , 0 , 0 , 8 , 0 , 0 , 0 , 2 , 0 , 3 , 4 , 0 , 0 , 7 , 0 , 2 , 0 , 0 , 0 , 0 , 6 , 0 , 3 , 1 , 5 , 0 , 0 , 0 , 0 , 0 , 0 , 6 , 0 , 0 , 9 , 0 , 1 , 3 , 0 , 6 , 0 , 0 , 0 , 0 , 2 , 0 , 0 , 4 , 7 , 0 , 0 , 6 , 0 , 0 , 2 , 0 , 9 , 0 , 4 , 0 , 0 , 0 , 0 ]; type Answer3 = Solve < Problem3 > ; /* -> [ 4, 5, 8, 3, 2, 6, 7, 1, 9, 6, 7, 2, 9, 1, 4, 3, 8, 5, 9, 1, 3, 8, 5, 7, 4, 2, 6, 3, 4, 1, 5, 7, 9, 2, 6, 8, 8, 9, 6, 2, 3, 1, 5, 7, 4, 7, 2, 5, 4, 6, 8, 1, 9, 3, 1, 3, 7, 6, 8, 5, 9, 4, 2, 5, 8, 4, 7, 9, 2, 6, 3, 1, 2, 6, 9, 1, 4, 3, 8, 5, 7 ] */ 無事数独を解くことができましたね! Problem2 のように答えが複数ある場合は解すべてのunion型が得られます。 また、 Problem3 のようなヒントの少ない難しい問題でも解くことができました。 最後に 今回使用したコードは こちら で試すことができます。
アバター
これは、FORCIA Advent Calendar 2021の21日目の記事です こんにちは。第二旅行プラットフォーム部エンジニアの浦上です。アドベントカレンダーの枠を取ってみたはいいものの特にネタが思いつかずフォルシアの過去のアドベントカレンダーを遡っていたところこのような記事を見つけました。 プログラミング言語ではなく、フォルシアの高速検索の鍵を握るSQLで数独を解く なるほど、アドベントカレンダーというのはCやPythonのような『普通の』プログラミング言語以外で数独を解けばよいのですね。 ということでこの記事ではTypeScriptの『型』だけを用いて数独を解いていこうと思
アバター
これは、 自然言語処理 Advent Calendar 2021 の20日目の記事です。 新卒2年目のエンジニア、吉成です。 普段はフォルシアのDXプラットフォーム部・技術研究所という2つの部署に所属し、web開発と自然言語処理の二足の草鞋を履いています。二兎を追う者は一兎をも得ずという言葉もありますが、今はひーひー言いながらも二兎を追えるエンジニアを目指しています。 ところで皆さん、依存構造解析してますか? 依存構造解析は自然言語処理の実応用において重要な基礎解析の1つです。文中のどの単語(あるいは句)がどの単語(句)に依存しているか、またそれらの単語(句)間はどんな関係を持っているのか(依存構造)を解析します。一般的に依存構造解析は、文を単語や形態素に分割したり、単語や形態素に品詞のラベルを付与したりする形態素解析と呼ばれる処理の後に行われます。 (画像:「部屋から見える夜景が美しかった。」の依存構造解析の結果) 上の図は、「部屋から見える夜景が美しかった。」という、ホテルのレビューを想定した自然言語文に対して依存構造を解析した結果です。単語の下に書いてある NOUN , ADP などの文字列はそれぞれ名詞、助詞といった品詞を表しています。また、依存関係を持つ単語と単語が矢印で繋がれており、それぞれの矢印には関係の種類を表すラベルが付与されています。ここではすべてのラベルを説明することはしませんが、例えば以下のような意味があります。 夜景→見える acl:形容詞的修飾節 美しかっ→夜景 nsubj:名詞句主語 依存構造解析は様々な場面で役立てられます。例えば、「料理が高級ホテルのように豪華で美味しかった。」という修飾的な語句のある文を「料理が美味しかった。」などのように要約することで、人間が大量の文章を理解するのを補助する文書要約システムや、「〇〇ホテルはいつ開業しますか?」という質問に対して「〇〇ホテルは12月20日に開業予定です。」というような文を元に「12月20日」と回答するような質問応答システムなどへの応用が考えられます。今回は簡単な例文を使って、依存構造解析を使ったレビュー文からの情報抽出にチャレンジしてみましょう! 準備 今回は依存構造解析を含む各種解析に GiNZA というPythonライブラリを使用します。GiNZAはMegagon Labsが開発したオープンソースの日本語NLPライブラリで、最先端の機械学習技術を組み込んだ自然言語処理のためのフレームワークである spaCy と、ワークスアプリケーションズ徳島人工知能NLP研究所で開発されたオープンソース形態素解析器のPython実装 SudachiPy を基盤技術としています。 今回の主役であり記事タイトルにもなっている DependencyMatcher はspaCyのAPIにあるものなので、本記事で DependencyMatcher の使い方を理解すればGiNZAによる日本語の自然言語処理だけでなく、spaCyで対応している他の言語の処理にも適用できます。 では早速、GiNZAをインストールしましょう。 pip install -U ginza ja-ginza-electra ja-ginza-electra というのは、GiNZA用の最新の言語モデルで、従来の言語モデルよりも高い解析精度を持ちます。 ただしメモリ容量16GB以上推奨とのことなので、メモリの小さいマシンで処理を行う場合や解析精度よりも実行速度を重視したい場合には、 ja-ginza-electra の代わりに ja-ginza をインストールしてください。 インストールが済んだら、ライブラリをimportして解析の準備を整えましょう。 import ginza import spacy nlp = spacy . load ( 'ja_ginza_electra' ) # 初回はモデルをダウンロードするために時間がかかります # nlp = spacy.load('ja_ginza') # ja-ginzaをインストールした場合のみ 続いて使用するテキストデータを準備します。 お手元にデータがあればそれを使用しても構いませんが、今回はサンプルとしてあるホテルのレビューを想定した3文を用意しました。 レビューデータは実務における自然言語処理でよく扱われるデータの1つです。 txts = [ "部屋から見える夜景が美しかった。" , "立地は悪いが食事が美味しい。" , "客室露天風呂は大人でも足がのばせてとても広かった。" ] Matcherによるマッチング まずは依存構造解析をせず、トークン(単語, 形態素)単位でのマッチングで情報を抽出してみましょう。 spaCyにはMatcherという、トークン単位でのマッチングに適したAPIがあります。 最初に、Matcherを使ってこのホテルのレビューにどのような形容詞が使われているのか見てみましょう。 from spacy.matcher import Matcher matcher = Matcher ( nlp . vocab ) # 日本語の語彙の集合を渡してMatcherオブジェクトを作る patterns = [ [{ "TAG" : { "REGEX" : "^形容詞" }}] # ルールの定義。品詞タグが「形容詞」で始まるもの ] matcher . add ( "adj" , patterns ) # Matcherオブジェクトにルール名adjとしてルールを登録 s = "" . join ( txts ) # 3つの文をつなげて1つの文字列にする doc = nlp ( s ) # 引数に与えられた文字列を文章として解析する matches = matcher ( doc ) # matchesの中にマッチング結果が入る ルールはマッチすべきトークンの属性を集めたdictのlistです。 例えば次のルールは、「品詞タグの名前が形容詞で始まるもの」を表すルールです。 [{ "TAG" : { "REGEX" : "^形容詞" }}] TAG は品詞タグを指定することを意味します。 TAG の代わりに LEMMA を使うことで見出し語に対する指定、 TEXT を使うことで文書中単語の文字列そのものに対する指定も可能です。 {"REGEX": "^形容詞"} は品詞タグが ^形容詞 という正規表現に当てはまるもの(=すべての形容詞)という意味です。正規表現を使わない場合には文字列で "形容詞-一般" などと指定できます。 GiNZA内部で使われている形態素解析器SudachiPyの品詞タグの表現方法では、ハイフンつなぎでより詳細な品詞を表現しています。そのため「名詞」「形容詞」「動詞」といった大きな分類のみ使いたい場合は、正規表現で品詞タグを指定すると良いです。解析結果の doc をforループで回すとどの単語にどの品詞タグがついているか見ることができます。 ルールの書き方についてもっと詳しく学びたい方は 公式ドキュメント や公式の USAGE をご参照ください。 doc = nlp ( txts [ 0 ]) for token in doc : print ( token . text , token . tag_ ) # 部屋 名詞-普通名詞-一般 # から 助詞-格助詞 # 見える 動詞-一般 # 夜景 名詞-普通名詞-一般 # が 助詞-格助詞 # 美しかっ 形容詞-一般 # た 助動詞 # 。 補助記号-句点 ルールを定義できたら、次にそれをMatcherオブジェクトに登録します。 matcher.add() の第1引数にルールの名前、第2引数にルールのlistを指定することで登録できます。 ルールのlistなので、複数の条件を与えることが可能です。 例えば、形容詞に加えて形容動詞(「親切だ」「不愉快だ」など)にもマッチングさせたい場合には、次のように書くことができます。 patterns = [ [ { "TAG" : { "REGEX" : "^形容詞" }} ], [ # 今回使用したツールの品詞体系における形容動詞相当の表現 # 参考:https://ccd.ninjal.ac.jp/unidic/glossary { "TAG" : "名詞-普通名詞-形状詞可能" }, { "LEMMA" : "だ" } ] ] matcher . add ( "multiple patterns" , patterns ) マッチング結果を見てみましょう。 公式ドキュメント を見てみると、返り値には (match_id, start, end) のようなタプルのリストが返ってくるようです。ルールにマッチした部分の文字列はもともとの文章を解析した doc に対して doc[start:end] で取得できます。 for _ , start , end in matches : print ( doc [ start : end ]. lemma_ ) # .lemma_ は見出し語を取得することを意味する 上記のfor文を実行すると、次のような結果が出力されます。 美しい 悪い 美味しい 広い ホテルに泊まった人がどんなことを感じたのかはなんとなく伝わっていますが、なんとなく以上のことは分かりません。 特に、「悪い」については一体何が悪かったのか気になってしまいますね。 そこでルールを改良し、その形容詞に対する「何が」が分かるようにしてみましょう。 例えば、次のようなルール定義が考えられます。 patterns = [ [ { "TAG" : { "REGEX" : "^名詞" }}, { "TEXT" : { "IN" : [ "は" , "が" ]}}, #「は」か「が」のいずれかにマッチ { "TAG" : { "REGEX" : "^形容詞" }}] ] ] これを先程のようにMatcherオブジェクトに登録し、マッチング結果を見ます。 matcher = Matcher ( nlp . vocab ) matcher . add ( "noun-adj" , patterns ) doc = nlp ( s ) # s は txt に含まれる文をすべて join したもの matches = matcher ( doc ) for _ , start , end in matches : print ( doc [ start : end ]. lemma_ ) すると、次のように出力されます。 夜景が美しい 立地は悪い 食事が美味しい なるほど。「悪い」と言われているのは立地で、夜景と食事は褒められているようです。 ですがここで、形容詞を抽出したときの結果をもう一度見てみましょう。 美しい 悪い 美味しい 広い 上の3つは何に対して言われているのか分かりましたが、何が「広い」と言われているのかは抽出できていません。 「広い」という単語が含まれていたのは以下の文でした。 客室露天風呂は大人でも足がのばせてとても広かった。 他の文と比べるとやや複雑な文ですね。 人間はこの文を見たときに「広いと言われているのは客室露天風呂(あるいは露天風呂)だな」とすぐに判断できますが、コンピュータにとっては自明ではありません。 「広かった」の直前の3単語は「のばせ」「て」「とても」で、単純に「広い」の直前を見れば良いというわけではありません。 助詞「は」「が」で判断する手もあります。実際、助詞「は」「が」を含む直前の文節が形容詞の意味の対象となっている場合も多くあります。ただし今回の場合、直前の「は」「が」がつく単語は「足」で、「足が広い」と言っているとは考えづらいですね。 そんなときに使えるのが今回のメインテーマ、依存構造解析です。 DepdencyMatcherによるマッチング 実はspaCyにはDepdencyMatcherという、依存構造を使ったマッチングのためのAPIもあります。 使い方はMatcherと似ていますが、ルールの書き方とマッチング結果の取り出し方が異なります。 文に対する依存構造は木構造をなしています。つまり、ただ1つだけ根となるトークンがあり、根以外のトークンはただ1つの係り先トークンを持っているということです。 この記事の最初の図も、よく見ていただくと「美しかっ」というトークンを根とする木構造になっています。 文に対する依存構造が木構造なので、依存構造を使ったルールも木構造になります。 依存構造を使ったルールはトークン単位でのマッチングを行うルールより少し複雑なので、最初にルールの例をお見せします。 ソースコードと木構造の図を対応付けながら見てみましょう。 今回の例はノードが2つだけですが、これも立派な木構造です。 patterns = [ [ { "RIGHT_ID" : "adj" , "RIGHT_ATTRS" : { "TAG" : { "REGEX" : "^形容詞" }} } ,{ "LEFT_ID" : "adj" , "REL_OP" : ">" , "RIGHT_ID" : "noun" , "RIGHT_ATTRS" : { "TAG" : { "REGEX" : "^名詞" }, "DEP" : "nsubj" } } ] ] (画像:DependencyMatcher用のシンプルなルールの図解) 図のように、DependencyMatcher用のルールは左を根として右側にノードを生やしていく木構造になっています。 Matcherのときと同様に patterns がルールのlistになっていて、その中に1つ1つのルールがdictのlistとして表現されています。 dictはそれぞれ木構造の各辺に対応しています。ここでポイントになるのは、ノードではなく辺が1つのdictに対応していることです。 根ノードの左には何の条件指定もないダミーノードがくっついていて、ダミーノードと根ノードの間の辺も張らなければならない、と考えると分かりやすいです。 各項目の意味は次のようになっています。 LEFT_ID :辺の左側のノードのIDとなる文字列。設定する場合、これまでの要素の RIGHT_ID で登場した文字列でなければなりません。 RIGHT_ID :辺の右側のノードのIDとなる文字列。他のノードとかぶらない名前をつけます。 REL_OP :左右のノードの関係。 > であれば右のノードが左のノードに直接依存していることを示し、 < であれば左右逆(左のノードが右のノードに依存)です。依存以外の関係も柔軟に定義でき、例えば . は文の中で右のノードが左のノードの直前に来ることを、 ; は右のノードが左のノードの直後に来ることを意味します。 RIGHT_ATTR :辺の右のノードに対応するトークンの条件を記載します。Matcher用のルールと同様に品詞タグや見出し語などを直接指定できます。 DEP を使えば依存関係の種類も指定できます。 専門的な説明にはなりますが、GiNZA/spaCyで日本語テキストを解析する際の依存関係の種類については次の論文に詳しくまとまっています。 浅原 正幸, 金山 博, 宮尾 祐介, 田中 貴秋, 大村 舞, 村脇 有吾, 松本 裕治, Universal Dependencies 日本語コーパス, 自然言語処理, 2019, 26巻, 1号, p.3-36, https://www.jstage.jst.go.jp/article/jnlp/26/1/26_3/_article/-char/ja . 慣れないうちは依存関係の種類を指定することが難しく感じる方も多いかもしれません。 しかしながら、実際に様々な文を解析にかけながら参照してみると徐々に分かってくると思います。 解析結果は例えば次のように可視化できるので活用してください。 s = "部屋から見える夜景が美しかった" doc = nlp ( s ) displacy . render ( doc ) # jupyter上で可視化する場合はjupyter=Trueを指定する 次のような画像が出力されます。 また、各ノードには必須の項目があります。 根ノードを定義する dict: RIGHT_ID RIGHT_ATTR 辺を定義する dict: LEFT_ID REL_OP RIGHT_ID RIGHT_ATTR 右のノードがどんな単語でも良い、という場合もありますが、その場合は RIGHT_ATTR に空のdict {} を設定しておきます。 もっと複雑なルールを書きたい場合には 公式ドキュメント や公式の USAGE をご参照ください。 さて、ルールの書き方の説明が長くなってしまいましたが、上記のルールでマッチングをしてみましょう。 from spacy.matcher import DependencyMatcher matcher = DependencyMatcher ( nlp . vocab ) patterns = [ [ { "RIGHT_ID" : "adj" , "RIGHT_ATTRS" : { "TAG" : { "REGEX" : "^形容詞" }} } ,{ "LEFT_ID" : "adj" , "REL_OP" : ">" , "RIGHT_ID" : "noun" , "RIGHT_ATTRS" : { "TAG" : { "REGEX" : "^名詞" }, "DEP" : "nsubj" } } ] ] matcher . add ( "adj_noun_pair" , patterns ) # txts = [ # "部屋から見える夜景が美しかった。", # "立地は悪いが食事が美味しい。", # "客室露天風呂は大人でも足がのばせてとても広かった。" # ] # s = "".join(txts) doc = nlp ( s ) matches = matcher ( doc ) DependencyMatcherのマッチング結果は、マッチングIDと、ルールとの対応付け(alignments)のタプルとして返されます。 alignmentsはルールの各要素で定義された辺の右側のノードにあたるトークンのindexが格納されています。 そのため、次のようにして結果を取り出します。 for _ , alignments in matches : print ([ doc [ alignment ]. lemma_ for alignment in alignments ]) # ['美しい', '夜景'] # ['悪い', '立地'] # ['美味しい', '食事'] # ['広い', '露天風呂'] 各マッチング結果について、最初にID adj に対応するトークンのindexが、次にID noun に対応するトークンのindexが格納されていることがわかります。 また、 客室露天風呂は大人でも足がのばせてとても広かった。 について、広いのが露天風呂であるということも拾えています。 欲を言うと広いのは「露天風呂」というより「客室露天風呂」としてほしい場面もあると思いますが、複合名詞を抽出できるようにする対応もDependencyMatcherのルールを改良することで可能になります。ご興味のある方はぜひ考えてみてください。 まとめ 本記事では、GiNZAとspaCyのDependencyMatcherを用いて日本語のレビューから情報を抽出する方法について、簡単な例を用いて説明しました。 また、DependencyMatcherの便利さを示すため、Matcherによる情報抽出も行いました。 GiNZAもspaCyも、実務で自然言語処理を行う際には非常に便利なライブラリです。 今回紹介した機能に限らず、GiNZA/spaCyを使いこなして世の中のテキストデータを有効に活用していきましょう!
アバター
これは、自然言語処理 Advent Calendar 2019の20日目の記事です。 新卒2年目のエンジニア、吉成です。 普段はフォルシアのDXプラットフォーム部・技術研究所という2つの部署に所属し、web開発と自然言語処理の二足の草鞋を履いています。 二兎を追う者は一兎をも得ずという言葉もありますが、今はひーひー言いながらも二兎を追えるエンジニアを目指しています。 ところで皆さん、依存構造解析してますか? 依存構造解析は自然言語処理の実応用において重要な基礎解析の1つです。 文中のどの単語(あるいは句)がどの単語(句)に依存しているか、またそれらの単語(句)間はどんな関係を持っているの
アバター
これは、 FORCIA Advent Calendar 2021 の19日目の記事です。 はじめに 第1旅行プラットフォーム部長の武田です。これまで検索アプリケーションの開発をメインに担当していましたが、最近は検索で利用する商品データを作成するサービスの開発をしています。 慣れ親しんだ検索アプリケーションとは異なりますが、そのノウハウを活かしつつ、新しいことに取り組んでいます。今回はそのサービス開発で利用している技術についてご紹介いたします。 フロントエンド Next.js フォルシアではアプリケーション開発で社内製のWebアプリケーションフレームワークを利用しています。現在は3代目までのWebアプリケーションフレームワークが存在します。3代目のフレームワークは内部で Next.js を利用しています。そのためNext.jsを採用するのは自然な流れでした。 この社内製Webアプリケーションフレームワーク開発の話は こちら です。 なお、今回開発しているアプリケーションは検索アプリケーションではなく、主にデータを登録する管理画面系のアプリケーションとなっています。通常のSpookの開発とは大きく異なるアプリケーションのため社内製フレームワークは利用しませんでした。 個人的には今回のアプリケーションでは Svelte の採用もありかと考えましたが、以下の理由から見送っています。 Reactでの開発に慣れているエンジニアが多い アプリ開発の規模も大きく、Svelteでどこまでうまく対応できるか未知数 antd デザインシステムとして antd を採用しています。 MUI(旧Material-UI) と比較して管理画面向けのコンポーネントが多い Form関連のコンポーネントも強力で、不要な再レンダリングが発生しないようになっている( React hook form も良い評判を良く聞きます) コンポーネントだけでなく、ガイドラインやデザインパターンについてのドキュメントも多く充実している といった点で採用しました。 現状、Next.jsでantdを利用していると特定のコンポーネントを使用した際に以下の Warning が出ます。 Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. Toavoid this, useLayoutEffect should only be used in components that renderexclusively on the client. See https://reactjs.org/link/uselayouteffect-ssr for common fixes. これはantdが内部的に利用している rc(react-component) がSSR対応されておらず、useLayoutEffectフックを利用しているためです。 対応 自体は進められており、近い将来解消すると思われます。 Storybook 今回の案件では、エンジニアがスタイリングを含めてコンポーネントを開発しており、 Storybook を利用しています。 GitLab CIを利用して、mainブランチをpushしたタイミングでStorybookをbuild、生成したhtmlをS3+CloudFrontにアップし、常に最新のコンポーネントを全メンバーが確認できる状態にしています。 Storybookを用意することでエンジニア以外のプロジェクトメンバーもどのようなコンポーネントがあり、開発中のデザインがどうなっているか確認できるというメリットがあります。 現時点ではコンポーネントの設計等も頻繁に変わるような開発初期段階のため、Storybookを利用したテストは書いていませんが然るべきタイミングでスナップショットテストを含むコンポーネントのテストを実装していこうと考えています。 Apollo client データフェッチのライブラリとして Apollo client を採用しています。 GraphQLを利用しており、バックエンドで生成したGraphQLのschemaと GraphQL Code GeneratorのTypeScript React Apolloプラグイン を利用して、hookを自動生成しています。 状態管理のライブラリとして recoil を使いたいと思っていますが、現時点ではGlobalな状態管理が必要になるケースがそこまでなく、 Context API や useState hook 等で十分な可能性があります。 このあたりは開発を進めていく中で検討できればと考えています。 バックエンド NestJS バックエンドのフレームワークとして NestJS を採用しました。 協力会社のエンジニアさんはJavaでの開発に慣れたエンジニアが多い 社内のエンジニアはJavaScript, TypeScriptでの開発に慣れたエンジニアが多い という状態でした。将来的には社内でメンテナンスしていくサービスになるため、開発言語をJavaにしてしまうと運用時に厳しくなる可能性が高いと考えました。 JavaのSpring Frameworkのような書き味のNestJSを採用することで双方が開発、運用しやすい形にできるのではと考え採用しました。 こちらは Yahoo! JAPAN さんのこちらのTech Blog も参考にさせていただきました。 デコレータやDIを利用した開発は私含め社内のエンジニアも慣れておらず、若干の取っ付きにくさを感じましたが、慣れてくるとまさに関心の分離により、メインのロジックに集中できるように感じました。 また、協力会社のエンジニアさんからも書きやすいという声をいただき、採用して良かったと思いました。 NestJSはGraphQL Serverとして実装していて、デコレータをたくさん書く必要がありますが cli-plugin を導入することにより、記述量を大きく削減できました。 GraphQLについて NestJSはGraphQL Serverとして実装しています。 今回のアプリケーションでは非常に多くの画面が存在し、それに伴いAPIの数も多くなることが見込まれました。 参照でGETのエンドポイント、データの追加でPOSTのエンドポイントを用意、データの更新ではPUT/PATCHといったエンドポイントを用意して...となると考えることも多く、特にRESTfulなAPIを設計する場合にエンドポイントの設計だけでも膨大な時間がかかってしまう可能性がありました。 GraphQLではデータ取得はquery、データを操作する際はmutation、あとはハンドラメソッド名を考えれば済むため、地味ではありますがエンドポイントについて考える時間はかなり短縮された感覚です。 GraphQLでは「フロントエンドでの取り回しは楽になるが、バックエンドの実装は複雑になる」といった話を何度か耳にしたことがあります。開発を進めてみてNestJSがよくできているのか、そこまで複雑になっている印象はありません。 現時点では、GraphQLのバックエンドの実装は通常のWeb APIとそこまで変わらないように感じました。 実際、REST APIで開発するか、GraphQLにするかは非常に悩んだポイントではありますが、チームメンバーの使ってみたさあります!という声や メルカリ Shops さんの記事 も採用の後押しになりました。 コードファースト?スキーマファースト? NestJSでGraphQL Serverを実装する際にコードファーストで進めるか、スキーマファーストで進めるか、という点がひとつ悩ましいポイントかと思います。 今回のプロジェクトではコードファーストのアプローチを取ることにしました。 理由は以下のとおりです。 フロントエンドの開発をした人がそのままバックエンドのAPIも実装する形式になっている フォルシアではフロントエンドとバックエンドでエンジニアが分かれておらず、どちらも実装する そのためスキーマベースで認識を合わせる必要がない モノレポ構成となっており、フロントエンドとバックエンドのコードが同一リポジトリで管理されている こちらは毎週実施している技術ディスカッションという社内MTGの場で相談したところ、上記のようなコメントをいただき、コードファーストで進めることにしました。 実際、コードファーストで進めてみて快適に開発できている印象です。 Prisma ORMとして Prisma を利用しています。 通常はPrisma schemaによるモデリングでデータベースの設計をしていくかと思いますが、フォルシアには独自のschema定義を用意してデータベースを構築する仕組みがあります。 データベースの構築は独自の仕組みを利用し、Prismaとの連携では prisma db pull (旧 prisma introspect )コマンドを利用してprisma.schemaファイルを生成しています。 データベースからのデータの取得が型安全になり、TypeScriptで補完が効く点も非常に良い開発体験が得られています。 一方、私含めフォルシアのエンジニアはパフォーマンス面も考慮したSQLを書くことを得意としていますが、PrismaのようなORMで効率的にデータを取得するノウハウは少ないため、少しずつ知見を貯めていければと考えています。 Prismaは周辺ツールも強力で、初期開発時のサンプルデータの作成では prisma studio を利用しています。 資金調達 もしており、引き続き活発な開発が進められていくことが見込まれる点も採用を後押ししました。 schemaspy 今回の案件では外部サービスからデータベースにアクセスするケースもあるため、テーブル定義書を用意する必要がありました。 schema定義と別でテーブル定義書を作成するのではなく、構築したデータベースから schemaspy を利用して定義書を生成するようにしました。 また、テーブルやカラムの論理名を表示したいという要件がありました。 PostgreSQLではテーブルやカラムに対してコメントを付与する機能があるため、こちらを利用しました。 コメントの1行目が論理名 2行目以降がテーブル、カラムのコメント本文 として、schemaspyをforkして機能を追加することで日本語の論理名も一緒に表示できるよう対応しました。 この機能を追加したもののソースコードは こちら に公開しています。 生成されるテーブル定義書は以下のようになります。 こちらもStorybookと同様にGitLab CIを利用してmainブランチにマージされたタイミングで最新のテーブル定義書がbuildされ、Web上で確認できるようにしています。 Dockerの利用について 開発環境では Docker を利用して、各プロセスはすべてコンテナで動作するようになっています。 環境構築用のinit scriptも用意しており、手元の環境にDocker、 Docker Compose をインストールしておけばすぐに開発に入れるようになっています。 また、バックエンドのNestJSで生成したGraphQLのschemaファイルは変更があったタイミングでフロントエンドのコンテナに同期されます。 これは chokidar-cli を動かすコンテナを別で用意して実現しています。 これによりフロントエンド側で参照していたフィールドを削除したケースなどは型エラーとして即時検知できます。 docker-compose up すれば自動生成ファイルの連携含めて、ホストマシン側では特に何のプロセスも動かさずに開発できるように、ということを意識しています。 バッチ処理まわり 外部サービスから連携データを取得し、取り込み 外部サービスへの連携データを生成 といったバッチ処理も存在します。こちらはSQLやshellコマンドなどを柔軟に実行する社内独自の仕組みを利用して実装しています。 実際に開発が進んできての印象 デザインを含めてエンジニアが実装していますが、デザインの調整にかなりの実装時間を割いてしまっている デザインの経験があるエンジニアが少なく、慣れていないという点が大きいです GraphQL、何もわからない状態からスタートしていましたが実際には単なるインタフェースであり、Web APIと大きく変わらないと感じてからは仲良くなれてきた気がします GraphQLを取り巻くエコシステムがかなり発達しており、このあたりも開発体験を向上させているように感じました データベースのテーブル定義が変わったときのコードの変更量が多い... このあたりはなるべく少なくなるように、というのを意識してはいましたが...開発初期段階ではどうしてもテーブル定義に変更が入ることが多く、ここの追従をいかに楽にするかは今後の課題です まとめ フォルシアでは検索Webアプリケーションの開発で培ったノウハウを活かしつつ、新しい技術も取り入れながら検索以外のWebアプリケーション開発にも取り組み始めています。技術選定の際に今回の記事が少しでも参考になりましたら幸いです。
アバター
はじめに 第1旅行プラットフォーム部長の武田です。これまで検索アプリケーションの開発をメインに担当していましたが、最近は検索で利用する商品データを作成するサービスの開発をしています。 慣れ親しんだ検索アプリケーションとは異なりますが、そのノウハウを活かしつつ、新しいことに取り組んでいます。今回はそのサービス開発で利用している技術についてご紹介いたします。 フロントエンド Next.js フォルシアではアプリケーション開発で社内製のWebアプリケーションフレームワークを利用しています。現在は3代目までのWebアプリケーションフレームワークが存在します。3代目のフレームワークは内
アバター
これは、 FORCIA Advent Calendar 2021 の18日目の記事です。 こんにちは。DXプラットフォーム事業部のエンジニアの多田です。 技術教育チームにも所属し、技術系の新入社員研修の運営に関わっています。 本日は総合職および、エンジニア向け技術研修の内容についてご紹介します。 総合職向けの研修 概要 目的:アプリ開発を通じ、エンジニアの業務に対しイメージをつけ業務でのやり取りに活かしてもらうため。 業務に必要なWEBアプリケーションの基礎知識を学習するため。 期間:3週間 研修内容 Webアプリケーション開発に必要な基礎概念の学習 HTML/CSS/JavaScriptの概念、基本構文の学習 外部サービス Progate を用いています。 JavaScript課題 配列の値を順に書き出す等、基本文法やデバッグの仕方に慣れるための課題を出しています。 技術講義 基礎的な技術の知識(WEBアプリケーションの仕組みやDBについて)や フォルシアの検索プラットフォームであるSpookの特長などを学習します。 アプリ開発  APIを用いて、検索可能なクライアントサイドのアプリ開発をします。 見積課題  作業コストを見積もる課題を行います。   課題作成にあたりやったこと 研修後の業務に活きるよう、営業/エンジニア社員それぞれに業務で知っておきたかったこと、 知っておいてほしいことをヒアリングしました。 「営業として顧客とのやり取りで使う技術について知っておくとよさそう」、「エンジニアとして営業の方によく説明する単語があるので、研修時点で知っていおいてもらえると嬉しい」など 色々な意見をもらい取り入れました。 課題の変わり目のサポートを加えました。 これまでの経験より、ProgateからJavaScript課題、JavaScript課題からアプリ開発と課題の変わり目で躓きがちで あまり本質的でない点で時間が溶けてしまうこともありました。そこで、それぞれの課題の差分を考え、必要なサポートを追加しました。 具体的に言うとエディタや開発者ツールの使い方を先に知っておいた方がスムーズに進みそうだったので、学べる課題を追加しました。 エンジニア向けの研修 概要 目的:配属後スムーズに動き出せる技術や自走スキルを身につけるため。 フォルシアのエンジニアとして配属までに到達してほしい技術レベルを獲得してもらうと同時に、調査力や質問力を高めてもらいたいと思っています。 期間:2.5カ月 下記のように前半・後半に分かれています。 前半1.5カ月:個人課題 後半1カ月:チームに仮配属、実務体験 研修内容 今回は前半の期間に行う個人課題についてご紹介します。 ※新卒社員は未経験の方からアプリ開発経験者まで、技術レベルは様々です。 そのため、未経験からでも問題ないことを前提としています。 WEBアプリケーション開発に必要な技術の習得 基礎的な学習 総合職向け研修と同様、外部サービス Progate を用いています。 JavaScript課題 前述のProgateより発展的な課題として、JSON.stringifyをはじめとした関数の実装課題を出しています。 SQL課題 フォルシアの検索アプリケーション開発中ではSQLを触る場面が多くあります。 ホテルや最寄り空港のデータを用いて、データ取込やテーブル作成、WHERE,ORDER BY, GROUP BY, JOINなど基本構文を使った課題を出しています。 技術講義 WEBアプリケーションの構造、HTTPやSSHの仕組み、セキュリティといった一般技術の理解、  GitやLinuxコマンド、サーバリソース調査、テストといった開発運用業務で必要な知識等々  一般的な知識からフォルシア独自の知識まで幅広く扱います。  講義の中で実際に手を動かしてみるような機会もあります。 アプリ開発  研修のメインテーマです!約1カ月に渡り、検索アプリケーションを開発します。  課題の大枠は用意していますが、明確なゴール設定はありません。 自ら設定し、解決のための道筋をたててもらいます。  研修詳細については下記をご覧ください。   19卒エンジニアが奮闘 名簿アプリ開発研修 課題作成にあたりやったこと 必要な技術レベルに到達してもらいつつ、自由度を持たせるため必達の課題+αの課題を作成しました。 技術講義は、講師側に教育チームより目標および「重点的に説明する点・ざっくり説明する点・とばしたほうが良さそうな点」を共有しました。 実際の講義では、目標に向け「わかりにくいから変更しよう」「これは手を動かす形にしてみよう」など講師が工夫して実施してくれました。 技術講義には教育チーム以外の技術社員にも協力してもらっており、新入社員との繋がりができる機会にもなっています。 おわりに 最後まで読んでいただきありがとうございました! 今回は研修内容に焦点をあてご紹介しましたが、その他にも社員同士の関係性構築・ソフトスキル面向上のための取り組みも実施しています。 下記の記事でご紹介していますので、ぜひそちらもご覧ください。 FORCIA Meetup #3 未経験者も即戦力にするフォルシアの技術教育 新人研修はその後スムーズに入っていくための準備期間、そして内容や取り組みを通じ、 参加者にその場(会社)への安心感をもってもらい次へと進む土壌を作る期間でもあると思います。 期間はほんの一瞬ですが、それを経て各自の持ち味が少しでもより発揮しやすい状態になるといいなと感じます。 フォルシアに興味をもってくださった方は、是非弊社採用ページもご確認ください!
アバター
こんにちは。DXプラットフォーム事業部のエンジニアの多田です。 技術教育チームにも所属し、技術系の新入社員研修の運営に関わっています。 本日は総合職および、エンジニア向け技術研修の内容についてご紹介します。 総合職向けの研修 概要 目的:アプリ開発を通じ、エンジニアの業務に対しイメージをつけ業務でのやり取りに活かしてもらうため。 業務に必要なWEBアプリケーションの基礎知識を学習するため。 期間:3週間 研修内容 Webアプリケーション開発に必要な基礎概念の学習 HTML/CSS/JavaScriptの概念、基本構文の学習:外部サービスProgateを用いています。
アバター
これは、 FORCIA Advent Calendar 2021 の17日目の記事です。 zx とは zxはNodeの child_process のラッパーで、JavaScriptで記述したスクリプトをNodeで実行し、 shellコマンドを発行できます。 一言で表すと、お手軽にJavaScriptで記述し、実行できるshellです。 googleから公開され、2021年初頭に話題になりました。(google/zx: https://github.com/google/zx ) 筆者は普段からスクリプトはbashで実行している一方、業務で使い慣れているTypeScriptの型をzxで使えるとマニュアルで見かけ、使ってみました。 zx の導入 zxを利用するには、まず、Nodeのバージョンが14.13.1以上である必要があります。 Nodeの準備が整っていれば、以下でインストールします。 npm i -g zx zxで実行するスクリプトは、top-level-awaitを利用すべく、 .mjs 拡張子での作成が推奨されています。 .js で作成する場合、コマンドの発行部分で void async function(){...}() と記述する必要があり、少し長くなってしまいます。 zx の使用 以下でいくつかzxの使用方法を解説します。 コマンドの発行 zxで利用するスクリプトでは、先頭に #!/usr/bin/env zx を記載し、それ以下へ処理を記述していきます。 また、以下のcommandの部分へ、shellで発行したいコマンドを記述します。 $`command` 以下はhello worldするスクリプトです。 変数の記述はJavaScriptで見慣れた記法そのものです。 #!/usr/bin/env zx const word = "hello world"; await $`echo ${word}`; 作成したスクリプトは、以下のように実行します。 zx ./hello.mjs $ echo $'hello world' hello world zx ./hello.mjs で実行した結果、 $ echo $'hello world' とコマンドが出力された後、 hello world がechoされていることが分かります。コマンドおよび実行結果をコンソールへ出力させない場合、 zx ./hello.mjs --quiet と実行することで、表示させないことができます。 関数/パッケージの利用 追加でインストールせずとも利用できる関数/パッケージについて一部紹介します。 sleep JavaScriptでの setTimeout をラップした関数です。以下ではneruをechoし、1秒待った後 okita がechoされます #!/usr/bin/env zx $`echo neru`; await sleep(1000); $`echo okita`; zx ./wakeup.mjs $ echo neru neru $ echo okita okita fetch zxではnode-fetchをラップしたfetch関数を以下のように利用できます。 #!/usr/bin/env zx const resp = await fetch("https://www.forcia.com/"); console.log(resp.ok); zx ./fetchForcia.mjs $ fetch https://www.forcia.com/ true TypeScript で書いてみる 筆者は業務において、JavaScriptを生で記述する機会はほとんどなく、TypeScriptを使用しています。 zxのマニュアルにも記載がありますが、zxのスクリプトをTypeScriptで記述し、実行してみます。 まず、実行するには ts-node , typescript が必要なので、インストールします。 npm i -g ts-node npm i -g typescript hello worldするスクリプトは以下のように記述できます。 #!/usr/bin/env zx import "zx/globals"; const word: string = "hello world"; void (async function () { await $`echo ${word}`; })(); ここでは以下で実行します。 ts-node tsZx.ts $ echo $'hello world' hello world TypeScriptでの記述なので以下のような不正な記述に対し、警告を発報してくれます。 もちろんts-nodeで実行してみても同じ型エラーが発報され実行はされません。 また、TypeScriptをVScodeで記述する際によく活用される、 "F12を押して関数へジャンプ"がここでも可能です。 以下画像はzxでの $ 関数へジャンプしている画像です。 さらに、以下のように別途 index.d.ts を用意します。 interface Hello { word: string; language: string; } export type HelloWords = Hello[]; それをzxで実行するスクリプトへimportして型を利用できます。 #!/usr/bin/env zx import "zx/globals"; import { HelloWords } from "."; const words: HelloWords = [ { word: "こんにちは", language: "Japanese", }, { word: "Hello", language: "English", } ]; Promise.all( words.map(w => { $`echo word: ${w.word} language: ${w.language}`; }) ); ts-node tsZxType.ts $ echo word: $'こんにちは' language: Japanese $ echo word: Hello language: English word: こんにちは language: Japanese word: Hello language: English 筆者はShellよりもTypeScriptでの記述の方が親しみがあるためか、 zxでの記述が見通しがいいように感じられます。 使ってみた感想 慣れた方は普通にShellで書いた方が早そうではありますが、JavaScriptに親しみがある方は使用してみてもいいかもしれません。 TypsScriptの実行環境を整える手間はありますが、TypeScriptの便利さをShellでも再現できたのには感動しました。
アバター
zx とは zxはNodeのchild_processのラッパーで、JavaScriptで記述したスクリプトをNodeで実行し、 shellコマンドを発行できます。 一言で表すと、お手軽にJavaScriptで記述し、実行できるshellです。 googleから公開され、2021年初頭に話題になりました。(google/zx: https://github.com/google/zx) 筆者は普段からスクリプトはbashで実行している一方、業務で使い慣れているTypeScriptの型をzxで使えるとマニュアルで見かけ、使ってみました。 zx の導入 zxを利用するには、まず、No
アバター