サービス設計: Depth vs Width

こんにちは。開発担当のマットです。

開発はとても楽しい仕事で、人が考えたアイディアに命を吹き込むような仕事だと思っています。

開発者として作っているサービスを成功させたいという気持ちがあるので、サービス設計に問題があるかどうかを判断できるよう、そのサービスの設計を多くの観点から見ることがとても大事です。

そこで、サービス設計の新しい見方を紹介したいと思います。
それは、サービスの「Depth」(深さ)と「Width」(幅)。

それって一体何でしょうか?

何かのサービス(ウェブサイト、アプリ、ゲームなど)を設計する時、1番に考えなければならないことは利用いただくユーザーのことだと思います。
作ったサービスがユーザーのニーズに満たさない場合、サービスは目的を果たしません。

開発者としてそれはとても悲しいことなので、どうしても避けなければなりません。

そうならないように設計者がサービスにできるだけ多くの機能を入れようとすることがありますが、それは果たして正解なのでしょうか?

Widthとは?

ユーザーが何かの行動を取ることを「アクション」と呼びましょう。

検索バーにキワードを入れて、サイトを検索することが「アクション」。
ソーシャルメディアに写真を投稿することも「アクション」。
Bボタンを押したら、マ○オがジャンプすることも「アクション」です。

ユーザーがたくさんのことを体験できるように、設計者がユーザーに多くのアクションを与えることがよくあります。

「アクションを与える」=「自由を与える」=「良い」

と思いきや、100種類のアクションがあると、ユーザーがそれらを覚えないと使えないことが発生してしまいます。
Width = 「ユーザーが取れる行動」ですが
Width = 「複雑さ」とも言えます。

起こせるアクション数が極めて多い、複雑さある、3Dモデレリングソフトの例

せっかくたくさんの機能を作っても、複雑すぎるとユーザーがサービスを使わなくなってしまいます。

では、Depthは何でしょう?

Widthが「ユーザーが取れる行動」だとしたら、Depthは「ユーザーが取った行動によって生み出せる結果の多様性」です。

少し、わかりにくいので、実際の例をあげたいと思います。
それは、どこの国の子供でも好きな「積み木」です。

Widthが極めて低い、Depthが極めてあるオモチャ、積み木

「積み木」がサービスだとしたら、アクションは極めて少ない(積み木を拾う、積み木を置く)ですが、その簡単なルールで生み出せるものは無限に近いです。

デジタルのゲームでいうと、やっぱりMinecraftが思い浮かびますが、多くのMMOで自分のキャラクターを自分なりに成長させることができますし、多くのシミュレーションゲームでは自分の思う通りの街や国を作ることができます。

ユーザーの選択によって、遊び方が変わる

ウェブサイトとアプリでいうと、ソーシャルメディアが一番の例だと思います。
インターネットにテキストや画像ファイルを上げることがずっと昔からありましたが、「プロフィール作成」、「人をフォローする」、「友達に共有する」だけで、世界がぐっと変わりました。

実際の世界に影響を及ぼしているソーシャルメディアの力

行動の組み合わせで、ユーザーに面白いものを作りあげる力を与えることがDepthの特徴です。

では、どうしたらいい?

上記で、「Depth」=「良い」に対して「Width」= 「悪い」、だと解釈しやすいですが、必ずしもそうとは言えないと思います。

例えば、将棋と囲碁を比較すると、将棋のルールの方が複雑で「Width」はありますが・・・だから囲碁がより良いとは言えません。

Widthも適切に設計することが大事だと思います。

機能が少なすぎてWidthが足りない場合、ユーザーはすぐに飽きてしまいます。
逆に、機能がありすぎて複雑になると、ユーザーがすぐに諦めてしまいます。
Depthが足りなければ使っても面白いものではないので、すぐに使われなくなるでしょう。

つまり、サービスに機能をたくさんつけて複雑さを増やしすぎてしまうことがないよう、サービス設計者と開発者がもっと考えるべきではないかと思います。

なお、たくさんの機能をつけても、それがユーザーにとって楽しいまたは便利な結果に繋ぐことができない場合も考え直すべきではないかと思います。

