「GraphQL APIだとバージョニングが不要」という言明は誤解を招くので避けるべき

1行で

  • 「特定の場合でバージョニングしなくても対応できることはある」程度なので、「バージョニング不要」とは言わないほうがよい

どういうことか

  • RESTful API から GraphQL へ、GraphQL から別の Web API systemへ、ということを考えると大きな意味でのバージョニングは必要
    • e.g. 実際にGitHub は GraphQL API を "API v4" と呼んでいる
  • 細かなレベルのバージョニング、たとえば1画面の仕様が微妙に変わるたびに /foo, /foo_201807_1, foo_201807_2 みたいにどんどん特定画面専用APIを定義していく、みたいな意味でのバージョニングは不要
  • fieldの追加は無造作に行ってよい
    • RESTful APIでもfieldの追加は普通はできる、ただし負荷に注意
    • 重いcomputed fieldの場合でも、GraphQLのほうは無造作に追加できる
  • fieldのrename, deleteはGraphQLでも気軽には行えない
    • @deprecated directiveはあるが、何かを保証するものではないので削除の際は利用者との間でコンセンサスをとる

DX: Developer Experience (開発体験)は重要だ

  • DX: Developer Experience (開発体験)とは、あるシステムを「気持ちよく開発・保守できるかどうか」を示すもの
  • 開発者は開発・保守という行為を通じたそのシステムのユーザーであり、DXはUXの一種である
  • DXがよいと日々の開発を楽しめるようになり、気持ちに余裕ができる
    • 気持ちの余裕がでるとコードの品質があがり保守時のデグレも減らせる
    • また、DXがよい事自体がDXを高める動機になり、正のスパイラルを見込める
      • つまり、「定められたタスク」(=義務)以上のことを行うようになる
  • DXが悪いと開発を楽しめず、「定められたタスク」以外のことをしたくなくなる
  • DXは放置すると悪化するので、「DXがよくも悪くもない」プロダクトは時間が経つに連れ「DXが悪い」になる
    • なので積極的にDXを良くしていく活動を奨励していくのがよい

いくつか興味深いフィードバックがあったので記しておきます。

⇢ PX: Programming Experience という概念を提唱している研究会があるようですね(DXとはかなり異なる概念のようですが): SIGPX: Special Interest Group on Programming Experience

TypeScriptの `ReadonlyArray<T>` を使いやすくするためにtslintを活用する

最近、Kibelaのtslint configの Rule: array-type を "generics" にしました:

+     "array-type": [
+       true,
+       "generic"
+     ],

以前は特に指定しておらず、 T[]Array<T> が混在してていて、それでよしとしていました。今でも、混在することによるデメリットは特にないと思っていいます。ただ、 Array<T> には一つだけメリットがあったのでこちらに統一することにしました。 autofixできるので、エディタ上では T[] と書いて保存時にtslintに Array<T> に直させるということができるため、導入のデメリットがないというのも大きいです。

さて Array<T> のメリットは、 ReadonlyArray<T> に直しやすいということです。 ReadonlyArray<T>Array<T> から破壊的変更を伴うメソッド(mutation methods)を取り除いたインターフェイスで、特にReactではstateやpropsに破壊的変更を与えてはいけないということになっているので、積極的に使うメリットがあります。

ReadonlyArray<T> についてTypeScriptの公式マニュアルではほとんど触れられていませんが、インターフェイスの章にすこしだけ記述がありますね。

https://www.typescriptlang.org/docs/handbook/interfaces.html

TypeScript comes with a ReadonlyArray<T> type that is the same as Array<T> with all mutating methods removed, so you can make sure you don’t change your arrays after creation:

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

なお同様に ReadonlyMap<K, V>ReadonlySet<T> もあるようです。

graphql-rubyの知見をscrapboxにまとめはじめた

scrapbox.io

React DOM elementsに focused プロパティがほしくなったとき

<input ref={(element) => this.input = element}/> しておいて this.input.focus() とか this.input.blur() とか呼び出すのはReact的ではないのでなんとかしたいものだなあ、と思って一旦こういう関数を作って運用してみることにしました。

// in TypeScript

