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