今後、サービスを設計する際、上記の「Depth」と「Width」のレンズを通して、サービスの強弱を洗い出して、より良いサービスを作れるよう考えてみていきたいと思います。


「RubyWorld Conference 2019」に協賛させていただきます

「RubyWorld Conference 2019」に、Goldスポンサーとして協賛させていただきます。

<期間>
2019年11月7日(木)、8日(金)
<会場>
島根県立産業交流会館「くにびきメッセ」
<主催>
RubyWorld Conference開催実行委員会
https://2019.rubyworld-conf.org/


SolrCloudのシャーディングとドキュメントルーティング(その2)

はじめに

前回はドキュメントルーターとして”compositeId”を選んだときの挙動を説明しました。今回取り上げるのはもう一つのドキュメントルーター”implicit”です。

implicitルーターの準備

$ cd server/solr/configsets
$ cp -r _default shard_test2
$ ../../scripts/cloud-scripts/zkcli.sh -zkhost localhost:9983 -cmd upconfig -confdir shard_test2/conf -confname shard_test2
$ ../../scripts/cloud-scripts/zkcli.sh -zkhost localhost:9983 -cmd upconfig -confdir shard_test2/conf -confname shard_test2
$ curl 'http://localhost:8983/solr/admin/collections?action=CREATE&router.name=implicit&name=shard_test2&shards=shard1,shard2,shard3,shard4&maxShardsPerNode=8&replicationFactor=1&collection.configName=shard_test2&wt=xml'

compositeIdのときはnumShardsでシャードの数を指定しますが、implicitではshardsパラメータで各シャードの名前を1個ずつ指定します。

データ投入

前回と同じデータを、シャードの指定なしで投入してみます。

$ curl 'http://localhost:8983/solr/shard_test2/update?commit=true&indent=true' --data-binary @data.json -H 'Content-Type: application/json'

どういうシャード分けされたかを確認。

$ for i in {1..4}; do curl -s "http://localhost:8983/solr/shard_test2/select?q=*:*&rows=0&shards=shard${i}"|jq '.response.numFound'; done
0
9236
0
0

分散されずに特定のシャードにすべてのデータが投入されていました。

シャードを指定してのデータ投入

一旦削除して作り直し。

$ curl 'http://localhost:8983/solr/admin/collections?action=DELETE&name=shard_test2'
$ curl 'http://localhost:8983/solr/admin/collections?action=CREATE&router.name=implicit&name=shard_test2&shards=shard1,shard2,shard3,shard4&maxShardsPerNode=8&replicationFactor=1&collection.configName=shard_test2&router.field=area&wt=xml'

投入時にシャードを指定するには、_route_パラメータを利用します。

$ cat d.json
[
{"id":"1","type":"官公庁","area":"住之江区","name":"軽自動車検査協会大阪主管事務所","address":"住之江区南港東3-4-62"}
]
$ curl 'http://localhost:8983/solr/shard_test2/update?commit=true&indent=true&_route_=shard1' --data-binary @d.json -H 'Content-Type: application/json'

d.json は1件だけのデータです。指定したshard1に入ったことを確認します。

$ for i in {1..4}; do curl -s "http://localhost:8983/solr/shard_test2/select?q=*:*&rows=0&shards=shard${i}"|jq '.response.numFound'; done
1
0
0
0

shard2に1件追加

$ cat d.json
[
{"id":"2","type":"官公庁","area":"住之江区","name":"軽自動車検査協会大阪主管事務所","address":"住之江区南港東3-4-62"}
]
$ curl 'http://localhost:8983/solr/shard_test2/update?commit=true&indent=true&_route_=shard2' --data-binary @d.json -H 'Content-Type: application/json'
$ for i in {1..4}; do curl -s "http://localhost:8983/solr/shard_test2/select?q=*:*&rows=0&shards=shard${i}"|jq '.response.numFound'; done
1
1
0
0

shard3に1件追加

