ransackで雑にRDBMSを使った簡易的なキーワードのOR検索を実装する

一般的なRDBMSを使ったよくあるLIKEでの曖昧検索実装では、検索ボックスに「寿司 和食」と入力してもただの単語での曖昧検索だと、 当たり前なんだけど、キーワードのOR検索を実現することができない。 select * from contents where LIKE body '%寿司 和食%' となってしまって、空白を含めた1単語として認識されてしまい、思うような結果が得られないと思う。

実際に組み立てるクエリは…?

多分イマイチなんだろうけど、簡易的にOR検索を実現するにはこんな感じになると思う。

"SELECT "contents".* FROM "contents" WHERE (("title" LIKE '%寿司%' OR "title" LIKE '%和食%') OR ("body" LIKE '%寿司%' OR "body" LIKE '%和食%'))"

これで擬似的にキーワードのOR検索を実装することができた。精度もまぁ無いよりかはマシくらいなもんだろう。 これでOKな事例というのは、検索対象が小規模な場合で、設定できるキーワードに上限を一応設けたほうが良いかもしれない。 以上の注意点を踏まえた上で、Railsで簡単に上記SQLを作る方法に進んでみよう。

実際にRansackで雑に実装

もちろん複雑なことはやっていないので、自分でさっきのSQL文を組み立てて投げるのでも良いのだけど、 面倒だし、検索機能を実装する時によく使われているRansackで実装してみようと思う。

github.com

以下のようにかけばOK。

Content.search(title_or_body_cont_any: ['寿司', '和食']).result

それでは上記で実行されるSQLを見てみよう。

irb(main):007:0> Content.search(title_or_body_cont_any: ['寿司', '和食']).result.to_sql

=> "SELECT \"contents\".* FROM \"contents\" WHERE ((\"contents\".\"title\" LIKE '%寿司%' OR \"contents\".\"title\" LIKE '%和食%') OR (\"contents\".\"body\" LIKE '%寿司%' OR \"contents\".\"body\" LIKE '%和食%'))"

おお、大丈夫そうだ。 あとは、JOINでスペースを区切り文字にして、splitでキーワードの配列作ればOKな感じ。

 # controller 
  def index
    if params[:keyword]
      @contents = Content.search(title_or_body_cont_any: params[:keyword].split(' ')).result
    else
      @contents = Content.all
    end
  end

これで完成。 意外とシュッとできた。やっぱり小規模用途だとransackは便利だった。

SendGridのWebhookを受けるのにFluentdが重宝した話

SendGridのWebhookについて

SendGrid の Event Webhook は、SendGrid経由でメールを送信する際に発生するイベントを、指定したURLにPOSTすることができます。 このデータの用途は、配信停止アドレスの削除、迷惑メール報告への対応、エンゲージできなかった受信アドレスの判定、バウンスされたメールアドレスの特定、メールプログラムの高度な分析などです。ユニーク引数やカテゴリパラメータを使用して、動的なデータを挿入することができるため、あなたのメールのシャープでクリアなイメージを構築するのに役立ちます。

ということなんだけど、 1度に送信件数が5000件以上だったりするとそれなりにOpenや送信したというイベントが飛んで来るので、 このWebhookをどうさばくかというのがちょっとした課題になる。 多分BigQueryに送信するだけであればGCPのLambda相当のサービスを利用すればOKな気がする。 というかGCPがサンプル乗っけてるので、コレはコレで便利なのでぜひ見て欲しい。

SendGrid Tutorial  |  Cloud Functions Documentation  |  Google Cloud

Webhookを受けて、レスポンス内容をMongoに格納する例

今回はFluentdのデフォルトに同梱されているHTTP Input Pluginを利用して、Webhookを受けて、Mongoに送信する方式を取った。 Webhookを受けるだけのなにかアプリケーションを作る必要あったのかと思ったけど、Fluentdでシンプルに解決できる方法があってよかった。

Fluentdのプラグインとして追加が必要なのは、fluent-plugin-mongo くらい。

そのためのfluentd.confをココにおいておく。

<source>
  @type forward
</source>

<source>
  @type http
  port 8080
</source>

<filter **>
  @type stdout
