TECH PLAY

MNTSQ

MNTSQ の技術ブログ

98

開発しているシステムの.NET バージョンを4.5にしたらメール件名が文字化けしたので、 エンコードを2回するコード を書いた全世界100万のソフトウェアエンジニアの皆さん、こんにちは。 MNTSQ株式会社でバックエンドエンジニアをしている沼井です。 弊社が提供している SaaS である「MNTSQ CLM」は、多数の エンタープライズ 企業へ導入されています。その結果、契約書はもちろん、それに付随する多くのドキュメントをデータとして扱う必要があります。 そのドキュメントにはメールも含まれており、メールは企業の(契約をはじめとする)業務ナレッジを含む重要な資産となります。 そのため、MNTSQではこれから送られるメールはもちろん、過去のメールについてもナレッジとして取り込み、活用できる機能を提供しています。 崩れたメールを語る ここまで読んで鋭い方はお気づきと思いますが、過去から長年蓄積されてきたメールをきちんと取り扱えるようにするには、様々な配慮が必要です。 標準的なメールファイル(emlファイル)だけでなく、 Microsoft Outlook からエクスポートされたmsg形式等のメールも取り扱う 多種多様な 文字コード が使用されたメールを取り扱う RFC に沿ってないフォーマットのメールを取り扱う とくに「 RFC に沿ってないメール」については、 メーラー ( MUA )と送信サーバ(MTA)の RFC 非準拠の実装、不具合その他によって引き起こされているのが実情かと思います。 もちろん完全に崩れて読み取れないもの(それはメールといえるのか?)を救出することはできないのですが、ヘッダーのごく一部だけが RFC 非準拠のフォーマットである、かつそれが頻発しているようなケースについては、 可能な範囲で救う必要があります。 ところで私はこうしたデータに向き合うとき、最新の RFC や最新のメール処理ライブラリの挙動だけをもとに、過去のメールファイルの扱いづらさを安易に断じるような姿勢をとるべきではないと考えています。長年の歴史のなかで複数のソフトウェアが介在して作成・転送・蓄積されたデータというのは、本質的にこうした問題をはらみがちで、それはソフトウェアの利用者の責任に帰すべきではないと思っています (一方で、未来においてこうした問題が起きにくい プロトコル やデータフォーマットを検討・提案していくことは、ITエンジニアの重要な責務としてあると思っています)。 閑話休題 で、以下では、 RFC 準拠のメールパーサーライブラリを使っているだけではエラーがおきたり、テキストを正しく表示できないようなメールを「崩れたメール」と呼びつつ、私達がどのようなメールに直面し、対処してきた(あるいは対処できなかった)かというナレッジを、一部ですが共有したいと思います。 文字化け メールデータで問題が起きるというと、やはり文字化けのことが思い浮かぶ方が多いと思います。 文字化けといっても実情は多様です。 件名、本文、添付ファイル名のそれぞれについて個別に文字化けが起き得るし、また、原因としても以下のような種類があります: Content-Typeヘッダーのcharset名指定(どの文字 エンコーディング 方式を使ったか) が、実際に使われた エンコーディング と異なる 例1: 実際はcp1250 ( windows -1250)で エンコーディング されているが、charset としてはiso-1250 という名前が指定されている 例2: 実際はcp932で エンコーディング されているが、charsetとしては shift_jis という名前が指定されている (ようするに、 機種依存文字 がまざっちゃってる、ということですね) 対処としては、 エンコード エラーがおきたときに、正しい(かもしれない)charset名指定でリトライするという方法があります 使用している プログラミング言語 処理系/ライブラリでそもそも対応していない文字 エンコーディング 方式が使われている 例: ISO-2022-KRが Ruby で取り扱えない メールも含めた 文字コード および文字化けの理解のためには、「 プログラマ のための 文字コード 技術入門」が非常に有用なため、一読をおすすめします。 Referencesヘッダーが壊れている これはかなり見かけるケースです。 メールの送信・返信を繰り返すたびにデータ(メッセージID)が追記されていくため、「複数の メーラー が介在し、どこかの メーラー が1つでも正しくないフォーマットで追記するとエラーになる」「ヘッダー行がどんどん長くなっていった結果、一定長をすぎたときに問題が発生してしまう」という少なくとも2つの要因がありそうだと考えています。 以下、例です: References: <ABCDEF@aaa.example.com> <BCDEFG@bbb.example.com> (中略。1000文字くらい) <CDEFGH@c cc.example> ヘッダー行が1000文字を超えたあたりで、余計なスペースが入り込んでいます これは、メールの1行998文字制限を超えたことでおきてしまう問題と思われます (RFC5322 2.1.1) 対処としては、メッセージIDのなかには通常スペースが入りえないので、それが見つかったらカットする、という方法があります 参考: https://sendgrid.kke.co.jp/docs/API_Reference/SMTP_API/building_an_smtp_email.html ヘッダは1行あたり最大1,000文字が上限となります。この制限に従わないと、中間のMTAがヘッダをスペース以外で分割してしまう可能性があります。これにより、最終的なメールにスペースが挿入されてしまう可能性があります。メールがSendGridに到達する前に別のMTAを通過する場合、最大ヘッダ長の制限がさらに厳しくなり、ヘッダが切り捨てられる可能性があります。 References: <ABCDEF@aaa.example.com> <BCDEFG@bbb.example.com>,<CDEFGH@ccc.example> メッセージIDの区切りの一部にカンマが混入しています 実際はカンマ区切りでなく、空白で区切られるのが正しいフォーマットです 対処としては、メッセージID外に見つかったカンマをカットする、という方法があります 実際にはこれ以外の壊れ方もいくつか見られるのですが、今回の記事はそれを書くには余白が狭すぎるため、省略します。 メールアドレスのフォーマットが RFC 準拠でない <hoge@example.co.> や <hoge..@example.co.jp> のような、 RFC 準拠でないメールアドレスが From や To ヘッダーなどに一定含まれるケースがあります (頻出ネタですね)。 対処としては、メールアドレス部分はフォーマットの検証をしないというのがあります。(このメールアドレスを使って後段でメール送信するのでなければ、文字列データとして扱えればよく、不正フォーマットであってもよい) 当たり前にあると思ったヘッダーが存在しない From, Date などの、最低限存在すべき ( RFC 5322 3.6) と定められたヘッダーが存在しないケースも存在します。 体験したことがある例としては、 MIME のマルチパートメッセージだけが残っていて、その他のヘッダーが存在しないというものです。 これについては、一般の MUA やMTAの不具合によって起きたものか? というと怪しい部分はあると思っていますが、原因追求は本題ではないので深堀りしないでおきます。 対処としては、 RFC 的に必須のヘッダーだとしても、無いことはありえると想定した処理をするという方法があります。 改行コード メールメッセージの行はCRLFで区切られるはずなのですが、CRCRLFで区切られているケースが存在します。 一部のMTAを経由したときにこのような事象が起き得る、という情報を見たことはありますが、やはり今回の記事の本題ではないので深堀りしないでおきます。 対処としては、 CRCRLF を CRLF へ変換する方法があります。 最後に 本記事では、崩れたメールについて見てきました。 読んでいただいた方の参考となれば幸いです。 今回は標準的なメールファイル(emlファイル)についてのみ語りましたが、実際には Outlook のmsgファイルについても語り尽くせないほどの話題があります。 ですが、今回はここまで(腹八分目)にとどめておき、今後の宿題としたいと思います。 MNTSQでは、メールを含む歴史的経緯のあるドキュメントをリスペクトし、それに向き合うことに価値を感じられるエンジニアを募集しています。 参考資料 メールの構文についての RFC : https://www.rfc-editor.org/rfc/rfc5322 (メールも含む) 文字コード や文字化けについて: 矢野啓介「[改訂新版] プログラマ のための 文字コード 技術入門」( 技術評論社 , 2018) https://gihyo.jp/book/2019/978-4-297-10291-3 この記事を書いた人 沼井 裕二 MNTSQ( モンテスキュー )社のソフトウェアエンジニア。今は主に Rails での開発をしています。はじめて触った メーラー は Mew です。
はじめに Python を対象とした静的型チェックツールとして mypy はよく知られています。静的型チェックを通じて プログラマー はより安全にコードを記述でき、安全にコードが記述できることで最終的にはソフトウェア開発の効率をより高めることができます。もちろん初期的な導入 工数 とCIパイプライン等での追加の時間的コストは発生するものの、それ以上の 複利 的な効果が期待できます。 静的型チェックよりも更に込み入った構文上の改良点や、未使用変数などの抽出を行ってくれるツールとして Linter があります。 pylint 、 flake8 もLinterの一種としてカテゴライズされます。 mypyは静的解析の過程でAST( Abstract syntax tree : 抽象 構文木 )を吐きますが、このASTを利用したlintを行うツールとして今回はRefurbを取り上げます。 github.com Refurb本体の説明としては Lots of static analysis tools already exist, but none of them seem to be focused on making code more elegant, more readable, or more modern. That is where Refurb comes in. とあり、既存のLinterよりもよりCode-readabilityや、モダンな書き方にフォーカスしたツールのようです。 Refurbの動作環境はPython3.10以降を前提にしていますが、解析対象はPython3.6以上のコードになります。 動作サンプル pip経由でのインストールが可能です $ pip3 install refurb 早速ですが動作を確かめるため次のサンプルコードに対してrefurbを回してみます。 # main.py for filename in [ "file1.txt" , "file2.txt" ]: with open (filename) as f: contents = f.read() lines = contents.splitlines() for line in lines: if not line or line.startswith( "# " ) or line.startswith( "// " ): continue for word in line.split(): print (f "[{word}]" , end= "" ) print ( "" ) $ refurb main.py main.py:6:17 [FURB109]: Replace `in [x, y, z]` with `in (x, y, z)` main.py:7:5 [FURB101]: Use `y = Path(x).read_text()` instead of `with open(x) as f: y = f.read()` main.py:13:40 [FURB102]: Replace `x.startswith(y) or x.startswith(z)` with `x.startswith((y, z))` main.py:19:9 [FURB105]: Use `print() instead of `print("")` Run `refurb --explain ERR` to further explain an error. Use `--quiet` to silence this message 各行にエラーの内容が出ています。 [FURB101]: Use `y = Path(x).read_text()` instead of `with open(x) as f: y = f.read()` などはファ イルハン ドリングの詳細まで踏み込んだメッセージで、pylint, flake8には見られないものです。 エラーの詳細について --explain フラグを使ってエラーコードを指定することで、エラーの詳細が表示されます。 $ refurb –explain FURB101 When you just want to save the contents of a file to a variable, using a `with` block is a bit overkill. A simpler alternative is to use pathlib's `read_text()` function: Bad: """ with open(filename) as f: contents = f.read() """ Good: """ contents = Path(filename).read_text() """ エラーの一覧 エラー一覧を確認できるコマンドオプションは提供されておらず、エラーの一覧を確認したい場合は テストデータのディレクトリ を通じて調査できます。比較的新しいライブラリなのでこのあたりは後の改善に期待したいところです。 カスタム プラグイン の作成 カスタムで自作エラーも定義できます。ただし静的解析結果をいじるため、 mypyのASTシンボル をある程度漁って既存モジュールの動作を確認しつつ開発をする必要があります。 github.com 簡易エラー検出器の作成 カスタム プラグイン を作成するほどのニーズではないが、一時的なエラー検出器を利用したいニーズに対しては refurb gen コマンドがあります。 これは CUI のプロンプトのガイドに沿ってASTのノード種別と検出器のファイルパス(.pyファイル)を指定すると、指定箇所にノード種別に対するエラー検出器サンプルを作成してくれるというものです。 作成した検出器をロードしてrefurbを利用する際には refurb file.py --load 検出器モジュールパス で利用できます。 その他機能 他にも追加の機能として ローカルでgitのcommit前にチェックするツール pre-commit との連携 設定ファイルによる一部エラー、対象解析モジュールの無効化 ができるようです。 この記事を書いた人 yad ビリヤニ 食べたい
このブログ投稿は、 Ruby on Rails でNTLM認証を実装する必要が出たので、その対応と追加調査の記録である。 NTLMにはv1とv2が存在するが、このブログで扱うのは主にv1である。 プログラマ も歩けばNTLMにあたるとはよく言ったもので、この記事を見ているあなたもおそらくうっかりNTLM対応をすることになったITエンジニアの一人だろう。そんなあなたの一助になれば幸いである。 NTLMとは NTLM(NT LAN Manager)とは、チャレンジ/レスポンス方式を使う認証方式の1つである。前世代にLM(LAN Manager)認証という認証 プロトコル があり、その後継にあたる。SMBを介して認証を行うNTLM over SMBや、HTTPを介して認証を行うNTLM over HTTPなど、各種 プロトコル を介した認証も提供されている。 DESやMD4など現在では安全でない技術を含むなどいくつかの理由で、 利用は推奨されていない 。ただ実態としては、社内ネットワーク 複合機 など一部では現役のようだ。 幸い、 詳しい仕様はMicrosoftのサイトに掲載されている ので、仕様の理解で困ることは無い。 チャレンジレスポンス認証とは チャレンジレスポンス認証とは、以下のような流れで生パスワードをネットワークに流さずに認証する方式である。 以下のような流れである: チャレンジレスポンス認証 クライアントからサーバーに認証を要求 サーバーからランダムな値を生成してクライアントに送付(チャレンジ) クライアントはパスワードとチャレンジを利用して アルゴリズム に沿って値を生成してサーバー側に送付(レスポンス) サーバー側でレスポンスを検証し、問題なければ認証成功の結果を返す NTLM v1 本題のNTLMのv1はどのような暗号化方式をとっているのか確認してみよう。 認証のフローは、チャレンジレスポンス認証と同じである。 引用: [MS-NLMP]: NTLM Connection-Oriented Call Flow | Microsoft Learn チャレンジとして、サーバーからは8バイトのランダムな乱数を1つクライアントに送信する。 受け取ったクライアントは、以下の流れでレスポンス(「応答鍵」とも呼ばれている)を生成する: NTハッシュを生成する パスワードを UNICODE として評価する MD4で暗号化する LMハッシュを生成する パスワードをすべて大文字に変換する 14文字に満たない部分をゼロフィルする それらを前半、後半で分離する それぞれを KGS !@#$%[b] という文字列を用いてDESで暗号化する 分離したバイト列を結合する NTハッシュとLMハッシュからそれぞれレスポンスを生成する それらを3分割する 3つそれぞれをキーとしてチャレンジをDESで暗号化する 生成されたバイト列を順番通り結合する 上記の処理でできた2つのハッシュを 所定のフォーマット に充当する 上記ロジックの都合でパスワードは14文字以内に制限されている。また、上記ではクライアントチャレンジを利用する拡張セッションセキュリティについては触れていない。 参考: [MS-NLMP]: NTLM v1 Authentication | Microsoft Learn 参考2: DESLなど各関数についてはappendixに記述 がある DESとは DES(Data Encryption Standard)とは、 共通鍵暗号 方式の1つである。 現在は鍵長が短く安全でないと言われており、AESという別の暗号化方式に取って代わられた。 MD4とは MD4(Message Digest 4)とは、一方向の(復元不可能な)ハッシュを生成する関数の1つである。 脆弱であることが判明したため、後継の MD5 が開発された。ただ、 MD5 も現在は脆弱であるとされており、更に後継とされている SHA-1 も非推奨となり、記事執筆現在ではSHA-2が比較的広範に利用されている。 メッセージの種類 前段の図で示された3種類のメッセージがある。細かなメッセージの仕様については以下の通りである。 [MS-NLMP]: NEGOTIATE_MESSAGE | Microsoft Learn クライアントからサーバーに対して認証を開始する際に送られるメッセージ [MS-NLMP]: CHALLENGE_MESSAGE | Microsoft Learn チャレンジレスポンス認証のチャレンジに相当するメッセージ [MS-NLMP]: AUTHENTICATE_MESSAGE | Microsoft Learn チャレンジレスポンス認証のレスポンスに相当するメッセージ バージョンはどこで見分けるか NTLMにはv1、v2と2つのバージョンが存在するが、どこで見分ければ良いだろうか? バージョンを判別する方法は 無さそうだ 。v1とv2ではレスポンスの長さや生成方法が異なるため、長さで判別するなどの方法はとれそうだ。バージョンが異なる場合、レスポンスが異なるため、認証失敗として終了する。 紛らわしいのだが、 NegotiateFlags という構造体のNTLMSSP_NEGOTIATE_VERSIONフラグは デバッグ 用途で OSのバージョン情報 の提供を要求するもので、これはNTLMのバージョンとは無関係である。 同様に、 NegotiateFlags に含まれているNTLMSSP_NEGOTIATE_NTLMフラグはNTLM v1の拡張ではないセッションセキュリティを利用せよというフラグであり、v1を指定するものではない。 NTLM over HTTP HTTPを利用したNTLM認証の話に移ろう。 HTTPを経由して認証する場合、以下のような流れになる: NTLM over HTTP 実装は比較的簡単で、最初のNEGOTIATE_MESSAGEは Base64 エンコード してAuthorization HTTPヘッダーに「NTLM ( Base64 エンコード したNEGOTIATE_MESSAGE)」の形式で指定する。 サーバーは ステータスコード 401と共にWWW-Authorization HTTPヘッダーにチャレンジを返却してくるので、それに対してまたAuthorization HTTPヘッダーに「NTLM ( Base64 エンコード したレスポンス)」を指定して応答する。 するとサーバー側から成功した場合は2xx系のステータスを、失敗した場合は4xx系のステータスを返却する。 これで認証は完了である。 Ruby のgem NTLM自体はかなり古いが(1990年代)、おそらく今なお一部で利用されていることもあり、ありがたいことにNTLMのgemはそれなりに存在する。 GitHub - macks/ruby-ntlm: NTLM authentication client for Ruby. GitHub - WinRb/rubyntlm GitHub - pyu10055/ntlm-http: ntlm authentication for http GitHub - at-point/net-http-ntlm: Adds NTLM authentication to HTTP requests using the rubyntlm gem. A drop-in replacement for ruby-ntlm with NTLMv2 support. 内部実装を見てみよう それぞれのgemの内部実装を眺めてみよう。 ruby -ntlm ruby-ntlm はNTLM v1 over HTTP/ IMAP / SMTP に対応したgemである。 各Net::HTTPなどのNet系モジュールを拡張するかたちとなっている。拡張部分は至ってシンプルに実装されている。NTLM実装部分はNTLMモジュールの中にまとまっており、各メッセージはMessageクラスの下に収まっている。 各所に2.2.2.1など仕様書のナンバリングが見受けられ、また、メソッドの分割単位やフラグの名称も仕様書に倣った内容となっており、仕様書をよく読み下していることが伺える。 サンプルのプログラムも同梱されており、好感が持てる。 rubyntlm rubyntlm はNTLM v1/v2 over HTTP/ IMAP / SMTP に対応したgemである。 インデントが揃っていない、Type1-3はそうなのだがわかりやすくはない、1文字や2文字の短い変数名が頻出する、メソッドがスネークケースで揃っていないなどあまりお行儀は良くない内容だ。 html-http ntlm-http はNTLM v1/v2 over HTTP/ IMAP / SMTP に対応したgemである。 こちらもタブインデントとスペースインデントが混在していたり、前段のrubyntlmと同じものを踏襲したものと推測される。 ほとんどのメソッドが800行程度の1ファイルに入れられており、見通しも良くない。 net-html-http net-http-ntlm は ruby -ntlm gemにNTLMv2サポートした代替gemである。サポートするNTLMv2 over HTTPをサポートする。 実装は確かに ruby -ntlmと同様の実装となっている。 ruby -ntlmをラッピングし、NTLMv2のサポートをする部分だけの薄いgemである。 実装してみよう この中でどれを利用するかというと ruby-ntlm かなと判断した。 これで実装してみよう。と言いたいところだが、実装は至って簡単で、以下が全容である。 http = Net :: HTTP .new( @host_name ) request = Net :: HTTP :: Get .new( @path ) request.ntlm_auth( @user , @domain , @password ) http.request(request) We are hiring! いかがだっただろうか。 弊社の相手は エンタープライズ 故に、クライアントが持つ古い技術と弊社が持つ新しい技術を ブレンド して解決を試みるようなこともままある。 こういった課題はネット上に情報がそもそも無いことも多く、自身で調査して実装する面白さがある。 弊社はとことん試行錯誤しながら課題を解決していくエンジニアを募集中である。 右上の「採用ページへ」のリンクから職種を探してみて欲しい。 参考文献 NTLM認証とは - 意味をわかりやすく - e-words NTLM 認証と LDAP 認証の違いは何ですか。 - Cisco NTLM認証 - 通信用語の基礎知識 NTLMとは?複合機のエラー発生時の対処法を紹介!|複合機リースの格安NO1|株式会社じむや チャレンジ/レスポンス認証とは - 意味をわかりやすく - e-words @IT:@ITハイブックス連携企画 > Windows サーバー セキュリティ徹底解説 NT LAN Manager - Wikipedia Microsoft NTLM - Win32 apps | Microsoft Learn LMハッシュ - Wikipedia NTLM Overview | Microsoft Learn パスワードWindowsローカル SAM データベースに LAN Manager (LM) ハッシュを格納AD防止する - Windows Server | Microsoft Learn NTLM ユーザー認証 - Windows Server | Microsoft Learn チャレンジレスポンス認証とは - 「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典 [MS-NLMP]: NT LAN Manager (NTLM) Authentication Protocol | Microsoft Learn 【図解】わかりやすいNTLM 認証の仕組みとシーケンス, pass-the-hash について | SEの道標 Windows NTLM認証とマン・イン・ザ・ミドル攻撃 - Cyber Security Management 2004年5月号 Data Encryption Standard - Wikipedia MD4 - Wikipedia [MS-NTHT]: NTLM Over HTTP Protocol | Microsoft Learn The NTLM Authentication Protocol and Security Support Provider この記事を書いた人 Yuki Nishimura 雑食系エンジニア
組織の情報爆発 突然ですが組織で働くすべての皆さん、所属組織のルールや方針、意思決定やその背後の理由についてどの程度把握されているでしょうか?突然理由も知らされず組織ルールや方針が更新されたり、日々の仕事において必要な承認や確認の取得に時間がかかっていないでしょうか。 MNTSQでは、 (センシティブな個人情報を除いて)組織の職位やロールによらず誰もがアクセスできるようにする そのためにドキュメント駆動で意思決定や相談記録を残し、誰もが非同期でそれらにキャッチアップしながら賛否や新案を提案できる GitHub のIssue機能を介して、ゴミの分別ルールからビジョンレベルの意思決定まで誰もが発議できる などの文化を大切にしています。これらの効用については様々な発信があるので、他の記事に譲りますが、同時に次のような悩ましい トレードオフ 問題に直面しています。 組織の拡大に伴って発信される情報が 非線形 に増える(活発なことは大変良いが) それに伴っていまのタスクや会議に必要な情報の アクセシビリティ が低下する(検索・優先付与の難度が上がる) 新しく入ってきた人ほど不明な概念が増え、キャッチアップ難度が上がっていく( SaaS という1枚岩プロダクトをみんなで連綿と作っているので、咀嚼しないといけない情報分量が大きいという弊社特性もある) ※COVID-19新規陽性者数と見紛うexponential 社内の発信メディアとしては、日々のslackやdocsや GitHub やらが枚挙できますが、入社して数ヶ月の私としては、 アサイ ンタスク遂行を丁寧に完遂することを主目標に据えつつも、 GitHub に挙がっている組織Issueのキャッチアップが蝸牛の歩のごとく進まないことを課題に感じておりました。 というのも入社時点でMNTSQの GitHub レポジトリにはIssue総数が2kほども転がっており、技術的なものはそれぞれが非常に込み入って見えました。(ちなみに rails レポジトリのイシュー数は執筆時点で0.3~0.4kぐらい。流石に vscode は5k超えとかだけど...) また GitHub のUI上、1本釣りやリストアップ型の体験は用意されているけども、サンプリングした上で全体の関係性や位置付け、優先順位を把握するのがどうも難しい印象を受けていました。 ネット上の共感者を探してみるとこんなのを見つけました。 Visualizing GitHub Issues Issues also show whenever they are mentioned by other issues or by commit messages. It is apparent that all these concepts and mechanisms are deeply interconnected. However, GitHub shows all this information in textual form and shows the interconnections through hyperlinks, which hinders a comprehensive understanding of the evolution of an issue. この文言が完璧に私のもやもや感を明言化していました。 GitHub ではUI上イシューの想起関係などが見えにくく、問題解決において重用なサブタスクに分割することなんかにも心理障壁があったりします。特にテキスト情報処理に優れた人と、映像や概念で情報処理する傾向にある人だと前者寄りのUIに感じていました。 この問題に対して、試しにIssue間のリンク構造を可視化してみることにしました。 そうすることで全体における位置付けや分類が多少なりとも進むのではないかという気がしたのです。 GitHub イシューの関係性をグラフ構造で可視化する Step1. GraphQLによるリソースの取得 ググってみると、同じア イデア を持った人がすぐに見つかりましたので、こちらを元に味付けしてみることにしました。 GitHub Issues Graph with Netlify and GraphQL 基本的なア イデア はIssueのメンション関係を取得し、グラフ化することです。同時に以下のような情報も含められるとよいと感じました。 誰の起案か=Ownerは誰か コメント 誰が参与しているかもフィルタしたかったので取得(例えば代表や社内のマネージャが関わっているイシューは優先度や解像度が高いかもしれないといったこと) ラベル(MNTSQでは各イシューに☆1~3でざっくりと優先順位をつけているため、これは最低限取得したい。) 以上、簡単な初期仮説を元に、一旦はopenなイシューに絞って社内Issueを管理しているレポジトリから必要な情報を GitHub のGraphQL API を介して取得します。 最終的な取得 スクリプト (TypeScript)は以下のような形で、この結果を一旦適当な場所に保存して、それをパースしていきます。 const { Octokit } = require( "@octokit/rest" ) const fs = require( 'fs' ) // Create a personal access token at https://github.com/settings/tokens/new?scopes=repo const octokit = new Octokit( { auth: "Your access token here." , //ここでは簡単のためベタ書き } ) const targetOwner = "hoge" const targetRepository = "huga" const fetchIssues = (endCursor: any): Promise<any> => new Promise((resolve) => { setTimeout(() => { let fetchRes = octokit.graphql( `{ repository(owner: ${targetOwner} , name: ${targetRepository} ) { issues( first: 100, ${endCursor ? `after: " ${endCursor} "` : '' } , states: OPEN) { totalCount pageInfo { startCursor hasNextPage endCursor } edges { node { number url title author { login avatarUrl } body labels(first: 10) { nodes { id name } } comments(first: 50) { edges { node { author { login avatarUrl } } } } timelineItems(first: 100, itemTypes: CROSS_REFERENCED_EVENT) { totalCount pageInfo { startCursor hasNextPage endCursor } nodes { ... on CrossReferencedEvent { source { ... on Issue { number } } } } } } } } } }` ) resolve(fetchRes) } , 500) } ) const getAllIssues = async () => { let curCallNum = 0 let endCursor = '' const maxCallNum = 20 let edges = [] while (curCallNum < maxCallNum) { // eslint-disable-next-line no-await-in-loop let res = await fetchIssues(endCursor) // res = JSON.stringify(res) endCursor = res.repository.issues.pageInfo.endCursor let hasNext = res.repository.issues.pageInfo.hasNextPage edges.push(...res.repository.issues.edges) console.log(endCursor, hasNext) if (!hasNext) { console.log( 'All issues has been fetched.' ) break } curCallNum = curCallNum + 1 // console.log(`fetched res: ${edges}`) } return edges } getAllIssues() .then((res: any) => { let resJSON = JSON.stringify(res) fs.writeFileSync( './data/issueData.json' , resJSON) } ) Step2. 取得した JSON オブジェクトをd3.jsでグラフ構造化/可視化 次に上記 スクリプト で取得した JSON データを元に実際にグラフ構造を描画していきます。 またグラフ描画ライブラリとしてd3.jsを採用しました。今回は ForceGraph を応用します。これを使うとこんな感じでグラフのnodeとedgeにいい感じに引斥力を与え、それを物理的にシミュレーションすることでいい感じに適当に集まったり離れたりしながら各nodeの位置を決めてくれます。 ここではコードの詳細は省略しますが、初期仕様としては上記情報爆発課題にかんがみて、以下のような機能を実装しました。 ノードが個別のIssue、エッジが参照関係となるグラフの描画 ただし孤立したIssueまで書き込むと情報爆発そのものを眺める羽目になるため、各ノードの次数(Incoming Edges+Outgoing Edges)に応じて描画するノードをフィルタできるようにする ノードの半径は上記の次数に応じて大きくする(ハブとなるイシューが一瞥できるようにする) 3段階の優先度ラベルに応じて金銀銅の外円を描画 ノードの中にownerアイコンを描画する ノードをクリックすると詳細を右ペインで把握できるようにする(ここは GitHub の検索時UIを模す) 俯瞰図 さて、この可視化の結果どのような示唆や効果があったでしょうか。以下 ドッグフーディング の結果をレポートします。 Issueグラフ作成の結果と示唆 このアプリケーションの使用感として、以下のような発見がありました。気づいた順に列挙してみます イシューをポチポチしていったときに、すべての情報がロード済みなのでダウンタイムなく内容確認できるスピード感はよい(俯瞰できる感は本家よりスムーズ) 上図でいうと、金/銀/銅色の円で一覧描画されるのでざっくり優先度をつかめる これはCOOを始めとして弊社各員の地道なイシュー回覧のおかげ GitHub でもラベルによるフィルタできるので比較した差分は小さいけど、金塊や銀塊あたりを発見するにはよい感じ 母艦イシューや関連性を中心に会社のメインイシューを把握できる 例として右下の クラスタ (金銀銅の混合山)はビジネスメンバが多く、関連するイシューは主に導入効率化に係るものが多い 弊社ではありがたいことに引き合い多く、一方で大企業法務のお客様は既に複雑かつ巨大な業務システムを抱えてらっしゃるケースが多く、このような事情で導入や整理が大変であるという関連課題がまとまっている 左下の銀山は社内事務の効率化にかかるもので、個人的に肌感もてている感じではないものの、複数の当事者の間で問題意識が共有されているあたり からし て無視できないのだろう 人の切り口で見る 左上の金銀山はsalesメンバーの巻山さんがownerで、見てみると新規顧客獲得に係るア イデア や議論のようだ。一人で関連イシューを出しており、強い気持ちを感じる! 福井さんは母艦イシューのownerであることが多く、コンサル出身で社内の情報整理に長けているので、納得できる マネージャーの稲村さんのイシューは海外製品の調査などに係るもので、私も似たイシューを立てたことがあった。完全な セレンディピティ だが、これを起点に似たモチベーションをもった人がちらほらみつかった 左中段の クラスタ には人事の細野さんがおり、採用や人事メンバに関連した課題群がみつかる といった具合にグラフ構造はメンバー間の想起の歴史を反映している点は意義深く、俯瞰性と偶発的発見に特化した可視化手段として一定有用だなと感じました。 改善案や課題 以上、良い体験や発見をベースに列挙していきましたが、改善点も同時に多くみつかりました。こちら列挙すると イシュー間のmentionが不十分 むしろ孤立したイシューのほうが多く、それらの中に重用イシューが散在している場合もある 検索によるフィルタリングが同時にできるとよい duplicatedなイシュー、似たイシューをツール内で見つけられるようにできるとUXとしてかなり改善しそう(ただしそれによる GitHub との差分は少ない) 既にイシューの概念構築がすすんでいれば、欲しい情報の一本釣りは GitHub で十分とも感じる また情報爆発の対処法は誰かが整理したもの(或いはこのように自動的に整理されたもの)を受動的に享受するだけだと片手落ちだと感じました。 イシューからはじめよ に記載ある通り、人間の「理解した」の本質は、概念間の関係を見出したり、逆に無相関であることが明らかになることによって訓練される想起ネットワークだと考えています。情報記述する側の各員による配慮やこういった自動的可視化の支援は最低限度必要であるものの、それらにかまけず自分からゼロベースで概念を整理する、配線するといった方向性を助ける方が、寧ろ本質的解決につながるかもしれないとも思い至りました。 とはいえグラフ構造はチャットツールのような1次元リスト構造や一通り関連情報がまとまっている クラスタ (docsに近い)、 wiki や マインドマップ のようなツリーに比べるとリソースフルかつ整理コストが低く(想起したらリンクすればよい!)、また上記の通り人間の概念把握 トポロジー に近いと思われるため、整理と抽出のバランスがよい点が改めて良いなと感じました。 参考: 階層整理型WiKiはスケールしない ー私は階層型整理は書き手側の読み手への配慮手段の一つだという考えですが(そのため書き手側に回るとつらさが出てくる)、ネットワーク型の情報整理の利点については scrapbox の考えが非常によくまとまっています。 この記事を書いた人 白井佑治 MNTSQ社で 機械学習 ・ 自然言語処理 に関わるもろもろをやっています。好きな食べ物は担々麺です。
新しい仲間 前回はHot Docsという bot を紹介した。Hot Docsは組織拡大に伴う情報爆発において、情報にattentionをつける仕組みだ。 tech.mntsq.co.jp 今回紹介するのは、Peer-to-Peerフィードバックを促す bot だ。 Peer-to-Peerフィードバックとは Peer-to-Peerフィードバックは、個人成長やチーム開発を目的として個人から個人へ行うフィードバックである。Peer-to-Peerフィードバックの効果や方法についてはGitLabの360 Feedbackがより参考になる。 about.gitlab.com 行為や責任の所在を明確にするためチームではなく個人に対してフィードバックすることが重要であるが、個人攻撃とならない/捉えられないために次のような点に注意する。 フィードバックを送る側: リスペクトを欠く言動をしない 個人そのものではなく個人の言動や成果等、具体的な事象に対してフィードバックする フィードバックを受ける側: 個人攻撃ではないことを理解する フィードバックによって何を良くしようとしているか理解する なぜ作ったか MNTSQでは全メンバーから見える形でフィードバックが行われることを推奨しているが、内容がソフトスキルに関する等でセンシティブである場合はプライベートで行われることを許容している。 プライベートなフィードバックは機会が少ないので、 bot により促進する仕組みを作った。 Peer-to-Peerフィードバック bot の仕組み この仕組みを作るにあたり、Ubieの人を介さない自動フィードバックのすすめを大いに参考にした。 note.com フィードバックのフロー bot によるPeer-to-Peerフィードバックのフローは次のとおり。事前/事後に1on1を行うこともある。 bot が受信者を日次で アサイ ンする 受信者が Google Formで送信者3人を選ぶ bot が送信者3人にフィードバックをリク エス トする 送信者が Google Formでフィードバックを送る bot が受信者にフィードバックを送る 受信者と送信者への通知はすべてSlackで行われる。 受信者の アサイ ンでは次のような通知が届く。メッセージやアイコンはランダムで変わる。これは社長の例。 受信者がフィードバックをリク エス トするとき、フィードバックのテーマを指定できる。 送信者がフィードバックするとき、内容は伊藤羊一氏のGoodとMottoで「今よりもっと良くなる」ことを意識して書く。 www.hito-link.jp フィードバック履歴の可視化 フィードバックの履歴はSpreadsheetに記録し、月1で集計結果をSlackに通知する。 グラフを見るとリク エス トに偏りがあることがわかるので、今後の改善に活かしたい。フィードバックの内容はセンシティブなので記録していない。 技術的な話 この仕組みを作る上でいくつか技術的なポイントがあるので挙げておく。 アーキテクチャ bot のプログラムはすべてGAS( Google App Script)で動かしている。 受信者の アサイ ンと可視化の通知は時間ベースのトリガーで、それ以外は Google Formの投稿をトリガーにしている。休日にSlackでメンションが来るのは治安が良くないので休日はスキップするようにしている。 Google Formにパラメータを渡す 送信者がフィードバックを送るときに受信者をデフォルトで埋めておきたい。 これは Google FormのURL https://docs.google.com/forms/d/e/hoge/viewform に ?entry.12345=itaya のようなクエリ文字列を繋げることで実現できる。 12345 は質問ごとに割り当てられるIDで、「 google form entry id」等で ググる と調べ方が出てくる。 SpreadsheetのグラフをSlackに投稿する SpreadsheetでプロットしたグラフのBLOBをSlackに投稿するには files.upload API を使う。しかし、これだけではやりたいことが実現できない。なぜなら、 chat.postMessage と異なり icon_emoji と username が変更できないからである。アプリの表示設定で変更できないことはないが、これだとちょっと都合が悪い。 そこで(美しくないが) files.upload で個人のチャンネルにアップロードしてから画像URLを取得して、 chat.postMessage の attachments に埋め込む形で投稿するようにしている。 api.slack.com api.slack.com 運用してみて 運用を始めて9ヶ月が経った。少なくとも運用を始めたことによって会社全体のフィードバックは増えていると感じる。 フィードバックをもらった感想 結構緊張する。筆者自身は顧客対面にもっと出ろというフィードバックをもらってミーティングに出るようになったり、声が小さいと言われて腹から声を出すことを意識するようになったりした。 他の方から感想を募集したところ、次のようなコメントをいただいた。 どきどきしますね。でもしっかりGoodでさんざんホメてくださった後に、すこしだけ丁寧なMottoがつづいていて、やさしさを感じた。Mottoの内容は家族からもたびたび指摘されていることだったので、マジで気をつけます FB者から見て自分がどう見えているのか、全然意識していなかった部分も知ることができてよかった 入社してすぐのタイミングでも、業務面、キャ ラク ター面の両方からフィードバックをもらえた。周囲が新メンバーに気を配ってくれていることがわかり、安心した。 フィードバックを送った感想 結構緊張する。1件あたり最大2時間かけて返すのでめちゃくちゃ大変だが、相手にとって僅かでも価値のあるフィードバックであることを願う。 こちらも他の方から次のようなコメントをいただいた。 結構大変。口頭でお礼言われたりもするので、やってよかったなと思う。 おしまい あなたの会社でもぜひ試してほしい。
MNTSQ( モンテスキュー )という契約書管理の SaaS 製品を開発する会社で、 アルゴリズム エンジニアをしている坂本です。 契約書に書かれた情報を自動で抽出する仕組みを作っています。 概要 正規表現とはなにか 正規表現を書きにくい、中ボス的なテキストが存在する regexモジュールのFuzzy match機能を使って、楽に中ボスを倒す Fuzzy matchの使用例 裏話 実は、採用ブログも兼ねています この記事を書いた人 概要 私も非エンジニア出身であるため、Techブログではあるものの、 幅広い方に読んでいただきたいと思いました。 このブログの内容をざっくり図解すると、こんな内容を扱います。 正規表現 とはなにか テキスト(=文字列)に対して、マッチさせるパターンのことです。 特定の文字を含むテキストを探すときに使います。 例えば、  テキスト1:「庭には2羽ニワトリがいる」  パターン1:「ニワトリ」 だった時、このパターン1はテキスト1にマッチします。 ほかにも、  テキ スト2 :「庭には2羽鶏がいる」  パターン2:「鶏」 のように書けば、パターン2はテキ スト2 にマッチします。 テキスト1とテキ スト2 のどちらにもマッチするパターンを書きたいときには、  パターン3:「(ニワトリ|鶏)」 のように「( )」と「|」という記号をつかって、「ニワトリ or 鶏」というパターンを書くことができます。 1つのパターンでテキスト1にも、テキ スト2 にも、マッチします。 このように、裏技的な記号を使えば、 ある程度のバリエーションを持つテキストに対するパターンが書けます。 正規表現 を書きにくい、中ボス的なテキストが存在する ところが、どうにも 正規表現 が書きにくいテキストが存在します。 それは、 ランダムにノイズが入る タイプのテキストです。 弊社の場合だと、紙の契約書を OCR ( 光学文字認識 )したテキストが該当します。 紙で締結した契約書を解析したい場合、 最初に OCR をして 画像(紙をスキャンしたPDFファイル)からテキストを取り出した後、 取り出したテキストを対象に、いろんな方法で、 契約当事者や、契約締結日を抽出したり、契約類型を推論したりしています。 OCR は、見た目に忠実に認識します。 印字のレイアウトの影響を受けますので、 たとえば、1文字1文字の間にスペースが挟まっていたり、 ほぼ完璧に認識できているのに1文字だけ間違っている、ということがよくあります。 単純な 正規表現 のパターンを書くと、 このようなエラーを含むテキストにはマッチしません。 そこで、記号を使ってある程度のバリエーションをカバーできるようにしたいのですが、 どこにエラーが出るかがまったくランダムなので、とても難しいのです。 regex モジュールのFuzzy match機能を使って、楽に中ボスを倒す 中ボスを楽に倒すということで、 課金アイテm 無料ですが、 別途インストールが必要な regexモジュール を使ってみます。 Python をインストールすると標準でついてくるモジュールの中にも、 re という 正規表現 を扱うためのモジュールがありますが、 regex にはreにはない便利な機能があります。 具体的には、Fuzzy matchと呼ばれる機能をつかって、 ある程度の揺れ幅を許容してパターンとテキストをマッチングさせることができます。 OCR 結果のように、多少のノイズが想定されるテキストを扱う際に重宝します。 Fuzzy matchの使用例 regex のFuzzy matchは、通常の 正規表現 を丸カッコでくくった後、 その直後につけた中括弧の中に細かい指示を書いて使います。 例えば、下記のtext1とtext2にマッチさせるパターンを作るには、 patternAのように書きます '(?: 業務委託契約 書){i:\s}'の後半にある {i:\s} がポイントで、 「\s(空白文字)」の[i(挿入)]を許容しています。 import regex text1 = '業務委託契約書' text2 = '業 務 委 託 契 約 書' patternA = '(?:業務委託契約書){i:\s}' regex.match(patternA, text1) <regex.Match object ; span=( 0 , 7 ), match= '業務委託契約書' > regex.match(patternA, text2) <regex.Match object ; span=( 0 , 13 ), match= '業 務 委 託 契 約 書' , fuzzy_counts=( 0 , 6 , 0 )> また、下記のtext1とtext3にマッチさせるパターンを作るには、 patternBのように書きます。 こんどは、'(?: 業務委託契約 書){s}'の後半にある {s} がポイントで、 文字の入れ替わり(substitution)を許容しています。 import regex text1 = '業務委託契約書' <br> text3 = '業務季託契約書' <br> patternB = '(?:業務委託契約書){s}' <br> regex.match(patternB, text3)<br> <regex.Match object ; span=( 0 , 7 ), match= '業務季託契約書' , fuzzy_counts=( 1 , 0 , 0 )> 実際には、データの傾向を見ながら、上記のパターンを調整します。 機械学習 で解決しても良さそうですが、 データが少ない場合や、試作を作りたいときに、 学習用データを作らなくても動作とデータの相性をざっくり掴むことができます。 裏話 実は、採用ブログも兼ねています 正規表現 、 自然言語処理 、 機械学習 、MLOps、検索、Elasticsearch といった分野に関係のある仕事をお探しの方で、 経営層と近い距離で、 裁量を持って領域横断的に働くことにご興味を持っていただけましたら、 ぜひ右上の採用ページからお問い合わせください。 また、法律分野、もしくは 言語学 の知識を生かして、 学習用データを作ったり実験サイクルを重ねて製品を作ることにご興味のある方も、 どうぞご連絡ください。 (私も、もともとは 言語学 でした。) この記事を書いた人 坂本明子 MNTSQ, Ltd.で アルゴリズム エンジニアをしています。 入社エントリはこちら www.wantedly.com
MNTSQ( モンテスキュー )株式会社で検索エンジニアをやっている溝口です。 MNTSQはMNTSQ CLM(ちょっと前までMNTSQ for Enterprise)という数十万件を超えるような契約書を管理するプロダクトを提供しており、以下のような検索機能を提供しています。 https://speakerdeck.com/mntsq/mntsq-careersdeck?slide=12 (公開時点) 今年の4月で入社してから2年が経ち、入社してからやってきたことの振り返りも兼ねて今回は検索処理周辺の コンポーネント の変遷をまとめてみました。 最初期(2020/08-2021/07) Index sizeの増加に伴う変更(2021/08-2022/04) インデクシングの高速化と複雑なクエリへの対応(現在) (今後)マルチテナントへ まとめ この記事を書いた人 最初期(2020/08-2021/07) 検索エンジン とインデクシング処理をコンテナとして独立させ、間にELBを挟んでリク エス トを分散していました。 オンプレミスの環境では、インデクシング処理はコンテナとして独立させたものの、サーバ台数の都合からシング ルノー ドで運用していました。 (MNTSQでは AWS 上だけでなく顧客の要件に応じてオンプレミスでもサービスを提供しているため、双方でよしなに動くようにインフラを設計する必要があります。) 1 , 2 Index sizeの増加に伴う変更(2021/08-2022/04) 契約の検索のみから、契約の管理へとプロダクトラインが広がるとともに 検索エンジン が提供する機能と扱うデータ量も増加していきました。 また、契約書は契約書間で似たような条項が存在したり(MNTSQは条単位での検索も可能です)、微妙に言い回しの違う締結前のドラフトなど、場合によりますが検索結果としてはグルーピングして表示してほしいものなどがあります。そういった機能を検索処理側で行っているため、徐々に検索パフォーマンスが落ちていくという問題に直面しました。 特に AWS 環境において従来の構成ではディスクI/Oによる検索速度の劣化が目立ってきたため、SREと相談してECS上での稼働をやめて自由度の高いEC2上に移動させ、ディスクをEBSからInstance storeに変更して速度劣化を解消しました。 ほぼ同時期にEC2の インスタンス を ARMアーキテクチャ に変更し、コストを維持したままCPUコア数を増やし、性能を 最大80%程度 向上させることに成功しました。 また、上記に加えてお客様が増えるにつれて当初の想定以上のデータをお持ちのお客様からの受注も頂いており、今後もデータ量の増加が予測できたことから、柔軟にシャード分割数を変更できるようにしました。 インデクシングの高速化と複雑なクエリへの対応(現在) 検索エンジン の役割が増えていくにつれて、変更された内容が登録 & 反映されるまでのリードタイムの影響が大きくなってきました。そのため、 ソフトウェアエンジニアの沼井の力を借りて インデクシング処理を Ruby on Rails に移管し、最大40%程度のリードタイムの短縮を実施しました。 また、当初は制約こそ非常に強いものの、それなりの結果を出せるようなクエリの仕組みを提供していましたが、より自由なクエリを提供する必要に迫られました。そのため、 クエリパーサーを実装して クエリの自由度を上げるとともに、その状態でもより良い検索結果が提供できるように アルゴリズム エンジニアの知見を検索処理に応用できるように、検索処理を Ruby on Rails から Python ベースのFastAPIに移行しています。 3 , 4 , 5 (今後)マルチテナントへ 現在、MNTSQでは 契約案件の管理プロダクトを開発中 であり、そこでも 検索エンジン を利用する機能を提供する予定です。 また、多くのお客様から受注をいただく中で、運用コストが非常に大きくなってきており、そこを減らすことが将来的に課題になると考えています。 そのため、マルチテナントをデフォルトの構成とし、インフラの スケールメリット の追求と運用コストの低減を実現したいと思っています。 まとめ MNTSQのこれまでの検索周りのインフラ構成を振り返りました。 紹介したのは大きめの変更をしたときのものですが、もっと細かい変化を含めるともっと活発に変化し続けています。 MNTSQの検索処理ではプロダクトの成長/プロダクトラインの増加、導入先の増加とともに進歩し続けています。その中で今回触れたような構成変更には当然SREやソフトウェアエンジニアとの協働が必要になってきます。また、当然ながら検索に利用する メタデータ の抽出は アルゴリズム エンジニアが担当している場合が多いため、 自然言語処理 分野では アルゴリズム エンジニアと協働することもあります。 上記のように検索処理はインフラ・ソフトウェア・ アルゴリズム ( 自然言語処理 )にまんべんなく携われるという魅力があるとおもいます。 検索処理以外でも、もしご興味を持っていただけたなら、右上の「採用ページへ」からご応募いただければ幸いです。 この記事を書いた人 溝口泰史 MNTSQ社で検索エンジニアをしています。 https://guides.rubyonrails.org ↩ https://www.elastic.co/elasticsearch ↩ https://guides.rubyonrails.org ↩ https://fastapi.tiangolo.com/ja ↩ https://www.elastic.co/elasticsearch ↩
こんにちは、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 ビリヤニ食べたい