LiBz Tech Blog

LiBの開発者ブログ

Algolia meets Rails

f:id:tkmiya34:20191206101621p:plain

はじめに

LiBバックエンドエンジニアの宮澤です。

LiBでは開発業務以外にもSlack絵文字職人、アニメ部長を兼任しています。

アニメ部は社内のアニメ好きがSlackに立ち上げた雑談チャンネルが徐々に参加者が増えて、いつの間にか社内部活動では最大派閥です。

tech.libinc.co.jp

今回もAlgoliaの紹介記事の続きを書いてみました。

前回は"フロントエンド開発だけで検索機能を実現する"というコンセプトで、フロントエンドの検索クエリの部分だけを実装しました。Algolia公式のJS向けのAPIクライアントのおかげでとてもお手軽に検索機能を作ることができましたが、Algliaは他にも多くの言語やフレームワーク向けにライブラリを提供しています。

今回はRails向けのAPIクライアントであるalgoliasearch-railsを使って、RailsアプリケーションとAlgoliaを連携させてみようと思います。

github.com

前回はAlgoliaへのレコード追加するのに「JSONファイルをアップロードする」豪快な方法をとっていました。実際のWebシステムでデータをJSONファイルで管理しているなんてことはないでしょうから、RailsからRDBにデータ保存すると同時にAlgoliaへもデータ同期することで、より現実的にWebアプリケーションへAlgoliaを組み込んだときのイメージを掴んで頂けるのではないかと思います。

開発環境

前回のコードベースはVuetifyやVue.jsのコンポーネントを使っていましたが、Algoliaの解説をするには余計だったのでイチから作り直しています。

バックエンドはRailsを使い、フロントエンドのVueはRailsのwebpakcerを使って環境構築ました。

実装

普通にRailsアプリケーションを作る

まずはAlgoliaを使わずにシンプルなRailsアプリケーションでユーザー情報を保存/表示する機能を作ります。

テーブル

class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email

      t.timestamps
    end
  end
end
class User < ApplicationRecord
end

名前、メールアドレス、誕生日を持つ会員モデルを作成しました。

APIエンドポイント

API::UserControllerに一覧を返すindexアクション、ユーザ追加を行うcreateアクションを作成しました。

class Api::UsersController < ApplicationController
  protect_from_forgery

  def index
    @users = User.all
  end

  def create
    user_params = params.require(:user).permit(:name,:email)
    User.create!(user_params)
  end
end

ユーザー追加画面

フォームに入力したユーザー情報をAPIへPOSTします

<template>
  <form @submit.prevent="createuser">
    <div>
      <label>Name</label>
      <input v-model="user.name" type="text">
    </div>
    <div>
      <label>Email</label>
      <input v-model="user.email" type="text">
    </div>
    <button type="submit">Commit</button>
  </form>
</template>

<script>
import axios from 'axios';

export default {
  data: function () {
    return {
      user: {
        name: '',
        email: ''
      },
      error: ''
    }
  },
  methods: {
    createuser: function() {
      let self = this;

      axios
        .post('/api/users', this.user)
        .then(response => {
          let u = response.data;
          this.$router.push({ name: 'UserShow', params: { id: u.id } });
        })
    }
  }
}
</script>

f:id:tkmiya34:20191206110137p:plain

フォームからAPIへリクエストするとDBに追加保存されます

ユーザー一覧画面

axiosを使ってAPIからユーザー一覧を取得します。

<template>
  <form @submit.prevent="createuser">
    <div>
      <label>Name</label>
      <input v-model="user.name" type="text">
    </div>
    <div>
      <label>Email</label>
      <input v-model="user.email" type="text">
    </div>
    <button type="submit">Commit</button>
  </form>
</template>

<script>
import axios from 'axios';

export default {
  data: function () {
    return {
      user: {
        name: '',
        email: ''
      },
      error: ''
    }
  },
  methods: {
    createuser: function() {
      let self = this;

      axios
        .post('/api/users', this.user)
        .then(response => {
          let u = response.data;
          this.$router.push({ name: 'UserIndex', params: { id: u.id } });
        })
        .catch(error => {
          console.error(error);
        });
    }
  }
}
</script>

f:id:tkmiya34:20191206110232p:plain

ユーザー追加画面で追加したユーザーが表示されていますね。

ここまでは普通のRailsアプリケーションですね。 ここからAlglioaとの連携部分を実装していきます。

Algolia連携

AlgoliaとRailsの連携には公式のGemを使います。

github.com

Gemをインストールしたら、RailsにAlgoliaの設定を追加します。 /app/initializers/algolia_search.rbを作成してAlgoliaのアプリケーションIDとAPIキーを設定します。 キー情報はcredentialで管理するのが良いでしょう。

