TECH PLAY

MNTSQ

MNTSQ の技術ブログ

91

こんにちは、MNTSQ( モンテスキュー )でSREをやっている中原です。 前回(?)、 突撃!隣のPCデスク!! MNTSQ キーボード・マウス編 というタイトルでブログを書かかせてもらいました。社内外から(特にエンターキーについて)大変反響をいただきうれしいなぁという気持ちでいっぱいです。 開発者がこだわるものというのは、そのこだわりの強さ故に争いを生むことがあります。時に「 宗教戦争 」とも称されるその争いのテーマとしては、前回取り上げたキーボードの他、ブラウザやOSなどもあげられようかと思います。 今回は、そんな「 宗教戦争 」になりがちなテーマの一つである「 きのこたけのk *1 エディタとターミナル エミュレータ 」を取り上げ、MNTSQ社内のエンジニアがどういった開発環境で仕事をしているのかを紹介していきます! 基本的に調査対象としては、MNTSQ社内でエンジニアリングまたは運用上コードを修正したり、サーバに入っての作業がある人となっています。また、複数回答可としています。 エディタ編 エディタについては、17名から25件の回答をもらうことができました。早速グラフを見ていきましょう。 ちなみに、第一候補だけだとこんな感じに。 いわゆる テキストエディタ と IDE とが、それぞれ入り混ざる結果となりました。その中でも Mac 、 Windows ユーザともに圧倒的な Visual Studio Code の強さが見て取れます。続いて Vim というところも、私の中での世の中一般の感覚とそれほど大きくはありません。ただ、PyCharm、RubyMine、WebStormといった、JetBrains製品を合計すると、 Vim を上回るという絶妙な数量というところはありそうです。 回答を見ていくと、コーディングでは IDE や重めに拡張を入れた Visual Studio Code 、軽い編集などの際に Vim などを使うという例が多そうです。実際私も、重めの編集では Visual Studio Code を、そうじゃない場合には Vim を使うことが多いです 軽い処理に使うエディタ: Vim 、CotEditor、 EmEditor Sublime Textの利用者の理由として「達人 プログラマ のなかで、一つのエディタを使っていったほうがいいという記述があり、それに則って他を使わないようにしている」というものがありました。おそらく対象はなんでもいいと思うのですが、自分の使う道具はしっかり使いこなせた方がいいというのはありそうですね Vim 一本という社員は「 Vim を使えない者、使わない者はエンジニアに非ず」と言っていました。5年前までは私もそうだったのですけど… neovimを使ってる人がいないのは意外かもしれません。移行しようとして結局できていないのがどうも私です なお、MNTSQでは必要に応じて、好きなソフトウェアの購入も可能です。もちろん、 Sublime TextやJetBrains製品も可能ですよ! ターミナル エミュレータ 編 こちらについては、複数のソフトを使い分ける人が多くないという性質上、割と平和な結果になっているように思います。グラフをどうぞ。 開発者には Mac を支給することが多いため、 Mac 標準のターミナルとiTerm2が2強となりました Windows をメインマシンとする社員はそれぞれ、WSL2とFluent Terminalを利用。人数が少ないのはありますが、 Windows Terminalがいなかったのは意外でした Fluent Terminalは初めて聞いたのですが、iTerm2のカラー スキーマ がそのまま使えるとのこと。 Mac でiTerm2を使ってる人の移行先としては候補になるかも知れませんね Ubuntu 利用の社員はHyperでした また、専用ソフトを使わずに Visual Studio Code のコンソールを利用するという人も。私も時々使いますが、ついうっかり、 vim コマンドを叩いてしまうことがあるんですよね… 総じてエディタに比べると戦争感は少ない印象ですね。 Windows のユーザがもう少し多ければ、 Windows Terminalや PuTTY 、 TeraTerm などもあったのかなと思います。 まとめ 以上、MNTSQの開発にあたって、エンジニアの開発環境をまとめてみました。個人的な感想としては、「意外とバラツキすくなかったかな?」という印象を持ちました。もっと「こういう環境がおすすめです」「なんで Emacs の人がいないんですか?」「 Poderosa 派です」という方がいらっしゃったら是非カジュアル面談へお越しください! この記事を書いた人 中原大介 MNTSQ社でSREをやってます。愛車を車検に出したら30万と言われましたが、乗り潰す覚悟で頑張ります。 *1 : なお私は きのこ派 です
アバター
書き出し MNTSQ( モンテスキュー )株式会社でプロダクトマネージャーをしています、野中と申します。 この記事を読んでいる貴方様も、きっと同じIT業界の住人なのではないでしょうか。 さて、IT系の、しかも社員エンジニアの方々(いちおう私含む)が背負う宿命といえばーーそう、テックブログの執筆、ですね。 昨今、優秀なエンジニアを採用したい企業が芋洗い的にごった返しており、「なんとかウチの良さをアピールしなければ!情報を発信するんだよ!当番制でテックブログ書こうよ!」というムーヴもすっかり業界に定着したように思えます。 もちろん弊社もごった返す芋のひとつですので、この辺り手を抜くわけにはいきません。私も、当番が回ってきたからには立派な記事を書き、弊社社員の優秀さをアピールしたい、と考えておりました。 ーーところで、現実って残酷ですよね。なんやかんややっているうちに、いつの間にか「テックブログ:下書き社内公開予定日」まで来てしまいました。そして今、私はこの「書き出し」を書いています。もちろん、テックブログのテーマはまだ決まっていません。 私が心の支えにしている言葉に「大事なのは、ここからどうするか、だ。まずそれに集中しろ」というものがあります。若き頃の先輩の教えです。そう、この状況から何ができるか、残りの時間でどんな落とし所に着地させるか。仕事に携わる人は常に、それを真剣に考えるべきなのです。 ということで、活路を見出しました。 このブログのテーマ テックブログをカッコよく一回分先延ばしにさせてもらう方法を開発する(検証つき) 課題 そう、私がいま直面している課題、そして解決すべき課題は、これなのです。そして、きっと同じ課題にぶつかる同業者の方も、多くいらっしゃることでしょう。私が今、ベストとはいわずともベターな一手を実践し結果を共有することが、業界への貢献にもつながるのではないでしょうか。そう思うと、実践する勇気が湧いてきます。 方策 ここで、現在の状況を整理します。 今日中に、記事の下書きを終わらせたい。 この後、社内公開&レビューがある。なので、それなりの品質にはしておきたい。 最終的には「読者に刺さる、弊社に興味を持ってもらえる面白い記事」にする。 なのでネタは尖りすぎず、広い層に気軽に読んでもらえるものにする 「夏休みの工作」的なハンズオンものにしたい 前から興味を持っていたガジェット(デジタル文具)がある これでなにか作ってみよう ということで、改めて以下の方針でトライしてみようと思います。 テックブログは 「前編/後編で書くつもり」 とする 今回は前編とし、風呂敷を広げて終わりにする 実際に 苦労する モノを作りレポートするのは、後編とする また、この方法なら以下の副産物的メリットも期待できます。 「すごいヤル気がある!」ことが伝えられる 万が一、前編で反響が芳しくない場合は、そこで打ち切ることができる(決定の先送り) 次回当番(=2ヶ月後)のネタも決まる 転んでもタダでは起きない。そういった精神が、現場では大事だと思います。 実践 方針が決まったので、次は実践です。 ここから先は、「最初から書くハズだったハンズオン系テックブログの前編記事」としてお読みください。 書き出し MNTSQ( モンテスキュー )株式会社でプロダクトマネージャーをしています、野中と申します。 この記事を読んでいる貴方様も、きっと同じIT業界の住人なのではないでしょうか。 この業界の方々は、やはりガジェットが好きな傾向があると思います。かくいう私も、以前から「これでなにか面白いことができそうだな」と目をつけていたモノがあります。今回はそのガジェットを実際に手に取り「日常が便利になるハックシステム」を作ってみようと思います(前編/後編の構成になります)。 本来のテーマ テプラの QRコード を使って身の回りの情報を管理しよう 話の起点 「テプラ」という有名な ステーショナリー があります。ラベルに印字してくれる、言わば機能特化型のミニプリンターです。 このテプラ(製品種別名はラベルライター)、高級モデル or 最近のモデル だと、 QRコード を印刷することができます。 QRコード は、いわずもがな、URL等をカメラ経由でモバイルデ バイス に転送するのに適した画像情報です。もっとも単純に使用する場合、数十文字程度のURLをそのまま QRコード に落とし込み、印刷し、配布します。印刷物を受け取った方は、自身の スマホ のカメラでそれを写し、ブラウザでURLにアクセスします。 この使い方を 「固定URL式」 とこの記事では呼ぶことにします。 やってみたいこと 家電ショップに並んでいるテプラを見て、ふと思いました。 「 QRコード の形で印刷されたURLを、プロキシサーバ宛てにしておいて、実際に利用するURLを後から書き換え可能にできたら、どうだろう?」 例えば、既に QRコード を印刷済みのラベルを先に持っておき、必要なときに(例:紙のノートに Wikipedia の参照を貼りたい)その「まだ紐付けされていない QRコード のラベル(ブランク QRコード )」を貼る。続けてその場で スマホ で QRコード を開くと、プロキシサーバ上の「URL登録フォーム」がブラウザで開き、そこで Wikipedia のURLを貼り付け登録する。それ以降は、その(同じ) QRコード を スマホ で開くと、直で Wikipedia がブラウザで開く。 この使い方を 「浮動URL式」 とこの記事では呼ぶことにします。 上記の例はとてもシンプルで、正直あまり驚きがないかも知れませんが、 QRコード およびプロキシサーバの以下の特徴を組み合わせることで、いろいろな ユースケース が発掘できそうです。 QRコード は、複製が可能である。 同じ QRコード を、複数の場所に貼っておける 不特定多数に配布できる QRコード は、だれでもアクセスが可能である またアクセスが容易である プロキシサーバでは、後から紐付けURLを更新することができる と、ここまで考えて「いや、こういうの、もうどこかにあるんじゃないか?」と思い、軽く探してみました。 市場、類似サービス 「 QRコード 市場」で調べると、以下の情報が出てきました。 4年後の QRコード 決済市場、12兆3,976億円に‐ 矢野経済研究所 が予測 https://news.mynavi.jp/techplus/article/20211021-2165641/ おお、そうか。 QRコード といえば電子決済。でもこれは「電子決済の規模」なので、あまり参考にならないですね。 株式会社 デンソー ウェーブ( https://www.denso-wave.com/ja/ ) QRコード を開発された会社です。 社員数:1,200名超、資本金 495M、という規模。 テプラ(ラベルライター)の市場規模……を確認したかったのですが、ぴったりの情報が出てきませんでした。 ちなみに、最大手商品の「テプラ」が、2018年で1,000万台突破(発売から30年)、とのこと またテプラの キングジム だけでなく、カシオ、ブラザー等が参入している また類似サービスでは、以下のものがヒットしました。 Mamoru Biz https://mamoru-secure.com/pay/?source=pay ラベル印刷した QRコード で物品管理ができる、とのこと 仕組み、システム的には、上記の「浮動URL式」の模様 オフィス向け。個人用ではない。 可変 QR https://qr.quel.jp/flex.php 「後から移動先URLを変更できる QR 」を提供 無償版は広告があるとのこと クルクルManager https://m.qrqrq.com/ 「可変 QRコード 」という呼称で、リダイレクト先を変更できる QRコード を提供 なるほど。 「個人用途の文具として、自由に使える浮動式 QR 」のサービス、製品は、まだ無いようです。 これは、夏休みの工作のお題としては、良いのではないでしょうか。 作ってみよう_1: ユースケース 実際に作るにあたり、ミニマムな ユースケース を書き出してみようと思います。 ちなみに、私は iPhone12 mini, MacBook Air を使っています。今回は、プロキシサーバ役をローカルネットワーク上の MacBook Air にさせることにします。 ミニマム ユースケース : 1:浮動式の QRコード を印刷したラベルをあらかじめ用意する 2:紙のノートにメモを書く 3:メモの横に、ラベルを貼る 4:ラベルを iPhone でスキャン 5: iPhone でブラウザが起動し、(プロキシサーバ上の)入力フォームが開く 6: iPhone 上で「メモと紐付けたい Wikipedia のページのURL」をフォームに入力し、保存 7:もういちど、ラベルを iPhone でスキャン 8: iPhone でブラウザが起動し、紐付けした Wikipedia のページが開く ここまでの一連ができれば、今回のハンズオンは成功!とします。 作ってみよう_2:システム構成 では次に、上記のミニマム ユースケース を実現するためのシステム構成、および構成要素の状態を洗い出します。 システム構成: A: QRコード 自体の作成 Mac のコンソール上で QRコード 画像を作成できる、という都合の良いgemがあったので、それを使う。 rqrcode https://github.com/whomwah/rqrcode B:ラベルライター( QRコード 印刷可能) まだ無し。10,000円以内で購入できる。 C: QRコード スキャナ 自前の iPhone を使用。 D:プロキシサーバ MacBookAir 上で、何かしらのサーバを立ち上げる Docker × Rails で良いか E:ブラウザ 自前の iPhone を使用。 こうみると、物品としては「ラベルライター」さえ調達すれば、あとは有りものでなんとかなりそうです。 作ってみよう_3:ラベルライターの調達 善は急げ、ということで、ラベルライターの調達まで進めましょう。 いろいろと考えた結果、せっかくならイイものを、ということで、以下を注文しました。 brother P-TOUCH CUBE PT-P910BT https://www.brother.co.jp/product/labelwriter/ptp910bt/index.aspx とても残念なことに、こちらが届くのが今度の週末になってしまうとのことです。 ですので、この続き「作ってみよう、使ってみよう」は、また次回の後編でまとめられればと思います。 終わりに いかがだったでしょうか? 特に、最後の「必要な機材の到着を待たなくてはならないので」という外的要因が提示されることで、無理なく自然な先送りが可能になるのです。 なお、ここまでを書くのに、中断を挟みながら4時間ほどかかりました。 実際、「最後までやりたいこと/書きたいこと」を完遂するには、追加でざっと2日ほど欲しいところです。 もちろん、完璧なものを最初から用意できればそれに越したことはないのですが、そうはいかないこともままあります。「漠然としたア イデア はあるんだが、カタチにする時間がなぁ」という状況に身を置かれるテックブログ同士におかれましては、こうい うしの ぎ方もありますよ、というメッセージをお届けできたなら幸いです。(最後に勝っていれば良いのです!) それでは、次回の後半をお楽しみに。 (モノは本当に作ります!) この記事を書いた人 mtq-nonaka MNTSQ株式会社でPdMをしています。
アバター
はじめに MNTSQ( モンテスキュー )株式会社 フロントエンド担当の安積です。 入社して4ヶ月とちょっと。 コードに取り組もうと入社して、まさに日々格闘しております。 私の後ろの席にはこんな バズ記事 書く人や、こんな イカつい記事 書く人が座ってまして、そんな プレッシャー 期待の中からお送りいたします。 tech.mntsq.co.jp tech.mntsq.co.jp 昨日はこんな記事も公開されています。 tech.mntsq.co.jp はじめに 現在のステータス またはMNTSQ考古学 リファクタリングやるぜっっ! 仕様書大事だよね 差分指向テストとは テスト環境の概要 テストデータ ブラウザ操作自動化 スクリーンショット比較 Playwriteの操作 ちょっとコードのサンプル 最後に この記事を書いた人 現在のステータス またはMNTSQ考古学 コードベースから見たMNTSQのフロントエンドは、0->1 のフェーズにおいて「 アーキテクチャ の精査」とか「クリーンなコードを書く」というよりかは様々な要素をとにかく形にして使ってみて、という繰り返しだった事が伺えるものになっていて、 Github の中で考古学的考察が必要な場面がかなりあります。 コードの行間から感じる事、色々あって解ってきた事かなりあるのですが、 MNTSQのコードは最初からきちんと管理されていて履歴を全て追える状態である、という事もあり考古学的見地から背景を読み解くのもなかなか面白みがある、って書いたら不謹慎でしょうか。 山積している課題も今までフロントエンド専任の方が居なかった事もあり、今までの経験で新たなPJに参画した時にはよく感じる事で、あとは程度問題、みたいに捉えています。 で、今進行している新機能の開発と平行して、既存のコードに対しての改善計画のロードマップを策定中です。 その一環として リファクタリング を行おうとしています。 リファクタリング やるぜっっ! 方針を幾つか書くと、 全体スコープでのリプレース、書き直しはしない MNTSQ入社以前に、リプレースの案件もそこそこ経験しているのですがリプレースが成功するには幾つかの条件があり、かなりハードルが高いです。 ネット上で探せば成功例が出てきますが、そういった成功例に価値があるのは失敗例が多いという事の裏返しでもあるのは皆さんご存知だと思います。 (この辺の話もいつか書きたいのですがここでは割愛) コーディングは命令的なスタイルから宣言的なスタイルに 命令的なスタイルは、その結果までが当該コードの関心事となる事から肥大化しがちです。 宣言的に書くことでイベントの連鎖からはある程度は解放されて、リアクティブなフロントエンド フレームワーク の恩恵を最大限受ける事ができるようになります。 膨大な スタイルシート に立ち向かう 上記の歴史からコード上では局所戦の跡がそこかしこに存在し、局所的に解決しようとすることでVueComponentではグロー バルス コープの CSS への上書き、重複したスタイル定義等が多数あり、コードの肥大の原因となっています。 ここが自分にとっても主戦場の一つになると想定しています。 スタイルシート って実はとても難しく、プロフェッショナルの戦場なのです。 進化が速いのに デバッグ が面倒、おまけに全てグロー バルス コープで定義されるものなので。 一言でいうと部分的に置き換えを進め、「 テセウス の船」みたいな事をやろうと思っています。 テセウス の船( テセウス のふね、英: Ship of Theseus)は パラドックス の一つであり、 テセウス の パラドックス とも呼ばれる。ある物体において、それを構成するパーツが全て置き換えられたとき、過去のそれと現在のそれは「同じそれ」だと言えるのか否か、という問題(同一性の問題)をさす。 テセウスの船 - Wikipedia その位、 リファクタリング の結果としての見た目は変えたくないと考えています。 本線は開発がどんどん進む中、「動く標的を撃つ」ような側面もあり、課題感あるのですがその話は別途。 ここで問題になるのが、 デグレ ーションをどうやって防ぐかという点です。 仕様書大事だよね 例えばTDD( テスト駆動開発 、Test Driven Development)においては、 まずテストを書く テストが通るような、固定値を返すコードを書く [Red] ロジック実装、テスト実行すると全ては通らない状態 [Green] テストが通るところまで実装 [Refactor] テストが通る状態をキープしながら、ブラッシュアップする という繰り返しで、コードを書く作業と並行してテストコードが蓄積されるように開発を進める手法が知られています。 テストを書くには当然ながら、テストが書けるようにケースが出せる状態まで仕様が落とせている事が必要となります。 そしてテストの関心事はこの「仕様が満たせているか」という所になります。 MNTSQの社内には"SSoT"という概念が浸透していて、仕様についてもSSoT化されてメンテナンスされています。 信頼できる唯一の情報源 (Single Source of Truth; SSOT) とは、情報システムの設計と理論においては、すべてのデータが1か所でのみ作成、あるいは編集されるように、情報モデルと関連するデータ スキーマ とを構造化する方法である。 信頼できる唯一の情報源 - Wikipedia ところが、MNTSQは「破壊的 PDCA 」を回すことを旨としており、やりたいことがコロコロ変わるということを前提とする必要があってですね。 Bizサイドからのリク エス トを都度仕様に落とすとしても比較的荒い解像度のものとなり、個別のケースについての仕様を全て落とし切るというよりかは、どんどん作る事を可能にしたい訳です。 一方、MNTSQは エンタープライズ SaaS で、扱っているデータの重要度の高さは言うまでもなく、要求される信頼性も並大抵のものではない訳です。 どうする、俺。 そこで、一旦現在の動作を正として、今後の改修のステップ毎に発生する「差分」に注目しようと考えました。 差分指向テストとは エンジニア観点では書いたコードが仕様を満たしているか担保したいのは当然なのですが(バックエンドチームはちゃんとやっているし、私自身も ユニットテスト の粒度でTDDするとフロー状態になってキモチイイのも知ってますが)、刻々と変わるフロントエンドについては、テスト項目をすべてコード化してメンテナンスし続けるよりは 改修と新機能追加の結果、変わった所はどこなのか 意図しない部分が変わっていないか(どちらかといえばこちらが重要) という辺りにフォーカスしようと考えました。 そして本当にクリティカルで動作を担保したい所だけテスト項目としてGreen/Redをチェック なおかつ、エンジニアがPullRequestを上げる前に手元で実行できるようになっていれば尚可、という方針としました。 つまり、差分指向テスト とは 開発作業の前後の出力の差分を比較することで、ケースまで落ちない粗い粒度の仕様からの実装であっても開発作業の結果を判りやすく、かつ不要な影響が出ていない事を確認するテスト というイメージです。 えっ、そんな、と思った方は詳しくお話聞かせて下さい! カジュアルに面談でお話しましょう! 今回は画面 スクリーンショット を例に取りますが、別にDOMでも良いし JSON でもdiffは取れます。 私は行動解析の経験もあるので、そういった辺りも差分は発生するので追って対象にしたいと考えています。 とにかく開発作業の結果、「変わった点」と「変わっていない点」にフォーカスします。 ちなみに呼び名は私が勝手につけたものです。(ここ重要) テスト環境の概要 テストデータ コードの出力の差分にフォーカスするので、それ以外の特に入力データは毎回同じものを利用する必要があります。 また、内容としても実際に使われるデータに近いものでないと意味は半減します。 (「ああああ」なんて文字列入れてテストしても気持ちが持てない、と思いませんか?) 幸い、MNTSQでは個々の開発者のローカル環境用にステージング環境のデータを取り込む機構が整備されており、これを利用します。 ブラウザ操作自動化 自動でブラウザを操作してログイン、シナリオに沿って自動で操作して目的の画面で スクリーンショット を次々に撮る形とします。 こういった用途には Selenium やPuppeteerが有名ですが、今回はPlaywrightを使います。 github.com MNTSQは対象ブラウザを Chrome と Chromium 版Edgeに限定しており、本記事でも Chrome を操作するのですがこのツールは Microsoft 製です。 世の中変わったよなぁ・・・と思います。 ブラウザの操作と状態取得は全てPromiseベースで、なおかつ画面を開いているブラウザに外から JavaScript を挿入し実行させて何かするという事も比較的簡単に出来ます。 ChromeDevToolにアクセスすることも出来ます。 スクリーンショット の取得もPlaywriteのコマンドで行います。 ページ全体に限らず、DOMの中の或るHtmlElementだけ指定して部分的に撮るという事も出来ます。 新しい機能の開発についてはStorybookを利用しているので、そちらの方で差分を確認する方法もあります。 ですが諸般の事情にて現時点では自前で書いている関係で、この部分的に スクリーンショット を撮れる機能、なかなか便利です。 大筋として管理側と利用者側、独立した2つのコンテキストでそれぞれページを開いて管理側での操作が利用者画面にどのように影響するかという観点でもテストを行います。 シナリオのプログラミング環境としてはTypeScript, JavaScript , Python , .NET , Java が利用可能で、今回はTypeScriptで書くことにしました。 (ここも様々な手段があるようですがフロントエンド担当ですし、細かな操作があることもあってこうしています。) スクリーンショット 比較 今回は img-diff-js を利用します。 シンプルな API で高速な動作が身の上のようです。 www.npmjs.com この他、レポート保存には API 経由でGoogleSpreadSheetに保存しようかと。 ここはまだ後回しです。 取りたいのはエラーの有無、画像のリスト (w/サイズとHTTPレスポンス ステータスコード )、リンクのリスト(w/有効or無効)、 スタイルシート のリストと カバレッジ 等です。 結構データ量があるので後処理も 楽しそう 大変そうなので スプレッドシート が向いているかと。 実はまだ開発中で実証コードが動いた段階なのですが、「なんとなく」書いた処理シーケンス貼っておきます。 処理シーケンスドラフト 何だよこれ、と思った方は是非お話聞かせて下さい! カジュアル面談でお待ちしております! Playwriteの操作 Playwriteにおけるブラウザの API は大きく分けて以下の3つとなります。 Browser 文字とおりブラウザ。 Chromium 、 WebKit とか Firefox とか。 BrowserContext ここちょっと解りづらいかも知れませんが、Contextを別ける事で複数のセッション(ログインセッションとか)を同時に扱うことが出来るようになります。 BrowserContexts provide a way to operate multiple independent browser sessions. BrowserContext | Playwright Page これがブラウザの一つのウインドウです。複数ページを同時に開く場合BrowserContextからPageの インスタンス を複数生成する形となります。 ちょっとコードのサンプル Browser生成 import { Browser , BrowserContext , Page , chromium } from 'playwright' ; const browser = await chromium.launch ( { channel: 'chrome' , headless: false , // ここをfalseにすることで実行中ブラウザ画面が表示されます。デバッグ用途 args: [ `--window-position= ${ windowPositionX.toString() } , ${ windowPositionY.toString() } ` , ] , //PC画面上でブラウザが開く場所を指定できます。複数開いてデバッグするのに便利です。 } ); BrowserContext生成 const browserContext = await browser.newContext ( { // ここで指定しておくことで、後のページ遷移はpathで指定できるようになります baseURL: 'http://localhost:8080' //.ここでviewportのサイズも指定できます。 viewport: { width: 1280 , height: 800 , } , } ); Page生成 const page = await browserContext.newPage (); Pageでページをpath指定して開く await page. goto( path ); ページ内のあるテーブルの2列目のセルから文字列(foobar)を検索して何行目にあるかを返す const searchNeedle: SearchNeedle = { needleText: 'foobar' , } ; /** * スクリプトをPage内で評価、実行して結果を返します。 */ const rollIdx: number = await page.evaluate (( param: SearchNeedle ) => { // この中がブラウザ側で実行されます。 const { needleText } = param ; // 検索結果 let resultIdx = 0 ; Array . prototype .forEach.call ( document .querySelectorAll ( '#target-table tr' ), ( elm , idx ) => { const cellText = elm.querySelector ( 'td:nth-of-type(2)' ) .textContent if ( cellText === needleText ) { resultIdx = idx ; } } , ) // ここでブラウザ上での処理結果を返り値とするとPage.evaluate関数の返却値として取得できます。 return resultIdx ; } , searchNeedle ); ページ上のリンクをクリック、画面遷移を待つ await page.click ( 'a.target' ); // 複数hitした場合、最初の要素がclickされます await page.waitForLoadState ( 'load' ); // 次のページのloadイベントまで待ちます スクリーンショット を取る(全体) import path from 'node:path' ; await page.screenshot ( { path: path.join ( SCREENSHOTS_IMG_DIR , fileName ), fullPage: true , } ); スクリーンショット を取る(一部エレメント) const part = await page.locator ( 'div.target' ); if (await part.count ()) { await part.screenshot ( { path: path.join ( SCREENSHOTS_IMG_DIR , fileName ), } ); } ; 画像のdiffを取る import { imgDiff } from 'img-diff-js' ; const result = await imgDiff ( { actualFilename: sourceFilePath , expectedFilename: destFilePath ), diffFilename: diffFilePath , // <- このpathに差分を強調した画像ファイルが出力されます。 } ); 最後に 駆け足でしたが、いかがだったでしょうか。 今は一人で取り組んでいるのですが、一緒に考えて進める仲間を探しています。 詳しくはページヘッダの採用ページへのリンクから! この記事を書いた人 安積洋 MNTSQ( モンテスキュー )社のソフトウェアエンジニア。実はギタリスト。 LAMP エンジニアとしてバックエンド担当が長かったがその後 O2O アプリのフロントエンド、アプリPM、行動解析、と渡り歩いてMNTSQではフロントエンド担当。 入社エントリはこちら! note.com
アバター
はじめに 次の文章をまずは眺めてみてください。 “Chief Executive Tim Cook has jetted into China for talks with government officials as he seeks to clear up a pile of problems in the firm's biggest growth market. Cook is on his first trip to the country since taking over from late co- founder Steve Jobs " Apple CEO in China mission to clear up problems | ロイター 文中で、ティム・クックを表す表現にはどのようなものがあったでしょうか? 文中の別々の表現が同じ実体を指しており、それらの関係を洗い出す要素技術は共参照解析(coreference resolution)と呼ばれ、検索や固有表現抽出との関連で重要なものになっています。 上記の文の中でティム・クックについて言及(mention)した部分にハイライトをつけたい場合は、以下のようなデータ構造がとれると良さそうです。 {Chief Executive Tim Cook: [Chief Executive Tim Cook, he, Cook, his]} このようなニーズは人名・物体名の中でもさらに限られたカテゴリの実体をマーキングするという意味では固有表現抽出の意味を狭めた応用になっています。 本記事では共参照解析を行うためのライブラリとしてneuralcorefを触ってみて、その入出力の中身や意味について見ていきます。 neuralcorefについて neuralcoref は分散表現に基づく共参照解析ライブラリです。 *1 ライブラリの周囲には参照関係をビジュアライズするクライアント NeuralCoref-Viz もあり、このようなグラフを出力できます。 モデルは標準で英語のみの対応になっていますが、訓練することで他言語のモデルにも 展開可能 です インストール方法 neuralcorefは spaCy プラットフォーム上で動くライブラリになっており、動作にはneuralcorefの他にspaCyのインストールも必要です。 筆者の環境 ( python ==3.8.10) 上では以下のコマンドでインストールし、ライブラリの動作を確認しています。 pip install spacy==2.1.0 pip install neuralcoref==4.0 --no-binary neuralcoref python3 -m spacy download en 動作確認 まずspacyのパイプラインに共参照解析を行うneuralcorefの プラグイン を追加します。 In import neuralcoref import spacy nlp = spacy.load(‘en’) neuralcoref.add_to_pipe(nlp) nlp.pipeline Out [('tagger', <spacy.pipeline.pipes.Tagger at 0x7f315b0d5e20>), ('parser', <spacy.pipeline.pipes.DependencyParser at 0x7f315b0bffa0>), ('ner', <spacy.pipeline.pipes.EntityRecognizer at 0x7f315b0e3520>), ('neuralcoref', <neuralcoref.neuralcoref.NeuralCoref at 0x7f315aec8970>)] これだけで共参照解析を行う準備が整いました。 先程の文章をパイプラインに入力し、どのような出力を得るか確認してみます。 In doc = nlp( "Chief Executive Tim Cook has jetted into China for talks with government officials as he seeks to clear up a pile of problems in the firm's biggest growth market. Cook is on his first trip to the country since taking over from late co-founder Steve Jobs." ) doc._.coref_clusters 冒頭に紹介したような出力を得ます。 Out [Chief Executive Tim Cook: [Chief Executive Tim Cook, he, Cook, his], China: [China, the country]] この ._.coref_clusters でアクセスできるプロパティは共参照解析の結果を表したものです。 共参照解析では実体ごとに対応した文中の表現の関係を クラスタ (Cluster)と呼び、このプロパティでは共参照解析の結果としての クラスタ のリストにアクセスできます。 クラスタ 経由で以下のattributeにもアクセスすることが可能で、共参照の実体と 参照元 のリストが取得可能です。 main: 共参照の実体と思われるテキストのSpan mentions: 共参照の 参照元 となっているテキストのSpan In doc._.coref_clusters[ 0 ].main Out Chief Executive Tim Cook In doc._.coref_clusters[ 0 ].mentions Out [Chief Executive Tim Cook, he, Cook, his] なお、 ._. はspacyのパイプライン特有の インターフェイス であるExtension attributeを利用するときに用いる表現で、nuralcorefでは ._. を通じて各種結果にアクセスできます。 解析時のオプションについて neuralcoref.add_to_pipe(nlp, greedyness=0.75) のようにパラメータを指定することで クラスタ の作られやすさを調整したり、頻度が少ない単語への個別対応を行えます。以下主なパラメーターです。 greedyness: 0~1の値を指定して共参照の クラスタ の作られやすさを調整できます(デフォルト0.5) max_dist: 共参照を生成する時にどれくらい手前の先行詞まで考慮するかを調整できます(デフォルト50) max_dist_match: 先行する実体へのメンションが名詞等だった時に、max_distを超えてどのくらい手前までさかのぼって参照を生成するかを調整できます(デフォルト500) blacklist: 共参照解析をI,meなどの代名詞を含めて行うかどうかを決められます(デフォルトTrue) conv_dict: 人名などの頻度の少ない単語の分散表現を頻度の多い単語の分散表現に置き換えて共参照解析します 共参照解析と照応解析との関係 共参照以外にも文中の言語表現が別の表現を参照する事象として「照応」という概念がありますが、これらは微妙に異なる概念です。共参照はテキスト中の表現から実体への参照を表すのに対し、照応は同格、代名詞などで表される照応詞が文中の表現を参照する方向にフォーカスします。この2つの概念の整理については コロナ社 の照応解析について書かれた書籍が詳しいです。 文脈解析- 述語項構造・照応・談話構造の解析 - (自然言語処理シリーズ) | 笹野 遼平, 飯田 龍, 奥村 学 |本 | 通販 | Amazon この記事を書いた人 yad ビリヤニ 食べたい *1 : Kevin Clark and Christopher D. Manning. 2016. Deep Reinforcement Learning for Mention-Ranking Coreference Models - ACL Anthology
アバター
認証認可とワンセットで語られることが多い印象だが、今回話すのは「認可(Authorization)」の話だ。「認証(Authentication)」の話は含まない。 (システムで言う)認可とは、大雑把に言うと「誰が」「何を」「どうすることが」「できる/できない」の要素に従って判定することだ。 どちらも略すと「Auth」になってしまってクラス名が衝突したりするので困ることがある。区別するために認証はAuthN、認可はAuthZと略されることがある。「WebAuthn」などは一例と言えるだろう。 弊社内ではまず話題になってこなかったため、実装の話が流れたとき、非エンジニアからは「認可?権限と何が違うの?おいしいの?」といった声が聞かれたり聞かれなかったりした。 認可制御の種類 MNTSQで採用した認可制御 認可のrailsのgemの紹介 pundit cancancan MNTSQの認可制御の実装 RBACは抽象的な仕様 RBACをどのように実装したか 実装してみてどうだったか 今後起こり得る事象 参考文献一覧 We are hiring! 認可制御の種類 認可制御にはいくつか種類があるので大雑把に紹介してみよう。 ACL 単純な「誰が」「何に」「どの操作を」することを許可するかのリスト DAC よくあるOwner、Group、Everyoneに対してRead、Write、eXecuteを設定する形式 所有者が権限を決定する MAC DAC の所有者ではなく管理者が権限を決定するバージョン RBAC 特定の「ロール(役割)」を想定したアクセス制御であり、アクセス権の束を「ロール」として設定し、そこにユーザーを割り当てる ABAC 属性ベースのアクセス制御。例えば「30代」「男性」「独身」などの属性でアクセス制御をする IBAC 認証後の認可は ACL その他、調べれば「XXAC」系の認可制御はたくさん出てくる。 MNTSQで採用した認可制御 ではMNTSQ( モンテスキュー )で採用した認可処理は何かというと、「RBAC」である。弊社のクライアントは主に大企業であり、基本的に部署や階級があるため、それに基づいたロールが設定する可能性が高かったことが選定理由となっている。おまけの話だが、SlackもRBACを参考に実装しているようだ。 slack.engineering ただ、RBACをベースにどのように実装するのか、そもそもRBACとは何であるか、どのような制御をするものかなどの資料が圧倒的に乏しいのが現状である。実際に書いてみて分かることだが、機密性の高い仕組みを公開することになってしまうので、公開されづらい傾向にあると思われる。 幸いなことに、RBACについては ANSIのPDF が公開されており、また 日本語の論文 も存在している。これらを読むことでRBACの仕様について理解を進めることが可能だ。 認可の rails のgemの紹介 認可について、主に2つのgemが有名である。いずれも簡易な ACL と言えそうだ。 pundit github.com モデル側から認可制御をする方式をとっているgemである。 対象のモデルがPostであればPostPolicyという名前のクラスを作成し、そこでどのような条件のユーザーにreadやwriteを許可するかを指定することが可能となっている。つまり、テーブルの数に比例して同名のPolicyクラスが増えていくことになる。 実装は以下のようにし、 class PostPolicy attr_reader :user , :post def initialize (user, post) @user = user @post = post end def update? user.admin? || !post.published? end end 実際の認可処理では以下のように「何を」「どうするか」を指定して実行する。 def publish @post = Post .find(params[ :id ]) authorize @post , :update? @post .publish! redirect_to @post end cancancan github.com 一見ふざけた名前だが、認可のgemとしては有名どころである。こちらは「誰が」側から認可を実装するかたちだ。 認可用のクラスを作成し、ユーザーをラップするかたちでインスタンタイズし、必要箇所で権限の有無の判定をすることができる。 実装は以下のようなかたちでクラスを実装し、 class Ability include CanCan :: Ability def initialize (user) can :read , Post , public : true return unless user.present? # additional permissions for logged in users (they can read their own posts) can :read , Post , user : user return unless user.admin? # additional permissions for administrators can :read , Post end end 以下のようにController、Viewなど各所で認可処理をすることができる。punditと同じく「何を」「どうするか」を指定する。 <% if can? :read, @post %> <%= link_to "View", @post %> <% end %> def show @post = Post .find(params[ :id ]) authorize! :read , @post end MNTSQの認可制御の実装 MNTSQでは上記のgemでは複雑性を回避しづらいため、これらのgemは利用していない。自前で実装をすることとなった。実際のコードについては公開を控えさせていただくので、その点ご了承いただきたい。 MNTSQで認可処理を実装する際のポイント MNTSQの認可制御の複雑さは、1つの書類に対して契約データベース、契約管理、案件管理という3つの方向から認可制御をする必要があることだ。 衝突が発生した場合には、それぞれのORをとることで解決することとした。なぜなら、ある書類を案件管理から閲覧可能であれば、契約データベースからでも閲覧可能であって良いという整理が可能だったからだ。逆に、例えば契約管理で閲覧不可と設定されていた場合、契約データベース、契約管理、案件管理どの角度から閲覧しようとしても不可にするという制御とした。 また、関連するテーブルの数が30以上に上り、個別に管理するとメンテナンス性が損なわれるため、それらを中央集権的に管理する必要性に迫られていた。 これらについては、テーブル名と閲覧権限のペアを定義し、それをベースにフィルタリングする仕組みを導入した。 RBACは抽象的な仕様 RBACを実際に適用するに際して苦労した点としては、仕様としてはどのようなシステムにも適用可能なように抽象度が高く書かれているため、RBACを実際のシステムに適用するための間を自前で埋める必要があることだった。 それらを AWS など他所のシステムの認可制御を参考にしながら、「MNTSQ用語」として落とし込んでいる。開発者や営業など非開発者の間での認識がずれにくいように、認可制御について記載したマークダウンを準備したり、認可の実装にあたって打ち合わせを何度か行い、認識合わせに注力した。ちなみに主に参考にしたのは、 AWS のIAMの仕組みだ。 RBACをどのように実装したか 弊社ではgrapeというgemを利用して API を実装しているが、これには Rails で言うApplicationController相当のroot.rbというファイルが存在しているので、そこで API 全体に対する認可制御を包括的に行うことにした。 github.com 認可制御についてはフロントエンドでも「書類の削除ができない場合は削除ボタンを出さない」などの認可制御が必要なため、対象のリソースに対する認可情報を問い合わせる API を実装した。 検索にはElasticsearchを利用しており、ここの認可処理については Rails の効力が及んでいないため、自前の認可制御の仕組みのデータを作り、独自の プラグイン を開発して認可を実装している。 構造上、速度に大きく影響する可能性が高かったため、キャッシュをする仕組みを導入することとした。キャッシュの設計をどうするかなどは当然RBACの仕様には書かれていないため、「誰」「何」「どのアクション」「ロール」のどのレベルでキャッシュを効かせるのが良いのかも悩みポイントとなった。ここはシステムによって最適なポイントが変わってくると推測されるが、MNTSQではユーザー側(「誰」)に寄ったキャッシュの持ち方をすることとした。 実装してみてどうだったか 弊社では rspec でテストを書いているため、一定のバグを防ぐところには至っているが、いかんせん仕組みが複雑なため、カバーし切れていない部分の修正を続けているところだ。 また、システムに合わない部分について、ドラスティックに仕組みの変更を進めている途上である。 今後起こり得る事象 今後に起こり得る事象について、まだ運用が始まったばかりであり、残念ながら申し述べることができない段階にあるのだが、面白い資料をご紹介しよう。 エンタープライズ ロール管理解説書 (第3版) この資料では企業において人事異動、組織改変、プロジェクト型や組織型など想定される ロールモデル 、今後どのような問題が起こり得るか、そのために事前にどのようなことを決めておくべきかといったことが書かれた資料となっている。ぜひとも参考にしていただきたい。 参考文献一覧 Access Control Acronyms: ACL, RBAC, ABAC, PBAC, RAdAC, and a Dash of CBAC - DZone Security Role Management at Slack American National Standard エンタープライズ ロール管理解説書 (第3版) アクセス制御の種類---DAC,MAC,RBAC | 日経クロステック(xTECH) アクセス制御 - Wikipedia What is Identity-Based IBAC Access Control? – Digital Masta Authorization in Rails controllers: Pundit versus CanCan We are hiring! 上記の通り、MNTSQはまだまだ進化の途上である。複雑な課題にチャレンジしてみたい人も、そうでない人もぜひ右上の採用ページから応募してみて欲しい。
アバター
MNTSQ( モンテスキュー )株式会社 ソフトウェアエンジニアの沼井です。 普段は Rails でのバックエンド開発をしつつ、Elasticsearchによる 全文検索 処理やインデクシングまわりの開発にも取り組んでいます。 私は現在、 Thinkpad X1 Carbon (2021年版)に Ubuntu 20.04をインストールして開発を行なっています。MNTSQ社以前の経験も含めると、業務での Ubuntu 使用経験は3年以上あります。 テック系スタートアップの、とりわけ Webサービス ・ スマホ アプリの開発シーンでは、 macOS ユーザーが99%(※個人の感想です)ということもあり、 macOS 以外の環境を(使いたくても)使うことが難しいと思っている人も多いと思います。 本記事では、業務での Ubuntu 利用の実情・課題・メリットなどを共有したいと思います。 TL; DR テック系スタートアップにおけるソフトウェア開発という部分に限れば、 Ubuntu 環境も十分実用的なラインだと思う 会社における普段の業務、という広い観点では、思わぬ落とし穴はやはりある macOS 以外の環境での開発可能性があることで、多様性の観点で中長期的なメリットはあるかもしれない そもそもなぜ Ubuntu なのか 弊社ではリーガルテック分野の SaaS を開発しており、Webのバックエンド・フロントエンドを担うエンジニアは基本的に macOS を使用しています。 そんな中で Ubuntu を使っているというのは、よっぽど思い入れやこだわりがあるから...というわけではなく、私自身が Ubuntu に慣れていて扱いやすいから、という理由が大きいです。 私はいくつかのソフトウェア開発会社を経験してきたのですが、.NETアプリを開発するチームでは Linux サーバとの連携機能のための シェルスクリプト 開発を担当し (他にやる人がいなかったので)、 Rails バックエンドを開発するチームではサブで動いている VB.NET システムを開発する (他にやる人がいなかったので) 等、エッジケースを担当することが多くありました。 結果として「 macOS でWeb系開発する」という典型ケースの経験が少なく、 Windows or Linux のいずれかを使用する経験を多く積んできたため、可能なら自分が慣れていて生産性の高いOSを選択したいという気持ちがありました。 とはいえ、チーム全体の環境統一による生産性向上のほうが優先されるべきという判断がされれば、それに従って macOS を使っていたと思います。結果的には、弊社も含めていままでに所属した会社では「 Ubuntu を使ってもいいよ」と受け入れてもらえたので、いまに至っています(ありがたい)。 Ubuntu で会社の業務をおこなえるか? : 実情と課題 最近は「 Ubuntu で開発環境構築してみた」系のテックブログ記事も増えていますが、「実務で」「継続的に」使えているのか? あるいはやっぱり無理だったので macOS (あるいは Windows ) に結局戻すことになってしまうのでは? 等気になる人はいると思います。 以下では自分の経験に沿って、 Ubuntu の普段の業務利用のあれこれを解説していきます。主にMNTSQ社での経験を中心としますが、以前の職場での経験も踏まえて書きます。 PCの選定 業務用PCは、一貫して Thinkpad X1 Carbonを使用しています。 ThinkPad X1 Carbon (2021年版) + Ubuntu 20.04 スペック: CPU: Intel Core i7 -1185G7 メモリ: 32GB ディスク: 1TB これまで複数世代のX1 Carbonを使ってきましたが、 Ubuntu のインストールでうまく行かなかったことはなく、安定して使えています。 Lenovo が公式で互換性を確認している Linuxディストリビューション 一覧を出してくれているので、それを確認するのがよいでしょう。 support.lenovo.com リリース直後の機種は、上記リストにすぐには掲載されないので、リリース直後の機種で Ubuntu をインストールするのはリスクが伴うため注意です。 (過去の実績から、X1 Carbon含むメジャーな機種で Ubuntu に対応しないことはほぼない、と思っていますが) 開発用にそこそこのスペックのCPU、メモリサイズを選びやすいことや、軽量で持ち運びやすいことも、X1 Carbon選定の理由です。 開発まわり以外 社内で使うシステム・サービスの多くはWeb化されている SaaS や スマホ アプリの開発をしているテック系スタートアップの場合、業務で使用するシステム・サービスの多くはWebに対応したもの(概ね SaaS 利用)が多いと思います。 そもそも社内のITエンジニアはほぼ macOS を使うこともあり、 Windows を使用するBiz側との混在状態を考えると、 Windows デスクトップ環境が必須となるようなシステム・サービスの選定自体がなされにくいと思われます。 弊社でも同様で、ほとんどの業務システムはWebに対応しています(下記例)。 弊社の利用サービスでWebに対応しているもの: 人事総務システム 勤怠管理システム 採用管理システム ビジネスコラボレーション ( Google Workspace) ビデオチャット ( Google Meet、Zoom、 Microsoft Teams 等) など (Web版もあるが) デスクトップアプリケーションを使用しているもの: Slack Webでまかなえない部分に多少のリスクあり とはいえ、Webでまかなえない部分というのも少なからずあり、それらについては「入社/チーム配属時は気づかなかったけど、あとから直面して問題があることに気づく」リスクが存在するように思います。 典型的には以下です: デ バイス 関連 (ネットワークプリンタ、 Bluetooth 機器、 指紋認証 など) VPN / リモートデスクトップ サービス デ バイス 接続については、典型的なデ バイス への接続は、そこまで大きな問題にはならない印象です。 ネットワーク上のプリンタへの接続は、さすがに現代の Linux では簡単に ( Ubuntu 20.04であれば「設定」アプリから) 行えます。 印刷オプションの詳細設定や印刷の質にこだわらなければ、利用も問題ありません。 Bluetooth 機器についても、マウスやスピーカーへの接続などを行なうことはもちろん可能です。 Bluetooth マネージャーとしてはBluemanを使用しています。 Ubuntu 20.04ではだいぶ安定してきた印象ですが、それでもたまに接続がうまく行かず(検出はされるが、ペアリングが失敗する等)、試行錯誤をすることもあります。 私が使用している Bluetooth 機器: マウス: エレコム M-XGM10BBBK スピーカーフォン(会議室設置): Jabraシリーズ また、 指紋認証 についても、 X1 Carbon 2021年版とUbuntu20.04 の組み合わせでは、「設定」アプリの「ユーザー > 認証とログイン」から簡単に行えるのは驚きでした。じつはこの 指紋認証 をセットアップしたのが最近なので、最初からできたのか、最近のドライバやOSのアップデートでできるようになったのかは未確認です。 設定 > ユーザー > 指紋認証 ログイン VPN / リモートデスクトップ サービスについては、入社直後には不要だったけど、いざ必要になったとき(たとえばリモートワークのとき等)に設定する、ということが少なからずあると思います。その段になって「 Linux は対応していません」ということになると辛いです。 幸いなことに、いままでの会社では、LinuxOSでも使用できるサービス(例: VPN なら、 OpenVPN 互換) を使っていたため、ここはなんとかクリアできました。ただし、 Linux 向けの手順書が会社で用意されていることはまず無いので、自分で公式マニュアルを読んで設定することは必須です。 そういう意味では、このあたりはチーム・会社に入る前に事前に確認するのがよいと思います。(リスクになりえる箇所を事前にすべて洗い出して確認するのは難しいですが...) 社外との相互運用性は課題 社外との相互運用性において一番大きいのはやはり、顧客と Microsoft Office (Word、 Excel 、 PowerPoint )のファイルをやりとりする場合でしょう。 Ubuntu での代替案としては、Office365 (Web版) を使用するか、 LibreOffice あるいは Google Workspace (Document、 Spreadsheet、 Slide)での互換表示を使用する、あたりでしょうか。 私は社内での開発がメインの業務であり、顧客と直接Officeファイルをやり取りすることがほぼないため、今のところ大きな問題にはなっていません。 ブラウザはOSよりもブラウザ間の差異のほうが問題になる ブラウザについては、 Google Chrome を使っていれば、OSの差異でトラブルになることはほぼない印象です。 私がメインで利用するブラウザは Firefox なのですが、 Firefox を使っているせいで問題になることのほうが多いです。近年は Firefox のサポートを明示的に、あるいは暗黙的に行わない Webサービス も増えているため、 Chrome を使うほうが安心でしょう。といいつつ、 Firefox の「ツリー型タブ」拡張に10数年慣れ親しんでいることもあり、私は Firefox を使っています(作者のPiroさん、いつもありがとうございます)。 余談ですが、 Ubuntu 上の Firefox については、直近では snap版で不具合がいろいろ発生している という逆風もあるようです。 (そういえばSlackクライアントも、snap版だと過去に日本語入力に問題があったので、 deb パッケージからインストールしています。 参考記事: Ubuntu18.04LTSでSlack日本語入力が出来ないときの対処方法 - Qiita ) なお、 Microsoft Edge にも Linux 版があり、2021年末に正式リリースされています(併用しています)。Edge for Linux を使っている人はかなりの数奇者かもしれません。 開発まわり 標準的な開発ツールはどのOSでも概ねカバーされている 普段は以下のようなツールで開発を行なっています: IDE : RubyMine Editor: Visual Studio Code Terminal: Hyper その他: Git、docker、docker-compose、 aws - cli など 標準的な開発ツールについては、 macOS 、 Windows 、 Ubuntu それぞれに対応しているか、あるいは各OS専用だけれど十分に使い物になるものがあるため、特に問題はないと思います。 エッジケースでは各OS専用ツールが有用な場合も Visual Studio Code は標準機能も 拡張機能 も充実しているため、たとえばテキストファイルのdiffの表示やバイナリ表示、 CSV のテーブル表示、Git リポジトリ の GUI 管理など、ひところは専用のツールがあったようなものが概ね Visual Studio Code でまかなえるようになってきています。そのため、 Ubuntu も含めてOSを変えても同じように利用できるのは便利なところです。 私が使っている Visual Studio Code 拡張の例: バイナリ表示: hexdump for Visual Studio Code Git: GitLens、Git History CSV のテーブル表示・編集: Edit csv とはいえ、エッジケースでは「あるOSでしか動かないけど便利なもの」もあります。たとえば大容量テキストファイル(なんらかの CSV やログファイル等) を扱いたい場合は、 Windows 向けである EmEditor などが有用だと思います。 一般に、大容量テキストを扱う場合に、 Visual Studio Code や IntelliJ 系 IDE の 拡張機能 などだとつらくなるケースが多いように思います。その場面では、あるOS上での専用ツールが勝る場合がありそうです。 一番大事なのは、開発している ソースコード を扱えるか 一番大事なのは、チームで開発している ソースコード やその周辺 スクリプト ・ツールが Ubuntu で動作するのかどうかであり、Web系の開発においては、経験的には以下がポイントとなります: (1) docker、 docker-composeで開発環境を構築する手順になっているか dockerを使用してれば、コンテナ内での動作については基本的には問題になることは少ないです。 数少ない問題としては、 Docker for Mac に依存する箇所でひっかかる部分(下記例)があり、これについては個別に対応する必要があります。 コンテナ内で作成されたファイルがroot権限になる場合、ボリュームマウント時のホスト側でもファイルがroot権限になるため、編集・削除などに支障がでる問題 対処法はいくつかあり。参考記事: dockerでvolumeをマウントしたときのファイルのowner問題 - Qiita host.docker.internal を開発環境で使っている 上記のような課題はあるのですが、 Docker for Mac に比べると、ボリュームマウント時のI/O速度が問題になることが少ないため、性能面で苦痛に感じることは少ないです。 数年前に経験した Rails アプリ開発 プロジェクトでは、 macOS 環境より私の Ubuntu 環境のほうが、 rspec の実行時間が4倍速いということもありました。(ただし、 Docker for mac + virtiofs という最近登場した構成に対しては、この差はいくらか縮まっているでしょう) (2) コンテナの外(ホスト側)で動作する スクリプト がどれくらい多いか、どれくらい macOS 依存になっているか macOS で開発しているチームが書いた bash シェルスクリプト は、当然ながら各コマンドのオプションの指定などが macOS 依存になる( BSD 由来のコマンドオプション) ため、 Ubuntu で動かないことは「まれによく」あります。 これについては シェルスクリプト を眺めて気付けることはほぼないため実行 => エラー発生 => 修正 を繰り返す必要があります。 修正は、 uname でOS判定して分岐するか、どちらでも動くようにオプション指定の仕方や利用コマンドを変える、の2つが典型的な方法です。 このような macOS およびDocker for Mac 依存の箇所の調査・対応でつまづいたり時間を浪費するのは、チームにとって新しい価値を生み出せていない時間になります。そのため「 macOS 利用者しかいないエンジニアチームに最初に入る時」は、ここの対応は緊張感をもってスピード対応をすることを心がけています。 (最初に完璧に直すより、開発にクリティカルに影響する部分から優先順位をつけて対応するのがよいでしょう。いずれにせよ、追加/変更に継続的に追従する必要はあるので) まとめ、あるいは語りきれなかったこと 現時点での私の状況をいうと、 Ubuntu で普段開発していてとくに問題になる所はなく、慣れているOSでスムーズに開発できており、その(個人的な)メリットを享受できています。その点では実用的なラインに達していると言えます。 とはいえ、改めてまとめてみると、インストールしてから業務が軌道に乗るまでにいろんな課題を解決し、乗り越えていく必要があるなあ、というのを再認識しました。 最後に、ここまでで触れてこなかったいくつかのトピックについて取り上げてみます。 Windows11 + WSL2 Ubuntu 構成の可能性について Windows11 + WSL2 には、弊社SREの二宮同様に可能性を感じており、開発以外( VPN とかデ バイス 接続とか)のトラブルを Windows 側で回避しつつ、開発はWSL2 Ubuntu 側で行なうというハイブリット構成は魅力だなと思います。 note.com Visual Studio Code にはRemote Development拡張があり、 IntelliJ 系 IDE にもJetBrains Gateway というリモートOS上で開発する機能があるため、 Windows 側の GUI を使いつつWSL2 Ubuntu 側で開発をすることはかなり現実味を帯びてきているように感じます。 とはいえ、同僚のエンジニア曰く、 JetBrains Gateway で Ubuntu に IDE バックエンドを入れたときの GUI ( Windows 側)の応答速度はまだまだ厳しいとのことでした(逆に Visual Studio Code のRemote Developmentのほうは十分イケてる、とのこと)。 RubyMine派の私は JetBrains Gateway を使いたいですが、 まだ軽く触っただけで本格的な開発には使ってないので、今後の検証課題としたいと思います。 なお、WSL2 Ubuntu 側にRubyMine本体をインストールし、WSLg で接続する、またはWSL2側へ リモートデスクトップ 接続をする形も試したことがあるのですが、こちらも動作がまだまだ重かったなという所感です(マシンスペックにも依存するでしょうが)。 性能問題については(ハード・ソフトともに) 時間が経てば改善していく可能性が高いので、今後に期待というところです。 macOS が使えない場合の代替手段になるというメリット(多様性の担保) 以前の職場でのWeb開発の経験になるのですが、あるとき社外ベンダーの多数のエンジニアに開発プロジェクトに参加いただいたことがあり、ベンダーのエンジニアの開発用マシンがほぼ Windows だったことがありました。その結果、私が開発環境を(自分のために) Ubuntu 対応させておいたことにより、ベンダーのエンジニアの方々にもスムーズにWindows10 + WSL2 Ubuntu で開発してもらうことができました。 結果論ではあるのですが、複数の選択肢(多様性)があったことによるメリットの享受といえるかもしれません。 弊社においても、一部の開発メンバーの MacBook のスペックの問題(メモリ容量が8GB)で開発が厳しい状況だったのを、メモリサイズの大きい (16GB) Windows マシン (WSL2 Ubuntu ) に乗り換えて開発している、という事例があります。 この記事を書いた人 沼井裕二 MNTSQ( モンテスキュー )社のソフトウェアエンジニア。「要はバランス」おじさん。 Mac を使おうとした時期が一瞬だけあり、そのときに GitHub のユーザーアイコンを MacBook にしたまま放置している。
アバター
新しい仲間 前回、MNTSQのSlackにいるいくつかの bot を紹介した。 tech.mntsq.co.jp 今日、そこに新しい仲間が加わったので紹介しよう。その名も "Hot Docs" だ。 Hot Docsとは Hot Docsとは、 Google Drive のファイルを 再帰 的に探索し、短時間にたくさんコメントがついたDocsをSlackに通知する bot である。こんな感じだ。 なぜ作ったか MNTSQでは Google Docs を使って非同期でコミュニケーションを取ることがある。誰かが提案やログをDocsで作り、他のメンバーがそこにコメントしまくる、というスタイルだ。 しかしこれでは議論がDocsに閉じてしまい、Docsの存在を知らないメンバーから見えないというOpenness観点の問題がある。 そこで議論が活発なDocsをピックし、Slackに通知する仕組みを作った。 導入してみて 好評だった。 筆者自身、異なる ドメイン のHot Issueを知ることで事業理解に役立っていると実感する。 Hot Docsの仕組み Hot Docsは Google App Scriptで作られており、30分おきにDriveを探索する。 ソースコード は次のとおり。 再帰 Generator listFiles で取得したファイルを2users以上かつ5comments以上という条件でフィルタしSlackに投稿する。 function notifyHotDocs() { notifyHotFiles( "application/vnd.google-apps.document" ); } function notifyHotSlides() { notifyHotFiles( "application/vnd.google-apps.presentation" ); } function notifyHotFiles(mimeType) { const updatedMin = new Date ( new Date ().getTime() - 30 * 60 * 1000); const files = Array .from(listFiles(FOLDER_ID, mimeType, updatedMin), file => { const comments = Drive.Comments.list(file.getId(), { maxResults: 100, updatedMin: updatedMin.toISOString() } ).items; return { name: file.getName(), url: file.getUrl(), comments: comments, authors: [ ... new Set(comments.map(x => x.author.displayName)) ] , commentsLength: comments.length + comments.reduce((n, x) => n + x.replies.length, 0) } ; } ) .filter(x => x.authors.length >= 2 && x.comments.length >= 5) .sort((a, b) => b.comments.length - a.comments.length); if (files.length === 0) return ; const colors = [ "#fcc800" , "#f3981d" , "#ea553a" ] ; // コメント数に応じて色を変える const attachments = files.map(x => { let colorIndex = Math.floor(x.comments.length / 10); colorIndex = colorIndex < 0 ? 0 : colorIndex > 2 ? 2 : colorIndex; return { author_name: x.authors.slice(0, 3).join( ", " ) + (x.authors.length > 3 ? ", etc." : "" ), title: x.name, title_link: x.url, color: colors [ colorIndex ] } ; } ); postMessage(CHANNEL_ID, "なんか盛り上がってるみたい!" , attachments, ":hotdog:" , "Hot Docs" ); } function *listFiles(parentFolderId, mimeType, updatedMin) { const folder = DriveApp.getFolderById(parentFolderId); const files = folder.getFilesByType(mimeType); while (files.hasNext()) { const file = files.next(); if (file.getLastUpdated() > updatedMin) { yield file; } } const folders = folder.getFolders(); while (folders.hasNext()) { const folder = folders.next(); yield* listFiles(folder.getId(), mimeType, updatedMin); } } function postMessage(channel, text, attachments, icon_emoji, username) { const url = "https://slack.com/api/chat.postMessage" ; let options = { "method" : "post" , "contentType" : "application/x-www-form-urlencoded" , "payload" : { "token" : SLACK_BOT_POST_TOKEN, "channel" : channel, "text" : text, "attachments" : JSON.stringify(attachments), "icon_emoji" : icon_emoji, "username" : username } } ; let response = UrlFetchApp.fetch(url, options); let data = JSON.parse(response.getContentText()); return data; } おしまい あなたの会社でもぜひ試してほしい。
アバター
こんにちは、MNTSQでサーバーサイドエンジニアのようなものをやっている西村です。今回は比較的簡単に Ruby on Rails のアプリを高速化する方法を書いてみようと思います。 内容的にはタイトルのとおり、平易なものが多いのですが、頻度高く見かけるものをまとめてみました。 preload/include/eager_loadを利用してN+1を回避する Rails では紐づくレコードが芋づる式になりがちで、N+1問題がよく発生します。弊社では grape というgemを利用して API を作成していますが、レスポンスを組み立てるタイミングでN+1問題がよく発生します。例えば、以下のような ActiveRecord があったとします。 class Document < ActiveRecord :: Base has_many :sections , dependent : :destroy end class DocumentEntity < Grape :: Entity expose :id , documentation : { type : ' Integer ' , required : true } expose :sections , documentation : { is_array : true , required : true }, using : SectionEntity end class SectionEntity < Grape :: Entity expose :section_type , documentation : { type : String , required : true } end それに対して以下のようなコードを書くと documents = Document .where( type : :pdf ) present documents, with : DocumentEntity 以下のようなクエリが発行されてしまいます。 Document Load ( 0 .8ms) SELECT `documents`.* FROM `documents` ORDER BY `documents`.`updated_at` DESC LIMIT 1 Section Load ( 11 .6ms) SELECT `sections`.* FROM `sections` WHERE `sections`.`document_id` = 3 Section Load ( 2 .6ms) SELECT `sections`.* FROM `sections` WHERE `sections`.`document_id` = 5 Section Load ( 1 .9ms) SELECT `sections`.* FROM `sections` WHERE `sections`.`document_id` = 8 Section Load ( 2 .3ms) SELECT `sections`.* FROM `sections` WHERE `sections`.`document_id` = 20 Section Load ( 0 .6ms) SELECT `sections`.* FROM `sections` WHERE `sections`.`document_id` = 21 その場合は includes/preload/eager_load を利用することで、N+1問題を抑制することができます。 documents = Document .preload( :sections ).where( type : :pdf ) present documents, with : DocumentEntity すると以下のようなクエリが発行されるようになります。IN句でまとめてクエリで取得されているのがご確認いただけますでしょうか。 Document Load ( 0 .8ms) SELECT `documents`.* FROM `documents` ORDER BY `documents`.`updated_at` DESC LIMIT 1 Section Load ( 11 .6ms) SELECT `sections`.* FROM `sections` WHERE `sections`.`document_id` IN ( 3 , 5 , 8 , 20 , 21 ) 余談ですが、筆者は「N+1」という表現が分かりにくいのであまり好きではありません。 参考: qiita.com 変数にキャッシュする 結果を変数にキャッシュして高速化する手法です。弊社ではフォルダ構造を表現するために ancestry というgemを利用していますが、このgemから提供されているメソッドをそのまま利用すると上記のN+1問題が発生します。また、この問題は上述の includes や preload 等では解決できません。この場合には一度呼ばれたフォルダのデータを二度呼ばないように変数にキャッシュしていきます。 dir_cache = {} document_scope.in_batches( of : 100 ).each do | documents_batch | doc_ids = documents_batch.pluck( :id ) directory_doc_id_pair = :: Directory .where( document_id : doc_ids).pluck( :document_id , :ancestry ).group_by(& :first ).transform_values { | val | val.first.second } dir_ids = directory_doc_id_pair.values.map { | ancestry | ancestry&.split( ' / ' )&.[]( 1 ...)&.map(& :to_i ) }.flatten.compact.uniq - dir_cache.keys dir_cache.merge!(:: Directory .where( id : dir_ids).pluck( :id , :node_name ).to_h) if dir_ids.present? documents_batch.each do | doc | path_names = directory_doc_id_pair[doc.id]&.split( ' / ' )&.[]( 1 ...)&.map(& :to_i )&.map { | dir_id | dir_cache[dir_id] } || [] p path_names.join( ' / ' ) end end 上記はどちらかというと込み入った例で、変数キャッシュのやり方としてよくあるのは、以下のようなやり方です: def document_count @document_count ||= user_document.count end これは document_count が何度も呼ばれることを想定し、その結果を インスタンス 変数に記憶しています。 この使い方で気をつける点としては、 インスタンス 変数に記憶しているので、 ActiveRecord を更新した際などに人力で変数をリセットする必要があることです。また、結果が判定falseのものの場合(falseや nil など)、キャッシュが効かないので、その点も注意が必要です。 SQL を一本化する ループ処理で大量のクエリが発行される場合、それらを1つのクエリで済ませるようにすると大幅に高速化できることがあります。例として、以下のとおり1ヶ月分の日々の商品ごとの売上高を計算する処理があったとしましょう。 prev_month_end = Time .zone.now.beginning_of_month - 1 .second prev_month_start = prev_month_end.beginning_of_month dat = [] (prev_month_start.to_date..prev_month_end.to_date).each do | date | dat << Recipient .where( created_at : (date.beginning_of_day..date.end_of_day)).group( :product_code ).pluck( ' sum(price) ' , ' count(*) ' , :product_code ) end これでももちろん動くのですが、売上が伸びるとかなり時間がかかるようになってきます。これを以下のように1クエリで取得するように変更することで、スピードアップが見込めます。 Recipient .where( created_at : (date.beginning_of_month..date.end_of_month)).group( :product_code , ' date(created_at) ' ).pluck( ' sum(price) ' , ' count(*) ' , :product_code , ' date(created_at) ' ) こういった問題は経年によってデータが溜まることにより顕在化することがあるため、継続的な速度計測をしておくと良いでしょう。 この手法は内容次第でメ モリー 不足でデータベースが死ぬことがあり、データベースにクエリを処理するために十分なメ モリー があるかを確認すると良いでしょう。 メ モリー ストアへのキャッシュ これは対策としては最も単純な部類です。Redisや memcached 等に ActiveRecord をキャッシュします。実装イメージとしては以下のようになります: # 最新10件をキャッシュする Rails .cache.fetch( CACHE_KEY , expires_in : CACHE_EXPIRE ) do order( updated_at : :desc ).limit( 10 ).to_a end 対応する際の注意点としては、列が増えたり減ったりした時にエラーを吐きがちなので、手元で以前のデータのキャッシュのままデプロイしてエラーにならないか、逆に新形式のデータのキャッシュがデプロイ前のサーバーでエラーにならないか等、綿密にチェックする必要があります。 いわゆるマスターデータは複数のテーブルにまたがったデータをキャッシュしたり、特定の条件のものを絞り込んでキャッシするということをよくやりますが、キャッシュキーをがたくさんできてしまい、どのキャッシュをどのレコードを更新したタイミングで消せば良いのかが煩雑になりがちで、それに起因した問題を踏みがちです。キャッシュに関連するテストは厚めにしておいたほうが良いでしょう。 実態をキャッシュできているか確認することも重要です。よくやりがちなのが、.to_aを忘れてしまって、キャッシュしたつもりが実は都度データベースへの問い合わせが発生しているというものです。これをやらかすと、なかなか気づきにくいです。 クラスキャッシュする toC 向けのサービスの場合、キャッシュをしてもキャッシュサーバーとのネットワークが ボトルネック になってしまうことがあります。その場合、 Rails サーバーの中でキャッシュをしてしまうことがあります。 ただし、設計に気をつける必要があります。そのサーバー上でその値で固定されてしまうことになるので、更新をかけるタイミングが難しくなります。 キャッシュ内容がメ モリー にそのまま乗るので、メ モリー に乗せられる量かつ単純な呼び出しのものが対象になります。メ モリー についてはこれらがサービスに対する インパク トが強ければ、逆に大量のメ モリー を抱えたサーバーを準備する方向もあるでしょう。 やり方としては以下のようにします: # 1分間データをクラスにキャッシュする def fetch_cache if @@cached_time && @@cached_time > 1 .minute.before return @@recently_updated unless @@recently_updated .nil? end @@cached_time = Time .zone.now @@recently_updated = order( updated_at : :desc ).limit( 20 ).to_a end 消費メ モリー 量は ObjectSpace.memsize_of() を利用して計測することができます。文字列であれば文字数によって変化するので、注意が必要です。20件キャッシュするのであれば、1つあたりのだいたい20倍ということになります。 > ObjectSpace .memsize_of( Project .first) => 120 データベースのインデックスをちゃんとする MySQL へのクエリが重くて、実はindexが効いてないということは稀によくあります。重いクエリを見かけた際は実際のクエリを吐き出してexplainで中身を見てみると良いでしょう。EXPLAINについて説明するとそれだけで立派なブログ記事が一本書けてしまうので、軽い紹介だけにとどめます。 > Project .all.explain => EXPLAIN for : SELECT ` projects ` .* FROM ` projects ` +----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------+ | 1 | SIMPLE | projects | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 100.0 | NULL | +----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------+ 1 row in set ( 0.00 sec) レコードの存在確認で件数を数えない 細かなチューニングとしては、レコードの存在を確認する時に以下のようなコードを書きがちです。 > User .where( suspended : true ).count > 0 SELECT COUNT(*) FROM ` users ` WHERE ` users ` . ` suspended ` = TRUE それを以下のようなかたちに書き換えます: > User .exists?( suspended : true ) SELECT 1 AS one FROM ` users ` WHERE ` users ` . ` suspended ` = TRUE これも結構ありがちで、件数を取得するための SQL 負荷は意外と高いので、見つけ次第修正してデータベースのリソースを有効に使いましょう。 お役立ちgemのご紹介 以下にスピードアップのために役に立つgemをいくつかご紹介します。 github.com ローカル環境でテストする時に、その画面で呼ばれている API や SQL 、それらにかかっている時間などを把握することができます。 github.com N+1を発見してくれるgemです。どう直したら良いかもアド バイス してくれます。 github.com 大量のデータを一度にインサートできるgemです。1万件のデータを1件ずつsaveしていたらかなりの時間がかかりますが、バルクインサートすることで大量のデータのインサート時間を短縮することができます。なお Rails 6以降であればinsert_allを利用すれば足りるでしょう。 github.com gemというよりサービスの紹介になりますが、Datadogの APM を利用することで遅いクエリをあぶり出すことができます。特に本番環境のデータ量でないと再現しない問題も多いので、継続的に監視することで問題を早期に発見し、未然に対処することができます。 重要なのは「不要不急のスピードアップをしないこと」 実はこれが最も重要です。すべてを最適化していたら相当な開発 工数 を消費するので、結果としてアプリのスピードの上昇率と比べて開発スピードが大幅に減速します。 一方で適切なタイミングでスピードアップする必要があります。そこのバランスはわりと職人芸になります。なぜ職人芸になってしまうかというと、プロダクトによって負荷のかかり方、組織の状況が異なるため、一概にこれが正解ということが言いづらいからです。 例えば、ITエンジニアリソースに余裕がある組織で toC 向けのサービスで多くの通信が発生するサービスであれば、N+1を撲滅し、すべてのデータをキャッシュし、キャッシュの削除部分の丁寧なテストを書いても良いでしょう。また創業したてでITエンジニアの人数も2-3名の組織では、スピードアップに割くリソースも無いため、問題が起きた部分だけチューニングをするという選択肢をとることもあるでしょう。 この記事を書いた人 Yuki Nishimura 雑食系エンジニア
アバター
こんにちは、MNTSQ( モンテスキュー )でSREをやっている中原です。しばらくコロナで帰省することができなかったのですが、つい最近久しぶりに帰るとともに、実家に放置してあった自分の車を関東に持ってきました。「これで夢のドライブライフだー!!」と思っていましたが、借りた駐車場が狭すぎて出し入れが大変しにくく、結局乗る機会は少なそうです。 ドライブや自動車競技をする場合、機能性の面からステアリングやシートを入れ替えたりしますが、やはり自分にあったものを選ぶと疲れにくかったりします。かつてレカロのシートを使っていたときは確かに身体は楽でした *1 。 さて、ITシステムに関わる企業に所属するものとして、作業の大半はディスプレイの前でキーボードやマウスを使っての作業になります。 ドライブなどと同じで、それぞれの身体や入力スタイルにあった機器でないと 疲労 や最悪の場合ケガなどにつながっていくのは必然です。 MNTSQでは、 入社時に椅子の専門店で実際にいろいろな椅子と試してみて、自分に合う椅子を注文して使うことが可能 であったり、ディスプレイについても自分の好みに合う画面サイズや解像度のものを選択可能です。 当然のことながら、キーボードやマウスについても、予算の範囲内で選択が可能です *2 。 そんな中で、MNTSQ社員がどういったインプットデ バイス で仕事をしているのかまとめてみました! SRE 中原: Lenovo ThinkPad TrackPoint Keyboard II, Logicool MX ERGO ( Mac ) まずは執筆者の机はこんな感じです。家ではIIではない トラックポイント キーボードを使っているのですが、当初はIIの方のクリック・右クリックの浅さにビックリしました。ワイヤレスのUSBと Bluetooth の両対応ですが、基本はUSB接続で使っています。 Bluetooth だとファンクションキーなどで干渉したりしてうまく使えないことがあるのですよね。 MX ERGOは角度を時々切り替えながら使ってます。また、たまに TrackPoint も使っていますよ。 今後の展望としては、同じキーボードをもう1台買って、デュアルキーボードにしてみたいな、というのは考えています。 PdM Kさん: Apple Magic Keyboard II (英字配列) + Logicool MX Master 3 ( Mac ) Kさんの机はあっさり。MX Master 3よりも、本当は Magic Mouse がよかったとのことですが、それはそれで平たすぎると言うことでこの選択に。概ね不満は無いそうです。 エンジニア Mさん: PFU Happy Hacking Keyboard Professional HYBRID Type-S + Kensington BladeTrackball ( Mac ) HHKB、そして ケンジントン の トラックボール というのはエンジニアにとっては一種の王道感がありますが、MNTSQでは珍しい感じです。 お話を聞くと、できるだけ入力機器に迷いたくないという意図もあるのか、MBPの英字キーボードか、HHKB以外は触りたくないなぁ、ということをおっしゃっていました。たしかに、慣れた機器以外使うと疲れたりするし、その気持ちはわからなくはない…。 リーガル Sさん: ThinkPad 標準キーボード + エレコム M-HT1DRBK ( Windows ) キーボードは ThinkPad でよしとのこと。自宅では エレコム の Bluetooth キーボードを使われているそうですが、最近お子さんに破壊され、Keychronに興味津々とのことです。 トラックボール はとりあえず人差し指型を値段で考えて選んだものの、使ってみたら ケンジントン の方がよかったなあ、とのこと。このあたりは普段使っているものとの慣れなどもありそうですね。 リーガル Iさん: ThinkPad 標準キーボード + Lenovo のマウス + Lenovo のテンキー( Windows ) 今回とりあげた方で唯一のテンキー追加派。いつ使うのか聞いたら「数字を打ち込む時」。 具体的には、契約書の アノテーション 中、分類を打ち込む時に QWERTY配列 で 0 or 1 を打ち込むのと比べると効率が段違いとのこと。 それ以外は至って標準的な構成ですが、自宅では Realforce 派とのことです。 カスタマーサクセス Mさん: PFU HHKB Professional + Logicool M575 ( Windows ) キーボードは今は徐々に数を減らしつつある有線のHHKBをあえて選択したそうです。オフィスではワイヤレスにする意味が薄く、乾電池がなくなったときのイラッとした感じを避けるためとのこと。白は汚れが目立つため黒系で、スミではなく刻印のあるタイプを選択されていますね。 ポインティングデバイス については、MX ERGOにしなかったんですか?と尋ねたら、「(価格的に)ちょっと遠慮した」とのこと。M575は、MX ERGOと異なり角度の変更や重さがありませんが、素材がプラスチックなため 加水分解 しないという特長がありますね。 セールス Oさん: 東プレ Realforce + エレコム EX-G ( Windows ) 2021年10月入社のOさんは、社内初の Realforce ユーザでした。選んだ決め手は「同居人のおすすめ」とのこと。まだ使い始めたばかりで、良いのはわかるけど手放せないほどではない、ということでしたが、これから先抜け出せなくなっていくことを期待しています! また、マウスについてもこれがいいというこだわりのものとのことです。 エンジニア Mさん: Kinesis Freestyle 2 for Mac (9インチ) + Apple Magic TrackPad II ( Mac ) Freestyle 2をつかっているのは下の安野と同じなのですが、間をつなぐケーブルが安野のものより短い9インチのモデルで、これ以上は間が広げられないとのこと。長い(20インチ)ケーブルのモデルがあることを知らなかったとのことで、次はそちらを買いたいとのことでした。 Mac はクラムシェルにされていました。なお、 トラックパッド の位置は右側で「右側じゃない人は邪道ですよ!」とのこと。なお…。 取締役 安野: Kinesis Freestyle 2 for Mac (20インチ) + Apple Magic TrackPad II ( Mac ) 取締役、またエンジニアとしても現役の安野は上記の通り、 トラックパッド は真ん中派です! 安野の場合、Freestyle 2でもケーブル長が20インチのため開いている幅は広めで、これについて彼は「猫背を買うか( 原文ママ )、まっすぐな背骨を買うかだから」という名言を残していました。 ディスプレイは一枚ですが、タイルUIを駆使して中でのウインドウの整理はバッチリです。 代表取締役 板谷 : Lenovo ThinkPad 標準 代表取締役 ・ 板谷 の元相棒PCのキーボードです。弁護士たるもの、またスタートアップ ベンチャー の経営者たるもの、やはり力強い打鍵が必要なのか、Bキーが吹っ飛び、エンターキーに至っては真っ二つになっていました。もしかしたら、常にタフな意思決定を求められるスタートアップの経営者というロールが、自然と打鍵を強くするのかもしれませんね。恐るべき指力です。 これ以外の人は、割と支給ノートPCである MacBook Pro や ThinkPad のキーボードや トラックパッド を標準で使っている方が多い印象が強いです。 以前の職場にも「良い道具を使っちゃうとそれ以外使えなくなっちゃう気がするから」という理由で大変粗悪なキーボードを使っていた同僚がいますが、そういう意識の方もいるかも知れません。 そもそも、 MacBook Pro や ThinkPad のキーボードの打鍵感がいいというのはありそうです。 MNTSQでは、様々なキーボード・ ポインティングデバイス を社内に布教しちゃうような、エンジニア、セールス、リーガル担当を広く募集しています! この記事を読んで「お前らこだわりが足りてないんじゃないか」「メ カニ カル派や自作派はいないのか!」ということを思われた方は、ぜひカジュアル面談にて 宗教戦争 お話しできればと思います! なお記事と全く関係ないですが愛車です。 愛車近影 *3 この記事を書いた人 中原大介 MNTSQ社でSREをやってます。ついに地元から愛車をもってきましたが、駐車場でマニュアルの坂道バック発進を強いられるため、乗る機会は控えめです。 *1 : 純正レカロのついた シャレード・デトマソ という車だったのですが、いろいろな思い出詰まっていて、機会があればまた乗ってみたい車だったりします *2 : セキュリティ的な観点から持ち込みは不可。自作キーボードの場合は要相談 *3 : 筒石駅 にて
アバター
はじめに テキスト情報から 自然言語処理 の 機械学習 モデルを構築する際には文字列データのみが解析の対象になりますが、文書全体から情報を抽出するモデルを構築する際には、文書レイアウト情報が重要になります。 通常の 自然言語処理 とは異なり、文書レイアウト情報は画像も入力の対象として想定されるため、文字の位置を表すBounding Box等が アノテーション として想定されます。 このように、文書に含まれる文字情報だけではなくレイアウトに関する情報も扱うタスクをDocument Analysisと呼んだりします。 本記事ではDocument Analysisタスクに関わるデー タセット の作成について考える一助とするため、 LayoutLM の論文で用いられたデー タセット を見ていきます。 IIT CDIP 1.0 dataset 原論文: Building a Test Collection for Complex Document Information Processing タバコ産業のドキュメントライブラリ:Legacy Tobacco Documents Library (LTDL) から取得したデータ データは ここで 公開されている 非公式では [D]Where can I find IIT CDIP 1.0 dataset? : datasets のスレッドで別の場所に ミラーサイト についての議論もある 研究利用は可。商用利用については明示的には書かれていない(コピーの配布を商用利用のために行うのはNG) XML の メタデータ も用意されており、以下のような属性が取得可能 タイトル ボディテキスト 書類の形式 日付 組織名 メーリングリスト の断片が属性の読み取りに役立つ [Trec-legal] 17-May-07 update of description of IIT CDIP v. 1.0 / TREC 2007 data RVL-CDIP Dataset 原論文: Evaluation of Deep Convolutional Nets for Document Image Classification and Retrieval 論文自体は画像からDNNをつかって文書分類するというもの 新しいデー タセット がContributionの一つになっている IIT CDIP 1.0 dataset を元にして各文書の画像に対して手紙、Eメール、フォームなどのカテゴリを アノテーション し、分類問題としてのタスクを想定している デー タセット 公開サイト: Evaluation of Deep Convolutional Nets for Document Image Classification and Retrieval ライセンスについて明示的な言及なし FUNSD 原論文: FUNSD: A Dataset for Form Understanding in Noisy Scanned Documents フォーム形式の文書に特化したデー タセット RVL-CDIP Datasetを元にしてフォームデータのテキスト位置のBounding Boxが アノテーション されている デー タセット 公開サイト: FUNSD 研究目的のみ利用可能 アノテーション データの形式は Json で、以下の情報を含む 意味のある文字のグループ フォーム内文字の意味を表したラベル(Question, answerなど) 単語一つ一つに対するBounding Box 文字のグループ同士に関係があるか SROIE 原論文: ICDAR2019 Competition on Scanned Receipt OCR and Information Extraction デー タセット の公開に合わせてコンペを開催した模様、コンペで成績の良かった手法も紹介されている レシート画像のデー タセット : Overview - ICDAR 2019 Robust Reading Challenge on Scanned Receipts OCR and Information Extraction ライセンスについて明示的な言及なし 各レシートについて、Bounding Boxと内部の文字情報が入った列が CSV 形式で アノテーション されている レシート全体から抽出できる メタデータ が Json 形式でまとめられている 以下 メタデータ のフィールド一覧 company date address total おわりに 簡単ではありましたが、今回は文書レイアウトに関連したタスクとデー タセット の紹介をしました。文書レイアウトを考慮したモデルの開発に本記事が少しでも役に立てば幸いです。 参考 [1912.13318] LayoutLM: Pre-training of Text and Layout for Document Image Understanding LayoutLM (Layout Language Model)を試したら精度がめっちゃ上がった件について - Cinnamon AI Blog この記事を書いた人 yad ビリヤニ 食べたい
アバター
いろいろな bot 組織をスケールさせる上でコーポレートエンジニアリングは非常に重要である。MNTSQではissue-drivenで誰でも気軽に bot を作ることができる。現在MNTSQのSlackにいるいくつかの bot を紹介しよう。 施錠と消灯を催促する bot 観葉植物の水やりを催促する bot (ガイド付き) 社員8名ぶんのサラダを社長に取りに行かせる bot (実際には当番制) 詳細は この記事 で紹介している。 にゃーんと言うと何かを返す bot デイリーの共有会でグループ分けをする bot セキュリティチェックの案内を出す bot 以下では「デイリーの共有会でグループ分けをする bot 」の仕組みを紹介する。 デイリーの共有会でグループ分けをする bot MNTSQでは毎日14:00-14:15でDaily-syncという共有会を行っている。Daily-syncは異職種間コラボレーションの質を高めるために、互いが何を何故やっているか共有しfeedbackしあう場である。以前はメンバー全員の状況を共有していたが、社員数の増加に伴いグループ分けする運びとなった。 この bot はDaily-syncのグループ分けを自動化するために作られた。 グループ分けの流れ 1. [ bot ] Daily-syncの1時間前にリマインドを送る 「参加します」スタンプを押すよう催促しているが、ダークテーマだと可視性がとても低い。 2. [参加者] 共有内容をSlackに投稿しスタンプをつける Daily-syncではこの内容をベースに議論する。 3. [ bot ] グループごとに場所と参加者が通知する スタンプをつけ忘れると抽選から漏れてしまうので注意が必要だ。 bot の仕組み Google Apps Script (GAS)を使う。私は普段 Python を使うことが多いので、行末の セミ コロン付け忘れを克服するのに苦戦した。GASの基本的な使い方は他の記事に譲るとして、ここではポイントだけ記しておく。 使用するSlack API これらの API が使えるように、事前に トーク ンの発行や権限設定を済ませておく。 conversations. history : 当日の参加者の投稿を集める chat.postMessage: リマインドやグループ分けの結果を投稿する 当日の投稿を集める conversations.history の oldest パラメータを使えば当日の投稿を集められる。 oldest にはSlackのタイムスタンプ形式で渡す。例えば2021-08-26は 1629903600 である。 let now = new Date (); let y = now.getFullYear(); let m = now.getMonth(); let d = now.getDate(); let ts = ( new Date (y, m, d).getTime() / 1000).toString(); let options = { "method" : "get" , "payload" : { "token" : token, "channel" : "C0123456789" , "limit" : 1000, "oldest" : ts } } ; let response = UrlFetchApp.fetch( "https://slack.com/api/conversations.history" ,options).getContentText(); let data = JSON.parse(response); 特定のスタンプがついた投稿を集める conversations.history の結果をfilterする。 let participants = data [ "messages" ] .filter(message => { return ( "reactions" in message) && (message [ "reactions" ] .filter(reaction => { return reaction [ "name" ] === "sankashimasu" ; // :sankashimasu: がついてる人 } ).length > 0); } ); グループ分けする なるべく職種でバラけるようにランダムにグループ分けする。 参加者を職種で バブルソート する 上からA,B,C,A,B,C,A,...のようにグループに割り当てる(この例だと3グループできる) グループ内でシャッフルする(共有の順番をランダムにするため) participants.map(v => v [ "team" ] = shainTeams [ v [ "user" ]] ); // 職種を割り当てる participants.sort((a, b) => ((a [ "team" ] < b [ "team" ] ) ? -1 : ((a [ "team" ] > b [ "team" ] ) ? 1 : 0)) // バブルソート ); const nGroups = Math.ceil(participants.length / 6); // 最大6人のグループを作る let groups = new Array (nGroups).fill().map((_, i) => shuffle(participants.filter((_, j) => (j + i) % nGroups === 0)) ); 特定の時刻に投稿する GASの GUI で設定できる時間ベースのトリガーは1時間単位で分は選べない。GASの組み込みモジュール ScriptApp を使えば任意の時刻で実行できる。例えば当日の13:58に main 関数を実行するには、次の関数をトリガーに設定しておく。 function setTrigger() { let now = new Date (); now.setHours(13); now.setMinutes(58); now.setSeconds(0); ScriptApp.newTrigger( 'main' ).timeBased().at(now).create(); } おしまい MNTSQはプロダクトだけでなく組織も育てがいのあるフェーズだ。様々な領域でエンジニアリング能力を発揮できるエンジニアを募集している。
アバター
こんにちは。MNTSQの堅山です。 去る8/10に、Ubieさんと共同で「Vertical AI Startup Meetup」というイベントを開催しました。 connpass.com 弊社MNTSQはいわゆるリーガルテックという領域で、企業法務に携わる方々を相手にプロダクトを提供しています。 Ubieさんも主に医療従事者の方々を対象にプロダクトを提供されており、以下のような共通点があるなぁと勝手に親近感を持っておりました。 ubie.life ドメイン の深い領域に取り組んでいる プロフェッショナルに対してプロダクトを通じてサービスを提供している ドメイン のプロフェッショナルがエンジニアと協働して製品を開発している 創業者に ドメイン のエキスパートがいる(弁護士、医師) UbieさんもMNTSQも、医療・法律といった特定の ドメイン (=Vertical)を扱うスタートアップです。 こういったスタートアップを表す表現として「Vertical AI Startup」というものが2017年頃に提唱されています。そして、うまく業界の課題を解決するためには上記のような特徴が必要だよね、ということが提案されています。まさに簡潔に我々のやりたいことを伝えられる概念だなと思っているのですが、日本ではあまり言及している人がいませんでした。そこでこのイベントは「Vertical AI Startup」概念を多くの方々に知ってもらい、また ドメイン の深い業界での問題解決に興味を持ってもらえればと思いまして、Ubieさんのご協力の下開催いたしました。 当日は質問もたくさん出まして、思っていた以上の反響をいただきたいへん盛り上がるイベントになりました。発表スライドは以下をご覧ください Vertical AI Startupとはなにか わたしからは、「Vertical AI Startup」ってなに、ということについて話しました。 Vertical AI製品の品質管理 Speaker: MNTSQ 稲村 @kzinmr 概要: ドメイン 知識の重要度が高いVertical AI製品の開発では、モデルの改善に先行してデータ中心の品質管理を行う費用対効果が高いことを説明し、 機械学習 製品開発におけるデータ品質保証の姿について議論する。 speakerdeck.com 医者の言葉、患者の言葉、エンジニアの言葉 Speaker: Ubie 奥田さん @yag_ays 概要:「テク ノロ ジー で人々を適切な医療に案内する」をミッションとするUbieでは、様々な領域で 自然言語処理 の技術を活用しています。 医療言語処理という括りの中にも、医師や患者といった話者の違いや、カルテ文書から 話し言葉 といった表現の違いに至るまで、様々な課題や研究対象が存在します。 ドメイン ならではの複雑さに対してどのような切り口で課題に向き合い、 自然言語処理 の実応用に向けた開発を経験するなかで感じた難しさや面白さを、実例とともにご紹介します 。 speakerdeck.com おわりに 今後とも、Vertical AI Startupの面白さを発信していければと思います。 MNTSQ、Ubieさんともに絶賛採用中ですので、ぜひご興味をお持ちいただいた方はカジュアル面談にいらしてください! www.mntsq.co.jp recruit.ubie.life また、MNTSQでは一緒に勉強会などを実施してくださる企業を募集しております!ぜひ ご連絡 ください この記事を書いた人 堅山耀太郎 MNTSQ社で取締役として 機械学習 ・ 自然言語処理 に関わるもろもろをやっています。好きな食べ物は担々麺です。
アバター
Webアプリケーションやバッチジョブを運用していくにあたって、エラーの影響範囲の調査のため、APIへのアクセスIDやバッチのジョブIDのついたログは欠かせないです。 このような類のIDをログとして残す場合には、そのIDの影響下にある全部の処理に対して該当のIDを渡したいです。 この類の処理をフルスクラッチで書こうとする場合、下記事項を考慮する必要があります。 ログのために既存のコードの引数を変えることはしたくないため、IDをロガー経由で渡す必要がある IDを渡されるロガーは処理の間はIDを保持してほしい、それ故ロガーはシングルトンインスタンスか、それに相当するモジュールになる asyncの入ったコードに対応するにはasyncio コルーチン間の割り込みを考慮する必要がある また、ログレベル、ログの出力時刻等の個々のログに付随する属性を解析する際にmachine readableになっていてほしいという要望もあります。 これらを考慮した処理を実現してくれるライブラリが今回紹介する structlog になります。 www.structlog.org Tomcatライクなロゴが印象に残ります。 導入 毎度おなじみpipです pip install structlog 処理IDが絡んだログのサンプル structlogを用いて処理IDをロガーにどのように出力させるか見ていきます。 まずユーザIDをログに出力するサンプルコードとして以下を考えます。 from structlog import get_logger log = get_logger() def some_other_function(): log.info('other event') def main(): some_response = {'user_id': 12, 'user_agent': 'Chrome'} # some_response = {'user_agent': 'Chrome'} if 'user_id' in some_response: log.info('some event', user_id=some_response['user_id']) some_other_function() else: log.info('exceptional prec') some_other_function() if __name__ == '__main__': main() structlogはロガーのdebug, info等のメソッドに任意のフィールドを追加して出力することができます。(l.14) この実装の場合だと以下のようなログ出力になり、冒頭で想定した「IDの影響下にある処理のログにすべてIDがついてほしい」という要件を満たせません。 2021-05-25 19:04.28 [info ] some event user_id=12 2021-05-25 19:04.28 [info ] other event ログコンテキストの構築 IDの影響下にある処理のログすべてにIDを持たせるには、次のように書きます。 import structlog from structlog.threadlocal import ( bind_threadlocal, clear_threadlocal, merge_threadlocal ) from structlog import configure configure( processors=[ merge_threadlocal, structlog.processors.add_log_level, structlog.processors.StackInfoRenderer(), structlog.dev.set_exc_info, structlog.processors.format_exc_info, structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M.%S", utc=False), structlog.dev.ConsoleRenderer() ] ) log = structlog.get_logger() def some_other_function(): log.info('other event') def main(): some_response = {'user_id': 12, 'user_agent': 'Chrome'} # some_response = {'user_agent': 'Chrome'} if 'user_id' in some_response: bind_threadlocal(user_id=some_response['user_id']) log.info('some event') some_other_function() else: log.info('exceptional prec') some_other_function() clear_threadlocal() if __name__ == '__main__': main() structlog.threadlocal モジュールはスレッド単位でログに出力される属性を管理します。 merge_threadlocal : ログの設定のprocessorにかませることでスレッドに固有の変数をログに出力するよう設定が行われます bind_threadlocal : スレッドに固有の変数のロードを行います clear_threadlocal : スレッドに固有の変数をクリアします このケースでは以下のログが出力され、user_id が見つかった後の処理すべてにIDが付与されることがわかります。 2021-05-25 20:30.50 [info ] some event user_id=12 2021-05-25 20:30.50 [info ] other event user_id=12 Python3.7 より導入されたcontextvarsモジュール関連の機能で structlog.contextvars モジュールもあります。これは asyncio を用いるようなサーバサイドAPIにおける、ID情報を付加したロギングに有効です。 構造化ログについて もう一点、structlogがサポートする構造化ログについても見ていきます。下記のログはpythonを開発していればよく出力する形式のログです。 2021-04-02 10:47:02 [INFO] some precessing log この類のログはhuman readableではありますが、ログの解析を行う際にはmachine readableではないので不便です。 構造化ログとはログの出力として頻繁に出される時刻、ログレベル、ログ本体等の情報がmachine readableな構造化データ(Json, TSV)にされたものを指します。 { "message": "some processing log", "lineno": 1590, "pathname": "/app/job/src/apiclient.py", "job_id": 20, "timestamp": "2021-04-02 10:47:02", "level": "info" } このような形式のログもstructlogはサポートしています。 Json形式のログ設定サンプル Jsonのログ出力用設定のサンプルです import sys import structlog from structlog.threadlocal import ( bind_threadlocal, clear_threadlocal, merge_threadlocal ) import logging structlog.configure( processors=[ merge_threadlocal, structlog.stdlib.filter_by_level, structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), structlog.processors.TimeStamper(fmt="iso"), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, structlog.processors.UnicodeDecoder(), structlog.processors.JSONRenderer() ], context_class=dict, logger_factory=structlog.stdlib.LoggerFactory(), wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=True, ) logging.basicConfig( format="%(message)s", stream=sys.stdout, level=logging.INFO, ) log = structlog.get_logger() def some_other_function(): log.info('other event') def main(): some_response = {'user_id': 12, 'user_agent': 'Chrome'} # some_response = {'user_agent': 'Chrome'} if 'user_id' in some_response: bind_threadlocal(user_id=some_response['user_id']) log.info('some event') some_other_function() else: log.info('exceptional prec') some_other_function() clear_threadlocal() if __name__ == '__main__': main() ロガーの出力を標準入出力に吐く設定がstructlog経由で行えないのは謎ですが、標準ライブラリのロガーを生成するようにstructlog経由で設定でき(l.25)、標準ライブラリのロガーに対して標準入出力に出力するように設定しています(l.29) 以下出力です {"user_id": 12, "event": "some event", "logger": "__main__", "level": "info", "timestamp": "2021-05-26T03:16:44.301541Z"} {"user_id": 12, "event": "other event", "logger": "__main__", "level": "info", "timestamp": "2021-05-26T03:16:44.301945Z"} 構造化されたログが出力されました。 参考記事 標準ライブラリの範囲で構造化ログで出力するようにしてみる - podhmo's diary この記事を書いた人 yad ビリヤニ食べたい
アバター
たくさんの文字列(や離散的な符号列)をメモリに載せないといけないんだけど、いろんな制約があって通常のList[str]では載らない…ということありませんか?(まぁあんまりなさそうですね) たまたまそういうことがあったので、その際に検討した内容をまとめておきます TL;DR メモリをもっと増やしましょう 富豪的 に解決できるならいつでもそれが最高です しかし、世の中それでなんとかならんこともたくさんあります 用途があうのであれば専用のデータ構造を採用する 例えばもし共通のprefixやsuffixが存在し、順序に興味がなければtrie treeなどが使えます 例えば、 弊社 であれば、法人名をメモリに持ちたいなんてときもあります。そういうときに法人名の辞書をtrieで持ったりすることがあります 「株式会社」「 一般財団法人 」や「銀行」といった共通語がたくさんでてくるのでtrie treeでごりごり削れます pygtrie とか marisa-trie とかが簡単に使えます 他にも様々なデータ構造はあるので、そういうものが使える場合もあるかもしれません 短い文字列が大量にある場合は、とりあえず結合し、各文字列の始点のindexをnumpyのint arrayで持つ Python では1つobjectを作るために50byte程度が消費されます。そして Python の配列はobjectのポインタの配列であり、配列の要素の数だけメモリ上の別の場所にobjectが存在しています したがって、短い文字列を大量に配列で抱えていると、配列内のobjectのポインタ(8byte) + objectを作るためのオーバーヘッド(50byte程度) で大量のメモリが浪費されます 長大な1つのstringのobjectと、indexを持つnumpy array配列であれば、これらの浪費は起きません numpy arrayは要素も含めて1つのobjectになっています 固定長であれば、numpyのmatrixで持つ 固定長の文字列であれば、numpyの2次元配列が活用できるかもしれません。 この場合、固定長であることを利用して、indexの配列が不要になります str型/bytes型の範囲で工夫する Python3.3以降のstr は含まれる文字の範囲によって、1,2,4byte/文字を使い分けてくれます たとえば、ASCIIであらわせる範囲の文字列だけしか含まれない場合、すべての文字が1byte/文字になりますし、ひらがな等の範囲の文字が含まれれば、すべての文字が2byte/文字になります。絵文字が含まれると4byte/文字になるイメージですね したがって、絵文字などを削除してstrを作り直せば、日本語が含まれていても多くの文字列は2byte/文字にはできる可能性があります さらに、bytes型にencodeする手法でもっと圧縮できる場合があります strの エンコーディング は UTF-8 であると思いがちですが、strは unicode のコードポイントをそのまま保存しているので UTF-8 /16/32のどれにも直接対応しているわけではありません。 UTF-8 や UTF-16 は可変長 エンコーディング であり、strのように1文字でもASCIIでない文字が入ったら全部2byte or 4byte / 文字になります、みたいなことはなくいい感じに圧縮して出してくれます 扱う文字列にあったencodingを選択することで、圧縮が可能です 使われる文字が決まっている場合は、bit単位での表現を用いることでメモリを削減できる さらに、出現する文字列の範囲を制限すればメモリを削減できるというア イデア は、1byte単位ではなくbit単位で適用することができます このような場合には bitarray を使えます。例えば、 塩基配列 であれば2bitで1文字を表すことができますが、bitarrayでは(変換処理が入るために)CPU時間を犠牲にコレを実現できます この記事では、ここのコードを載せます 細かく見ていくと… さて、まとめとしてはこれくらいなのですが、以下、2,3,4,5のところについて詳しくみていきます モチベーション: Python のList / numpyの文字列型は意外とメモリを食う まず、いろいろな形式で保存した際のメモリサイズを測定してみましょう。 突然ですが、 Python において以下の文字列のメモリサイズは何byteでしょうか?ナイーブに考えると、char型で表現するとして、”a”は1byte、[“a”,”b”]は2byteですね クイズ "a" ["a","b"] np.array(["a","b]) np.array(["a","bc"]) こたえ 1: 50byte >>> import sys >>> sys.getsizeof("a") 50 でっかいですね!これは Python には GC などもついているので、各オブジェクトにそのための参照カウンタなどがついていたりするからです 2: 172byte >>> sys.getsizeof(["a","b"]) + sys.getsizeof("a") + sys.getsizeof("b") 172 超でかいですね! なぜこの計算になるかというと、 Python のListはそれぞれのオブジェクトへのポインタの配列となっているからです。 以下のように、普通にgetsizeofをすると72byteに見えるのですが、先述の通り、 ポインター の配列なので、これには”a”や”b”といった中身のメモリが含まれてないんですよね。 なので、文字を増やしても同じ長さです >>> sys.getsizeof(["a","b"]) 72 >>> sys.getsizeof(["alphabet","beta"]) 72 要 素数 に合わせて、PyObjectへの ポインター (8byte)が増えていっているのがわかりますね >>> sys.getsizeof([]) 56 >>> sys.getsizeof([0]) 64 >>> sys.getsizeof([0,1]) 72 3: 112byte numpyのarrayは1つのobjectとしてまるっと測定できます >>> import numpy as np >>> sys.getsizeof(np.array(["a","b"])) 112 1つの要素を増やすごとに4byte = 32bitを使ってますね >>> sys.getsizeof(np.array(["a","b","c"])) 116 >>> sys.getsizeof(np.array(["a","b"])) 112 >>> sys.getsizeof(np.array(["a"])) 108 numpyの配列は、要 素数 を増やせばほとんど Python のobjectのオーバーヘッドは考えなくて良くなりそうです 4: 112byte かといってなんでもnumpyにstrをつっこめばいいかというとそうではありません。 1文字増やしただけなのに、8byte = 64bit増えました。 >>> sys.getsizeof(np.array(["a","bc"])) 120 なぜかというと、実は、dtypeが”<U1”から”< U2 ”に変わっているからです。Uは Unicode という意味で、1は文字数です。numpyは行列演算ライブラリですから、各要素のメモリ上のサイズは一緒になるようにレイアウトすることが前提になっています。今までは Unicode を1文字扱えるメモリを確保して各要素を扱っていたのが、2文字のデータが入ってくるようになったので、すべての要素を2文字でようにしているわけですね。結果として、4byte x 2要素分 = 8byte確保されたメモリが増えたのでした。ASCIIの文字列の範囲でも1文字が4byteなのは、冒頭に述べたようなPython3.3以降の工夫などを採用せず、numpyではすべての文字をコードポイントそのまま、つまりint32的に扱っているからのようですね。行列演算ライブラリとしての設計上の要請だと思いますが。 データによって型が決まるのはこのときだけで、この後に長い文字列を追加しても自動的に型が変わるわけではありません。以下のように U2 の配列だと3文字目は落ちてしまいますね >>> arr = np.array(["a","b","cd"]) >>> arr[0] = "aaa" >>> arr array(['aa', 'b', 'cd'], dtype='<U2') さて、長々書いてきましたが、こういう感じなので、すっと思いつくコンテナはこういう欠点がありそうなことがわかります Listで持つと Python のObjectのオーバーヘッドが大きいため、要 素数 が多いとメモリを浪費する(1objectあたり50byteとかそういうオーダー) numpyの文字列型は文字列の最大長x4byteのメモリを確保する Listに関しては、長文であればまぁ別に良いとも言えますが、文ごとに配列に持っているみたいな場合だと、実は数十パーセントが Python 関連のメモリに使われてしまいます。心理学の日本語論文の1文は平均72字らしいですが、例えばこの記事の冒頭の75字の文は224byteになりますが、このうちの50byte近くが Python のわちゃわちゃなわけで、20%以上のメモリが浪費されていそうです >>> sys.getsizeof("学生の作文を添削した際、僕が「1文ずつが長い」と指摘すると、彼は「『何文字程度』というような基準はあるのでしょうか?」と尋ねて(反駁して?)きました。") 224 numpyの場合は、文字数が揃ってないと相当メモリ量が厳しそうです。また、 ナチュラ ルにやると1文字4byteになってしまいます。コレを防ぐには、文字列表現の型をちゃんといじればよさそうですね。 適切な型やencodingを選択する str型の1文字が何byteなのかというのは実は1,2,4byteの3通りがあります 。含まれる文字の unicode コードポイントの最大値が1,2,4byteのどの範囲に入るかによって変わります。そのため、こんな挙動になります >>> sys.getsizeof("a") # asciiのみなら1byte/文字 50 >>> sys.getsizeof("aa") 51 >>> sys.getsizeof("aaa") 52 >>> sys.getsizeof("あ") # ひらがなが入ると2byte/文字 76 >>> sys.getsizeof("ああ") 78 >>> sys.getsizeof("あああ") 80 >>> sys.getsizeof("ああa") # ascii範囲の文字も2byte/文字になる 80 >>> sys.getsizeof("💫") # 絵文字が入ると4byte/文字 80 >>> sys.getsizeof("💫💫") 84 >>> sys.getsizeof("💫💫💫") 88 >>> sys.getsizeof("💫💫a") # ascii範囲の文字も4byte/文字になる 88 ですから、絵文字みたいな文字を削除するとさくっとメモリ専有量が半分になるかもしれません。 さらに、 UTF-8 のような 可変長エンコーディング を使うとより圧縮できます。1文字にXbyte使う、ということを先に決めず、適応的に変更するためより効率よく表現できる場合があります。 >>> sys.getsizeof("今日はいい天気ですね。It's nice weather today.") 144 >>> sys.getsizeof("今日はいい天気ですね。It's nice weather today.".encode("UTF-8")) 90 >>> sys.getsizeof("今日はいい天気ですね。") 96 >>> sys.getsizeof("今日はいい天気ですね。".encode("UTF-8")) 66 この場合、型がbytes型になることに注意が必要です >>> "今日はいい天気ですね。".encode("UTF-8") b'\xe4\xbb\x8a\xe6\x97\xa5\xe3\x81\xaf\xe3\x81\x84\xe3\x81\x84\xe5\xa4\xa9\xe6\xb0\x97\xe3\x81\xa7\xe3\x81\x99\xe3\x81\xad\xe3\x80\x82' bitarrayを使う 特定の ユースケース では文字種別がもっと少ない場合がありそうです。 例えば、英語の大文字とスペースだけしか入ってこないテキストであれば、28種類しか トーク ンは存在しないわけで、これは2^5=32>28なので本当は5bitで表記できそうです。 塩基配列 のようなデータの場合、 トーク ンはATCGの4種類、つまり2bitで表せます。ここまでくると、いい感じの型が用意されてないですね。こういうときに bitarray が便利です。 github.com より頑張る方法として、複数の文字を1つの単位にpackする方法がありますが、そこまではだるいので今回はやりません。本当は例えば、5種類の トーク ンであれば、何も工夫しなければ、3bit (2^3=8>5) をそれぞれの トーク ンに割り当てることになりますが、5x6 =30 < 32 = 2^5なので、2つの トーク ンを3bit x 2 = 6bitではなく、5bitであらわせます(2 トーク ン目として6をかけているのは、2 トーク ン目が存在しない場合を表さないといけないので)。また、 トーク ンの出現頻度に応じてビット列を割当てる Huffman Codingもできる のですが、これも今回はやめときましょう。 では、ここではbitarrayを用いて大量の 塩基配列 データを持つことを考えましょう。bitarrayのobjectをたくさん持つと、 Python のobjectがいっぱい生成されてしまうので、長いbitarrayのobjectを用意し、各stringのindexを別の配列に持つようにします from random import choice, randint from bitarray import bitarray import numpy as np # 文字種をさだめます。塩基配列を例に取ります alphabets = [ "A" , "T" , "C" , "G" ] alphabet_size = len (alphabets) # 何bitを1文字に割り当てればよいかを決め、bit列(bitarrayクラスのインスタンス)と実際の文字の対応関係をdict型で作ります bits_per_char = int (np.ceil(np.log2(alphabet_size))) dictionary: Final[Dict[ str , bitarray]] = { ch: bitarray( bin (idx)[ 2 :].zfill(bits_per_char)) for idx, ch in enumerate (alphabets) } dictionaryの中身はこんな感じです >>> dictionary {'A': bitarray('00'), 'T': bitarray('01'), 'C': bitarray('10'), 'G': bitarray('11')} 変換するデータを用意します。ここでは1文字以上最大長100文字の一様分布の長さの 塩基配列 を10000シーケンス用意します max_len = 100 n = 10000 data_to_convert = [ "" .join([choice(alphabets) for _ in range (randint( 1 ,max_len)) ]) for x in range (n)] 変換しましょう。 indices に各シーケンスの開始地点を持っておきます converted_data = bitarray() indices = [] for seq in data_to_convert: indices.append( len (converted_data)) converted_data.encode(dictionary,seq) indices = np.array(indices,dtype=np.int32) # numpyのint32arrayに変換しておきましょう これで、変換ができました。bitarrayのindex accesorを直接叩くとbit列が得られますが、 >>> converted_data[:100] bitarray('1001011001011011100111000110110111100100101000010011000100101011010000111111000111110011000101001010') さきほどの辞書を用いて復号すると、きちんと文字列が得られます >>> seq = converted_data[indices[0]:indices[1]] >>> "".join(seq.decode(dictionary)) 'CTTCTTCGCTGATCGTGCTACCATAGATACCGTAAGGGATGGAGATTACCCCTATCGCGCACCTA' 一致しますね。 >>> data_to_covert[0] 'CTTCTTCGCTGATCGTGCTACCATAGATACCGTAAGGGATGGAGATTACCCCTATCGCGCACCTA' メモリ使用量を確認してみると… >>> sys.getsizeof(converted_data) + sys.getsizeof(indices) 170282 >>> sys.getsizeof(data_to_convert) + sum([sys.getsizeof(x) for x in data_to_convert]) 1082618 1,082,618byte - 170,282 = 912,336byte 20%弱くらいになってますね!うれしーい。 ここでは、冒頭で書いた2,5の工夫を両方取り入れていますが、 寄与分 を試算してみると、だいたいこんな感じっぽいですね 2bitで表現した分の寄与 (もともとのstr(ascii範囲)は1byte/文字=8bit/文字ぐらい) 10,000シーケンス x 平均50文字 x -6bit = -3,000,000bit = -375,000byte 10,000object -> 1objectになったことの寄与 減少分: -50byteぐらい x 10,000シーケンス = -500,000byte numpy配列の追加分: 4byte (int32) x 10,000シーケンス = +40,000byte 実際には、さまざまな ユースケース でどの工夫がどれくらいきくかはデータ依存かと思いますが、これくらい削減できることもあるということを知っておくともしかしたら役立つことがあるかもしれません MNTSQ株式会社( モンテスキュー )では大量の文字列を扱うエンジニアを募集しています!(が、実際あんまりこういうことはしませんね…) www.mntsq.co.jp この記事を書いた人 堅山耀太郎 MNTSQ社で取締役として 機械学習 ・ 自然言語処理 に関わるもろもろをやっています。好きな食べ物は担々麺です。
アバター
入社して3ヶ月が経った。事業戦略・組織文化・プロダクトに対する解像度はだいぶ高まった実感があるが、実はまだメンバー1人1人のことを良く知らない。 そうだ、Slackのログを分析しよう。 当社では多くのコミュニケーションをSlackで行う。また、情報のopennessを重視しており、Slackのpublic channelの割合は高い水準を維持している。 private channelもそれなりにあるが、弊社のprivate channelの多くはメンバーのロールに対応するように作られており、実際にはほとんどのメンバーが参加しているため、opennessは保たれていると言って良い。 Public and private channel Slackには アナリティクスダッシュボード がという機能がある。上のグラフはアナリティクス ダッシュ ボードの スクリーンショット だ。簡単な分析ならこれで十分かもしれない。しかし、メンバー1人1人のことを知るにはもう少しパーソナライズされた情報が欲しい。 Slack API を使ってメッセージを取得する Slack API を使えば全メッセージを取得可能だ。使用する API と取得するデータは次の通り。 users.list 各ユーザの名前を取得する。 bot やゲストは対象外とする。 conversations.list channelの一覧を取得する。ここではpublic channelのみとする。 conversations.history 各channelのメッセージを取得する。 conversations.replies 各メッセージのスレッドを取得する。 ユーザとchannelは一度に全て取得できるが、メッセージはサイズが大きいので分割して取得する必要がある。conversations. history とconversations.repliesは Tier 3 なので毎分50回となるように間隔を空けて API を呼び出す。 前処理 正規表現 でテキストに含まれるメンション・URL・コードブロックを除去する。 また、事務やテスト用途のchannelはノイズになるので除外する。個別にチャネル名を指定しても良いが、手作業でやるには数が多いので、ここでは異なり語数で 閾値 を下回るものを除外する。他にも トーク ン比(Type-Token Ratio)の 閾値 を使うことも検討したが、 トーク ン比の分母である延べ語数がchannelごとに大きく異なりそのままでは比較できないので見送る。 データの外観 期間: 2020/5/1 - 2021/4/30 ユーザ数: 25 channel数: 69 メッセージ数: 35200 リアクション数: 19678 ユーザ数の推移 メッセージの投稿ユーザを月毎にユニークカウントした。この1年で10人増えて25人になったようだ。めでたい。 ユーザごとのメッセージ数 ユーザごとにメッセージ数、上位10件をプロットした。1位はBoard4人を抑えてSEの西村だ。彼はMNTSQの サラダ技術顧問 でもある。メンバーは入社時期が異なるので単純な比較はできないが、それでもサラダ技術顧問の圧倒的存在感には恐れ入る。 人気のリアクション リアクション数、上位10件をプロットした。なんとポジティブなリアクション。メンバー同士の高いリスペクトが伺える。 ユーザごとのリアクション数 ユーザごとのリアクション数、上位10件をプロットした。リアクション最多は4月に入社したばかりであるはずの広報の高井だ。この勢いは凄まじい。社内のあらゆる場面でリアクションし、活性化を促している証拠だろう。 “口癖”を分析する メンバーごとにTF-IDFの総和を求め、値が高い上位10件を“口癖”とした。 トーク ナイザーには SentencePiece を使う。SentencePieceのパラメータは次の通り。 spm.SentencePieceTrainer.Train( input = './data/texts.txt' , model_prefix= 'models/sentencepiece' , vocab_size= 8000 , character_coverage= 0.9995 , ) TF-IDFのパラメータは次の通り。 vectorizer = TfidfVectorizer( strip_accents= None , token_pattern= r'(?u)\S\S+' , max_df= 0.01 , max_features= 10000 ) SentencePieceで1文字の トーク ンが大量に作られるが、それらは token_pattern=r'(?u)\S\S+' で除外する。また、 max_df=0.01 で「です」や「から」を除外する。代表してBoard4人の結果を次に示す。 率直に述べると、「わかる」という気持ちになる。 板谷 の「...!」や生谷の「あー」「はい」はものすごくよく見るし、安野の「done」「あざます」はSlackに限定されずリアルでもよく使われる。堅山は上から下まで全てそれっぽい。他のメンバーに対しても分析してみたが、非常に興味深い結果で、社内でも好評だった。 職種ごとの人気のリアクション 職種をBoard・Engineer・Legal・Sales/CS/PRの4つに分類する。SalesとCSとPRは、あまり詳細にすると人数が少なくなり統計的性質の観察が難しくなるので、便宜上同じカテゴリにした。単純なカウントでは前述した人気のリアクションと被るので、少し工夫をする。職種ごとの特徴を出したいのでTF-IDFで全員がよく使うリアクションが上位に来ないようにする。TF-IDFのパラメータは次の通り。 vectorizer = TfidfVectorizer( strip_accents= None , token_pattern= r'(?u)\S\S+' , stop_words=[ '+1' ], ) 職種ごとのTF-IDFの総和を求め、上位5件をプロットしたものが次の図である。 職種ごとに結構好みが分かれているように見える。Boardに女性はいないはずだが、なぜかwoman-gesturing-okが上位にある。Sales/CS/PRは状態管理のためにリアクションを使っているようだ。 “返信”の関係を可視化する “返信”とはチャンネル内またはスレッド内のあるメッセージと1つ前のメッセージの関係で、チャンネル内では2つのメッセージの時間間隔が1時間以内のものとする。“返信”の数を重みとする隣接行列をPCAで平面に写し、職種ごとに色分けしたものが次の図である。 図を見ると、なかなか綺麗に職種で分かれていることがわかる。 (0.05, -0.1)付近の緑は弁護士で、他のLegalとは仕事が異なりSalesチームと活動しているため、Salesの付近にあると考えられる (0.0, -0.05)付近の赤はCSで、顧客コミュニケーションのために他職種のメンバーと頻繁に情報共有しているため、他職種間の中心にあると考えられる (-0,05, 0.05)付近の橙3点はEngineerの中でも 機械学習 を主に扱うメンバーで、彼ら(私もその1人だが)は アルゴリズム 開発のためにLegalと活発に議論しているため、Legalの付近にあると考えられる 実際にはSlack以外でも GitHub Issueやオフラインでの議論も活発に行われており、この図に表れない関係は存在するものの、例えば平面上で距離が遠いLegalとSales/CS/PRのコミュニケーションに改善点はないか?といったフィードバックを得ることができる。
アバター
特許・契約書・ 有価証券報告書 ・企業関連ニュースなど、実応用上の 自然言語処理 では、会社名を認識したいという場面に非常に多く出くわす。 会社名らしい文字列をテキストから抽出することは、 形態素解析 器の辞書を用いたり固有表現抽出モデルを学習することである程度実現される一方で、抽出した会社名をレコード化して分析などに用いる際には、いわゆる 名寄せ の問題が発生する。 自然言語処理 における 名寄せ に似た問題は、エンティティリンキングや共参照解析といったアプローチで探求されており、実応用上は前者のアプローチが採られることが多い印象がある。 *1 名寄せ タスクをエンティティリンキング的に解くためには、帰着先の知識ベース・辞書が予め存在していることが必要だが、研究の文脈では知識ベースとして Wikipedia が採用されることが多い。 Wikipedia を用いる利点は多くあり、様々なエンティティ種に対してそこそこの カバレッジ がある点、単なる辞書に比べた場合にマッチングに使える素性がエンティティの文字表層以外にも豊富にある点、ページへのアクセス数などテキスト外の統計情報も使える点、データ処理のツールが充実している点などが挙げられる。 一方で、会社名のような特定エンティティ種に対してきちんと対応していきたい場合、 Wikipedia の カバレッジ や信頼性では満足できないこともある。 当記事では、そのような場合にクイックに利用を検討できる日本の会社名辞書を2つ紹介する *2 : 国税庁 法人番号データ 全件ダウンロード可能・商用利用可能 NISTEP *3 企業名辞書 全件ダウンロード可能・商用利用可(CC-BY-SA) 国や事業者が整備している企業名関連データは他にも存在し、たとえば以下のようなリソースについては当記事では触れていない: EDINET ( 金融庁 管轄): ダウンロード可能かつ商用利用可能であり財務分析に有用 事業所母集団データベース ( 総務省 管轄): 公的機関の統計調査用途;事業所の利用は不可(?) 登記情報提供サービス (法務局管轄):網羅性は高いが、閲覧有料(件数あたり課金) 特許情報標準データ , 整理標準化データ(2019年まで) ( 特許庁 管轄): 要ダウンロード申請、特許分析に有用 その他、企業経済・与信情報のような調査分析用途の有償企業情報サービス: 帝国データバンク ・ 東京商工リサーチ ・ 東洋経済 データサービス・各種与信情報、等 国税庁 法人番号データ あらゆる法人に対して振られる法人番号データと、その法人名が対応付けられたデータである。 法人番号とは 番号法 により定められる、税金や保険の手続きに用いられる番号のようだ。 登記手続きに用いられる 会社法 人等番号と並んで、あらゆる法人に紐づくID情報であるという意味で、企業名のマスタ情報に適した参照情報であるといえる。 https://www.houjin-bangou.nta.go.jp/setsumei/ 「 行政手続における特定の個人を識別するための番号の利用等に関する法 律」(以下「 番号法 」といいます。)に基づき、法人に対して法人番号を指定し、指定後速やかに、商号又は名称、本店又は主たる事務所の所在地及び法人番号を公表するとともに、対象の法人へ法人番号を通知しています。 抽出可能な情報は リソース定義書 から参照でき *4 、「法人番号」と「商号又は名称」や「英語表記」「フリガナ」、場合によっては「国内所在地」といったフィールドが分析の基本となるだろう。 また、以下で紹介するフィールドを用いることで、法人格を考慮した 名寄せ 処理や、合併等の組織再編や商号変更・組織変更などを考慮した企業の同一性認識を行うことができる: 「法人種別」:いわゆる法人格に相当する情報で、以下のような区分コードが入っている。数にしては株式会社がほとんどを占めるが、 法人格は多岐にわたる ので「その他の設立登記法人」も多く存在する。 "101": "国の機関", "201": "地方公共団体", "301": "株式会社", "302": "有限会社", "303": "合名会社", "304": "合資会社", "305": "合同会社", "399": "その他の設立登記法人", "401": "外国会社等", "499": "その他", 「承継先法人番号」「変更事由の詳細」:いわゆる商号変更履歴を追うのに役立つ。登録法人の商号変更履歴が 国税庁 データのサイトで閲覧できるが、おそらくこのフィールドの情報を使用していると推測される。参考のために、商号変更履歴の多い企業の例を挙げておく。 三菱ケミカル 合併対象の「承継先法人番号」の値の例: 6010001146760 合併対象の「変更事由の詳細」の値の例: 平成31年4月1日東京都千代田区丸の内一丁目1番1号三菱ケミカル株式会社(6010001146760)に合併し解散 「訂正区分」:商号変更・登記抹消・削除といった登録データの修正操作情報が入っている。新規追加以外の操作を持つ法人名は 重複登録 されることになるので、重複・バージョン管理が必要な場合に役立つ。 "01": "新規", "11": "商号又は名称の変更", "12": "国内所在地の変更", "13": "国外所在地の変更", "21": "登記記録の閉鎖等", "22": "登記記録の復活等", "71": "吸収合併", "72": "吸収合併無効", "81": "商号の登記の抹消", "99": "削除", 国税庁 法人番号データを使用している事例としては、TISが作成・公開されているJCLdicという企業名辞書がある。 企業名の重複除去に加えて、簡易的に表記ゆれパターンも生成していて、企業名の 名寄せ を試す用途には便利な辞書となっている。 www.tis.co.jp このデータを辞書に、BCCWJや 毎日新聞 データを コーパス にした固有表現抽出のデー タセット も生成されている。 ただし、企業名には一般的な名称が混じっていたり、 国税庁 データの企業名分布は特に非常にLong-tailであるという都合上、企業名の固有表現ラベルを辞書マッチで生成するのはなかなか難しそうだと推察される。 Long-tail性の参考として、 中小企業庁が公開している中小企業と大企業の数 (2016年)は以下のような内訳となっている *5 : 大企業:1万1157社(0.3%) 中小企業:357.8万社(99.7%) 中小企業の定義 また、 日本取引所グループが公開している 上場企業の総数は3,770社(2021/04/27)となっている。 資本金や法人格別などの企業数内訳に関する統計については、 国税庁 の統計調査結果でも公開されている: www.nta.go.jp NISTEP企業名辞書 国税庁 データの企業名分布は特に非常にLong-tailであるという点を指摘した。 これはあらゆる法人名を登録するという都合上避けられない問題であり、 名寄せ を文字表層ベースで行う場合には特に問題になる。 具体的には、非常に似通った企業名であっても、一方は上場企業、もう一方はほとんど知られていない会社というケースなどがそれにあたり、表記ゆれ対応泣かせの問題が 国税庁 データでは多く発生する。 こういった問題に対して関心のある企業群を絞りたい需要が発生するが、特許分析・産業 イノベーション 研究の一貫としてNISTEP企業名辞書というデータが公開・管理されている。 最新版のマニュアルは以下のページのダウンロードページから参照できる。 www.nistep.go.jp 管理対象となる企業名の収集基準を以下に項目で示す(ver.2020_2マニュアルより引用;詳細はマニュアル参照): 企業名辞書に掲載する企業は、原則、次の 5 つの条件の何れかを満足する企業の 論理和 で構成する。 ① 特許出願数累積 100 件以上 ② 株式上場企業 ③ 特許出願数の伸び率大 ④ NISTEP 大学・公的機関名辞書掲載企業 ⑤ 意匠・商標登録数 ⑥ 大学発ベンチャー 企業 (中略) その他、上記条件に該当しない企業として、次の事由による掲載企業がある。 ⑦ 持株会社 制移行に伴い設立された事業会社 ⑧一部事業の譲渡に伴い設立された会社 ⑨名称変更又は吸収合併した企業が登録事由に該当 ⑩その他 利用事例としては、特許情報標準データを加工した IIPパテントデータベース *6 に対して、特許申請人の 名寄せ に当辞書が用いられている事例が紹介されている。 www.nistep.go.jp ダウンロード形式としては、 RDB にインポート可能なテーブルテキスト形式と、xlsx形式とがある。 マニュアルより企業名辞書のERDを引用する: 企業名辞書ERD(NISTEP 企業名辞書 利用マニュアル (ver.2020_2 対応版)より引用) 件数を比較すると、 国税庁 法人番号データ(20200731)ののべ件数が4,874,674件(重複含む)に対し、NISTEP企業名辞書(ver.2020_2)ののべ件数が24,414件。 国税庁 データとの類似点・差分点をベースに列情報の比較を行うと、商号変更履歴情報に加えて、連結企業情報や財務・業種情報などがデフォルトで含まれるのは非常に便利な点である。 また、外部情報との連携も対応しているため必要に応じて参照されたい。 以下、分析に有用なテーブル・列情報をいくつか紹介する(「」で囲まれたものはマニュアル内に解説のあるテーブル・フィールド名): 商号変更履歴情報 「沿革テーブル」 / history , history_id 名称使用開始事象: {1:"設立", 2:"名称変更"} 名称使用終了事象: {1:"現存", 2:"名称変更", 3:"合併", 4:"破産", 5:"清算"} 連結企業情報(親子会社関係) 「連結企業テーブル」 財務・業種・ ベンチャー タイプ情報 「EDINET コードテーブル」 「 証券コード テーブル」 「企業規模テーブル」 「業種( 証券コード 協会)テーブル」 「業種(日本標準産業分類)テーブル」 「 大学発ベンチャー テーブル」 各種外部テーブル連携 IIP パテントデータベースとの接続テーブル NISTEP大学・公的機関名辞書との接続テーブル 東洋経済新報社 『日本の会社データ4万社』との接続テーブル 等(他詳細は HP参照 ) 「法人格コード」 前株・後株・中株が区別されてる点に注目 「法人番号」 国税庁 データと連携可能だが、法人番号に紐付けられていない事例も存在するため全てに対して単純なJOINはできない 法人番号を値に持つもの: 12365件 法人番号を値に持たないもの: 12049件 以上、企業名の解析に有用な公開辞書データを2種類紹介した。 この記事はMNTSQ株式会社の業務時間内に書かれた。 MNTSQ株式会社では業務 ドメイン 知識の深化と 自然言語処理 技術の適用による製品の高品質化にご協力いただける方を募集しています: hrmos.co この記事を書いた人 稲村和樹 自然言語処理 エンジニア。爬虫類が好き。 *1 : 抽出と 名寄せ (エンティティリンキング)をend-to-endで解くモデルも提唱されている: GitHub - facebookresearch/GENRE: Autoregressive Entity Retrieval *2 : ライセンス等 利用規約 の詳細は公式マニュアルを参照ください *3 : 文部科学省 直轄の研究機関: https://www.nistep.go.jp/ *4 : ダウンロードできる csv にはヘッダがないので対応付けは目視で頑張ってください *5 : 2020年の 国税庁 データでは重複排除前の総件数は約490万件 *6 : 商用利用不可
アバター
この記事では、 Google スプレッドシート で当番表を作り、 Google Apps Scriptで当番をSlack通知する機能を実装する。 この記事は以下の記事の続編である。 未読の方は先に読んでおくことをお勧めしたいところだが、実はあまり関係が無い。 note.com 西村、サラダ技術顧問に就任するってよ Googleスプレッドシートのサンプル Slack IDの取得方法 スクリプトを書く スクリプト エディタを開く Google Apps Scriptでスプレッドシートを読み込む Google Apps ScriptからSlackに通知する 「当番リスト」シートからSlack IDを取得する SlackのIncoming Webhook URLを発行する Slack通知を実装する スクリプトの全体像 定期実行を仕掛ける 機能拡張編 事前に通知する 複数人への通知に対応する スプレッドシートで当番を管理する際の問題点 このエントリーで対応していないポイント Slack Appへの移行 当番表で設定した日数を過ぎた場合の対応 当番を自動で決める処理を入れる 土日の通知を避ける 仲間募集中 この記事を書いた人 西村、サラダ技術顧問に就任するってよ さて、私はある日サラダ技術顧問に就任する運びとなった。 そこで任されたのが当番表の作成である。 弊社では「黙っていても健康が運ばれくる」という触れ込みの元、サラダの サブスクリプション の会員を増やし、サラダ宅配の労力の低減を図ろうとしている。 これまでは当日の朝にランダムに抽選されており、予定が読めずその時に予定がある人に債権が溜まっていく問題があった。 債権が溜まってもずっと解消されない状態が慢性化しており、抽選されても取りに行かなくても良くなってしまい、あまり精神衛生上良くなかった。 この当番制はそれを解決する試みである。 当番表を作ること自体は難しくはない。 おもむろに Google スプレッドシート を開き、日付と当番の列を作り、そこに土日祝を避けて当番を順番に入力していくだけである。 とは言え、 Google スプレッドシート を開いて誰が当番かなど誰も確認しないので、誰もサラダを取りに行かない事態が発生してしまう。 そこでSlack通知をすることになった。 Google Apps Script(GAS)の出番である。 developers.google.com Google スプレッドシート のサンプル サンプルとして、以下のような Google スプレッドシート を利用している。 「当番表」シート 「当番リスト」シート Slack IDの取得方法 SlackのIDについては、Slackで対象の人の名前をクリックする→「全プロフィールを表示する」→「その他」→「メンバーIDをコピー」から可能だ。 これが結構めんどくさいのだが、Slackの仕様変更によりSlack IDを利用しないと通知できないようになっている。 スクリプト を書く スクリプト エディタを開く 通知を導入したい スプレッドシート を開く。 そこから スクリプト エディタを開く。 メニューから「ツール」→「 スクリプト エディタ」から開くことができる。 ちなみにここの画面遷移は微妙で、 複数アカウント で利用している場合はうまく遷移できない事がある。 その場合は https://script.google.com/d/hogehoge... の https://script.google.com/ と hogehoge の間に u/0/ や u/1/ など入れてみると良い。 すると以下のような画面が開いたはずだ。 ここからは一応テックブログらしく、コードらしきものを貼っていきたい。 以下の箇所にコードを入力していって欲しい。 function myFunction() { // この部分 } Google Apps Scriptで スプレッドシート を読み込む まずシートを読み込む。 const spreadsheet = SpreadsheetApp.openById( 'シートのID' ) 「シートのID」は、URLの https://docs.google.com/spreadsheets/d/12345asdfghj-ASDFGHJK2345678hjklasdfg/edit#gid=0 となっている「12345asdfghj-ASDFGHJK2345678hjklasdfg」の部分だ。 ハイフンなど挟まっていてダブルクリックで選択できないのが難点である。 次に、読み込んだ スプレッドシート から「当番表」シートを開く。 const sheet = spreadsheet.getSheetByName( '当番表' ) シートから入力のある最後の行を取得する。 const lastRow = sheet.getDataRange().getLastRow() とりあえず一覧をコンソールに出力してみるとしよう。 for ( var i = 2; i <= lastRow; i++) { console.log(sheet.getRange(i, 1).getValue()) console.log(sheet.getRange(i, 2).getValue()) } ここまでの全体像は以下のようになる。 function myFunction() { const spreadsheet = SpreadsheetApp.openById( 'シートのID' ) const sheet = spreadsheet.getSheetByName( '当番表' ) const lastRow = sheet.getDataRange().getLastRow() for ( var i = 2; i <= lastRow; i++) { console.log(sheet.getRange(i, 1).getValue()) console.log(sheet.getRange(i, 2).getValue()) } } ここまで書き終えたら、上の「実行」ボタンをクリックしてみよう。 実行時に小難しい権限確認のウィンドウが表示されるが、「許可」すればOKだ。 すると以下のような実行ログが表示されるはずだ。 これを参考に、今日の日付の担当者を取得しよう。 JavaScript なので、Date()クラスが利用できる。 const nowDate = new Date () ただ、出力してみると、現在時刻なので00:00:00ではなくてマッチできない事が分かる。 Sun Apr 12 2021 11:39:44 GMT+0900 (Japan Standard Time) これをマッチさせる方法はいくつかあるが、ここでは文字列にしてしまう事でマッチさせる事にする。 const nowDateStr = Utilities.formatDate(nowDate, 'JST' , 'yyyy/MM/dd' ) すると以下のような形式で取得できる。 2021/04/12 それでは先ほどのforループを更新して、当日の当番を取得しよう。 for ( var i = 2; i <= lastRow; i++) { const dateVal = Utilities.formatDate(sheet.getRange(i, 1).getValue(), 'JST' , 'yyyy/MM/dd' ) if (dateVal === nowDateStr) { console.log(sheet.getRange(i, 2).getValue()) break } } 実行すると当日の当番が取得できたはずだ。 山田 Google Apps ScriptからSlackに通知する 続いてSlack通知を実装していこう。 「当番リスト」シートからSlack IDを取得する まず先に、先ほどの当番を変数に格納しておく。 var toubanUser = null for ( var i = 2; i <= lastRow; i++) { const dateVal = Utilities.formatDate(sheet.getRange(i, 1).getValue(), 'JST' , 'yyyy/MM/dd' ) if (dateVal === nowDateStr) { toubanUser = sheet.getRange(i, 2).getValue() break } } 次に、当番を取得するのと同じ方法でSlack IDを取得しよう。 const slackIDSheet = spreadsheet.getSheetByName( '当番リスト' ) const slackIDLastRow = slackIDSheet.getDataRange().getLastRow() for ( var i = 2; i <= slackIDLastRow; i++) { const cellVal = slackIDSheet.getRange(i, 1).getValue() if (cellVal === toubanUser) { console.log(slackIDSheet.getRange(i, 2).getValue()) break } } これで当番が取得できたはずだ。 取得できたら、先ほどと同様に変数に格納するように スクリプト を更新する。 var todayToubanID = null for ( var i = 2; i <= slackIDLastRow; i++) { const cellVal = slackIDSheet.getRange(i, 1).getValue() if (cellVal === toubanUser) { todayToubanID = slackIDSheet.getRange(i, 2).getValue() break } } SlackのIncoming Webhook URLを発行する 取得した情報をベースに、Slackに通知していくのだが、そのためにはIncoming Webhook URLを発行する必要がある。 Slackの左上から、「その他管理項目」→「アプリを管理する」 するとSlack App Directoryが表示されるので、「カスタムインテグレーション」をクリック。 ここでIncoming Webhookを追加するのだが、弊社は追加設定済みのため、以下を参考に追加いただきたい。 slack.com なお、Incoming Webhookは現時点で非推奨で、Slack Appの利用が推奨されているのだが、まだ移行ができていないため、ご了承いただきたい。 「Slackに追加」をクリックする。 投稿したいチャンネルを選択して、「Incoming Webhookインテグレーションの追加」をする。 すると以下のようにWebhook URLが発行されるので、それを利用する。 Slack通知を実装する 必要情報が揃ったので、Slackへの通知を実装していこう。 まず先ほど取得したWebhook URLと投稿先のSlackチャンネルを定数として宣言する。 const SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/HOGE/FUGA/PIYO const SLACK_CHANNEL = '#touban' Slackに送信する文字列を組み立てていく。 まず表示する文字列を作る。 var msgStr = '' if (todayToubanID !== null ) { msgStr = "今日の当番は <@" + todayToubanID + "> です! \n " } ポストするチャンネルを含んだ JSON を組み立てる。 const jsonData = { "text" : msgStr, "channel" : SLACK_CHANNEL } const payload = JSON.stringify(jsonData) Google Apps Scriptでメソッド渡す際に必要な諸情報を含めたハッシュを作る。 const options = { "method" : "post" , "contentType" : "application/json" , "payload" : payload } それをWebhook URLにPOSTする。 UrlFetchApp.fetch(SLACK_WEBHOOK_URL, options) 書けたら実行してみよう。 実行する際に追加の権限を求めるダイアログが表示されるので、「許可」する。 すると、以下のような投稿がSlackに表示されたはずだ。 おめでとう、これで実装は完了だ。 スクリプト の全体像 全体としては以下のような スクリプト になったと思われる。 function myFunction() { const spreadsheet = SpreadsheetApp.openById( 'シートのID' ) const sheet = spreadsheet.getSheetByName( '当番表' ) const lastRow = sheet.getDataRange().getLastRow() const nowDate = new Date () const nowDateStr = Utilities.formatDate(nowDate, 'JST' , 'yyyy/MM/dd' ) var toubanUser = null for ( var i = 2; i <= lastRow; i++) { const dateVal = Utilities.formatDate(sheet.getRange(i, 1).getValue(), 'JST' , 'yyyy/MM/dd' ) if (dateVal === nowDateStr) { toubanUser = sheet.getRange(i, 2).getValue() break } } const slackIDSheet = spreadsheet.getSheetByName( '当番リスト' ) const slackIDLastRow = slackIDSheet.getDataRange().getLastRow() var todayToubanID = null for ( var i = 2; i <= slackIDLastRow; i++) { const cellVal = slackIDSheet.getRange(i, 1).getValue() if (cellVal === toubanUser) { todayToubanID = slackIDSheet.getRange(i, 2).getValue() break } } const SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/HOGE/FUGA/PIYO' const SLACK_CHANNEL = '#touban' var msgStr = '' if (todayToubanID !== null ) { msgStr = "今日の当番は <@" + todayToubanID + "> です! \n " } const jsonData = { "text" : msgStr, "channel" : SLACK_CHANNEL } const payload = JSON.stringify(jsonData) const options = { "method" : "post" , "contentType" : "application/json" , "payload" : payload } UrlFetchApp.fetch(SLACK_WEBHOOK_URL, options) } 定期実行を仕掛ける 最後に忘れてはならないのが、定期実行である。 毎朝手動で誰かが実行ボタンを押すのは避けたいからだ。 まず スクリプト エディタの左メニューから「トリガー」をクリックする。 続いて右下の「トリガーを追加」をクリック。 以下のように設定した: 実行する関数を選択: myFunction デプロイ時に実行: Head イベントのソースを選択: 時間主導型 時間ベースのトリガーのタイプを選択: 日付ベースのタイマー 時刻を選択: 午前9時〜10時 エラー通知設定: 今すぐ通知を受け取る 動作確認はメソッドの日付やトリガーの時刻を編集して行うと良いだろう。 機能拡張編 事前に通知する 初期実装としては上記のとおりだが、以降も地道な改善が成されている。 当日の朝に当番が判明したところで、予定が入ったなどの場合に対応ができないため、前日に通知されることになった。 部分のみだが、次の日を取得するには、以下のようにする。 var dt = new Date () dt.setDate(dt.getDate() + 1) 複数人への通知に対応する この スクリプト は様々な当番がある箇所に流用されている。 弊社ではブログポストを当番制で回しており、来週は誰が書くのかという通知に利用している。 また、隔週でデプロイを行っており、その担当の通知にも利用している。 デプロイは複数人で担当して行っているため、複数人に対してメンションをする必要がある。その場合は、以下のように配列などに代入すれば良い。 toubanUser = sheet.getRange(i, 2).getValue() // ↓ slackIDLastCol = slackIDSheet.getDataRange().getLastRow() const toubanUsers = new Set() for ( var j = 2; j <= slackIDLastCol; j++) { const val = sheet.getRange(i, j).getValue() if (val !== null && val !== '' ) { toubanUsers.add(val) } } Slack IDの検索も同様にループ処理をする。 そしてSlackに送信するメッセージ文字列の組み立てをループ処理に変更すると良い。 var msgStr = '来週のデプロイ担当は' for (i = 0; i < nextWeekToubanIDs.length; i++) { msgStr += " <@" + nextWeekToubanIDs [ i ] + "> " } msgStr += 'です!' スプレッドシート で当番を管理する際の問題点 色々便利な Google Apps Script(GAS)と スプレッドシート だが、問題が無いわけではない。 スプレッドシート は別の スプレッドシート のシートを参照できないので、このような使い方をした場合、Slack IDのシートが通知したい スプレッドシート の分だけ増殖してしまう。 この問題に対する解決策は現状見つけられていない。 このエントリーで対応していないポイント このエントリーで解説していない部分があるので、まとめておく。ぜひともトライしてみて欲しい。 Qiitaなどで記事にしていただいても良いだろう。 Slack Appへの移行 先にも述べた通りだが、カスタムインテグレーションは非推奨となっており、そのうち利用できなくなる可能性がある。 それを回避するためにはSlack Appへの移行をする必要がある。 当番表で設定した日数を過ぎた場合の対応 設定した最後の日の担当まで来るとサイレントに通知が来なくなる。これを回避するためには、最後の日を検知して「そろそろ設定が無くなりそうだ」と通知すると良い。 当番を自動で決める処理を入れる 基本的に順番に割り振っていくことが予想されるので、切れれば自動的に補充されるような処理も検討できるだろう。 そこで問題になってくるのが祝日の処理だろう。 土日の通知を避ける 1日前の通知だと、つまり月曜日のサラダ当番は日曜日に通知されてしまう。 これを回避するためには、土日を判定してその次の日をピックする必要があるだろう。 仲間募集中 弊社ではこのように、日常の些細な作業にもITエンジニア的なアプローチで改善を試みている。 ローマは一日してならず、運用改善も同様に地道な努力の積み重ねなのである。 弊社はこういった地道な改善ができるエンジニアを募集中である。 hrmos.co hrmos.co hrmos.co hrmos.co なお#saladチャンネルで利用しているBROS TOKYOのサラダもオススメだ。 1ヶ月契約して毎日食べ続けると800円弱というお手頃価格で、結構ボリュームのあるサラダを頼めて、好きなトッピングを3点選べて、体にも良いのでオススメだ。ちなみに私は契約していない。 BROS TOKYO この記事を書いた人 Yuki Nishimura 雑食系エンジニア
アバター
背景 久しぶりに NFS を触るかもしれないということで、ちょっと素振りをしてみました。 NFS を最後に触ったのは10年くらい前、まだあの頃は学生だったと思います。そんなわけで、久々なのであらためて、手順を頭に入れなおしてみました。 今回試す条件は緩いものなので、プロダクトのなかで使う場合やより高い要求がある場合には、ここの手順だけでは圧倒的に足りませんが、大まかな流れをつかめればいいかなと思います。 NFS とは ここでは簡単に NFS について説明します。 ネットワークをまたいでストレージをマウントさせるための技術 最新の プロトコル のバージョンは4 実質的な最初のバージョン、2が発表されたのは 1984 年のこと。私などよりも年上の歴史ある技術 構築や設定は少し面倒な部類 運用も大変なイメージ 歴史のある技術とは書きましたが、複数のマシンから共有ストレージとして利用する場合には今でも有力な選択肢の一つです。また、物理的なサーバや AWS のEC2などで、自分でストレージを増設できる、あるいはEBSなどのストレージをアタッチできるということであればあまりありがたみはないのですが、ストレージの増設のできない VPS や、共有サーバなどで自分の判断で気軽な増設が出来ない場合にも、選択肢となり得ます。 NFS を動かしてみよう Linux 環境で簡単に試してみましょう。 今回は、 Windows 上に Vagrant / VirtualBox でCentOS7の環境を二つ用意し、ファイルの共有をしてみます。 それぞれ IPアドレス は、サーバ側を192.168.33.150、クライアント側を192.168.33.151で作っています。 NFS の環境構築(サーバー側) 必要なパッケージをインストールしていきます。 $ sudo yum install nfs-utils 本来はここで、各種の設定が必要となりますが、今回はおもむろにサービスを起動していきます。 $ sudo systemctl start nfs 本来はここまででパケットフィルタなどの対応が必要ですが、今回は割愛します。 NFS で共有する ディレクト リについて ストレージを共有すると言うことは、サーバに付属するストレージの一部を他のマシンと共有すると言うことです。まずは、その共有する部分を決定する必要があります。 NFS ではこの部分を exports というコマンドを使って管理を行います。 exportsの設定と反映 exportsの設定は /etc/exports ファイルに記載することになります。 今回は簡単化のために、接続元ホストの設定にクライアント1台のみを指定していますが、ここはネットワークセグメントをまるごと指定する( 192.168.33.0/24 や 192.168.33.0/255.255.255.0 )なども可能です。 今回は、 /export/share_dir を共有してみます。あらかじめ、 ディレクト リは作っておきましょう。 $ sudo mkdir -p /export/share_dir $ sudoedit /etc/export /export/share_dir 192.168.33.151(rw,async,no_root_squash) exportfs コマンドを使って、このファイルの設定を反映させます。 $ sudo exportfs -ra 設定が反映されたか確認してみましょう。 $ sudo exportfs -v /export/share_dir 192.168.33.151(async,wdelay,hide,no_subtree_check,sec=sys,rw,secure,no_root_squash,no_all_squash) どうやらされているようです。 NFS の環境構築(クライアント側) サーバに接続してくる側もパッケージを入れましょう。 $ sudo yum install nfs-utils 次に、マウントしてしてみましょう。今回は、 /mnt/ にマウントしてみます。 $ sudo mount -t nfs4 192.168.33.150:/ /mnt/ $ cd /mnt/export/share_dir/ $ pwd /mnt/export/share_dir というわけで、到達できました。 クライアント側でファイルを作ってみましょう。 $ sudo touch /mnt/export/share_dir/hoge $ ls -l /mnt/export/share_dir/hoge -rw-r--r--. 1 root root 0 Apr 7 09:47 /mnt/export/share_dir/hoge サーバ側で確認してみましょう。 $ ls -l /export/share_dir/ total 0 -rw-r--r-- 1 root root 0 Apr 7 09:47 hoge 同期されました。 今回は、サーバ側で no_root_squash という設定を入れています。このため、クライアント側のroot権限でサーバ側に書き込むことができてしまいます。ここは 潜在的 にセキュリティ上の懸念となるため、設計時の注意が必要です。 大きく NFS 導入の流れはこういった形になります。 EFSを NFS として扱う さて急に話は変わりますが、 AWS のサービスの一つであるEFSというものがありまして、これは NFS のフルマネージドなサービスです。 この場合でいえば、サーバ側の設定については AWS がいい感じに設定してくれますし、クライアント側にしても、専用のクライアントを導入すれば AWS のリソース名(ARN)でマウントすることができます。 AWS にロックインする形で利用するのももちろん便利なのですが、 NFS のフルマネージドサービスですので、当然 NFSの形式でマウントして利用することができます 。 EFSには直接 グローバルIP を付与することはできませんが、適切なセキュリティグループ設定をしてEC2経由で SSH ポート フォワ ーディングを利用すれば、 ローカルのマシンからも接続が可能 になります。 大きなファイルなどを直接流し込みたいときには使ってみてもいいかもしれません。 最後に さまざま運用上、セキュリティ上で考慮すべきの多い NFS ですが、EFSという形でフルマネージドなサービスが登場するほどに需要のあるものです。 使い方と使いどころを考慮して、長く付き合っていけたらいい技術ですね。 NFS に限らず、セキュアなインフラの構築や運用を進めたいMNTSQでは現在もエンジニアの募集をしています。ご興味があれば、 Wantedly にさまざま記事などを掲載していますので、ぜひアクセスしてみてください!! この記事を書いた人 中原大介 MNTSQ社でSREをやってます。最近は 乗り鉄 してることが多いですが、コロナ禍で実家に帰れず、実家においてきた愛車に乗れないことへの腹いせです。
アバター
MNTSQで検索エンジニアをしている溝口です。 今回はElasticsearchでハイライト処理を行う際に利用するUnifiedHighlighterの挙動について簡単に調べる機会があったので、それを簡単に記事にしました。 ハイライト処理とは 検索結果一覧が表示された際に、以下のようにヒットした該当箇所が強調表示される機能のことです。(以下のスクリーンショットだと検索キーワードの「ハイライト」が太文字になっていると思います。) この機能を実現するために、Elasticsearchでは以下の3種類のハイライターが利用可能です。 PlainHighlighter FastVectorHighlighter UnifiedHighlighter それぞれ特徴は異なりますが、今回はハイライトするにあたってそれぞれのハイライターが必要とするリソースにフォーカスします。 PlainHighlighter かつてデフォルトだったハイライターです。ハイライト処理をするためにテキストを再解析します。 必要なリソースは少ないですが、テキストの再解析を行うため、ハイライト処理にかかる時間は長くなりがちです。 FastVectorHighlighter 名前の通り高速なハイライト処理が可能ですが、ハイライト処理をするためにTermVectorという属性を必要とします。TermVectorはディスクに保存する必要があるので、必要なディスク容量は大きくなります。 UnifiedHighlighter 最も新しいハイライターで現在のデフォルトです。かつて存在したPostingsHighlighterというハイライターを改善して作られました。ハイライト処理をする際に以下を適切に選択して処理の高速化をはかります。 POSTINGS (posting listを利用する) TERM_VECTORS (FastVectorHighlighterと同じく、TermVectorを利用する) ANALYSIS (PlainHighlighterと同じく、ハイライト時にテキストを再解析する) POSTINGS_WITH_TERM_VECTORS (posting listとTermVectorの両方を利用する) さて、前置きが長くなりましたが、ElasticsearchのReferenceを読むと、上記のようにそれぞれのハイライターを使う場合にどのようなフィールドのオプションを指定すれば良いかが書いてあります。 https://www.elastic.co/guide/en/elasticsearch/reference/7.11/highlighting.html#offsets-strategy しかしながら、上記のうちUnifiedHighlighterのPOSTINGS_WITH_TERM_VECTORSの記述が見当たらないことに気づきます。 そのため、今回はElasticsearchからUnifiedHighlighterを使うときのOffset strategy(ハイライトを高速化するときに必要な追加の情報)で、 そもそもPOSTINGS_WITH_TERM_VECTORSは利用可能なのか を明らかにし、それに加えて それぞれのOffset strategyを利用した場合のディスク使用量と性能の向上度合い を簡単に計測してみようと思います。 Setup 以下の条件でデータを登録して、indexサイズ、ハイライトの平均処理時間を簡単にみてみたいと思います。 MacBook Pro CPU: 2.4GHz 8Core Intel Core i9 RAM: 32GB DDR4 OS: BigSur Docker: Docker Desktop for Mac 3.2.2 CPUs: 4 Memory: 8GB Elasticsearch: 7.11.2 Elasticsearchは以下のDockerfileでanalysis-kuromojiだけ入れて、イメージをbuildし、そのイメージを元にしたコンテナを以下のコマンドで起動します。 Dockerfile FROM docker.elastic.co/elasticsearch/elasticsearch:7.11.2 RUN /usr/share/elasticsearch/bin/elasticsearch-plugin install analysis-kuromoji 起動コマンド $ docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" 6571a46f9f37 検証に使うデータには、日本語Wikipediaのダンプデータから適当に10万件の本文情報のみを抜き取ったものを使います。 利用するindex名と、調査対象のフィールドの設定差分は以下の通りです。 共通 analyzer = kuromoji wiki_text_a (上述のANALYSISに相当) index_options = positions term_vector = no wiki_text_p (上述のPOSTINGSに相当) index_options = offsets term_vector = no wiki_text_t (上述のTERM_VECTORSに相当) index_options = positions term_vector = with_positions_offsets wiki_text_pt (上述のPOSTINGS_WITH_TERM_VECTORSに相当) index_options = offsets term_vector = yes 一例を挙げるとこんな感じです。 $ curl -XPUT localhost:9200/wiki_text_a -H 'Content-Type: application/json' -d '{ "mappings": { "properties": { "text": { "type": "text", "analyzer": "kuromoji", "index_options": "positions", "term_vector": "no" } } }, "settings": { "number_of_shards": 1, "number_of_replicas": 0 } }' なお、TermVectorを有効にする時の指定がwith_positions_offsetsとyesになっているなど、微妙な違いがありますが、これはElasticsearchではTermVectors/TermPositions/TermOffsetsという異なる情報をterm_vectorという属性にまとめていることに起因しています。 Offset strategyがPOSTINGS_WITH_TERM_VECTORSの場合には、TermVectorsのみを、TERM_VECTORSの場合にはTermVectors/TermPositions/TermOffsetsの3種を全て利用するので、上記のような設定になっています。 indexingが終わったら、以下の通りそれぞれのindexをforce mergeします。 $ curl -XPOST "localhost:9200/wiki*/_forcemerge?max_num_segments=1" $ curl -XGET "localhost:9200/_cat/segments?v" index shard prirep ip segment generation docs.count docs.deleted size size.memory committed searchable version compound wiki_text_a 0 p 172.17.0.2 _2 2 100000 0 389.1mb 1172 true true 8.7.0 false wiki_text_p 0 p 172.17.0.2 _2 2 100000 0 471.8mb 1172 true true 8.7.0 false wiki_text_pt 0 p 172.17.0.2 _3 3 100000 0 584.6mb 3004 true true 8.7.0 false wiki_text_t 0 p 172.17.0.2 _2 2 100000 0 638.7mb 3004 true true 8.7.0 false 結果の取りまとめ前ですが、上記でとりわけ目につくのは、wiki_test_t(term_vector: with_positions_offsets)を指定しているインデックスが、デフォルトのindexオプションのものの1.6倍にもなっていることです。 なお、wiki_test_pt(postings + term vector)は、上述の通りterm_vectorの一部の属性のみを追加しているので、それよりは大きくなっていないことが確認できます。 ハイライト性能は、jmeterを使って簡易に測ります。 今回は簡単に以下の設定で測りたいと思います。 スレッド数: 2 Ramp-Up期間: 1 ループ回数: 10000 ベンチマークに使う検索キーワードはindexに含まれているtermで出現回数が多い&3文字以上のものを10000件ピックして使うことにしました。 クエリには以下を使います。(${text}の部分は10000件のtermが代入されます) { "query": { "match": { "text": "${text}" } }, "size": 10, "_source": ["id"], "highlight": { "fields": { "text": {} }, "type": "unified" } } また、日本語検索ではmatchクエリが使われることはあまりないので、上記の他にmatch_phrase_prefixクエリでも計測してみます。 結果 結果をまとめると以下のようになりました。 matchクエリ index名 Throughput Average Median 95% Line 99% Line wiki_test_a 77.1 25 22 49 71 wiki_test_p 226.6 8 8 11 13 wiki_test_t 208.6 9 8 13 14 wiki_test_pt 220.1 8 8 12 14 ANALYSISとそれ以外では大きな違いが見受けられますが、これならPOSTINGSが最も優秀そうです。 match_phrase_prefixクエリ index名 Throughput Average Median 95% Line 99% Line wiki_test_a 74.6 26 23 43 51 wiki_test_p 73.6 26 24 44 52 wiki_test_t 205.9 9 8 13 14 wiki_test_pt 195.6 9 9 14 16 matchクエリの時とは一転して、POSTINGSはANALYSISと大して変わらない性能となってしまいました。 まとめ ElasticsearchにおけるUnifiedHighlighterとindexの属性指定の関係をまとめて、簡単な性能比較をしてみました。 結果としては、 1. POSTINGS_WITH_TERM_VECTORSは有効 2. 多く情報を保存することでハイライト処理は高速になる。一方で、必要となるディスク領域が大きくなっていく 3. 利用するクエリに応じてどの情報を保存するべきかを判断すべき ということが確認できました。 今回は簡単にディスク容量とハイライトの速度の関係をまとめてみました。それぞれのStrategyにおける使用メモリ量なども機会があれば記事にできればと思います。 この記事を書いた人 溝口泰史 MNTSQ社で検索エンジニアをしています。
アバター
preloadはけっこう難しい mntsqのソフトウェアエンジニアチーム所属のhagiwaraです。 Rails アプリケーションのパフォーマンスチューニングとしてN+1問題を潰すというのはよく行われます。 教科書的には簡単に書けるのですが、現実のアプリケーション開発ではpreloadで頭を悩ませることがあります。 長く開発されてきた Rails アプリケーションは、さまざまな歴史的経緯があり、モデルのリレーションツリー構造が深く、特定の条件下でのみ必要になるテーブルがあちこちにぶら下がっているということが珍しくありません。 preloadは一般的にcontrollerの最終段階で行うことになると思いますが、コードベースが巨大だとリレーションのpreloadの要不要の判定が難しいことがあります。 そんな中で、可能性のあるリレーションを全て保守的にeagar_load/preloadした結果、パフォーマンスに問題を抱えているアプリケーションは決して少なくないという現実があります。 ar_lazy_preload gemを試してみる preloadをlazyに行うことで、上のお悩みを軽減できる可能性のあるgemです。 以下のようなmodelを用意して実験しました # Rails 6.1.3 / ruby 3.0.0 class User < ApplicationRecord has_many :boards end class Board < ApplicationRecord belongs_to :user end User.count #=> 10 Board.count #=> 100 (各userに10) まずは素の Rails の動作から users = User.all # どのuserのboardsもpreloadされていない users.any? { |u| u.boards.loaded? } # => false # 一つのユーザーのboardを一つ参照してみる users[0].boards[0] # user[0]以外のboardsはロードされていない users.all? { |u| u.boards.loaded? } # => false lazy_preloadを使用して動作を見ます users = User.lazy_preload(:boards).all # どのuserのboardsもpreloadされていない users.any? { |u| u.boards.loaded? } # => false # 一つのユーザーのboardを一つ参照してみる users[0].boards[0] # 全てのuserのboardsがpreloadされている users.all? { |u| u.boards.loaded? } # => true 4層のような深い子孫関係にある場合でも期待した動作をしています。 クエリの自由度が高く、クエリパラメータによってpreloadしなきゃいけないものが変化する テンプレートや埋め込む変数が動的に変動して予測が難しい などのケースで保守的に全部preload刺していたようなところは少し楽ができそうです。 この記事を書いた人 hagiwara 実家の猫をリモートで愛でる毎日です
アバター