LoginSignup
1
0

More than 1 year has passed since last update.

Gemを利用せずにRailsにApache Solrを導入する

Last updated at Posted at 2023-02-25

1.本記事では何をやるのか

既存のRailsアプリケーションにApache Solrを導入します。

2.対象となるRailsアプリケーションの簡単な説明

人生の様々な失敗談を投稿する掲示板サイトです。

一般的な掲示板と同じように、投稿された記事は検索フォームから検索することができます。
下記画像における青枠部分が検索フォームです。
スクリーンショット 2023-02-02 23.05.30.png

3.なぜApache Solrを導入するのか?

ある程度の柔軟性の高い検索機能を提供し、ユーザーが記事を探しやすい状態を実現したいからです。

現状だと、「大学受験」で検索した場合に、「大学を受験」というフレーズにはヒットしません。
なぜなら、記事の検索時には下記のようなSQLを利用して検索を行なっているからです。

select * from posts where headline = "%キーワード%" or comment = "%キーワード%";

裏テーマ

この取り組みを通じて、RailsアプリケーションにApache Solrを導入するための知見を増やしたいからです。
そのため、下記を意識して取り込みたいと思います。

  • Solrの詳細な機能ではなく、RailsとSolrをシステム的に繋ぎ合わせるためにはどうすれば良いか?に関して重点的に知識を深める。
  • RailsとSolrをよしなに繋ぎ合わせてくれるようなGemは利用しない。

4.環境

本記事では、ひとまず開発環境で動かすことを目指します。
開発環境におけるバージョンは下記の通りです。

  • Ruby 2.7.3
  • Rails 6.1.3.2
  • SQLite 3.37.0
  • Apache Solr 6.6.6

5.設計

考えるべきことは下記の2つです。

  • どうやって記事を登録するか?(インデックスするか?)
  • どうやって記事を検索するか?

記事をSolrにインデックスする

インデックス.png

Solr用のテーブルを作成し、その内容をインデックスする

既存では、記事が投稿されると、postsという投稿内容を保持するテーブルに記事が保存されるだけです。
それに対して、Solrを導入した場合にはpostsテーブルの他にSolr用のテーブルに記事が保存されるようにします。
そして、SolrのData Import Handler(DIH)という機能を利用して、Solr用のテーブルの内容を元にインデックスを行えるようにします。

なぜ、Data Import Handlerを利用するのか?

記事をSolrにインデックスする際のマスターデータを残すことができるからです。

例えば、DIHを利用しない設計として、記事を登録した際に記事の内容を都度HTTPリクエスト経由でSolrにインデックスする方法が考えられます。この方法だと、Solrを稼働しているサーバーが落ちてしまった際に、「どの記事までインデックスできているのか?」が分かりにくくなってしまいます。

それに対して、Solr用のテーブルというマスターデータを残しておけば、何かトラブルが発生したとしても、そのテーブルの内容をDIHを利用してインデックスし直せばOKです。

なぜ、Solr用のテーブルを作成するのか?

postsテーブルもSolr用のテーブルも両方とも投稿内容を保持するテーブルです。
Solr用のテーブルを新たに新設した理由は、このアプリケーションにおいてAction Textを導入してるからです。
それにより、投稿内容の実体は別テーブルに保持されており、かつHTMLタグを含んだものになっています。
Solrにインデックスする際には、HTMLタグは不要なので、それらを取り除いたものをSolr用のテーブルに保持することにしました。

Solr用のテーブルには何を入れるのか?

記事のタイトルと本文で検索を行いたいので、Solr用のテーブルには下記のデータを入れることにします。

  • 記事のid(postsテーブルのid)
  • 記事のタイトル(postsテーブルに入っているタイトル)
  • 記事の本文(postsテーブルに入っている本文)

Solrのスキーマ定義は?

Solr用のテーブルのスキーマに合わせて、下記のようになフィールドを定義します。
image.png
デフォルト通りのオプションについては説明を割愛し、特筆すべき箇所(赤字)のみ説明します。

