Elasticsearch で不適切投稿のバリデーションチェックを実装してみた話
Elastic Stack (Elasticsearch) Advent Calendar 2020 の 12月21日のエントリーです。
突然ですが、みなさんは不特定多数の人が投稿するシステムで、不適切な用語やNGワードを投稿されないようにしたいという要望を実現するために、どのようなシステムを設計しますか?
文章がスペースで区切られている英語でも複数形や過去形さまざまな形があります。日本語はさらに難しく、言語処理だけでも大変です。
また、不適切な単語やフレーズを検出するロジックをプログラミングしてしまうと、新たに発生する要件を実現するにはプログラムの変更が必要です。
パフォーマンスはどうでしょう?チェックする用語が膨大になっても大丈夫ですか?
と、スクラッチで開発しようとすると結構いろいろ大変ですよね。
Elasticsearch を使わない手はない
言語処理と言えば Elasticsearch ですよね?これを使わない手はありません。Elasticsearch を中心に不適切投稿のバリデーションチェックを実現した場合の良いところをまとめてみました。
- 言語処理を Elasticsearch に任せられる。
- 多言語にも容易に対応できる。
- 条件をデータで管理できるので柔軟なシステムを構築できる。
- 条件をデータで管理できるのでプログラム側がシンプルになる。
- 条件をデータで管理できるので開発が短期間で終わる。
- チェックする用語が増えてもパフォーマンスは問題ない。結構大規模でも大丈夫
こんなところでしょうか?今回の仕組みを適用したい情報がすでに Elasticsearch にインデックス済であれば、かなり短期間で実現することができます。
Elasticsearch を使ってバリデーションチェックを実装した経緯
今回紹介する Elasticsearch を使った不適切投稿のバリデーションチェックは VELTRA Kite というサービスで実装しています。
このサービスでは100名を超える世界各国の旅に関わるプロフェッショナルたちが、コロナ状況下の現地の様子を投稿してくれてます。
日本語と英語サイトを公開していますが、投稿する言語は規定していません。英語でも中国語でも、日本語でも投稿することが可能です(実際は、日本語か英語での投稿がほとんどですが)。
サービスリリース当初は、投稿毎に人の目で見てチェックして公開していました。これ以外にも、写真に人の顔が写っていれば、許可を得ているのか確認したり運用負荷が高かったため、本格的なシステム化に合わせて自動化しました。
投稿内容確認の自動化は、多言語かつ未知の要件が多く、ビジネスロジックをプログラムせずに未知の要件にも対応できるようにしたかったため、Elasticsearch を基盤に設計することにしました。
Elasticsearch はビジネスロジックもデータで管理できるすぐれもの
今回紹介する仕組みを実現するために、知っておきたい Elasticsearch の機能があります。それが、「Percolate query」です。
通常の検索の仕組みでは、検索したい情報を事前にインデックスしておき、検索条件にマッチする情報を抽出します。
- 検索対象:「投稿内容」
- 検索条件:「不適切な投稿と判断するための条件」
この仕組みの場合、一つの投稿に対して、全ての条件を付き合わせる必要があり、あまり効率的とは言えません。100個条件セットがあれば100回検索する必要があります。あとは、事前に投稿をインデックスしておく必要があります。
一方 Percolate query では、この逆のことを実現してくれます。
- 検索対象:「不適切な投稿と判断するための条件」
- 検索条件:「投稿内容」
Percolate query を使うと、どんなに条件が多くても1回のリクエストで処理が済んでしまいます。また、「不適切な投稿と判断するための条件」という複雑なビジネスロジックをデータ(検索対象)として管理できるので、プログラムの修正無しに管理者が自由に条件を追加できるシステムが構築できます。
構築したシステムイメージ
システムの構成を絵にしてみましたが、Percolate query そのままですね。。
管理サイトでは、不適切な用語(フレーズや単語)セットの管理に加え、それぞれのセットで、一つの用語が該当すればNGにするのか?文章全体の10%が該当すればNGにするのか?などの調整もできるようにしています。工夫したところの一つですが、これも Elasticsearch の minimum_should_match パラメータを利用しているだけなので実現は難しくありません。
Percolate Query API について
Percolate query API についても少し触れておきましょう。Elasticsearch v7.x をベースにします。
Percolate query は、分析したいドキュメントのマッピングフィールドとその検索条件をインデックスしておくためのフィールドを用意する必要がります。
以下の例では、分析したいドキュメントのフィールドとして message
という名前のフィールドを定義しています。通常の検索と同じように日付型や数字型などのフィールドを含めることができます。
※ 不適切用語だと不快に思われる方もいらっしゃるかもしれないので、例は別の例で記載します。
PUT /post
{
"mappings": {
"properties": {
"message": {
"type": "text"
},
"query": {
"type": "percolator"
}
}
}
}
検索条件をインデックスするためのフィールドを用意し、タイプに percolator
を指定します。
準備ができたら以下の例のように通常のドキュメントをインデックスする要領で検索条件をインデックスします。
PUT /post/_doc/1?refresh
{
"query": {
"match": {
"message": "天気 晴れ"
}
}
}
このリクエストは、検索ではなくインデックスです。こんがらがってきますよね。
最後に、“東京の今日の天気は晴れです” この文章がマッチする検索条件が存在するかどうかリクエストしてみましょう。通常の検索クエリーに percolate
というオペレータを使って組み立てます。 percolate
も条件式の一つなので、他のクエリーと組み合わせて使うこともできます。ハイライト機能を使って文章内のどこにマッチしたのかを検索結果に含めることも可能です。
GET /post/_search
{
"query": {
"percolate": {
"field": "query",
"document": {
"message": "東京の今日の天気は晴れです"
}
}
}
}
検索結果は次のようになります。通常の検索結果とほぼ同じです。
{
"took": 13,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped" : 0,
"failed": 0
},
"hits": {
"total" : {
"value": 1,
"relation": "eq"
},
"max_score": 0.26152915,
"hits": [
{
"_index": "post",
"_type": "_doc",
"_id": "1",
"_score": 0.26152915,
"_source": {
"query": {
"match": {
"message": "天気 晴れ"
}
}
},
"fields" : {
"_percolator_document_slot" : [0]
}
}
]
}
}
もちろん Percolate query をリクエストするときは、検索条件を含めてフィルタリングすることもできるので検索対象の検索条件に属性を含めてインデックスすることもできます。
PUT /post/_doc/1?refresh
{
"category": "天気予報",
"query": {
"match": {
"message": "天気 晴れ"
}
}
}
この例のようにデータを作成しておき、Percolate query をリクエストする時に category が “天気予報” の検索条件だけを対象にすることもできます。
設計するときに悩ましいのは一つだけ
Percolate query も基本的には通常の検索と同じプロセスで設計できるので、それほど悩むところはありません。
- データスキーマの設計
- 各種言語処理及びマッピング定義
- インデックス作成単位(日付単位とか、カテゴリ単位とか)
- インデックスのシャード構成
- Percolate Query
こんな感じでしょうか。マッピング定義で percolate
タイプのフィールドを用意して、あとは Percolate Query を設計するくらいです。
実際に設計してみて、ちょっと悩んだポイントを一つ挙げるとすると、Percolate query 専用のインデックスを作成するのか?検索用途のインデックスと同じインデックスで Percolate query もインデックスするのか?というところです。
Percolate query 専用インデックスの場合
例えば、投稿以外の文章も同じロジックでバリデーションチェックしたいというような場合は、Percolate query 専用でインデックスを作成した方が汎用性が高そうです。
検索用のインデックスを日別などで分けて設計している場合も、Percolate query 専用で設計した方が、どのインデックスに Percolate query をリクエストすれば良いか明確になります。
検索用途と異なる言語処理でバリデーションチェックを実現したい場合は Percolate query 専用でインデックスを作成します。こうすることで、バリデーションチェック時の言語処理が変わっても検索用途のインデックスには影響はありません。
その他、検索用途で管理してない外部の情報のバリデーションチェックを実現したいこともあるでしょう。
ポイントをまとめると
- 複数の検索インデックス用に共通の Percolate query を使用したい
- 検索とは言語処理の異なる Percolate query を使用したい。
- 外部の情報用途に Percolate query を使用したい。
Percolate query 共用インデックスの場合
投稿データと一口に言っても、投稿文章、公開日付、投稿者など様々な情報があります。例えば、投稿者の種別が管理者以外で不適切用語を含むと言ったような投稿データに特化した Percolate query を実現したい場合は、検索用途のインデックスと共用で設計した方がマッピングの変更など二重に管理しなくても良いので、メンテナンス性が良さそうです。
ポイントをまとめると
- 検索用途と同じ言語処理で Percolate query を使用したい。
- 特定の情報に特化した Percolate query を使用したい。
VELTRA Kite の場合は、2、3年で何百万件という投稿データになるわけでもない(インデックスは当面1つで運用)。1つのインデックスで多言語カバーしている。言語処理は基本検索と同じ。という理由から共用インデックスで設計しました。
まとめ
ビジネスロジックをデータとして管理できる Percolate query は、システムをシンプルかつ柔軟にしてくれるため、Elasticsearch が提供する機能の中でもトップ3に入る好きな機能です。
今回は、Percolate query を不適切用語のバリデーションチェックで紹介させていただきました。他にも、商品の大きさや場所によって配送料が違うようなECで、Percolate query に価格とそれに該当する条件をインデックスしておくとか、ルールベースのデータ分類を実現したいとか、色々な用途で活用できます。
「こんな用途でPercolate query 使ってるよー」などありましたらコメントいただけると嬉しいです。
それではまた。