🔎

MeiliSearch使ってみる Rails編

2021/10/05に公開

https://zenn.dev/yagince/articles/037746ab3ebfd1
↑前回、簡単にMeiliSearchを使ってみたので今回はRailsから使ってみるのを試してみます

環境

  • OS: Ubuntu 20.04
  • MeiliSearch: 0.22.0
  • Ruby: 3.0.2
  • Rails: 6.1.4.1

Rails環境を作る

rails new rails-sample --api --database=postgresql -B -M -C -S -J -T --skip-yarn --skip-jbuilder
Dockerfile
FROM ruby:3.0.2-alpine3.13 AS ruby

## Development
FROM ruby AS dev

RUN apk update \
    && apk add --no-cache \
        gcc \
        g++ \
        libc-dev \
        linux-headers \
        make \
        postgresql \
        tzdata \
        git \
        graphviz \
    && apk add --virtual build-dependencies --no-cache \
        postgresql-dev \
        libxml2-dev \
        build-base \
        curl-dev

RUN mkdir -p /app
ENV HOME /app

WORKDIR $HOME

COPY ./Gemfile* ./
ARG bundle_install_options="--without development test doc --jobs 4"
RUN bundle config github.https true \
  && bundle install $bundle_install_options \
  && apk del build-dependencies

COPY . /app
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]

## Production
FROM ruby AS prod

RUN apk update \
    && apk add --no-cache \
        gcc \
        g++ \
        libc-dev \
        linux-headers \
        make \
        postgresql \
        tzdata \
  && cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime \
  && apk del --purge tzdata

RUN mkdir -p /app
ENV HOME /app
ENV RAILS_ENV production

WORKDIR $HOME

COPY --from=dev /usr/local/bundle /usr/local/bundle
COPY --from=dev /app/app $HOME/app
COPY --from=dev /app/bin $HOME/bin
COPY --from=dev /app/config $HOME/config
COPY --from=dev /app/db $HOME/db
COPY --from=dev /app/lib $HOME/lib
COPY --from=dev /app/public $HOME/public
COPY --from=dev /app/Gemfile* $HOME/
COPY --from=dev /app/Rakefile $HOME/
COPY --from=dev /app/config.ru $HOME/

RUN mkdir -p tmp/pids

CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
docker-compose.yml
version: "3.7"
services:
  meilisearch:
    container_name: meilisearch
    image: getmeili/meilisearch:v0.22.0
    volumes:
      - meili-data:/data.ms
    environment: []
    ports:
      - 7700:7700

  # 追加
  postgres:
    image: postgres:13.2-alpine
    container_name: postgres
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_HOST=postgres
      - POSTGRES_DB=rails_sample_dev
    ports:
      - "5433:5432"
    volumes:
      - pg-data:/var/lib/postgresql/data

  rails:
    container_name: rails
    build:
      context: ./rails-sample
      args:
        bundle_install_options: --jobs 10
      target: dev
    volumes:
      - ./rails-sample:/app
    environment:
      RAILS_LOG_TO_STDOUT: 1
      TZ: Asia/Tokyo
    ports:
      - 3300:3000

volumes:
  meili-data:
    driver: local
  pg-data:
    driver: local
Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '3.0.2'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main'
gem 'rails', '~> 6.1.4', '>= 6.1.4.1'
# Use postgresql as the database for Active Record
gem 'pg', '~> 1.1'
# Use Puma as the app server
gem 'puma', '~> 5.0'
# Use Active Model has_secure_password
# gem 'bcrypt', '~> 3.1.7'

# Use Active Storage variant
# gem 'image_processing', '~> 1.2'

# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.4.4', require: false

# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
# gem 'rack-cors'

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
end

group :development do
  gem 'listen', '~> 3.3'
  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

# 追加
gem "meilisearch-rails", "~> 0.2.3"

検索対象のモデルはUserにします

/app # bin/rails g model user
Running via Spring preloader in process 74
      invoke  active_record
      create    db/migrate/20211004144942_create_users.rb
      create    app/models/user.rb
20211004144942_create_users.rb
class CreateUsers < ActiveRecord::Migration[6.1]
  def change
    create_table :users do |t|
      t.string :given_name, null: false
      t.string :family_name, null: false
      t.integer :age

      t.timestamps
    end
  end
end

