背景
私が開発しているサービスに、いわゆる「Trello的な」並び替え機能があります。
これと Elasticsearch を使った全文検索を併用しようとして若干ハマったので記事にしました。
主に Ruby on Rails の話になります。
並び替えの仕組み
並び替えには、 ranked-model という gem を利用しています。
これは、 データに row_order
というカラムを持たせて、それによってソートをするのですが、差し込んだ時の全体の再配置やリバランスを自動的に行なってくれるものです。
差し込む場所は、ちょうど中間地点にくるように計算されます。
ちなみに、開発はそんなにアクティブではありません。
並び替えの難点
row_order
というカラムは MySQLで言う Integer
に保存されるのですが、ご存知の通りいろいろと問題があります。
- 隣接した整数の間に差し込むことができない(例: 100と101の間に整数はない)
- 最大値と最小値があるのでそれより外側に配置できない(例: 2147483647より大きい整数はない)
これを解決するために、ranked-modelは、差し込もうとしている位置に 無理 がある場合に、全体を1つずつ移動させたり、差し込めるように両側の値を外側に移動させたりしてくれるヘルパーを持っています。
この gem の価値はここのロジックにあります。これをライブラリ中では rearrange
や rebalance
と呼んでいます。
以降、この動作のことを記事では「リバランス」と呼びます。
リバランスの問題点
ところが、このリバランスはElasticsearchと併用しようとすると、問題点が出てきます。
それは、リバランスの動作が update_all
で行われているので、コールバックを指定できないという点です。
その為、リバランス後の値をElasticsearchに反映させるタイミングがわからなくなります。
しかも、リバランスは常に行われるわけでは無いので、「変更があれば全てElasticsearchに変更を送る」というのは無駄が多すぎます。
パッチで対応
そこで、リバランスを行うメソッドにコールバックを設定できるようなパッチを当てることで対応しました。
これを利用すると、モデルに after_rearrange_ranks
と after_rebalance_ranks
というメソッドがある時に、コールバックとして実行してくれるようになります。
module RankedModel::MapperExtensions
private
def rearrange_ranks
super
if instance.respond_to?(:after_rearrange_ranks)
instance.after_rearrange_ranks
end
end
def rebalance_ranks
super
if instance.respond_to?(:after_rebalance_ranks)
instance.after_rebalance_ranks
end
end
end
class RankedModel::Ranker::Mapper
prepend RankedModel::MapperExtensions
end
class Model < ActiveRecord::Base
# Sidekiqなどでテキトウに更新させる
def after_rearrange_ranks
Model::RebalanceRanksWorker.perform_async(...)
end
def after_rebalance_ranks
after_rearrange_ranks
end
end
考察
Trelloはどうやって解決しているのかいろいろ試してみましたが、そもそも検索結果と並び替えは併用していませんでした。
しかも、検索の反映まで数秒以上はかかるみたいです。
やっぱり、こういった検索・フィルタリングの精度とリアルタイム性はトレードオフの関係にあるのかなぁ