プログラミングなんてわからないんですけど〜

元プログラマによるプライベートでのプログラミング日記。1/3のつもりだけどソフト関連はここがメイン

自鯖に流れてきたtootを全文検索するのは面倒だなあ

mastodon全文検索は標準ならElasticsearchを使います。今回はこの仕組みを使う上でのお話です。「DBを直接読めばいいやんけ」という人は頑張ってください。

さて、この全文検索ですがいろいろ制限というか面倒な話があります。

  1. 標準のソースコードでは検索インデックス作成が日本語には適していないので改造が必要
  2. 標準では連合タイムラインに流れてきたtootを検索できない
  3. 検索インデックスの作り方が複数あるが、やり方によって向き不向きがある

まあこんな感じなので、正直言って標準のままではElasticsearchを利用してまで全文検索をする必要はありません。「でも自分のサーバに届いたtootくらい全文検索したい!」という人もいるとは思いますので、こんなやり方がありますよというお話を書きます。

まずは日本語での検索をどうするかというお話。これは、ぜまさんとのえるさんのブログをひとまず読んでみてください。

kurage.cc

blog.noellabo.jp

で、私はどうしたかはすでに記事にしていたので省略。

www.kaias1jp.com

~/live/app/services/search_service.rb

  def relations_map_for_account(account, account_ids, domains)
    {
      blocking: Account.blocking_map(account_ids, account.id),
      blocked_by: Account.blocked_by_map(account_ids, account.id),
      muting: Account.muting_map(account_ids, account.id),
 -      #following: Account.following_map(account_ids, account.id),
 +      following: Account.following_map(account_ids, account.id),
      domain_blocking_by_domain: Account.domain_blocking_map_by_domain(domains, account.id),
    }
  end

のところだけですね、現在の修正点は。ここをコメントアウトすることで公開tootも検索できると思っていたのですが、実はインデックスの問題でここはコメントアウトを外して元のコードに戻したほうが良いです、たぶん。

では次。連合タイムラインに流れてきたtootを検索できないという話です。実は検索インデックスの作り方によっては、連合TLに流れてきた公開tootがインデックスに含まれないという問題というか仕様があります。それプラス検索するときに検索する人に関係するものしか検索しない実装になっているので、結果としてDBにデータがあっても検索できません。

インデックスの作り方については最後にまとめて書きますので、ここでは検索するときの実装について書きます。

~/live/app/services/search_service.rb

  def perform_statuses_search!
 -    definition = parsed_query.apply(StatusesIndex.filter(term: { searchable_by: @account.id }))
 +    definition = parsed_query.apply(StatusesIndex).order(id: :desc)

の部分です。「StatusesIndex.filter(term: { searchable_by: @account.id })」の部分が「自分のアカウントで検索可能なtootだけフィルタリングしておく」になっています。ここを外すと連合TLに流れてきたtootも検索できるようになります。「同じサーバの他のアカウントのDMとか見えるんじゃね?」と思うかもしれませんが、「 def relations_map_for_account()」で定義している部分の「following」を有効にすることで、最終的にRejectして取り除かれているようです。もし検索できるようでしたらお知らせください、調べますので。
ちなみに、「order(id: :desc)」はID順ソートすることで日付順ソートとみなしている部分です。

では今回の記事の本題、検索インデックスの作り方です。
検索インデックスをはじめてつくる方法は、2021年1月時点では2つの方法があります。「bin/tootctl search deploy」と「bundle exec rails chewy:deploy」です。
昔は「chewy:deploy」しか方法がなかったのですが、「tootctl search deploy」が追加されました。ソースコードを見る限りでは、「tootctl search deploy」の方が確実にかつ簡単に検索インデックスを作成できます。理由は、「sidekiqを利用することで通信エラーが起きない範囲で投入数を分割しており、なおかつエラー時の再実行も保証している」からです。ただし、今回やりたい「公開tootも全文検索したい」という場合は、ソースの改造なしでは無理です。それは、「searchable_by」を見て標準のコードでは検索しないtootは検索インデックスから取り除く」設計になっているからです。ではどうするか。私はソースを改造して検索インデックスから削除する処理を外しています。ソースの改造の仕方は先ほどの私の記事に書いてあります。

