SendGridで1つのメールを最大1000件のメールアドレスにまとめて送信する方法

これはよく見るとドキュメントに載っているのだけど、 結構見落としがちだったので、記載しておく。

ドキュメントの内容

v3 Mail Send API概要 - ドキュメント | SendGrid

SendGridのMailSendAPIを使えばOKな感じです。

Rubyのコード

これで終了だとあまりにもつまらないので、 SendGrid公式でRubyのGem(SDK)が提供されているので、それを使った際のサンプルを載せておく。 Rubyのコードにするとざっくりこんな感じだろう。

def mail_send
  sendgrid = SendGrid::API.new(api_key: ENV['SENDGRID_API_KEY'])
  sendgrid.client.mail._("send").post(request_body: params)
end

def params
  param = {
    "custom_args" => {
      "test_mode" => true
    },
    "from" => {
      "email" => 'example@example.com',
      "name" => 'サイト名'
    },
    "content" => [
      {
        "type" => 'text/html',
        "value" => 'テスト'
      }
    ],
    "mail_settings" => {
      "sandbox_mode" =>  {
        "enable" => false
      }
    }
  }
  
  param["personalizations"] = User.all.find_each do |user|
    {
      "to" => [
        {
          "email" => user.email,
          "name" => user.name
        }
      ],
      "substitutions" => {
        "[USER_NAME]" => user.name,
        "[USER_EMAIL]" => user.email,
      },
      "subject" => 'メールタイトル',
      "custom_args" => {
        "site_user_id" => user.id.to_s,
      }
    }
  end
  param
end

mail_send

この方法で運用すれば1000つの宛先へのメール送信をまとめることができる。 配信メールアドレスが30000件なら、30回のリクエストに分割してメール配信リクエストをSidekiqやDelayedJob等でWorkerの数を調整して処理してやれば送信リクエストはサクッと完了するだろう。 ただ、流石に30,000件とかのユーザーへの一斉送信はMarketing Campaigns API Overview - ドキュメント | SendGrid を使うべきでは…? と思ってしまっています。*1

テンプレートに変数をアサイ

他にもMailAPIでは、テンプレートを変数を定義しておき、送信時に変数を展開することができる。(コードのsubstitutionsあたりを参照してください。) それぞれのユーザーでメールアドレスだけでなく、ユーザー名や宛名なんかも異なるだろうし、メールの文章中にそういった動的な文言も含めることができる。

まさにSendGridさまさまである。

*1:今回はAzureからの利用だったため、Marketing Campaigns APIは使えなかったのです。

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