この記事は「eureka Advent Calendar 2019」17日目の記事です。

16日目はPairs JP iOS Teamの木村(Muukii)による「SwiftUIに適したアプリケーション設計を思考する」でした。

こんにちは!こんにちは!
APIチームのコジマ(@__bsoo )です。

今回はPairsの検索システムについて、勉強会やブログで小出しにしてはいたのですが、全体像をまとめて紹介したことが無かったのでこの機会に紹介できればと思います。

※サービスの性質上内容を一部ぼかしている箇所があります。ご了承ください。

■ システム構成

まずは全体の構成について紹介します。

言語:Go 1.13
検索エンジン:Elasticsearch 6.4

Pairsの全体構成図から検索部分のみを抜粋
Pairsの全体構成図から検索部分のみを抜粋
Pairsの全体構成図から検索部分のみを抜粋

Pairsの検索システムと呼んではいますがマイクロサービスではないので、Pairsアプリケーションの一部を抜粋しています。

こちらに関しては基本的には一般的なウェブアプリケーション構成だと思います。

Elasticsearchに関しては初期はAmazon Elasticsearch Serviceを利用していたのですが、現在はチューニングの都合上EC2での運用を行っています。

■ データ同期

Elasticsearchにデータを同期するのは2種類の機構が存在します。

基本的には定期実行ジョブによりBulk APIを利用したドキュメントの完全更新をするようにしています。ただしこの場合実際にプロフィールが変更されてからElasticsearch上のデータが更新されるまでに時間差が発生してしまいます。リアルタイムに更新を反映させたい項目の場合はUpdate APIを使用し、API上からドキュメントを部分更新を行っています。

■ 検索フロー

簡単にですが検索のフローをご紹介します。

  1. クライアントから送信された検索条件と、内部で定義されたスコアリングからクエリを生成
  2. Elasticsearchにクエリを投げて検索
  3. 非表示にすべきデータをフィルタリング
  4. レスポンス生成

スコアリングにはFunction Score Queryを利用しています。条件やスコアの重みなどを柔軟に指定することができます。

Pairsの検索基盤として肝なのが 3のフィルタリング部分です。
オンラインデーティングサービスの性質上、検索者自身と表示されるお相手との間には複数の関連(いいね送った、受け取った・マッチング・非表示している、されている・退会済み etc… )があり、それらの表示・非表示を全て適切にハンドリングする必要があります。

当たり前の内容ではあるのですが、表示すべきではない結果を誤ってでも表示されてしまうとサービス品質・お客様からの信用を大きく落としてしまう可能性もあるため得に重要としています。
Elasticsearchで検索するクエリでもフィルタリングを行っているのですがデータの量の問題や同期タイミングの問題で完全にはフィルタリングしきれていないため、3でさらにフィルタリングを行うようにしています。

■ スコアの計算

現在Pairsで利用している一部の検索スコアはリアルタイムで計算することが難しく、事前計算が必要となっているものがいくつか存在しています。
ものによってはユーザーの組み合わせの数だけ処理が必要になっているため全てを処理するために大量のリソースと速度が必要となります。そのためスコア計算に関してはLambdaを利用しています。

Image for post
Image for post
Lambdaでのスコア計算システム構成
  1. Job Workerより計算に必要な共通で利用するパラメータを取得してS3にアップロード
  2. 別のJob Workerで計算対象とするユーザーを取得し各ユーザー毎のパラメータをSQSに送信
  3. SQSへのEnqueueをイベントとしてLambdaが起動。S3上のパラメータを取得し、ユーザーのパラメータと外部APIへ送信してスコアを取得
  4. DynamoDB等のデータストアへ保存

現在もまだ調整・改善な箇所がありますが、これによりある程度の計算速度を担保できるようになっています。
LambdaからRDSへ接続する際のコネクションプーリング問題があったため、RDSに接続できればどれだけ楽なのだろうと思っていた矢先、AWSさんよりこの問題を解決できる「RDS Proxy」の発表がありました。まだプレビュー版なので、正式リリースされたら真っ先に導入しようと計画しています。

■ 終わりに

現状の検索システムについて紹介させていただきました。
長いこと運用しているシステムではあるのですが、まだまだ足りていない部分が多くあり、今後も事業の成長に合わせてシステムの改善を予定しております。
現在APIチームでは一緒に働く仲間を絶賛募集中です。カジュアル面談も実施しておりますので、ご興味を持って頂けたら是非一度お話しましょう!


Image for post
Image for post
Gopher by Takuya Ueda
The Gopher character is based on the Go mascot designed by Renée French.

こんにちは。エウレカAPIチームのコジマ (@__bsoo)です。
今回はGoogle Cloud FunctionsでLINE Messaging APIを利用して、メッセージを送ると固定メッセージを返してくれるところまでbotを作ってみたので、手順の記録と共にポイントを説明していこうと思います。

今回作成したレポジトリ: https://github.com/bsoo/gcf-line-bot-sample

やること

□ gcloud コマンドラインツール

はじめに gcloud コマンドラインツールをインストールや コンポーネントのインストール、プロジェクトの設定が必要です。
こちらを参考に設定をしてください。