$ cat d.json
[
{"id":"3","type":"官公庁","area":"住之江区","name":"軽自動車検査協会大阪主管事務所","address":"住之江区南港東3-4-62"}
]
$ curl 'http://localhost:8983/solr/shard_test2/update?commit=true&indent=true&_route_=shard3' --data-binary @d.json -H 'Content-Type: application/json'
$ for i in {1..4}; do curl -s "http://localhost:8983/solr/shard_test2/select?q=*:*&rows=0&shards=shard${i}"|jq '.response.numFound'; done
1
1
1
0

shard4に1件追加

$ cat d.json
[
{"id":"4","type":"官公庁","area":"住之江区","name":"軽自動車検査協会大阪主管事務所","address":"住之江区南港東3-4-62"}
]
$ curl 'http://localhost:8983/solr/shard_test2/update?commit=true&indent=true&_route_=shard4' --data-binary @d.json -H 'Content-Type: application/json'
$ for i in {1..4}; do curl -s "http://localhost:8983/solr/shard_test2/select?q=*:*&rows=0&shards=shard${i}"|jq '.response.numFound'; done
1
1
1
1

検索

特に指定しなければ、全シャードを対象にした検索になります。

$ curl -s 'http://localhost:8983/solr/shard_test2/select?q=*:*'{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":15,
    "params":{
      "q":"*:*"}},
  "response":{"numFound":4,"start":0,"maxScore":1.0,"docs":[
      {
        "id":"1",
        "type":["官公庁"],
        "area":["住之江区"],
        "name":["軽自動車検査協会大阪主管事務所"],
        "address":["住之江区南港東3-4-62"],
        "_version_":1634782971384823808},
      {
        "id":"2",
        "type":["官公庁"],
        "area":["住之江区"],
        "name":["軽自動車検査協会大阪主管事務所"],
        "address":["住之江区南港東3-4-62"],
        "_version_":1634782988669550592},
      {
        "id":"3",
        "type":["官公庁"],
        "area":["住之江区"],
        "name":["軽自動車検査協会大阪主管事務所"],
        "address":["住之江区南港東3-4-62"],
        "_version_":1634782999227662336},
      {
        "id":"4",
        "type":["官公庁"],
        "area":["住之江区"],
        "name":["軽自動車検査協会大阪主管事務所"],
        "address":["住之江区南港東3-4-62"],
        "_version_":1634783009317060608}]
  }}

シャードを指定しての検索。

$ curl -s 'http://localhost:8983/solr/shard_test2/select?q=*:*&shards=shard4'
{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":4,
    "params":{
      "q":"*:*",
      "shards":"shard4"}},
  "response":{"numFound":1,"start":0,"maxScore":1.0,"docs":[
      {
        "id":"4",
        "type":["官公庁"],
        "area":["住之江区"],
        "name":["軽自動車検査協会大阪主管事務所"],
        "address":["住之江区南港東3-4-62"],
        "_version_":1634783009317060608}]
  }}

implicitルーターが向くデータ

ここまで見てきたように、compositeIdルーターは自動で程よく分散検索を実現させてくれるルーター、implicitルーターは自分で手動で制御したいときに向いたルーターです。今回使ったデータはcompositeIdルーターに向いたデータと言えるでしょう。

implicitルーターに向いているのは、たとえばログデータです。月単位でシャードを分けてシャードを指定しつつデータを投入、新しい月が来たらシャードを追加、といった使い方ができます。


SolrCloudのシャーディングとドキュメントルーティング(その1)

はじめに

SolrCloudの機能の1つにシャーディングがあります。
たとえば4台のノードが利用できるときに、インデックスを4つのシャードに分けることにより、1ノードごとの検索負荷を小さくし、検索速度を向上させることができます。

シャーディングの設定

コレクション作成時に指定することでインデックスをいくつかのシャードに分けることができます。

まずサンプルの _default をコピーして configsets を用意します。

$ cd server/solr/configsets
$ cp -r _default shard_test
$ ../../scripts/cloud-scripts/zkcli.sh -zkhost localhost:9983 -cmd upconfig -confdir shard_test/conf -confname shard_test

