こんにちは、スタメンのエンジニア、津田です。以前、弊ブログでも「TUNAGの全文検索を支える Elasticsearch × Rails」として紹介させていただいたように、TUNAGでは検索機能の実装にElasticsearchを利用しています。検索クエリとしては主にMulti Matchを利用しているのですが、RDBに登録されているレコードを利用しやすい形でElasticsearchのドキュメント化する方法について試行錯誤したため、共有させていただきます。
TL;DR (概要)
Elasticseachでは、Multi Match Queryを使って、複数のフィールドを検索対象とするクエリを発行できます。検索時に対象とするフィールドを指定することができるため、一つのインデックスを、検索対象フィールドの異なる複数の用途で共用することができます。ただし、一つのインデックスに含められるフィールド数には制限があるため、注意が必要です。
実現したかったこと
Elasticsearchを検索用途で導入した場合、RDBには1:Nで格納されている情報を、Elasticsearchには1つのドキュメントとして登録するケースがあります。たとえば、「1つの商品 : N個の商品情報」、を、「1つの商品情報」ドキュメントにまとめてから、Elasticsearchに登録するような場合です。「N個の商品情報」を「1つの商品情報」に折りたたむ際には、「検索時にどのような情報を検索対象とするか」を考える必要があります。
検索の用途として、
- 店舗利用者が、購入時に参照可能な商品情報のみを検索する
- 店舗担当者が、店舗が参照可能な商品情報のみを検索する
- システムの管理者が、すべての商品情報を検索する
という3つのユースケースがあったとします。やり方として、二通り考えられます。
- ドキュメント"登録時"に必要な情報を予めまとめてしまう
- ドキュメント"検索時"に必要なフィールドのみを検索する
順に説明します。
ドキュメント登録時に必要な情報を予めまとめてしまう
この場合は、たとえば、以下のように3つのフィールドに情報をまとめる、具体的には必要な文言を結合して登録することで、情報を折りたたむことが可能です。
- 商品情報
- 店舗利用者用フィールド: 「商品名、商品の特徴、保証について、etc...」
- システム管理者検索用フィールド : 店舗利用者用フィールド +「販売時の注意点、店舗での管理方法、etc...」
- 店舗担当者用フィールド : システム管理者用フィールド + 「システム管理上必要な情報、etc...」
このようにドキュメントを登録した場合、検索時には1つのフィールドを対象とすればいいので、Match Queryで検索することができます。
ただし、この方法では、検索時により細かい調整をすることができません。たとえば、「このページでの検索は、商品名、特徴のみから検索しよう」となった場合、利用できるフィールドがないため、ドキュメントの再登録をしない限り対応できません。
ドキュメント検索時に必要なフィールドのみを検索する
そこで、以下のようにドキュメントを登録します。
- 商品情報
- 商品名
- 商品の特徴
- 保証について
- 販売時の注意点
- etc...
このように構築したインデックスに対してMulti Match Queryを使えば、検索時に対象とするフィールドを変更して対応することが可能です。
Multi Match Queryでは、複数のフィールドに対してMatchクエリを実行するようなクエリです。以下のタイプがあり、タイプによって挙動が異なります。
- best_fields
- most_fields
- cross_fields
- phrase
- phrase_prefix
今回のような用途では、cross_fiedsを使います。
cross_fieldsタイプの検索では、「指定されたフィールドを大きな一つのフィールドとして扱い、与えられたクエリと一致しているものを検索する」ような動きをします。
たとえば、「金槌 丈夫」で検索した場合、マッチする情報を持つのは「商品名」フィールドと、「商品の特徴」フィールドです。cross_fieldsタイプのクエリであればこれはうまくマッチしますが、たとえば「best_fields」ではヒットしません。「best_fields」も複数のフィールドを対象として検索するのですが、ヒットするためには、同じフィールドに「金槌 丈夫」が含まれている必要があるからです。
cross_fieldsを指定したmulti_matchクエリは、以下のような形になります。
{ "query": { "multi_match" : { "query": "金槌 丈夫", "type": "cross_fields", "fields": [ "商品名", "商品の特徴", etc... ], "operator": "and" } } }
問題点
このように便利なcross_fieldsですが、今回は上記のやり方では使いませんでした。
今回、登録するドキュメントは、インデックス単位で見ると、「商品の特徴」がN個あり動的に増えるものでした。さらに複数のアナライザ(ngramとkuromoji)でフィールドを分けていたため、
- 商品情報
- 商品特徴1_kuromoji
- 商品特徴1_ngram
- 商品特徴2_kuromoji
- 商品特徴2_ngram
- ...
となっており、フィールド数の上限を設定するのが難しかったからです。
Elasticsearchには「1つのフィールドに含まれるフィールド数の上限(デフォルトでは1,000)」が存在しています。これは、インデックス作成時に、index.mapping.total_fields.limitを設定することで変更可能ではあります。
しかし、そもそもこの制限は、Add limit to total number of fields in mappingにあるように、クエリ実行時にMapping Explosionが発生するのを避けるためのものです。
実際に、動的にフィールドが増加する形でドキュメントを登録する場合は、事前に予想されるフィールド数上限と、上限に付近に達した状態での動作検証が必要と思われます。
まとめ
Elasticsearchでの検索は非常に高速で、クエリも高機能なものがありますが、それを活かすためにはINDEXの設計からきちんと行っていかなければいけないものだと感じました。
スタメンではエンジニアを募集しています。もし興味がありましたらこちらからご連絡ください。