カテゴリー: テクノロジー

巨大なJSONをSolrに投入する

今回は小ネタです。

先日、1GB近くある巨大なJSONファイルをSolrに投入する機会がありました。とあるシステムからダンプしたデータで、以下のような形になっています。

[{"id":"10001","name":"名前1","description":"説明文1","timestamp":"2018-01-01 12:00:00"},{"id":"10002","name":"名前2","description":"説明文2","timestamp":"2018-01-02 12:00:00"},{"id":"10003","name":"名前3","description":"説明文3","timestamp":"2018-01-03 12:00:00"},{"id":"10004","name":"名前4","description":"説明文4","timestamp":"2018-01-04 12:00:00"},{"id":"10005","name":"名前5","description":"説明文5","timestamp":"2018-01-05 12:00:00"},...]

要するに、改行のない巨大な1行のテキストファイルです。
SolrにJSONファイルをPOSTしてインデックスを作成させることはできますが、1GBはちょっと大きすぎるので、分割することを考えました。

1行1レコードになっていれば話は簡単で、適当な行数で分割してからJSONの配列になるように加工すればいいだけのことですが、全部が1行になっているのでそういう訳にはいきません。

スクリプト言語でJSONを読み込んで分割することも考えましたが、JSON全体を一括で読み込んで処理するタイプのJSONパーサーでは1GBを扱うのは辛いものがあります。SAXタイプのJSONパーサーを探さないといけないかなあと考えているうちに、jq コマンドを使うのがいいんじゃないかと思い当たりました。

$ jq '.[]' sample.json
{
  "id": "10001",
  "name": "名前1",
  "description": "説明文1",
  "timestamp": "2018-01-01 12:00:00"
}
{
  "id": "10002",
  "name": "名前2",
  "description": "説明文2",
  "timestamp": "2018-01-02 12:00:00"
}
{
  "id": "10003",
  "name": "名前3",
  "description": "説明文3",
  "timestamp": "2018-01-03 12:00:00"
}
{
  "id": "10004",
  "name": "名前4",
  "description": "説明文4",
  "timestamp": "2018-01-04 12:00:00"
}
{
  "id": "10005",
  "name": "名前5",
  "description": "説明文5",
  "timestamp": "2018-01-05 12:00:00"
}

一番外の配列を外して各レコードを取り出すことはできました。1レコード1行になっていると加工しやすいので-cオプションを指定します。

$ jq -c '.[]' sample.json
{"id":"10001","name":"名前1","description":"説明文1","timestamp":"2018-01-01 12:00:00"}
{"id":"10002","name":"名前2","description":"説明文2","timestamp":"2018-01-02 12:00:00"}
{"id":"10003","name":"名前3","description":"説明文3","timestamp":"2018-01-03 12:00:00"}
{"id":"10004","name":"名前4","description":"説明文4","timestamp":"2018-01-04 12:00:00"}
{"id":"10005","name":"名前5","description":"説明文5","timestamp":"2018-01-05 12:00:00"}

ここまでくれば後は簡単で、1000行程度ずつ読み込んでまとめてPOSTするスクリプトを作成して無事に投入することができました。


SolrのSQLインタフェースでdistinct

はじめに

前回の記事で、SQLで言うdistinctをSolrで実現する方法を採り上げましたが、実はSolrでは部分的にではありますがSQLをサポートしており、もっと直接的にdistinctを実現することができます。

SolrのSQLサポート

Solrでは/sqlハンドラでSQLによるリクエストを受け付けます。/sqlハンドラは暗黙の内に設定されているもので、利用者が特に設定をすることなく利用できます。

サポートしているのは SELECT のみです。以下の機能が使えます。

  • WHERE 句で Solr の検索式が書ける
  • ORDER BY句によるソート
  • LIMIT句による件数の指定
  • SELECT DISTINCT句
  • GROUP BY句による集約
  • HAVING句

SELECT DISTINCT