4個のシャードからなるコレクションを作成

$ curl 'http://localhost:8983/solr/admin/collections?action=CREATE&router.name=compositeId&name=shard_test&numShards=4&maxShardsPerNode=8&replicationFactor=1&collection.configName=shard_test&wt=xml'

サンプルデータ

サンプルとして大阪の施設情報を利用します。

$ grep -v ^# osaka_shisetsu20140106.txt |head -3
158	34.6164938333333	135.438210722222	http://lodosaka.hozo.jp/class/施設情報	官公庁	国の機関	住之江区	軽自動車検査協会大阪主管事務所	軽自動車検査協会大阪主管事務所	住之江区南港東3-4-62	
157	34.6190439722222	135.442191833333	http://lodosaka.hozo.jp/class/施設情報	官公庁	国の機関	住之江区	大阪陸運支局なにわ自動車検査登録事務所	大阪陸運支局なにわ自動車検査登録事務所	住之江区南港東3-1-14	
381	34.6109641111111	135.491388722222	http://lodosaka.hozo.jp/class/施設情報	官公庁	国の機関	住吉区	住吉税務署	住吉税務署	住吉区住吉2丁目17番37号	http://www.nta.go.jp/osaka/guide/zeimusho/osaka/sumiyoshi/index.htm

スクリプトを通してJSONに変換します。

{"id":"158","type":"官公庁","area":"住之江区","name":"軽自動車検査協会大阪主管事務所","address":"住之江区南港東3-4-62"},
{"id":"157","type":"官公庁","area":"住之江区","name":"大阪陸運支局なにわ自動車検査登録事務所","address":"住之江区南港東3-1-14"},
{"id":"381","type":"官公庁","area":"住吉区","name":"住吉税務署","address":"住吉区住吉2丁目17番37号"},
  :

データを投入

$ curl 'http://localhost:8983/solr/shard_test/update?commit=true&indent=true' --data-binary @data.json -H 'Content-Type: application/json'

データ件数を確認

$ curl -s 'http://localhost:8983/solr/shard_test/select?q=*:*&rows=0'|jq '.response'
{
  "numFound": 9236,
  "start": 0,
  "maxScore": 1,
  "docs": []
}

特に何も指定しなければ、ドキュメントID(この場合はidフィールド)のハッシュ値に基づいてシャードが割り振られます。シャード毎に何件ずつ入っているかを確認します。

$ for i in {1..4}; do curl -s "http://localhost:8983/solr/shard_test/select?q=*:*&rows=0&shards=shard${i}"|jq '.response.numFound'; done
2379
2360
2207
2290

4つのシャードにだいたい均等に分かれていますね。

ドキュメントルーティング

負荷分散さえできれば良い場合なら上記のようなシャード分けで十分訳に立ちますが、
検索パターンによっては特定のフィールド値を持つレコードが同じシャードに配置される構造になっていると都合が良いことがあります。


たとえばCMSの検索のような場合は、ユーザを指定して検索することが多くなります。この場合、同じユーザのインデックスが同じシャードに配置されるようになっていれば、検索時に特定のシャードにだけクエリを投げれば良く、分散検索に関するオーバーヘッドを抑えることができます

これを実現するのがドキュメントルーティングです。

2つのドキュメントルーター

何らかの値に基づいてレコードを各シャードに割り振るのがドキュメントルーターの役割です。ドキュメントルーターとして以下の2つのどちらかを選ぶことができます。

  • compositeId (デフォルト)
    • ドキュメントIDのフィールド値に基づいて自動的にルーティングする。!区切りで任意の値をルーティング用のキーとして含めることができる
  • implicit
    • レコード毎にどのシャードにインデックスするかを手動で指定する

この記事では compositeId の動作を見て行きます。

compositeId

コレクション作成時にパラメータ router.name を指定しない場合にはデフォルトである compositeId ルータが使われます。(もちろん明示的に router.name=compositeId を指定しても良い)

上の例でも見たように、特に指定しなければドキュメントIDのハッシュ値に基づいてシャードが振り分けられます。何らかの値に基づいてシャードを振り分けたい場合には、その値を!区切りでドキュメントIDに含めます。

