🐰

【SDK 利用編】Rails API × Meilisearch SDK をつかってみる

2024/01/11に公開

この記事の続きです。

Rails を利用して API の開発を行っています。
検索機能を Meilisearch を用いて実装したいので、meilisearch-rails gem という Rails 向けの Meilisearch SDK を利用して User インデックスの検索・ドキュメント作成を rails console 上で確認します。

環境構築

下記記事内で、テストデータを作成していますが、作成しなくても大丈夫です。

https://zenn.dev/ndjndj/articles/1be14c27323294

meilisearch-rails について

間違いなく公式が詳しいです。
ここでは私が個人的に注目したい部分を紹介します。

前提

こんなかんじのモデルを例にいろいろ書いていきます。

class User < ApplicationRecord
  include MeiliSearch::Rails
  
  # Meilisearch 上のプライマリーキー
  meilisearch primary_key: :id
  
  meilisearch do
    # インデックス追加対象となるフィールド
    attribute :id, :name, :name_ruby, :url
  end
end

その他 Meilisearch でのオプションに対応しています。

自動インデックス作成

RDB 側の users テーブルの変更時に、自動的に Meilisearch のインデックスと同期します。

# rails console で実行

# レコード作成
User.create(name: "namae", name_ruby: "ナマエ")
# -> RDB users テーブルにレコードが追加される
# -> Meilisearch にドキュメントが自動的に追加される

# レコード削除
User.find(1).destroy 
# -> RDB users テーブル id = 1 のレコードを削除
# -> Meilisearch の対応するドキュメントも自動的に削除される

# レコード更新
user = User.find_by(id: 2)
user.update(name: "updated_name", name_ruby: "updated_name")
# -> RDB users テーブル id = 2 の属性を更新
# -> Meilisearch の対応するドキュメントも自動的に更新される

以前、Meilisearch + Lambda で検索機能を実装していたころは DynamoDB Stream をつかったり、バッチ処理でドキュメント追加をぐるぐる回したりしてなかなか大変だった記憶があったので、この機能はフレームワークならではという感じでかなり便利だと感じました。

後から導入した際の indexing

既存のモデルに後から Meilisearch を導入する際は reindex! メソッドを使用することで Meilisearch のインデックスを作成することができます。
また、再定義などを行った場合には、インデックスのドキュメントを一度全削除して再度インデックスを作成することもできます。

# rails console で実行
User.clear_index! # ドキュメントの全削除
User.reindex! # インデックス作成・ドキュメント追加

検索

meilisearch-rails では、以下のように検索を行うことができます。
Meilisearch API で検索を実行し、インデックスから得られた primary-key の配列について RDB 上のテーブルに対して SELECT をかけるというふうな流れで検索を実行しているみたいです。

つまり、以下のコードは、

hits = User.search('東京都')
# => [User, User ...]

以下の2つのステップを経て得られる結果ということになります。

curl \
  -X POST 'http://meilisearch:7700/indexes/User/search' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer meili-master-key' \
  --data-binary '{ "q": "東京都" }'
SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2, $3...) 

このような仕様があるためかどうかは不明ですが、公式ではクライアントサイドからの API 利用を推奨しています。
確かにあいまい検索のハードルを越えることはできそうですが、RDB への問い合わせを行う時点でその分実行時間は増えるので Meilisearch の速度を活かしきれないような気がします。
とはいえ、ActiveRecord の仕組みを活用することで他の API に倣った実装ができることや、キーを隠蔽することができたり、Meilisearch 側で余分なデータを持たなくてよくなるのはメリットといえそうです[1]

一応 ms_raw_search を使えば Meilisearch API を叩いた結果の生データのみを取得することができますが、それをするくらいなら Ruby 用の SDK を使うか、クライアント側で頑張った方がいいのかなと。

互換性

meilisearch-rails のメソッドは search, index などのメソッドを提供します。
すでにモデル側で同名のメソッドが定義されている場合、再定義はされることはありません。
メソッドのプレフィックスに ms_ を指定して実行することもできます。

hits = User.ms_search('東京都')
# => [User, User ...]

SDK を試してみる

事前準備

事前準備として、データを用意します。
今回は前回の記事でも使用した架空の User データを使用します。

はじめるまえに

前回の記事で環境構築を行い、テストデータまで作成していた場合は、一度きれいにインデックスごと削除することをおすすめします。

削除方法
curl \
  -X DELETE 'http://localhost:7700/indexes/{インデックス名}' \
  -H 'Authorization: Bearer meili-master-key'
  
# コマンドプロンプト用
curl -X DELETE "http://localhost:7700/indexes/{インデックス名}" -H "Authorization: Bearer meili-master-key"

モデル作成

Rails 側でモデルを作成します。

rails g model user

自動生成された db/migrate/yyyymmddxxxxxx_create_user.rbmodels/user.rb をそれぞれ記述し、db:migrate します。