前回の記事の、スポーツ施設で対応できるスポーツの一覧を取得する例は以下のように書けます。

$ curl -s --data-urlencode 'stmt=SELECT sports, count(*) AS cnt FROM sportare GROUP BY sports LIMIT 10' http://localhost:8983/solr/sportare/sql
{
  "result-set":{
    "docs":[{
        "sports":"",
        "cnt":1}
      ,{
        "sports":"BMX",
        "cnt":5}
      ,{
        "sports":"アイスホッケー",
        "cnt":48}
      ,{
        "sports":"アメリカンフットボール",
        "cnt":32}
      ,{
        "sports":"アルペンスキー",
        "cnt":1}
      ,{
        "sports":"アーチェリー",
        "cnt":113}
      ,{
        "sports":"インディアカ",
        "cnt":16}
      ,{
        "sports":"インラインスケート",
        "cnt":10}
      ,{
        "sports":"ウィンドサーフィン",
        "cnt":1}
      ,{
        "sports":"エアロビクス",
        "cnt":262}
      ,{
        "EOF":true,
        "RESPONSE_TIME":155}]}}

試行錯誤中に、以下の問題を見付けました。

  • テーブル名(Solrではコレクション名)に’-‘が含まれているとSQLの文法エラーになる。これはコレクション名のエイリアスを設定すればなんとかなる。
  • LIMITで取得件数は指定できるが、OFFSETが指定できない。OFFSETを指定しても文法エラーにはならないものの、機能はしていないようです。
$ curl -s --data-urlencode 'stmt=SELECT sports, count(*) AS cnt FROM sportare GROUP BY sports LIMIT 10 OFFSET 5' http://localhost:8983/solr/sportare/sql
{
  "result-set":{
    "docs":[{
        "sports":"",
        "cnt":1}
      ,{
        "sports":"BMX",
        "cnt":5}
      ,{
        "sports":"アイスホッケー",
        "cnt":48}
      ,{
        "sports":"アメリカンフットボール",
        "cnt":32}
      ,{
        "sports":"アルペンスキー",
        "cnt":1}
      ,{
        "sports":"アーチェリー",
        "cnt":113}
      ,{
        "sports":"インディアカ",
        "cnt":16}
      ,{
        "sports":"インラインスケート",
        "cnt":10}
      ,{
        "sports":"ウィンドサーフィン",
        "cnt":1}
      ,{
        "sports":"エアロビクス",
        "cnt":262}
      ,{
        "EOF":true,
        "RESPONSE_TIME":168}]}}

おわりに

distinctつながりで、SolrのSQLサポートを調べてみました。distinctに限らず、制限事項がいろいろと存在するので使いどころが案外難しいという印象です。通常の検索処理でというよりもインデックスに対する統計処理などで使うのが良さそうです


オンライン決済stripeを触ってみた

ナイトプールでウェイウェイしてる夢を見ました。私の心は夏模様。
マエダです。

 

Webサービス開発しているとオンライン決済のニーズがでてきますよね。
そんな開発者の悩みを解決してくれるのがオンライン決済サービスstripe。

https://stripe.com/

“デベロッパー・ファースト”と謳っている通りで非常にかんたんなステップでシステムに決済機能を導入できます。

 

Ruby on Railsの場合

vi Gemfile

gem 'stripe'

bundle install –path vendor/bundle

あとはよしなに。

PHP Laravelの場合(定期決済)

composer update
composer require laravel/cashier

あとはよしなに。
https://readouble.com/laravel/5.5/ja/billing.html

 

全然その先説明してないじゃん!って思われたかと思いますがQiitaとかで日本語での情報も十分ありますので単発決済でもサブスクでも必要に応じて活用してみてください!

<参考>
公式ドキュメント:https://stripe.com/docs


サービス設計: 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」のレンズを通して、サービスの強弱を洗い出して、より良いサービスを作れるよう考えてみていきたいと思います。


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