// This function emulates "focus" attributes for React DOM elements
export function createFocusHandler<E extends HTMLElement>(focused: boolean) {
  return (element: E | null) => {
    if (element && element.matches(":focus") !== focused) {
      if (focused) {
        element.focus();
      } else {
        element.blur();
      }
    }
  };
}

これを次のように ref へのイベントハンドラとして渡せばOKです。

<input ref={createFocusHandler(focused)}/>

ラッパー component を作ってやっていくほうがいいような気もしますが、まあ動くのでとりあえずこれで。

yarn-deduplicate(1) で yarn.lock の重複エントリを最適化する

追記(2019/03/18): yarn-toolsからyarn-deduplicateが独立して使いやすくなり、 --strategy でdedupeの方法を選べるようになっています。タイトルも変更しました。

yarnpkg(1) を使って依存関係を管理しているとき、 yarnpkg upgrade-interactive は対話的にライブラリのアップデートができるので大変便利です。しかし、これを実行すると yarn.lock に不必要に重複エントリが作られることがよくあります。

nodejsで実行するケースでは重複があっても問題がないことが多いのですが、 クライアントサイドでは重複があると単純にファイルサイズが大きくなり。また、@types/* や react, jquery といったフレームワークはどの環境でも動作に問題が出たりするので、重複エントリは問題です*1。そこで今までは、 yarnpkg upgrade-interactive 後に 差分を眺めて重複エントリを1つにまとめるというタスクを手動でやっていました。

とはいえこの yarn.lock 最適化ロジックは機械的にできるはずなので、いいツールはないかと探していて、なければ作るのでもいいかと思っていいたところ、 yarn-deduplicate(1) というツールがその機能を提供しているようでした。

github.com

手元のかなり大きな yarn.lock でもちゃんと動いたので、 package.json の scripts セクションに次のようなdedupe scriptを登録して、 npm run dedupe で運用していこうと思います。

npx yarn-deduplicate --strategy=fewer && yarn install --force

こういうとき、 npx(1) を使うとプロジェクトの依存関係をいじらなくて済むのでいいですね。

*1:特にTypeScriptの型定義はグローバルスコープだったりするので、微妙に異なるバージョンの .d.ts が node_modules 以下に復数存在すると TS2300: Duplicate identifier が起きがちです。

nodejsをリビルドすることなくIntl APIをfull-icu相当のデータで使う

追記(2019/08/23): https://github.com/nodejs/node/issues/19214 によると、将来的にはfull-icuビルドがデフォルトになりそうです。


gfx.hatenablog.com

上記エントリの続きです。

その後調べた結果、 full-icu というNPM moduleでfull-icu相当のデータをインストールできることがわかりました。

https://github.com/unicode-org/full-icu-npm

中身については精査していませんが、 unicode-org が提供しているものなのでそれなりに信頼できるでしょう。

このモジュールをインストールすると node-icu-data-path(1) が提供されるので、それで得たパスを NODE_ICU_DATA 環境変数にいれるとnodejsの Intl APIで日本語を含めた多くのロケールデータを利用できるようになります。

HTC U11 は安心して人におすすめできるシムフリーAndroid端末だ!

www.htc.com

発売日に買って2週間ほど使ってますが、非常に快適です。

ここ1年で Android Z4 ⇢ Huawei P9 Lite ⇢ HTC U11 と変えてきましたが、一番ストレスフリーかもしれないです。シムフリーだし*1、変なビルトインアプリはあまり入ってないし*2、指紋認証がついてるし*3、Felica もついているし*4、USB debugも有効にできます*5。今のところ気になるのは、ボトムナビゲーションのホームボタンが指紋認証デバイスとかぶせているせいで独自実装なのがちょっと慣れないと使いにくいのと、ボトムナビゲーションにあるはずのIME切り替えボタンが存在しないのが不便だということくらいです。

Felicaと指紋認証のついているシムフリーのハイエンドAndroid端末というのがなくて困っていたわけですが、HTC U11は今のところ申し分のない出来です。よかったよかった。

*1:Xperia Z4はいい端末だがシムフリー版を手に入れるのがちょっと難しい。

*2:Huaweiのビルトインアプリは出来が良くなくてトラブルが起きがち。

*3:Xperia Z4は指紋認証が付いていないのが普段使いする上で唯一の欠点だった。

*4:Xperia Z4 はFelica がついている。

*5:Huawei P9 LiteはUSB Debugを有効にできない。

NodeJSのIntlサポートを調べた

  • IntlというのはECMAScript Internationalization APIのこと
  • NodeJSでも利用可能だが、ビルドオプションによって様子が変わる
  • NodeJSのICUサポートは none, system-icu, small-icu, full-icu というオプションが用意されている
    • https://nodejs.org/api/intl.html
    • 公式バイナリのオプションは small-icu で、これは英語だけをサポートしている
    • 起動時にICU dataを与える事も可能
    • この情報は node -e 'console.log(process.config.variables)' で調べられる
  • Intl によってI18N処理をするときに日本語サポートをしたい場合、3つの選択肢がある
    1. full-icu でビルドしたnodejsを使う
    2. small-icu ビルドのままで、NODE_ICU_DATA 環境変数でICU dataを別途与える
    3. intl.js でpolyfillする
  • intl.js はメンテが活発でもなくバグも放置気味
  • 日本のサービスで Intl を使うならfull-icu でビルドしたものを使うのがいいのかもしれない

発表資料: Elasticsearchによる 全文検索の実装

Rails Developers Meetup 2018 (#railsdm) で話した資料です。Railsの話はほとんどなくて、全文検索の仕組みとスコアリングについてのまとめが主です。

Q&Aシステムでの質問もここで回答します。

Q. データの同期はどうされていますか?

同期はActiveRecordのcallbackでActiveJobに更新jobを投げる形で非同期で行っています。また、データ構造などの更新がある場合にindex再構築するときのためのblue-green deployment用のバッチがあります。

Q. 何かgemを使われていますか?使われているなら、どんな選定理由ですか?

いまはelasticsearch-railsを使っていますが、このエントリの後半にあるような理由で捨てようと思っています。移行先はまだ決めていません。

Q. 辞書を作ったりしていますか?

Amazon Elasticsearch Serviceなのでカスタム辞書は使っていません。AESがカスタム辞書に対応したら考えます。

Q. 社内用語のような一般的ではない単語(かつ形態素解析で微妙な位置で区切られてしまう)で検索されるケースが多いとおもいますが、そういうものも、ngramとのハイブリッドで解決していますか?

現在のところ特に工夫はしていません。ngramとのハイブリッドである程度は解決出来ると思っています。

Q. スコアリングの評価について自動化や定量化は行えていますか? 何をもって改善したあるいはその逆であったと言えるか。

現在はスコアリングの評価について自動化や定量化はしていません。現在はユーザーからのフィードバックを元に行っている改善が主です。

ただ定量化自体はすべきだと思っていて、たとえば(私の前職でもある)クックパッド社では「検索成功率」というKPIを定義しており*1、これをもとに検索体験の改善を行っています。Kibelaでも同様にKPIを定義してそれを観測するということは必要だろうと思います。


最初は elasticsearch-rails の使い方の話を中心にしようと思ったのですが、まず全文検索そのものの話を整理しないとそこにたどり着かないなということでその話を中心にしました。

なおKibelaでは現在 elasticsearch-rails を使っていますが、次の理由から別のelasticsearch clientに移行する予定です。

  • elasticsearch-rails はあまりメンテされていない
  • elasticsearch-rails は ActiveRecordのmodel classにフォーカスしすぎていて多くの機能が不要
    • たとえば Model.search というメソッドが定義されるが、基本的に復数modelにまたがった検索しかしないのでこのメソッドを使うことがない
  • elasticsearch-rails から返るモデルが同値比較できなくてテストで困る問題 - Islands in the byte stream で示すようなモンキーパッチがいくつかあって厳しい
    • PR を送ろうにもローカルでテストを走らせるのが難しく、またそもそもあまりメンテされてないっぽいのでやる気がそがれる

じゃあ移行先はどうかというと searchkick はクエリの抽象化が高度すぎてもっと生のelasticsearch queryを使いたいんですよね。GitHubが開発している elastomer-client はわりとよさそうだなと思ってます。