db/migrate/yyyymmddxxxxxx_create_user.rb
class CreateUsers < ActiveRecord::Migration[7.1]
  def change
    create_table :users do |t|
      t.string :user_system_id, :null => false, comment: "ユーザーID"
      t.string :name, :null => false, comment: "名前"
      t.string :name_ruby, comment: "名前(フリガナ)"
      t.integer :age, comment: "年齢"
      t.string :pref, comment: "都道府県"
      t.integer :pref_code, comment: "都道府県コード"
      t.string :tel, comment: "電話番号"
      t.string :url, comment: "URL"
      t.timestamps
    end
  end
end
models/user.rb
class User < ApplicationRecord
  include MeiliSearch::Rails

  meilisearch primary_key: :id

  meilisearch do
    attribute :id, :name, :name_ruby, :url
  end
end
rails db:migrate

テストデータ作成(RDB)

db:seed でテストデータを作成します。
手元にサンプルデータ用の json があれば seeds.rb を以下のように書き換えて db:seed でテストデータを生成することができます(参考記事: Qiita Railsでjsonファイルのデータをdbに入れる)。

ちなみにレコード数は1000件くらいがちょうどいいと思います。前回の記事で作成したデータを10万件そのままぶん回したら当然のように2時間くらいかかりました。

seeds.rb
user_json = ActiveSupport::JSON.decode(File.read(
  Rails.root.join("db", "sample.json")
))

user_json.each do |d|
  User.create(
    uid: d["user_system_id"],
    name: d["name"],
    name_ruby: d["name_ruby"],
    age: d["age"],
    pref: d["pref"],
    pref_code: d["pref_code"],
    tel: d["tel"],
    url: d["url"]
  )
end
rails db:seed

meilisearch-rails の導入

meilisearch-rails gem を導入します。
Gemfile に以下を追記して bundle install します。

gem "meilisearch-rails", "~> 0.10.2"
bundle install

Meilisearch 用の設定

config/initializers/meilisearch.rb を作成するか以下のコマンドを実行して Meilisearch 用の設定ファイルを作成します。

rails meilisearch:install

以下のように書き換えます。

MeiliSearch::Rails.configuration = {
  meilisearch_url: ENV.fetch('MEILISEARCH_HOST', 'http://meilisearch:7700'), 
  meilisearch_api_key: ENV.fetch('MEILISEARCH_API_KEY', {設定した API キー})
}

ドキュメントでは、MEILISEARCH_HOSTlocalhost を指定していますが、 Docker を利用している場合は、コンテナ名を指定する必要があるので注意が必要です。

Meilisearch にインデックスを作成する

rails console 上で最初のインデックス作成を行っていきます。

rails c
> User.reindex!

プレビュー画面で、RDB 側のテーブルのレコード数と Meilisearch 側のドキュメントのレコード数が一致しているか確認してください。

疎通確認

rails console 上で、検索とインデックスの自動作成がうまくいくか、試してみます。

検索

> User.ms_search("ヒガシタニ")
# => User Load (41.9ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2,...) 

インデックスの自動作成

> User.create(user_system_id: "tekiteki", name: "適当な名前", name_ruby: "tekitonanamae")
# => TRANSACTION (0.3ms)  BEGIN
# => User Create (4.1ms)  INSERT INTO "users" ("user_system_id", "name", ...)
# => TRANSACTION (3.9ms)  COMMIT
> User.ms_search("tekitonanamae")
# => User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1...

さいごに

API 開発なら、クライアントから Meilisearch の API を SDK を利用して呼び出した方が速度も担保できるのではと思ったが、RailsserializerActiveRecord の仕組みを活用できるので他の API に倣った実装ができること、キーを隠蔽できたりするのはメリットといえそう。

そうした Rails の各種機能が使えること + あいまい検索の機能向上 + 検索ロジック・ルールをバックエンドで持てること、などのメリットに対して、 SQL での問い合わせによるパフォーマンス低下をどのくらい許容できるのかは考えた方がいいと思う。

一方で、meilisearch-rails を導入することによる、RDB -> Meilisearch のオートインデックス機能はかなりのメリットがあると感じた。

Meilisearch に全乗っかりするならクライアントでもバックエンドでも API ごりごり使っていくのもいいかも。

次回はこの続きで、検索 API とレコード作成 API を作っていきます。

参考

Meilisearch 関連

https://www.meilisearch.com/docs/reference/api/overview
https://zenn.dev/ndjndj/articles/1be14c27323294
https://zenn.dev/ndjndj/articles/c8b6359d2b42b0

meilisearch-rails 関連

https://github.com/meilisearch/meilisearch-rails#-getting-started
https://zenn.dev/takeyuwebinc/articles/1a8ba1885c0f4d#meilisearch-rails
https://www.youtube.com/watch?v=pPn-DUqUf1E&t=4s
https://zenn.dev/yagince/articles/7b1e21cf7cc409

rails db:seed

https://qiita.com/wnoonw/items/009daa7e284238e39ff0

脚注
  1. 一応クライアント側でキーを暗号化するなどの方法も一応あるらしい。そもそもとにかく隠蔽しないといけないものではないのかもしれない(Google MAP の API KEY も確かそんな感じだった気がする) ↩︎

Discussion