(例)
元のドキュメントID: 「1234」
区名に基づいてシャードを分けたいとき: 「中央区!1234」

このルールを使って投入用のJSONデータを少し変更します。

{"id":"住之江区!158","type":"官公庁","area":"住之江区","name":"軽自動車検査協会大阪主管事務所","address":"住之江区南港東3-4-62"},
{"id":"住之江区!157","type":"官公庁","area":"住之江区","name":"大阪陸運支局なにわ自動車検査登録事務所","address":"住之江区南港東3-1-14"},
{"id":"住吉区!381","type":"官公庁","area":"住吉区","name":"住吉税務署","address":"住吉区住吉2丁目17番37号"},
  :

先程投入したデータを全部削除してから新しいデータを投入します。

$ curl -s 'http://localhost:8983/solr/shard_test/update?stream.body=*:*&commit=true'
$ curl -s 'http://localhost:8983/solr/shard_test/update?commit=true&indent=true' --data-binary @data2.json -H 'Content-Type: application/json'

シャードごとに、どの区のデータが何件含まれているかを出力してみます。特定のシャードには特定の区だけが存在していることが分かります。

$ curl -s 'http://localhost:8983/solr/shard_test/select?facet.field=area_str&facet=on&q=*:*&rows=0&shards=shard1'|jq .facet_counts.facet_fields.area_str
[
  "中央区",
  509,
  "鶴見区",
  310,
  "福島区",
  270
]
$ curl -s 'http://localhost:8983/solr/shard_test/select?facet.field=area_str&facet=on&q=*:*&rows=0&shards=shard2'|jq .facet_counts.facet_fields.area_str
[
  "北区",
  566,
  "住之江区",
  402,
  "東住吉区",
  402,
  "天王寺区",
  349,
  "都島区",
  319,
  "大正区",
  318,
  "東成区",
  318
]
$ curl -s 'http://localhost:8983/solr/shard_test/select?facet.field=area_str&facet=on&q=*:*&rows=0&shards=shard3'|jq .facet_counts.facet_fields.area_str
[
  "淀川区",
  467,
  "住吉区",
  409,
  "生野区",
  401,
  "西淀川区",
  360,
  "西区",
  357,
  "此花区",
  349,
  "旭区",
  274,
  "浪速区",
  236
]
$ curl -s 'http://localhost:8983/solr/shard_test/select?facet.field=area_str&facet=on&q=*:*&rows=0&shards=shard4'|jq .facet_counts.facet_fields.area_str
[
  "平野区",
  610,
  "東淀川区",
  469,
  "城東区",
  449,
  "西成区",
  388,
  "港区",
  322,
  "阿倍野区",
  308,
  "大阪市以外",
  76
]

ドキュメントIDのハッシュ値に基づいて分割した場合にはほぼ均等に割り当てられていましたが、特定のフィールド値に基づいて分割する場合には、そのフィールド値の偏りが分割の偏りとなってしまうことに注意が必要です。

$ for i in {1..4}; do curl -s "http://localhost:8983/solr/shard_test/select?q=*:*&rows=0&shards=shard${i}"|jq '.response.numFound'; done
1089
2674
2853
2622

検索時のドキュメントルーティング

シャードに関するパラメータが無い場合には、すべてのシャードにクエリが投げられます。

$ curl -s 'http://localhost:8983/solr/shard_test/select?debugQuery=on&rows=0&q='`echo "area_str:中央区 AND name:消防署"|jq -s -R -r @uri` |jq '.debug.track.EXECUTE_QUERY|keys'
[
  "http://localhost:8983/solr/shard_test_shard1_replica_n1/",
  "http://localhost:8983/solr/shard_test_shard2_replica_n2/",
  "http://localhost:8983/solr/shard_test_shard3_replica_n4/",
  "http://localhost:8983/solr/shard_test_shard4_replica_n6/"
]

「中央区」が存在するシャードにだけクエリを投げられれば、分散検索に関するオーバーヘッドを抑えることができます。たとえばshards=shard1という風にshardsパラメータでシャードを直接指定できます。