あとでフルネームを検索対象にする為に、姓・名を分けておきました。

データ投入確認

/app # bin/rails db:migrate
== 20211004144942 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0076s
== 20211004144942 CreateUsers: migrated (0.0077s) =============================
/app # bin/rails c
Running via Spring preloader in process 136
Loading development environment (Rails 6.1.4.1)
irb(main):001:0> User.create!(given_name: "太郎", family_name: "山田", age: 10)
  TRANSACTION (0.1ms)  BEGIN
  User Create (0.5ms)  INSERT INTO "users" ("given_name", "family_name", "age", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["given_name", "太郎"], ["family_name", "山田"], ["age", 10], ["created_at", "2021-10-04 14:56:38.430087"], ["updated_at", "2021-10-04 14:56:38.430087"]]
  TRANSACTION (2.4ms)  COMMIT
=> #<User:0x00007febd8d77a00 id: 1, given_name: "太郎", family_name: "山田", age: 10, created_at: Mon, 04 Oct 2021 14:56:38.430087000 UTC +00:00, updated_at: Mon, 04 Oct 2021 14:56:38.430087000 UTC +00:00>
irb(main):002:0> User.create!(given_name: "花子", family_name: "鈴木", age: nil)
  TRANSACTION (0.3ms)  BEGIN
  User Create (0.4ms)  INSERT INTO "users" ("given_name", "family_name", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["given_name", "花子"], ["family_name", "鈴木"], ["created_at", "2021-10-04 14:56:53.526337"], ["updated_at", "2021-10-04 14:56:53.526337"]]
  TRANSACTION (2.5ms)  COMMIT
=> #<User:0x00007febd62b9b60 id: 2, given_name: "花子", family_name: "鈴木", age: nil, created_at: Mon, 04 Oct 2021 14:56:53.526337000 UTC +00:00, updated_at: Mon, 04 Oct 2021 14:56:53.526337000 UTC +00:00>

Rails内のMeiliSearch設定

https://github.com/meilisearch/meilisearch-rails#-getting-started
これを見て進めていきます

MeiliSearchの設定

config/initializers/meilisearch.rb
MeiliSearch.configuration = {
  meilisearch_host:    "http://meilisearch:7700",
  meilisearch_api_key: ENV["MEILI_MASTER_KEY"],
}

Modelの設定

app/models/user.rb
class User < ApplicationRecord
  include MeiliSearch

  meilisearch {
    attribute :given_name, :family_name, :age
    attributes_to_highlight ['*']
  }
end

Indexにデータを登録

/app # bin/rails c
Running via Spring preloader in process 82
Loading development environment (Rails 6.1.4.1)
irb(main):001:0> User.search("山田")
=> []

当然まだデータが無いので結果はからっぽですね
データを入れます

irb(main):002:0> User.reindex!
  User Load (0.3ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1000]]
=> nil

検索してみます

irb(main):003:0> User.search("山田")
  User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1  [["id", 1]]