□ LINE Messaging APIの利用設定

LINEのAPIを利用するために開発者登録、アプリの登録、tokenの発行などを行います。

開発者登録

LINEのアカウントでログインすることで、LINE開発者サイトアカウントが登録されます。

チャネル・プロバイダーの作成

公式のドキュメントに従ってチャンネル・プロバイダーの作成を行ってください。
作成が完了したらコンソールから作成したプロバイダー→チャネルを選択しチャネルの基本設定画面を開きます。

Image for post
Image for post
チャネルの基本設定画面

基本設定を開いたら「メッセージ送受信設定」のアクセストークン(ロングターム)の欄にある発行ボタンを押してアクセストークンを発行します。
次に同じページにある「LINE@機能の利用」の欄の自動応答メッセージをOffにします。右側にある「設定はこちら」のリンクから設定画面に移動し、応答メッセージをOffに切り替えてください。

□ 実装

任意の場所にプロジェクトディレクトリを作成します。

mkdir gcf-line-bot-sample
cd gcf-line-bot-sample

続いて以下のファイルをディレクトリに作成していきます。

webhook.go

○初期化

Cloud Functionsでもinit関数やグローバル変数を使う事でインスタンス単位ではリソースを使い回すことができるので、毎リクエスト毎に必要のないものはグローバルで持つなどすることで処理速度が大幅に改善できるかもしれません。

参考: Go ランタイム

○依存関係

Cloud FunctionsのGoのversionは1.11がサポートされています。ライブラリの管理にはgo …


こんにちは。エウレカAPIチームの小島です。

2018年も残すところ後少しとなりましたが、皆さんいかがお過ごしでしょうか。僕は残りの営業日に怯えながら今年中のタスクをこなす毎日を過ごしています。

そんな話はさておき、普段のコードを書いていく中でどうしても気になってしまうコードの実行速度。Golangはベンチマークが簡単に取れるのでなおさら色々やって計測してみたくなると思います。

そんなベンチマークですが、ふと昔のコードを見返していたところベンチマークの書き方について色々思うところがあったのでまとめて書いてみました。

今回mapとsliceどちらが速いのかを計測する以下のコードを題材にしてみようと思います。

上記コードの実行結果は以下になります。

BenchmarkSlice-4 200000 11172 ns/op
BenchmarkMap-4 100000 13820 ns/op

一度計測したいだけなら上記のコードでもよいのですが、基本的にベンチマークを取る際は利用するケースになるべく近い状態で取ることがベストだと思います。そこで以下の観点が重要になってきます。

上記を踏まえて以下の様なコードにしました。

それぞれ説明していきます。

条件を変えてのテストがしやすいか

簡単に説明してしまうと通常のテスト同様にテーブルテスト形式でケースを複数用意し、for文で回して条件を変えてテストができるように引数を追加しています。

mapとsliceの話でいうとそれぞれのデータの数によりデータを取得する際のアクセス速度が違ってきます。ちなみにどう変わるかというと件数が少ない場合にはsliceが速いのですが、mapはデータ件数が大きくなってきても大きく速度が変わらない性質があります。そのため、件数によってどちらが速いか遅いかが変わります。なので少ない場合と多い場合のテストや、閾値がどこなのかを探るためにも複数条件でテストがかけるようにしておくことが望ましいです。

比較しやすいか

こちらも簡単な話で結果の並び順の問題だと思います。テーブルテストにしたことでケース毎の順番を入れ替えられるようにしています。BenchmarkSliceMap の中に書いてあるように b.Run を利用することで1つの関数内で複数のベンチマークを実行できるようになります。実行箇所を一箇所に集めることで実行時にわかりやすい表示になるように、また実行順序を入れ替えやすいようにしています。

比較条件は公平か

sliceとmapでデータ作成とデータ取得を計測したい場合は最初に上げた例でも条件は公平になっていると思うのですが、取得時の差を取りたい場合は、データ作成の時間も含まれてしまっているため純粋に取得時間だけを測ることができておらず、データ作成に時間が掛かる分mapの方が遅くなり、公平ではありません

これを解消するために b.ResetTimer() を使用します。比較したいロジックの前で呼ぶことでそれまでにかかった時間をなかったコトにします。データ作成後かつデータ取得前に入れることでデータ作成にかかる時間を含めず計測することができます。

特にプロダクションコードを計測する場合などは計測がしやすいコードを事前に書いておく必要があります。これはテストしやすいコードとも言えるので詳細は割愛します。

最後に

まとめて書いてみると思ったよりも当たり前の内容だなと自分でも思ったのですが、自分の書いたコードでも結構多く、見落としている方も多いようにおもいます。

今回1ケースで考えたので足りない、違うなどあるかもしれませんが何かあればご意見お待ちしています。

また弊社では一ヶ月に一回Goもくもく会を開催しております。基本的に各自の作業するだけでなく、LT等簡単なイベントも少々考えているので、もしよろしければご参加くだいさい。

About

Hiroki Kojima

2014年に株式会社エウレカに新卒として入社。 現在はAPIチームに所属し、Pairsのバックエンド開発を担当。検索周りの機能をメインに担当しています。

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store