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 はわりとよさそうだなと思ってます。

elasticsearch-rails から返るモデルが同値比較できなくてテストで困る問題

elasticsearch-rails はElasticsearchとRails / ActiveRecordのインテグレーションをしてくれるgemです。

こいつを使っているとき、 特定条件下でモデル同士を比較するテストがコケるようになる という現象に悩まされてきました。pryなどで確認しても、たとえば model_a がes-railsからの返却値であるとき、 model_a == Model.find(model_a.id) がコケるのです。これはクラスのリロードがうまくいっていないからではという示唆を得てSpringを切ると、たしかに再現しなくなりました。そこで elasticsearch-rails を調べたところ、 Elasticsearch::Model::Registry がモデルクラスのインスタンスを保持しているようでした。 Springのドキュメントにも触れているとおり、これはNGです。

https://github.com/rails/spring#class-reloading

Rails前提のgemがSpringに配慮したコードになっていないというのはバグといえるかどうか微妙なところですが、とにかく動かないのは困るのでモンキーパッチを当てて凌ぐことにしました。

Monkey Patch

module ElasticsearchMultipleRecordsExtension
  if defined?(::Spring)
    # Workaround for elasticsearch-rails's bug that Elasticsearch::Model::Registry keeps class instances but they are not reloaded on Spring
    ORIGINAL_TYPE_FOR_HIT = Elasticsearch::Model::Adapter::Multiple::Records.instance_method(:__type_for_hit)

    def __type_for_hit(hit)
      type = ORIGINAL_TYPE_FOR_HIT.bind(self).call(hit)
      Object.const_get(type.name)
    end
  end
end

Elasticsearch::Model::Adapter::Multiple::Records.prepend(ElasticsearchMultipleRecordsExtension)

elasticsearch-rails にもpull-requestを送ろうと思ったのですが、bundle installがコケるレベルでメンテされてないので一旦諦め。

Elasticsearchのドキュメントの直し方

Elastic社の @johtani さんに教えてもらったんですが、たとえば Elasticsearchのドキュメント(たとえばFunction Score Query )をみて不足があるなーと思ったとき、Edit リンクからGitHubの該当ページを直接開けるので即pull-reqを作れます。

f:id:gfx:20180228150033p:plain

あとはCLAにサインして数日待つとレビューを経てマージされたりされなかったりします。

実例: [document] s/logarithm/common logarithm/ by gfx · Pull Request #28821 · elastic/elasticsearch

DroidKaigi 2018で『すばらしきGraphQLのSEKAIへようこそ』という発表をしました

DroidKaigi 2018 に参加して発表してきました。

今回はDroidKaigiということでクライアントサイドの視点から解説しています。よって、サーバーサイドの実装についてはまったく触れていません。サーバーサイドの実装については以前発表をしたことがあるのでそちらもどうぞ。

RejectKaigi 2017で「GraphQL on Rails」という発表をしました - Bit Journey's Tech Blog

今回のDroidKaigiはday 1で当日スタッフとして司会もしましたし、個人的にもDroidKaigi Prelude というイベントをするなどして思い出深いkaigiになりました。また来年もできるといいですね。

DroidKaigi PreludeのKPT

やりました。 運営の @okoysm さん、tarappo さん、および会場提供のDeNA様、ありがとうございました。また、一緒に解説していただいた @muumuumuumuu さんもありがとうございました。もう一人の解説者(予定)だったshirajiさんは家庭の事情で参加できず残念でした。インフルエンザが流行しがちな季節は避けたほうが無難かもしれない…。

総じて楽しかったので次に繋げるために一人KTPを残しておきます。

Keep

  • DroidKaigiの前にtimetableをざっと眺める機会があるのはよい
  • 「ニッチそうなセッションを中心に攻める」というのを裏テーマとしてやったのはよかった
    • 非公式ということもありあまり公平性にはこだわらず、好きなように喋れた
    • 「公平性を重視する」みたいにしたらちょっとつらかったと思われる
  • 30分 x 2という時間は丁度よかった
    • 終盤は疲れてきたのでこれ以上時間を書けるのは体力的に無理だったと思われる
  • プレミアムフライデータイム(16:00~19:00)なので家族持ちにはちょうどよかった
  • LT (Rejected LTs + α) もわりと盛り上がったのでよかった

Problem

  • プレミアムフライデータイムなので参加者が限られてしまった
    • 個人的にもすぐ帰らなければならず、懇親もあまりできなくて残念だった
    • プレミアムフライデータイムのミートアップは総じて見ると自分にとってはプラスだけど、マイナス面も無視できない
  • 徹底解説するつもりが、「timetableをネタに適当に喋る」だけだった感はある
    • これについては「徹底解説」と名付けたのがイマイチだったかも
    • それぞれの話のバックグラウンドなどにもちょっと触れたかったが余裕がなかったという反省もある
    • 個人的には、この1年はAndroid開発から離れているので企画自体かなり無理があったともいえる…

Try

  • このミートアップ自体はとてもよいと思うし、来年は自分はやらないと思うけど誰かやってほしい
    • 「DroidKaigiのtimetableの眺めながら1年を振り返る座談会」みたいなのでもいいかもしれない

JavaScript Quiz: (new Array(2)).map($ => 'hello') === ?

まあ、分かってしまえばなんということはないんですが、結構面食らいますね。

MDNにはちゃんと書いてありました。

Array.prototype.map() - JavaScript | MDN

(callback) is not called for missing elements of the array (...) Due to the algorithm defined in the specification if the array which map was called upon is sparse, resulting array will also be sparse keeping same indices blank.

つまり、空要素に対してはcallbackは呼ばれず、配列のsparse状態(空要素があるという情報)も維持されるということなので、仕様どおりの挙動ではあります。

この「空要素(missing element)」はちょっと分かりにくいんですが、要素の置き場所だけが確保されていて実際には何の値も入っていないという要素です。式として評価すると undefined になります。こんなふうに:

console.log( (new Array(2))[0] === undefined ); // true

in 演算子だと空要素かどうかを調べられます。

console.log( 0 in (new Array(2) )); // false
console.log( 0 in [undefined] ); // true

そして Array.from(iterable)[...iterable] はこのsparse状態は保たず、undefined に変換してしまうようです。

console.log(new Array(2)); // [ <2 empty items> ]
console.log([...new Array(2)]); // [ undefined, undefined ]
console.log(Array.from(new Array(2))); // [ undefined, undefined ]

面白いですね!