=> [#<User:0x00007f7999f004f0 id: 1, given_name: "太郎", family_name: "山田", age: 10, created_at: Mon, 04 Oct 2021 14:56:38.430087000 UTC +00:00, updated_at: Mon, 04 Oct 2021 14:56:38.430087000 UTC +00:00>]

取れましたね。

irb(main):009:0> u = User.search('山田').first
  User Load (0.6ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1  [["id", 1]]
=> #<User:0x00007f7998a04bf0 id: 1, given_name: "太郎", family_name: "山田", age: 10, created_at: Mon, 04 Oct 2021 14:56:38.430087000 UTC +00:00, updated_at: Mon, 04 Oct 2021 14:56:38.430087000 UTC +00:00>
irb(main):010:0> u.formatted
=> {"id"=>"1", "given_name"=>"太郎", "family_name"=>"<em>山田</em>"}

ハイライトされたデータは formatted で取得できるみたいです。

Indexへのデータ追加・更新・削除

定期的に reindex! してもいいんですが、create時に自動で連携してほしいですね

irb(main):004:0> User.create!(given_name: "拓哉", family_name: "木村", age: 50)
  TRANSACTION (0.4ms)  BEGIN
  User Create (0.6ms)  INSERT INTO "users" ("given_name", "family_name", "age", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["given_name", "拓哉"], ["family_name", "木村"], ["age", 50], ["created_at", "2021-10-04 15:14:40.150366"], ["updated_at", "2021-10-04 15:14:40.150366"]]
  TRANSACTION (2.5ms)  COMMIT
=> #<User:0x00007f7996db8ed8 id: 3, given_name: "拓哉", family_name: "木村", age: 50, created_at: Mon, 04 Oct 2021 15:14:40.150366000 UTC +00:00, updated_at: Mon, 04 Oct 2021 15:14:40.150366000 UTC +00:00>

irb(main):005:0> User.search("木村")
  User Load (0.4ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1  [["id", 3]]
=> [#<User:0x00007f7996b4a098 id: 3, given_name: "拓哉", family_name: "木村", age: 50, created_at: Mon, 04 Oct 2021 15:14:40.150366000 UTC +00:00, updated_at: Mon, 04 Oct 2021 15:14:40.150366000 UTC +00:00>]

どうやら、デフォルトでそうなるみたいです。

destroyも同様です

irb(main):011:0> User.last
  User Load (0.4ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT $1  [["LIMIT", 1]]
=> #<User:0x00007f79971970e0 id: 3, given_name: "拓哉", family_name: "木村", age: 50, created_at: Mon, 04 Oct 2021 15:14:40.150366000 UTC +00:00, updated_at: Mon, 04 Oct 2021 15:14:40.150366000 UTC +00:00>

irb(main):012:0> User.last.destroy
  User Load (0.5ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT $1  [["LIMIT", 1]]
  TRANSACTION (0.1ms)  BEGIN
  User Destroy (0.3ms)  DELETE FROM "users" WHERE "users"."id" = $1  [["id", 3]]
  TRANSACTION (0.6ms)  COMMIT
=> #<User:0x00007f799712ac88 id: 3, given_name: "拓哉", family_name: "木村", age: 50, created_at: Mon, 04 Oct 2021 15:14:40.150366000 UTC +00:00, updated_at: Mon, 04 Oct 2021 15:14:40.150366000 UTC +00:00>

irb(main):013:0> User.search("木村")
=> []

ActiveRecordからデータを更新してみます

irb(main):001:0> User.last
  User Load (0.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT $1  [["LIMIT", 1]]
=> #<User:0x00007f03d68fbdb8 id: 7, given_name: "ぱなこ", family_name: "田中", age: 1, created_at: Tue, 05 Oct 2021 12:19:55.103263000 UTC +00:00, updated_at: Tue, 05 Oct 2021 12:25:15.523735000 UTC +00:00>

irb(main):002:0> User.last.update!(age: 60)
  User Load (0.5ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT $1  [["LIMIT", 1]]
  TRANSACTION (0.1ms)  BEGIN
  User Update (0.5ms)  UPDATE "users" SET "age" = $1, "updated_at" = $2 WHERE "users"."id" = $3  [["age", 60], ["updated_at", "2021-10-05 12:28:42.820953"], ["id", 7]]
  TRANSACTION (2.4ms)  COMMIT
=> true

irb(main):003:0> u = User.search("木村").first
  User Load (0.4ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1  [["id", 4]]
=> #<User:0x00007f03d7e1d9f8 id: 4, given_name: "拓哉", family_name: "木村", age: 66, created_at: Mon, 04 Oct 2021 15:21:40.019247000 UTC +00:00, updated_at: Mon, 04 Oct 2021 15:25:07.344447000 UTC +00:00>

irb(main):004:0> u.update!(age: 100)
  TRANSACTION (0.2ms)  BEGIN
  User Update (0.4ms)  UPDATE "users" SET "age" = $1, "updated_at" = $2 WHERE "users"."id" = $3  [["age", 100], ["updated_at", "2021-10-05 12:29:56.073582"], ["id", 4]]
  TRANSACTION (2.4ms)  COMMIT
=> true
irb(main):005:0> User.search("木村").first.formatted
  User Load (0.5ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1  [["id", 4]]
=> {"id"=>"4", "given_name"=>"拓哉", "family_name"=>"<em>木村</em>", "age"=>100}

ちゃんと更新されてますね。

非同期で更新

ActiveJobとSidekiqを使う為、先に準備します

Sidekiq準備

docker-compose.yml
version: "3.7"

x-rails-envs: &rails-envs
  RAILS_LOG_TO_STDOUT: 1
  REDIS_URL: redis://redis:6379/0
  TZ: Asia/Tokyo
  MEILI_MASTER_KEY: api-key
x-rails-build: &rails-build
  context: ./rails-sample
  args:
    bundle_install_options: --jobs 10
  target: dev

services:
  meilisearch:
    container_name: meilisearch
    image: getmeili/meilisearch:v0.22.0
    volumes:
      - meili-data:/data.ms
    environment:
      MEILI_MASTER_KEY: api-key
    ports:
      - 7700:7700

  postgres:
    image: postgres:13.2-alpine
    container_name: postgres
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_HOST=postgres
      - POSTGRES_DB=rails_sample_dev
    ports:
      - "5433:5432"
    volumes:
      - pg-data:/var/lib/postgresql/data

  redis:
    container_name: redis
    image: redis:6.2.5-alpine3.14
    ports:
      - 6380:6379
    volumes:
      - redis-data:/data
    command: redis-server --appendonly yes

  rails:
    container_name: rails
    build: *rails-build
    volumes:
      - ./rails-sample:/app
    command: /bin/sh -l -c 'bundle && bundle exec puma -C config/puma.rb'
    environment: *rails-envs
    ports:
      - 3300:3000
    depends_on:
      - postgres
      - redis

  sidekiq-worker:
    build: *rails-build
    command: /bin/sh -l -c 'bundle && bundle exec sidekiq -c 2 -q default -q meilisearch'
    restart: always
    volumes:
      - ./rails-sample:/app
    environment: *rails-envs
    depends_on:
      - postgres
      - redis

volumes:
  meili-data:
    driver: local
  pg-data:
    driver: local
  redis-data:
    driver: local
  • redisを追加
  • sidekiq-worker を追加
    • MeiliSearchは meilisearch という queueにJobをキューイングするらしいので、オプションでmeilisearchキューも処理するように設定
  • railssidekiq-worker はビルド設定と環境変数が同じなので共用の値として定義した
Gemfile
...
# 以下追加
gem "sidekiq", "~> 6.2"
gem "hiredis", "~> 0.6.3"
gem "redis-namespace", "~> 1.8"
config/initializer/sidekiq.rb
redis_url      = ENV["REDIS_URL"]
redis_password = ENV["REDIS_PASSWORD"]
redis_config   = { url: redis_url, password: redis_password, driver: :hiredis, namespace: :sidekiq }

Sidekiq.configure_server do |config|
  config.redis = redis_config
end

Sidekiq.configure_client do |config|
  config.redis = redis_config
end
config/application.rb
module RailsSample
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.1

    ...
    config.active_job.queue_adapter = :sidekiq
  end
end

起動確認

docker-compose exec rails sh
/app # sidekiq
2021-10-05T12:04:58.813Z pid=109 tid=2h5 INFO: Booting Sidekiq 6.2.2 with redis options {:url=>"redis://redis:6379/0", :password=>nil, :driver=>:hiredis, :namespace=>:sidekiq}


               m,
               `$b
          .ss,  $$:         .,d$
          `$$P,d$P'    .,md$P"'
           ,$$$$$b/md$$$P^'
         .d$$$$$$/$$$P'
         $$^' `"/$$$'       ____  _     _      _    _
         $:     ,$$:       / ___|(_) __| | ___| | _(_) __ _
         `b     :$$        \___ \| |/ _` |/ _ \ |/ / |/ _` |
                $$:         ___) | | (_| |  __/   <| | (_| |
                $$         |____/|_|\__,_|\___|_|\_\_|\__, |
              .d$$                                       |_|


2021-10-05T12:04:58.888Z pid=109 tid=2h5 INFO: Booted Rails 6.1.4.1 application in development environment
2021-10-05T12:04:58.888Z pid=109 tid=2h5 INFO: Running in ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-linux-musl]
2021-10-05T12:04:58.888Z pid=109 tid=2h5 INFO: See LICENSE and the LGPL-3.0 for licensing details.
2021-10-05T12:04:58.888Z pid=109 tid=2h5 INFO: Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org
2021-10-05T12:04:58.919Z pid=109 tid=2h5 INFO: Starting processing, hit Ctrl-C to stop

起動しましたね。

Index更新を非同期化

https://github.com/meilisearch/meilisearch-rails#queues--background-jobs

app/models/user.rb
class User < ApplicationRecord
  include MeiliSearch

  meilisearch enqueue: true do
    attribute :given_name, :family_name, :age
    attributes_to_highlight ['*']
  end
end

meilisearchenqueue: true という引数を指定する。

実行してみましょう

irb(main):001:0> User.create!(given_name: "はなこ", family_name: "田中", age: 1)
  TRANSACTION (0.1ms)  BEGIN
  User Create (0.4ms)  INSERT INTO "users" ("given_name", "family_name", "age", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["given_name", "はなこ"], ["family_name", "田中"], ["age", 1], ["created_at", "2021-10-05 12:19:55.103263"], ["updated_at", "2021-10-05 12:19:55.103263"]]
  TRANSACTION (2.4ms)  COMMIT
Enqueued MeiliSearch::MSJob (Job ID: 04ff0d39-d334-4add-80c5-1fdf036468e6) to Sidekiq(meilisearch) with arguments: #<GlobalID:0x00007f03d7bc6c18 @uri=#<URI::GID gid://rails-sample/User/7>>, "ms_index!"
=> #<User:0x00007f03d7db8850 id: 7, given_name: "はなこ", family_name: "田中", age: 1, created_at: Tue, 05 Oct 2021 12:19:55.103263000 UTC +00:00, updated_at: Tue, 05 Oct 2021 12:19:55.103263000 UTC +00:00>

MeiliSearch::MSJobがキューイングされたっぽいですね
Sidekiqのログを見てみます

sidekiq-worker_1  | 2021-10-05T12:21:23.990Z pid=1 tid=18nt class=MeiliSearch::MSJob jid=82a00e7bf6f3fc6c12c5b0d2 INFO: start
sidekiq-worker_1  | 2021-10-05T12:21:24.133Z pid=1 tid=18nt class=MeiliSearch::MSJob jid=82a00e7bf6f3fc6c12c5b0d2 elapsed=0.144 INFO: done

ちゃんと処理されてました
検索してみます

irb(main):003:0> User.search("はな")
  User Load (0.4ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1  [["id", 7]]
=> [#<User:0x00007f03da149468 id: 7, given_name: "はなこ", family_name: "田中", age: 1, created_at: Tue, 05 Oct 2021 12:19:55.103263000 UTC +00:00, updated_at: Tue, 05 Oct 2021 12:19:55.103263000 UTC +00:00>]

取れました。
念の為、ActiveRecordからデータを更新してみます。

irb(main):004:0> u = User.search("はな").first
  User Load (0.5ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1  [["id", 7]]
=> #<User:0x00007f03d5b12e18 id: 7, given_name: "はなこ", family_name: "田中", age: 1, created_at: Tue, 05 Oct 2021 12:19:55.103263000 UTC +00:00, updated_at: Tue, 05 Oct 2021 12:19:55.103263000 UTC +00:00>

irb(main):005:0> u.update!(given_name: "ぱなこ")
  TRANSACTION (0.4ms)  BEGIN
  User Update (0.4ms)  UPDATE "users" SET "given_name" = $1, "updated_at" = $2 WHERE "users"."id" = $3  [["given_name", "ぱなこ"], ["updated_at", "2021-10-05 12:25:15.523735"], ["id", 7]]
  TRANSACTION (2.4ms)  COMMIT
Enqueued MeiliSearch::MSJob (Job ID: b5f42646-c3f2-4683-ab93-ff349ef5599b) to Sidekiq(meilisearch) with arguments: #<GlobalID:0x00007f03da562248 @uri=#<URI::GID gid://rails-sample/User/7>>, "ms_index!"
=> true

irb(main):006:0> User.search("はな")
  User Load (0.5ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1  [["id", 7]]
=> [#<User:0x00007f03d8945bc8 id: 7, given_name: "ぱなこ", family_name: "田中", age: 1, created_at: Tue, 05 Oct 2021 12:19:55.103263000 UTC +00:00, updated_at: Tue, 05 Oct 2021 12:25:15.523735000 UTC +00:00>]

irb(main):007:0> User.search("はな").first.formatted
  User Load (0.5ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1  [["id", 7]]
=> {"id"=>"7", "given_name"=>"ぱ<em>な</em>こ", "family_name"=>"田中", "age"=>1}

更新時もJobが追加されてますね。

削除してみます

irb(main):003:0> User.last.destroy
  User Load (0.4ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT $1  [["LIMIT", 1]]
  TRANSACTION (0.1ms)  BEGIN
  User Destroy (0.3ms)  DELETE FROM "users" WHERE "users"."id" = $1  [["id", 8]]
Enqueued MeiliSearch::MSJob (Job ID: 2ce16c3c-14f9-4451-a709-d32f99070efa) to Sidekiq(meilisearch) with arguments: #<GlobalID:0x00007f03d69cdc78 @uri=#<URI::GID gid://rails-sample/User/8>>, "ms_remove_from_index!"
  TRANSACTION (1.6ms)  COMMIT
=> #<User:0x00007f03d67df240 id: 8, given_name: "はなこ", family_name: "田中", age: 1, created_at: Tue, 05 Oct 2021 12:32:29.613021000 UTC +00:00, updated_at: Tue, 05 Oct 2021 12:32:29.613021000 UTC +00:00>

irb(main):004:0> User.search("はな")
  User Load (0.5ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1  [["id", 8]]
=> []

ちゃんと削除されました

Custom Attribute

https://github.com/meilisearch/meilisearch-rails#custom-attribute-definition
モデルの属性以外の値をattributeに入れることもできるみたいです

app/models/user.rb
class User < ApplicationRecord
  include MeiliSearch

  meilisearch enqueue: true do
    attribute :given_name, :family_name, :age
    attribute :full_name do
      "#{family_name} #{given_name}"
    end
    attributes_to_highlight ['*']
  end
end

indexを作り直します

irb(main):005:0> User.reindex!
  User Load (0.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1000]]
=> nil

まずは作り直す前の検索結果

irb(main):010:0> User.search("山田 太郎")
  User Load (0.5ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1  [["id", 1]]
=> [#<User:0x00007f03d63843a8 id: 1, given_name: "太郎", family_name: "山田", age: 10, created_at: Mon, 04 Oct 2021 14:56:38.430087000 UTC +00:00, updated_at: Mon, 04 Oct 2021 14:56:38.430087000 UTC +00:00>]

irb(main):011:0> User.search("山田 太郎").first.formatted
  User Load (0.5ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1  [["id", 1]]
=> {"id"=>"1", "given_name"=>"<em></em>太郎", "family_name"=>"<em>山田</em>", "age"=>10}

次にfull_nameを追加した場合の検索結果

irb(main):014:0> User.search("山田 太郎")
  User Load (0.6ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2)  [["id", 1], ["id", 6]]
=>
[#<User:0x00007f03d68bb5b0 id: 1, given_name: "太郎", family_name: "山田", age: 10, created_at: Mon, 04 Oct 2021 14:56:38.430087000 UTC +00:00, updated_at: Mon, 04 Oct 2021 14:56:38.430087000 UTC +00:00>,
 #<User:0x00007f03d68bb4e8 id: 6, given_name: "たろう", family_name: "田中", age: 1, created_at: Tue, 05 Oct 2021 12:16:32.679603000 UTC +00:00, updated_at: Tue, 05 Oct 2021 12:16:32.679603000 UTC +00:00>]

irb(main):015:0> User.search("山田 太郎").map(&:formatted)
  User Load (0.5ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2)  [["id", 1], ["id", 6]]
=> [{"id"=>"1", "given_name"=>"<em></em>太郎", "family_name"=>"<em>山田</em>", "age"=>10, "full_name"=>"<em>山田</em> <em></em>太郎"}, {"id"=>"6", "given_name"=>"たろう", "family_name"=>"田中", "age"=>1, "full_name"=>"<em>田</em>中 たろう"}]

うーん、Afterの結果がちょっと ? な感じではありますが、結果が変わりましたね。

まとめ

長くなってきたので今回はこの辺で...

  • Railsから使う場合、MeiliSearchへの接続先設定と、Modelへの設定だけで簡単に使える
  • indexの更新は自動的に実行されるので楽
    • 非同期更新もデフォルトで対応している
    • ただしmeilisearchというキューになるので注意が必要
      • 実はこれに気づかなくてちょっとハマった
      • Jobを自前実装することもできるので、その場合はdefaultキューとか使えると思う
  • 新たに導入した場合に既存データのindexingは Model.reindex! で済む
  • 総じて使いやすそう

TODO

  • Railsから使ってみる
  • Rustから使ってみる
  • k8sでの運用(冗長化など)

Discussion