</filter>

<match sendgrid.**>
  @type mongo

   database "#{ENV['DB_NAME']}"
   host "#{ENV['DB_HOST']}"
   port "#{ENV['DB_PORT']}"

   collection "#{ENV['DB_COLLECTION']}"

   user "#{ENV['DB_USER']}"
   password "#{ENV['DB_PASSWORD']}"

   capped
   capped_size 100m

   ssl true
</match>

これでFluentdを再起動し、SendGrid上でport:8080を接続先に設定すればOK。

そもそもBigQueryやTresureDataではなく、 Mongoに送っている理由としては、短い保持期間であればMongoの方が短い時間で結果が返ってくるのかなということでそうしている。 この場合たくさんデータがあふれてくると結局処理時間が長くなるのでは??? という懸念があるので、格納するデータの保持期間については別途検討する必要がありそう。

保持期間を設定してそれまでは保存しておき、古いドキュメントを自動で削除するみたいなのが、 少し簡単にできるかも。今後はコレを参考にして検証する予定である。 qiita.com

サンプルはMongoに送信しているだけだけど、TresureDataやBigQuery、はたまたフォーマッティングして、 S3にもOutputするようにすれば良いんじゃないですかねという感じ。

余談だけど、どこでも動かせるようにということでDockerイメージに固めている。 Azureを利用しているのでWeb App for Containersで動かしている感じ。 多分AWSではECSでも良いだろうし、きっともっと面倒ならElasticBeanstalkでも良いと思う。

Webhookを受けるのにも便利なFluentd

Fluentdはバッファリングしてくれる機構を持っているし、こういったものを自前で実装するよりも信頼性は高い。 Webhook受けて何処かに書き込むような簡単な処理だけなら、わざわざRailsアプリケーションを立てる必要はなくFluentdで十分だと思っている。*1

*1:ロスト確率と、対応時間のコストの兼ね合い考えると、少し重複する分に問題ないなら良い選択と思っている。

Firebase HostingをCircleCIからDeployする

とにかく簡単なので、プロトタイピングのときから初めておくといい。

fierebaseを使ったことがあるなら入っていると思うけど、firebase-tools を事前にインストールしておこう npm install -g firebase-tools

CIからDeployするためのDeployキーを取得する。

firebase login:ci って打つとCI用のDeployキーを払い出してくれる。

Waiting for authentication...

✔  Success! Use this token to login on a CI server:

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Example: firebase deploy --token "$FIREBASE_TOKEN"

払い出されたキーはCircleCIの環境変数に登録して、Deploy時に実行すればOKという感じ。 ほんとうに簡単。

ちなみにプロジェクトを指定してDeployしたい場合は、このように指定すればオッケー

 firebase deploy --project "myproject-xxxxx" --token "$FIREBASE_TOKEN"

FaradayでHTTPリクエストを並列で実行する方法

FaradayはRubyでHTTPリクエストを行うことができるライブラリだけど、 特に工夫もなしに利用すると1つ1つリクエストを順番に直列に実行するようになっている。

もしなにかしらの事情でAPIを並列に叩きたい事情というものもあるので、 その場合は以下の用に対応すればOK。 事前にtyphoeusというGemが必要なので、予めインストールしておこう。

github.com

require 'typhoeus'
require 'typhoeus/adapters/faraday'

response1, response2 = nil

conn = Faraday.new(:url => "http://example.com") do |faraday|
  faraday.adapter :typhoeus
end

conn.in_parallel do
  response1 = conn.get('/one')
  response2 = conn.get('/two')

  # these will return nil here since the
  # requests haven't been completed
  response1.body
  response2.body
end

# at this point the response information you expected should be fully available to you.
response1.body # response1.status, etc
response2.body

こちらにはもう少し詳しく記載されている。 github.com

さらに、Faraday別に使わなくてもtyphoeus単体で利用することも当然可能です。 Faraday別にいらなくね?という場合はこちらを。

github.com

で、効果の程は...?

一体どのくらい早くなるのかと、処理した結果の順番が保証されているかが気になるところなので、 雑に実際のAPIにリクエストして簡単に比較してみることにした。

