タイトルままですが、Rails の ActionText で作ったコンテンツの中身を全文検索するために試したことを記載します。
一応動きますが、これが良い方法かどうかはわかりませんという前提で書いてみます。
事前準備
Gemを追加
elasticsearchを使用するためのGemを追加します。
gem "elasticsearch-model"
gem "elasticsearch-rails"
ActionTextを使う
model として Message を作っている前提で行きたいと思います。
MessageモデルはActionText要素としてhas_rich_text :content
を含めています。
# == Schema Information
#
# Table name: messages
#
# id :bigint not null, primary key
# title :string
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_messages_on_title (title)
#
class Message < ApplicationRecord
has_rich_text :content
end
ActionText で使える関数
ActionTextでは、データベース上にHTML形式でデータを保有しています。
全文検索するときは、HTMLタグは必要ないのでタグを除いたデータが欲しいです。そんなときは用意されているto_plain_text
を使用できます。
# https://edgeapi.rubyonrails.org/classes/ActionText/RichText.html#method-i-to_plain_text
message = Message.first
message.content.to_plain_text
こんな感じで取得できます。全文検索にはこのto_plain_text
で取得したデータをElasticSearchにインデックス登録しました。
ActiveSupportConcernを作る
Concernを使ってElasticSearchを使用できるようにします。
# app\models\concerns\message_searchable.rb
module MessageSearchable
extend ActiveSupport::Concern
included do
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
# インデックスするフィールドの一覧
INDEX_FIELDS = %w(title updated_at created_at content).freeze
# インデックス名
index_name "message_#{Rails.env}"
# マッピング情報
settings index: {
number_of_shards: 1,
number_of_replicas: 0,
analysis: {
filter: {
pos_filter: {
type: 'kuromoji_part_of_speech',
stoptags: ['助詞-格助詞-一般', '助詞-終助詞'],
},
greek_lowercase_filter: {
type: 'lowercase',
language: 'greek',
},
},
tokenizer: {
kuromoji: {
type: 'kuromoji_tokenizer'
},
ngram_tokenizer: {
type: 'nGram',
min_gram: '2',
max_gram: '3',
token_chars: ['letter', 'digit']
}
},
analyzer: {
kuromoji_analyzer: {
type: 'custom',
tokenizer: 'kuromoji_tokenizer',
filter: ['kuromoji_baseform', 'pos_filter', 'greek_lowercase_filter', 'cjk_width'],
},
ngram_analyzer: {
tokenizer: "ngram_tokenizer"
}
}
}
} do
mappings dynamic: 'false' do
# 今回は title と content に対して全文検索してみる
indexes :title, analyzer: 'kuromoji', type: 'text'
indexes :content, type: :object do
indexes :to_plain_text, analyzer: 'kuromoji', type: 'text'
end
end
end
# インデックスするデータを生成
# @return [Hash]
# def as_indexed_json(option = {})
# self.as_json.select { |k, _| INDEX_FIELDS.include?(k) }
# end
# contentのbodyの部分にAction Textの中身が入っているので"to_plain_text"で取り出す。
def as_indexed_json(options={})
as_json(
include: {
content: {methods: [:to_plain_text], only: [:to_plain_text] }
}).select { |k, _| INDEX_FIELDS.include?(k) }
end
# Queryのカスタマイズ
def self.query(keyword)
__elasticsearch__.search({
size: 10000,
query: {
multi_match: {
fields: INDEX_FIELDS,
type: 'cross_fields',
query: keyword,
operator: 'and'
}
}
})
end
end
module ClassMethods
# indexの作成メソッド
def create_index!
client = __elasticsearch__.client
client.indices.delete index: self.index_name rescue nil
client.indices.create(index: self.index_name,
body: {
settings: self.settings.to_hash,
mappings: self.mappings.to_hash
})
end
end
end
そしてこれをMessageモデルに繋げます。
class Message < ApplicationRecord
include FinanceSearchable #追加
has_rich_text :content
end
これでMessageモデルへの変更都度、インデックスが登録されるようになりました。to_plain_text
で取得したActionTextの中身も登録されるはずです。
Controllerから検索する
search_param[:search]
のような感じで、GETのリクエストパラメータを受け取ったら、それをsearch
メソッドに渡すだけ!
@messages = Message.search(search_param[:search]).records.order(updated_at: "DESC")
すばらしく簡単ですね。content
の中身もちゃんと検索されました。
今回はこれで以上です。