まず、indexedオプションについてです。記事のタイトルと本文で検索を行いたいので、それらのみtrueに設定します。
次に、storedオプションについてです。後ほど詳しく述べますが、Solrから検索結果として取得したいのはpostsテーブルのidのみのため、それだけをtrueに設定します。
最後に、requiredオプションについてです。記事のid、タイトル、本文はどれも必ず値を持つことが期待されるためtrueに設定します。

どうやって定期的に差分取り込みするのか?

Gemを利用してSolrに取り込みのリクエストを送るRakeタスクをcron登録します。

記事をSolrで検索する

検索する.png

どのように検索を実現するか?

既存の設計では、先述したようなSQL文によりDBに対して記事の検索が行われていました。
それに対して、今回は検索キーワードに合うような記事のidをSolrから取得し、その他の「タイトル」や「投稿日時」といった情報をDBから取得するようにします。

6.実装

下記の2段階に分けて実装を行います。

  1. 記事をSolrにインデックスする
  2. 記事をSolrで検索する

また、記事をSolrにインデックスする実装は下記の手順に分けます。

  • [Rails]Solr用テーブルを作成する

  • [Rails]記事投稿時にSolr用テーブルも更新する

  • [Solr]コアを作成し、スキーマを定義する

  • [Solr]DIHを設定する

  • [Rails]Rakeタスクで定期的に取り込みを行うようにする

  • [Rails]Solrにリクエストを送る。

  • [Rails]Solrのレスポンスを元にDBにリクエストを送る。

[Rails]Solr用テーブルを作成する

設計で記載した通りにテーブル、モデルを作成しました。ここでは、そのテーブルを仮名称としてpost_for_solrsテーブルを呼びます。
また、詳しい実装手順は省略します。

[Rails]記事投稿時にSolr用テーブルも更新する

Postモデルのafter_saveで、post_for_solrsテーブルも更新されるようなコールバックを設定しました。

app/models/post.rb
class Post < ApplicationRecord
  # 色々と省略

  after_save :register_to_post_for_solrs

  def register_to_post_for_solrs
    PostForSolr.register_post(self)
  end
end
app/models/post_for_solr.rb
class PostForSolr < ApplicationRecord
  # 色々と省略

  class << self
    def register_post(post)
      # PostForSolrをcreate or updateするような処理
    end
  end
end

[Solr]コアを作成し、スキーマを定義する

Schema API経由で、設計で記載した通りにスキーマを定義しました。
詳しい手順は省略します。

[Solr]DIHを設定する

本やインターネット上の記事にあまり手厚い説明が無かったため、詳しく書きます。

SQLite用のJDBCドライバをダウンロードし、Solrに配置する

下記リンクからダウンロードしてください。
一応、私は3.36.0.3を選択し、動作確認ができています。

ダウンロードしたら、#{SOLR}/server/libにドライバを配置してください。

solrconfig.xmlを修正する

<config>下に下記の記述を追加してください。
SQLite用のJDBCドライバと、DIH用のファイルを読み込んでいます。

<lib dir="${solr.install.dir:../../..}/lib/" regex="sqlite-jdbc-.*\.jar" />
<lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-dataimporthandler-.*\.jar" />

同じく<config>下に下記の記述を追加してください。
/dataimportdb-data-config.xmlは適宜変更してください。

<requestHandler name="/dataimport"  class="org.apache.solr.handler.dataimport.DataImportHandler">
  <lst name="defaults">
    <str name="config">db-data-config.xml</str>
  </lst>
</requestHandler>

db-data-config.xmlを作成する。

#{SOLR}/server/solr/#{コア名}/confにdb-data-config.xmlを作成してください。(ファイル名はsolrconfig.xmlの設定に合わせて適宜変更してください)

db-data-config.xmlには取り込み元のデータベースや取り込みを行う際のクエリを指定します。
ざっくりとしたイメージとしては下記の通りです。

