Unitrad APIの設計

新しいAPIを設計するためにカーリルが取り組んだこと

2016/9/11 Code4Lib Japan Conference

Ryuuji Yoshimoto CC-BY

新規開発は楽しいよ

今日の話

Server

Client

Web API

React

伝統的な横断検索の再発明

API First

先にAPIを決めて、

スクレイピングエンジンとAPI

フロントエンド

それぞれ並走して開発していく

 

設計が品質を決める → がんばらない

どうやってAPIを設計する?

ニーズと課題

既存の横断検索システムは、

AJAXの採用で使いにくくなった

画面の頻繁な変化はストレス

RESTfulな応答はできない

横断検索先は依然として遅い

レガシーな環境・検閲に対応しなければならない

i-FILTERの影

カスタマイズのニーズに対応

APIよりうしろはさわらない

都道府県の横断検索はもはや動いていない

動くものをつくる

横断検索先が増えると遅くなる

横断検索の流れ

検索クエリ

検索結果

検索結果

検索結果

Server

Client

データストリーミングの手法

  • ポーリング

  • ロング・ポーリング

  • WebSocket

  • Socket.IO

    あなたにWebSocketは必要ないかも
    http://postd.cc/you-might-not-need-a-websocket/

自分が解決しようとしている問題をメッセージングパターンの観点から考え、データ同期についても考慮してください。そうすれば、シンプルで、より適切なHTTPベースの手法が見つかるでしょう。

ポーリング+α

  • 変化があるまでは待機(ロング・ポーリング)

  • タイムアウト時間を指定 
       ポーリングの最適化はクライアントに任せる

転送量の削減

 

カーリルローカル → ポーリング 毎回全件取得

横断検索先の増加すると遅くなる

スマホでつらい

効率的な差分転送の検討

JSON-delta: a diff/patch pair

  for JSON-serialized data structures

 

 

  • http://json-delta.readthedocs.io/en/latest/

  • JSONの差分データを生成するライブラリ

  • PythonやJavascriptでの実装がある
  • 追加/変更/削除

横断検索の流れ

検索クエリ

初期データ

差分

差分

Server

Client

JSON-delta

実装したらうまくいかない!

 

 

100件のデータに10件増える… 数ミリ秒で差分生成

1000件のデータに100件増える... 10ミリ秒以内で差分生成

10000件のデータに1000件増える .... 3000ミリ秒以内で差分生成

 

 

データが増えると差分生成コストが高くなる傾向

マージはわりと速い

心地よい制約

  • 差分データの生成は重い
       → 増分ならどうか

  • 横断検索データは減ることはない
    そもそも読んでいるうちに動くのはストレス

  • 書誌データに特化した、
    データが増加・または変化することしか想定しない
    差分フォーマットの開発

差分生成
(Python)



def diffs(new, old):
    ret = {
        'insert': [],
        'update': []
    }
    for line, x in enumerate(new):
        if line < len(old):
            _tmp = {}
            for k, v in x.items():
                if isinstance(v, unicode):
                    if old[line][k] != v:
                        _tmp[k] = v
                elif isinstance(v, list):
                    if old[line][k] != v:
                        _tmp[k] = list(set(v) - set(old[line][k]))
                        _tmp[k].sort()
                elif isinstance(v, dict):
                    diff = set(v.keys()) - set(old[line][k].keys())
                    if len(diff) > 0:
                        _tmp[k] = {}
                        for key in diff:
                            _tmp[k][key] = v[key]
            if len(_tmp.keys()) > 0:
                _tmp['_idx'] = line
                ret['update'].append(_tmp)
        else:
            ret['insert'].append(x)
    return ret

マージ
(ES2016)

        Array.prototype.push.apply(this.data.books, data.books_diff.insert);
        for (key in data) {
          if (data.hasOwnProperty(key)) {
            if (key !== 'books_diff') {
              this.data[key] = data[key];
            }
          }
        }
        ref = data.books_diff.update;
        for (i = 0, len = ref.length; i < len; i++) {
          d = ref[i];
          for (key in d) {
            if (d.hasOwnProperty(key)) {
              if (key !== '_idx') {
                if (Array.isArray(d[key]) === true) {
                  Array.prototype.push.apply(this.data.books[d._idx][key], d[key]);
                } else if (d[key] instanceof Object) {
                  for (k in d[key]) {
                    this.data.books[d._idx][key][k] = d[key][k];
                  }
                } else {
                  this.data.books[d._idx][key] = d[key];
                }
              }
            }
          }

ちょっとした制約で高速化

 

 

100件のデータに10件増える… 1ミリ秒以内で差分生成

1000件のデータに100件増える... 1ミリ秒以内で差分生成

10000件のデータに1000件増える .... 5ミリ秒以内で差分生成

 

 

コストバランスがとても重要

サーバー側の実装

  • すべてのバージョンのデータをメモリ上に保持
  • クライアントが持っているデータのバージョンと

   サーバーが持っているバージョンの差分を返す

   → サーバー側はステートレス

  • HTTP/2 
  • GZIP圧縮  
     → HTTP層の様々な技術に乗ってさらに高速に


 

横断検索に見られる無意味なチェックボックス群

押すとチェックが外れるんじゃなくてリンクしちゃう

横断検索先の選択など仕様に入れるものか

クライアントの実装で勝手にやってください

API設計はサービスのゆるい運用ポリシーでもある

 

それは制約の設定である

 

なにをして、なにをしないのか

結局、全部作ったけど、

それほど大変ではなかった。

メモリは潤沢に、そしてオーバーコミット

エラーになったら潔く死ぬ

自動リカバリー

頑張らなくていいよ。

Special Thanks