業務で20個のAPIに対してRubyでHTTPリクエストをする必要が出たので、簡単にAPIを20個叩くようなサンプルを作って雑にベンチ取った。 実際のエンドポイントを叩いているのだけど、これは自分の持っているWebhook送信のデバッグ用のアプリケーションなので、お気になさらずに。

gist22ea9304be7d995147528f0e6713ba59

まとめ

  • 20個のHTTPリクエストは並列化したほうが早い
  • 並列処理の場合、処理結果の順番とかに変更がないか気になるところだけど、順番は保証されているようだった。

Webhookを作るときのデバッグに役に立つものを作った

その名もWebhookDebugger 実はRailsのアプリケーションで15分位でシュッと書いたものを単機能だったのを、素のRackアプリケーションにして書き直したものである。

github.com

以下に簡単にこのアプリケーションについての機能を簡単に説明しておく。

基本はオウム返しをするだけのアプリケーションだが...

ロジックはすごい簡単で、GETで送るクエリパラメータやPOSTのform-dataないしはjsonをレスポンスに含めて返してくれるものである。 それだけだとただのオウム返しAPIでWebhookを作るときに何も便利にはならない。 そこで一工夫を凝らしてみることでWebhookを作るときに便利にしてみた。

1. どのパス、どのHTTPメソッドを指定してもリクエストを受ける取ることができる。

request.binなどの外部サービスでも良いのだけど、これは受け取る先のURLパスが決まってしまっている。 どのパスに送信するかというのをユニットテストレベルではなく、ちゃんと目で確認したいときに重宝するだろう。

  curl -XPOST http://localhost:9292/hoge/fuga | jq
  {
      "path": "/hoge/fuga",
      "request_method": "POST",
      "status_code": 200
  }

  curl http://localhost:9292/nick | jq
  {
      "path": "/nick",
      "response_status_code": 200,
      "request_method": "GET",
      "status_code": 200
  }

2. 任意のHTTPステータスコードを返させることができる

パラメータに response_status_codeというものが存在し、これに対して任意のHTTPステータスコードを設定することができる。 コレにより、5xx系のエラーレスポンスの場合はX分後に送信キューに再送処理を積む。他にも4xx系だったら再送処理無しで異常終了にする。あとは5回以上エラーになったら再送をやめる。 といったことの確認が簡単に出来るようになる。 以下のコードの場合、レスポンスのHTTPステータスコードが必ず401になるようになる。

  curl -XPOST http://localhost:9292/hoge/fuga?response_status_code=401 | jq
  {
      "path": "/hoge/fuga",
      "response_time": 1000,
      "response_status_code": 401,
      "request_method": "POST",
      "status_code": 200
  }

3. レスポンスの待機時間を設定することができる

パラメータに response_timeを設定することで、任意のミリ秒分だけ、レスポンスを返す時間を遅らせることができる。(waitで雑に設定しているだけなので、きっかり1秒とかではなく、ベストエフォートです。) コレによりサーバレスポンスに時間がかかってしまった場合、一旦失敗として扱って送信キューに再送処理を積む。といったことの確認が簡単になった。

  curl -XPOST http://localhost:9292/hoge/fuga?response_time=1000 | jq
 {
      "path": "/hoge/fuga",
      "response_time": 1000,
      "request_method": "POST",
      "status_code": 200
  }

以上の3つがあることで、 オウム返しによる、レスポンスでの送信内容にチェック、 エラー時のWebhookの再送機能やレスポンス待機時間によるエラーハンドリングなどが実現可能になった。

Webhook送信元のアプリケーションがdocker-composeで管理されている場合、Webhookの送信先として、このアプリケーションのコンテナを定義しておくのが良いと思う。

なかなかWebhook送信先のアプリケーションの実装について知識がないと、 こういうテストがなかなか難しかったり、送信先のアプリケーションの完成を待たないとちゃんとテストできないとかがありがちなのだけど、 このAPIを利用することでエラーケースのテストや再送のテストが用意になって、効率がグッと向上した。

どういったわけか、業務上Webhookを作る機会が何故か多かったし、 これからもWebhook処理も書く必要はかならずあるので、このAPIは今後も重宝することだろう。