<dataConfig>
  <dataSource driver="org.sqlite.JDBC" url="jdbc:sqlite:データベースのパス" />
    <document>
      <entity
        pk="id"
        query="全件取り込みを行う際に必要なレコードとカラムを取得するクエリ"
        deltaQuery="差分取り込みを行う際の取り込み対象のレコードの主キーを取得するクエリ"
        deltaImportQuery="deltaQueryで取得した主キーを元に取り込み対象のレコードとカラムを取得するクエリ"
      >
        <field column="カラム名_1" />
        <field column="カラム名_2" />
        <field column="カラム名_3" />
      </entity>
    </document>
</dataConfig>

開発環境でSQLiteを利用している場合、データベースのパスは下記の手順で取得できます。

# Railsアプリケーション下で実行
rails dbconsole
> .database

また、db-data-config.xmlの書き方の詳しい説明は下記に記載されています。

[Rails]Rakeタスクで定期的に取り込みを行うようにする

まずは、取り込みを行うためのRakeタスクを作成します。
lib/tasks下に新規ファイルを作成します。
作成するタスクのイメージはざっくりと下記のようなイメージです。

require_relative "../../app/api_clients/base"
require_relative "../../app/api_clients/solr_data_import_caller"

namespace :solr_data_import do
  desc "全件取り込みを行う"
  task :full_import do
    api_caller = SolrDataImportCaller.new
    api_caller.call(full_import_flg: true)
  end

  desc "差分取り込みを行う"
  task :delta_import do
    api_caller = SolrDataImportCaller.new
    api_caller.call(full_import_flg: false)
  end
end

Solrにリクエストを送る際には、独自に作成したApiClientクラスを用いています。
ざっくりとしたイメージは下記の通りです。

require "net/http"
require "json"

# Solrで検索を行う際にも利用するので、なるべく汎用的に使えるようにする
class Base
  def initialize(end_point)
    @end_point = end_point
  end

  def get(options: nil)
    uri = URI.parse(@end_point)
    uri.query = URI.encode_www_form(options) unless options.nil?

    response = Net::HTTP.get_response(uri)
    JSON.parse(response.body, symbolize_names: true)
  end
end

class SolrDataImportCaller < Base
  END_POINT_SERACH_POST = "#{solrのエンドポイント}/dataimport/"

  def initialize
    super(END_POINT_SERACH_POST)
  end

  def call(full_import_flg: true)
    command = full_import_flg ? "full-import" : "delta-import"
    options = {
      command: command,
      wt: "json"
    }
    response = get(options: options)
  end
end

Rakeタスクを作成したら、Wheneverを利用してcron登録します。

[Rails×Solr]記事の検索を行う

Solrで検索リクエストを送る際にも、独自のApiClientクラスを利用します。
ざっくりとしたイメージは下記の通りです。

今回は投稿のタイトルと本文でOR検索を行いたいので、qパラメーターにそのように記述します。
また、Solrから取得したいのは投稿のidだけなので、flパラメーターにはpost_idだけを指定します。

require "net/http"
require "json"

class SolrSearchPostsCaller < Base
  END_POINT_SERACH_POST = "#{Solrのエンドポイント}/select/"

  def initialize
    super(END_POINT_SERACH_POST)
  end

  def call(search_word)
    options = {
      q:  "title:#{search_word} OR content:#{search_word}",
      wt: "json",
      fl: "post_id"
    }
    response = get(options: options)
    result = {}
    result[:post_ids] = response[:response][:docs].map{|hash| hash[:post_id] }
    result
  end
end

今まで、SQLを利用して記事の検索を行なっていたPostモデルのsearchメソッドがあったので、それを下記のようなイメージで書き換えます。

class Post < ApplicationRecord
  # 色々と省略

  class << self
    def search(search_word)
      return nil if search_word.blank?
      api_client = SolrSearchPostsCaller.new
      response = api_client.call(search_word)
      post_ids = response[:post_ids]

      Post
      .where(id: post_ids)
      #
      #好きなようにクエリを追加する
      #
    end
  end
end

7.実際に検索してみる

例として下記のような投稿を行います。

image.png

「大学受験」で検索するとヒットします。
本文には「大学を受験」というフレーズしかありませんが、ヒットさせることができていますね。

image.png

また、「友人」で検索してもヒットします。
こちらも、本文には「交友関係」しかありません。

image.png

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0