REQLY開発ブログスマートフォン用の表示で見るREQLY開発ブログプロの料理レシピ専門検索エンジンREQLY(レクリー)の開発ブログです。この広告は、90日以上更新していないブログに表示しています。RailsはじめにREQLYでは、料理レシピ検索サービスをWeb上で提供しています。したWebサービスはどういうように作られているのでしょうか。Webサービスの全体像というのは、外部からうかがい知ることはできないものです。そこで今回の記事では、REQLYがどのように成り立っているのかという技術構成を紹介します。reqly.tokyoREQLYの技術基盤REQLYでのレシピ検索の手順を簡単に説明すると、以下の手順になります。REQLYは、以下の5つのサービスから構成されていることになります。Railsは、REQLYを提供する基本的なバックエンドサーバです。クライアントからのリクエストを解釈し、処理し、適切なレスポンスを行います。PostgreSQLは、レシピのメタデータ(材料一覧やレシピサイト名など)を保存しておくためのデータベースです。Elasticsearchは、オープンソースの全文検索エンジンです。材料名やレシピ名をクエリとして検索すると、ヒットしたレシピが類似度スコアと共に返されます。ランキングサーバーは、Rustで独自に実装した、検索結果のさらなる絞り込みと並び替えを行うサーバーです。Elasticsearchの類似度スコア、検索フィルタとのマッチ度、人気度など複数の指標を総合的に考慮し、最終的な検索結果とその順番を決定します。フロントエンドの中で一部の動的なコンポーネントはReactで実装されています。モノリシックなRailsサービスが、デーモンとして動いている各種サービスと適宜通信して、クライアントからのリクエストに応答しています。Railsと各サービス間の連携ここからは、上の図で示したそれぞれのサービスとRailsがどのようにして連携しているのか、具体的に掘り下げて見ていきましょう。Elasticsearchはsearchkickを介して連携しているElasticsearchは、Javaで実装された全文検索エンジンで、APIを通してリクエストを叩くと検索結果が返ってきます。APIでは、jsonをbodyとして渡してリクエストを発行すると、jsonが返ってくるという仕様になっています。APIの挙動を確認する最もシンプルな方法は、curlコマンドを使って手書きのリクエストを送ってみることです。Elasticsearchがlocalhostの9200番ポートに立っていて、そのデータベース名がrecipes_developmentのときに、その中でタイトルに「寿司」が含まれるものを取得するクエリを実行するには、以下のように行います。'{"query":{"dis_max":{"queries":[{"match":{"name.analyzed":{"query":"寿司","analyzer":"searchkick_search"}}}]}}}'Elasticsearchに対するクエリは、基本的にはjsonでコードできるドメイン固有言語で記述する必要があります*1。複雑なクエリを構成することで、検索結果に対する重み付けや絞り込み条件を付与することができますが、複雑なクエリを手書きするのはElasticsearch側の十分な知識が必要とされるので、ここでは必要最小限のrubyコードからよしなにElasticsearchのクエリを生成してくれるようなラッパーライブラリを利用しました。データベースに対応づけられたRubyのクラスとの対応付けを容易にした、より抽象度の高いラッパーライブラリです。searchkickを用いて、以下のように設定しています。index時には、レシピidと、そのレシピに関連するデータベースのレコードをドキュメントとしてElasticsearchに取り込ませます。検索時には、Elasticsearchに検索対象の文字列を投げ、類似度スコアとレシピidの順序付き集合を受け取ります。Elasticsearchが返すのは、Elasticsearchのスコアに基づいたRecipeIDの順とスコアです。この情報をもとに、下のランキングサーバにレシピIDとスコアを渡しています。ランキングサーバーをHTTPプロトコルで呼び出しているランキングサーバーは、Rustで実装した独自のものです。これはHTTPで他のプロセスと通信するようなwebサーバとして実装しました。ランキングサーバーはjsonを受け取ってjsonを返すサーバーとして実装しているので、結果を得るためにPOSTメソッドでjsonを送ると、jsonがレスポンスとして返ってくるという形になっています。これに対しては本質的にはcurlコマンドを叩くことを行えばよいので、Railsの内部においては薄いlibcurlのラッパーを利用しています。具体的には、Rubyの連想配列オブジェクトをjsonに変換してテキスト情報としてリクエストを送って、帰ってきたテキスト情報としてのjsonを再びRubyの連想配列に戻すということを行っています。したサーバーをHTTPプロトコルに基づくWebサーバーとして実装したのは、HTTPがハイパーテキストで表現できるような情報をやりとりする汎用プロトコルであるからにすぎません。ランキングサーバには、Elasticsearchによって絞り込まれたレシピIDと、検索フィルタの内容を送っています。ランキングサーバ内で、オンメモリの特徴量情報からレシピIDに対して再び総合的なスコアを振り、それをもとに絞り込みと並び替えを行い、結果としてレシピIDを返します。PostgreSQLはActiveRecordを介して連携しているバックエンドのデータベースとして、RailsではPostgreSQLを利用しています。したデータベースに入っているデータに対して、どのようにアクセスすればよいでしょうか。このときランキングサーバーのように、ほとんど生のAPIを触るようなプリミティブなラッパーを使うことをまずは考えてみましょう。#{values}")itemsテーブルに問い合わせをするためにはこのようなsql文を書けばいいかもしれません。このvaluesという変数に入っている取得したい値の情報は、どのようにSQL文にマッピングすればよいでしょうか。いえば、SQLから返ってきたitemテーブルの要素は、どのようにruby側で処理すればよいでしょうか。+----+-----------+-------------+----------------------------+-------------------------+-------------------------+----------+---------+こうしたデータベースのテーブルに対応したクラスがRuby側にあれば、DBのそれぞれのレコードが、そのクラスのインスタンスに対応するようにできると考えられます。:name_enendつまり具体的には、Rubyの側にSQLのレコードに対応した、受け取る結果を入れておくためのクラスを作り、返ってきたメッセージとそれとの対応付けを行うということを自動でやってほしいのです。この処理は、SQL側のテーブルのスキーマを決めることができれば、それとのrubyのオブジェクトの対応づけはある程度、機械的に行うことができるからです。Rubyの「クラス」と、DBの「テーブル」が対応するとする。これはExcelのシートに対応する。そのクラスの「インスタンス変数」が、DBの「カラム」に対応する。Excelにおける列に対応する。そのクラスのインスタンスオブジェクトが、DBの「レコード」に対応する。Excelにおける行に対応する。した処理を行ってくれるライブラリのことを総称して、オブジェクトリレーショナルマッパー(ORM)と呼びます。Railsでは生のSQLラッパーよりも高機能に、RubyのオブジェクトとDBを対応づけるようなライブラリを利用しています。Recordと呼ばれるライブラリであり、SQLの1テーブルがRubyの1クラスに、SQLの1レコードがRubyの1クラスにおける1インスタンスになるように対応づけることで、あたかもRubyのオブジェクトであるかのようにデータベースの中身を扱うことができるのです。データベースサーバとソケット通信して、DSLによって記述されたクエリをSQL文に変換してSQLサーバにリクエストを送り、返ってきたSQLのレコードをRubyのクラスに対応づけることができます。REQLYの内部には、レシピを管理するためのRecipeクラスがあり、その中にすべてのメタデータが入っています。300このように、SQL文をラップするRubyのメソッドが提供されています。したクエリをメソッドチェーンでつなげていくことで、SQL文を生で書かずに、複雑なクエリを構成することができ、そのクエリの戻り値をRubyのオブジェクトとして扱うことが可能になります。PostgreSQLはデーモンとして動くSQLサーバーです。Socketを介して行われ、「フロントエンド/バックエンドプロトコル」と呼ばれる、その上で独自プロトコルで通信が行われています。Recordを用いると、こうした通信のプロトコルや生のAPIに触れることなく、開発者はSQLを抽象化して扱うことができるのです。Socketで接続することもできます。Socketで接続することもできますが、REQLYではdockerを用いてそれぞれのプロセスにつき1コンテナを設定しており、その間はTCP/IP通信でデータのやりとりをするようにしています。Recordを利用してレシピIDの情報からPostgreSQLを介して、そのメタデータを得ることができます。抽象的には以下のようなコードで記述されます。Recordを利用してレシピに関する情報をSQLからRails側にロードした後、その情報をHTMLとして書き出します。このときにREQLYでは、一部の動的なコンポーネントはReactを用いて描画しています。Reactをなぜ選択したか、あるいはReactをどのようにRailsに統合すべきかについて行った技術選択については措きますが、このライブラリを利用すると、ReactとRailsを1つのプロジェクトルート内で開発することができます。Options)というヘルパー関数に対応します。Railsのビューをエントリーポイントとしてpropsを渡すと、以後はreactの世界でコンポーネントを描画できるという形になっています。実際に書いたReactのコードがどのように動作するかというと、webpackを用いてクライアントサイドで動作するjavascriptにトランスパイルされます。そのjavascriptがクライアント上でレンダリングを行います。クライアントに送られる情報はReactの内容がコンパクトになったjavascriptと、「どの場所にどういうPropsのReactのコードが埋め込まれるか」というHTMLデータが送られます*2。まとめ要点は以下です。Railsは、ビジネスロジックやバックエンドサービスとのやりとりを含めた、リクエストからレスポンスを返すデータフローを担っています。Ruby上で実装された様々なラッパーによって、バックエンドサービスとのやりとりはカプセル化されているので、開発者は生APIをさわることや、それによって埋め込んでしまうかもしれないバグを回避することができ、抽象的なコードを書くだけで済むようになるので、開発の効率化が促進されると考えられます。卵といえば親子丼だろう。REQLYで、じゃがいもを利用した料理を探してみた。こんにちは!エンジニアのkenboです。»プロフィールid:reqly-tokyoプロの料理レシピ専門検索エンジンREQLY(レクリー)の開発ブログです。アーカイブはてなブログをはじめよう!reqly-tokyoさんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。