$ curl -s 'http://localhost:8983/solr/shard_test/select?shards=shard1&debugQuery=on&rows=0&q='`echo "area_str:中央区 AND name:消防署"|jq -s -R -r @uri` |jq '.debug.track.EXECUTE_QUERY|keys'
[
  "http://localhost:8983/solr/shard_test_shard1_replica_n1/"
]

ただし、検索対象がどのシャードに属するのかがクエリ作成時には分からない場合がほとんどです。分割の基準となるフィールド値を_route_パラメータで指定することでシャードを指定することができます。たとえば、中央区だけを対象としたい場合には _route_=中央区 を指定します。

$ curl -s 'http://localhost:8983/solr/shard_test/select?debugQuery=on&rows=0&q='`echo "area_str:中央区 AND name:消防署"|jq -s -R -r @uri`'&_route_='`echo 中央区\!|jq -s -R -r @uri` |jq '.debug.track.EXECUTE_QUERY|keys'
[
  "http://localhost:8983/solr/shard_test_shard1_replica_n1/"
]

おわりに

大量の文書をインデックスするにあたってシャーディングの利用は不可欠です。想定される検索シーンのパターンに応じてドキュメントルーティングすることによってリソースを効率よく利用することができます。


ついに手に入れた最強アイテム

こんにちわ。
リエです。

暑い日が続いておりますが、いかがお過ごしでしょうか。

いよいよ夏本番というところですが、わたしは紫外線がとても怖い。
紫外線対策は通年を通してやっていますが、夏は他の季節よりも気合を入れて対策をしないとすぐに日焼けをしてしまいます。(当たり前ですが)

使っていた日傘が古くなってきたので、いいのないかな〜と探していたところ、ずっと欲しかった日傘を手に入れることができました。
その名も【SUN BARRIER100】
https://uv100.jp/
SUN BARRIER100とは?
紫外線・赤外線・可視光線など、地上に届くすべての光を100%カットできる完全遮断の機能が備わっている日傘。(※日傘以外の商品もあり)
独自開発した3層構造の生地を使用しているので、効果は生地が破れない限り半永久。傘の色によって遮断率が変わらないことも特徴の一つ(一般的には黒い色ほど光を遮断すると言われています)
引用元:https://uv100.jp/secret.html

ひとつひとつ手縫いで作っているため大量生産ができず、大人気なこともあり入荷してもすぐに売り切れ。わたしも入荷待ちをしてやっと買えました。
ちなみに購入したサイズはSサイズです。
サイズはSサイズの他に折りたたみ(2段or3段)やLLサイズまであり、幅広いサイズがあるのも嬉しいポイントです。

実際使ってみての感想

SUN BARRIER100の前にも完全遮断100%の日傘を使っていましたが、SUN BARRIER100は軽い!そして使っていた日傘も完全遮断100%なのに涼しさが違う!というのが率直な感想です。Sサイズは小さいかなと思っていましたが、日常生活に使うにはちょうどいいサイズでした。(日傘は大きいと結構邪魔になるので、、)これから大事に長く使いたいと思います。

※画像はサイトからお借りしました

実は日傘と併せてこれも・・・

日傘が使えないシーンもあるので、帽子もほしいな〜と思っていたら、運よく帽子も入荷しており一緒に購入しました。
あっ、SUN BARRIER100は日傘の他にも帽子やサンバイザーなどあるんです。しかも日傘と同じ紫外線・赤外線・可視光線など、地上に届くすべての光を100%カットしてくれるのです。(回し者ではありません)
機能重視でかぶってみたら微妙なのかな〜と思っていましたが、届いたのを見てかわい〜!となりました。普段着ている服にも似合いそうでホッと一安心。
さっそく外出時に使ったのですが、つばが広くて肩までカバーしてくれてびっくりしました。あとかぶっていると涼しい!
海やレジャーなどで大活躍してくれそうです。

※画像はサイトからお借りしました

この最強アイテムたちと共に今年の夏を乗り切りたいと思います。