AlgoliaSearch.configuration = {
  application_id: Rails.application.credentials.algolia_search[:application_id],
  api_key: Rails.application.credentials.algolia_search[:api_key]
}

次にUserモデルにAlgoliaを組み込みます。

algoliasearchブロックではデータ同期するカラムや、同期の条件などのオプション指定ができます。 今回はattriburtesメソッドでデータ同期するカラムを指定しています。うっかりパスワードや個人情報などを同期してしまわないように注意しましょう。

class User < ApplicationRecord
  include AlgoliaSearch

  algoliasearch do
    attributes :name
  end
end

最後にユーザ追加のAPIで、DBへの保存後にalgoliaへインデックスを作成します

class Api::UsersController < ApplicationController
  protect_from_forgery

  def create
    user_params = params.require(:user).permit(:name,:email)
    uuser = User.create!(user_params)

    # algoliaにインデックス作成
    user.algolia_index!
  end
end

この状態でもう一度ユーザ情報を追加してからAlgoliaのダッシュボードを見てみましょう

f:id:tkmiya34:20191205231308p:plain

f:id:tkmiya34:20191206110458p:plain

Algoliaにデータが同期されていますね!

今度は一覧画面のデータ取得をRailsのAPIではなくAlgoliaから取得し、検索機能も追加してみましょう

<template>
  <div id="app">
    <!-- テキスト検索のフォームを追加 -->
    <input v-model="search_text" placeholder="edit me">

    <table>
      <tbody>
        <tr>
          <th>ID</th>
          <th>Name</th>
          <th>Email</th>
          <th>Birtday</th>
        </tr>
        <tr v-for="u in users" :key="u.id">
          <td>{{ u.id }}</td>
          <td>{{ u.name }}</td>
          <td>{{ u.email }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
// axiosではなくalgliaのAPIクライアントを使う
import algoliasearch from 'algoliasearch';

export default {
  data: function () {
    return {
      users: [],
      search_text: '',
      index: null
    }
  },
  created: function() {
    let self = this;
    // Algolia APIクライアントを初期化
    let searchClient = algoliasearch(
      'APP_ID',
      'SEARCH_ONLY_API_KEY'
    )
    // 使用するindexを指定
    self.index = searchClient.initIndex('User');
    this.searchUser()
  },
  watch: {
    search_text: function () {
      this.searchUser()
    }
  },
  methods: {
    searchUser: function () {
      let self = this;
      // ここでAlgoliaのAPIへ検索リクエストを投げています
      // searchメソッドの第一引数が検索文字列です。
      self.index.search(self.search_text, (err, { hits } = {}) => {
        // 検索結果をデータバインドしているusersに格納
        self.users = hits;
      });
    }
  }
}
</script>

<style scoped>
p {
  font-size: 2em;
  text-align: center;
}
</style>

f:id:tkmiya34:20191206110541p:plain

検索も機能していますね! フォームに「あ」と入力したところ、名前に「あ」がつくユーザーだけが表示されています。

これでRailsアプリケーションへの組み込みイメージを持っていただけたでしょうか。

最後に

今回はRails側での単一テーブルでそのままAlgolia側のインデックスを作成しましたが、モデルの algoliasearch do ~ end のブロックに様々な設定を追加することでより柔軟なデータ連携が可能です。

  • データ連携するカラムを指定して機密情報が同期されないようにする、通信データ量を調整する
  • 削除フラグ、公開フラグなど特定の条件に一致したレコードだけを同期する
  • ActiveJobsなどでの同期の非同期実行
  • has_many / belongs_toなどの関連テーブルも1レコードにとめて同期する ( AlgoliaはレコードをJSONで保存するドキュメント型データベース )

もちろんalgoliasearch-railsでバックエンドでAlgoliaからデータ取得できます。

例えば、ユーザーのフォロー機能などがあって、ユーザーの検索結果画面に自分がそのユーザーをフォローしているか?といった情報を記載したいなら

  • フロントエンドからRailsのAPIへ検索パラメータをリクエスト
  • Railsで検索パラメータを受け取ったら、algoliasearch-railsでAlgoliaへ検索リクエスト
  • 検索結果の配列に含まれるユーザーIDを使って、RDBからフォロー情報を取得
  • フォロー情報を配列に追加してフロントエンドへレスポンスを返す

こういった手順で、検索はAlgoliaへ任せつつ、検索結果にAlgolia同期していない情報も含めることも出来ます。

これで実際のRailsアプリケーションに組み込むイメージが出来てきたのではないでしょうか。