これで最初の検索インデックスに公開tootが含まれるようになりますが、そのあとにも少し工夫が必要です。作成した検索インデックスに対してのインデックス追加処理は「そのサーバのローカルアカウントが行ったtootなどの操作」時のみ行われます。これはそういう実装だからです。これを「他のサーバからtootが流れてきたときに検索インデックスに追加する」実装に改造するのは難しそうです。まあ、私にはできませんでした。なので、次のような運用で対応しました。

実は、「bundle exec rails chewy:sync」を定期的に実行すれば、新しいtootが検索インデックスに追加されます。ただし、この処理はサーバのCPU数やメモリサイズや他に動いているサービスがどれくらいあるかによって負荷がいろいろ変わってきます。最低限、これをしておけば楽になるよということを書きます。

chewy:sync時に一番起きやすいのは、timeoutによる異常終了です。たぶん、ここをチューニングしないとなかなかうまくいかないと思います。実は、chewyのtimeout時間は設定ファイルで指定できます。

~/live/config/chewy.yml

production:
        request_timeout: 1800

最初はこのファイルはありません。まずは無い状態で以下のコマンドを実行して試してみます。

SDATE=`env TZ=JST-9 date`&&echo $SDATE&&RAILS_ENV=production bundle exec rails chewy:sync[StatusesIndex]&&echo $SDATE&&env TZ=JST-9 date

「chewy:sync[StatusesIndex]」でtootの検索インデックスのみ更新をかけています。検索インデックスには他にアカウントとタグ用のがあるのですが、これらは標準の実装で「データが届いたときに追加・削除」されますのでわざわざやらなくても良いです。
このコマンドを実行してtimeoutで異常終了しなければtimeout時間はdefaultのままでかまいません。もし、timeoutで異常終了する場合はchewy.ymlを作成して数値を10分(600)・15分(900)という風にのばして再度実行してみてください。大体うまくいく数値が決まるはずです。もし異常に大きな数値になってもうまくいかないときは現在のサーバ構成に無理があると思われますので見直しが必要です。

さて、私の環境は1800(30分)を指定して無事更新処理が完了しました。ただし、実際の実行時間は1時間以上かかりました(先ほどのコマンドで開始時刻と終了時刻を日本時間で最後に表示するようにしてあります)。また、このコマンドの実行時にはWebUIでのTL更新などは無理でした。というわけで、私の場合は日本時間の深夜0時に更新処理をcronで走らせるようにしています。

0 15 * * * /bin/bash -c 'cd ~/live && RAILS_ENV=production /home/mastodon/.rbenv/shims/bundle exec rails chewy:sync[StatusesIndex] >/dev/null 2>&1'

「0 15 * * *」の部分は、UTC15時0分に実行という指定です。また、「>/dev/null 2>&1」の部分は、標準出力とエラー出力をmailとして受け取らないようにするおまじないです。mailサーバが入っていない環境だと、これを書いておかないとコマンド実行時にエラーになります。「bundle」がフルパスで書いてあるのは、私の環境固有かもしれません。bashの「-l」オプションを使えばよさそうですが面倒なのでこうしています。

これで、前日に届いたtootが翌日全文検索できるようになります。ここまですると、全文検索できるありがたさが出てきます。正直言って面倒と言えば面倒ですw。
なお、ラズパイ4 8GBモデル2台にmastodonサーバと他のDBなどを分けて運用している環境では、15分に1回cronで更新処理をかけても問題ないです。CPUとメモリに余裕があるので「chewy:parallel:sync[4,StatusesIndex]」で複数プロセス実行させています。

というわけで、ざっくりとまとめてみました。試せる方は試してみてください。