42
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Elasticsearchで部分一致検索と完全一致検索の両方を実現する

Last updated at Posted at 2021-01-07

数か月前に、Elasticsearchを利用した、部分一致検索(普通の検索)のアプリを作成しました。
その後、完全一致検索(Googleにおける検索語を「""」で囲んだ検索)を追加しました。
途中で少し詰まったところがあるのでまとめます。
Elasticsearchへのアクセスは、pythonのライブラリを用いています。

環境

部分一致検索と完全一致検索

部分一致検索と完全一致検索という言葉があいまいなので、この記事でのイメージを記載します。
(一般的な定義じゃないです)

  • 検索対象文字列が「関西国際空港」の場合
検索語 関西 国際 空港 関西空港 大阪空港 新大阪駅
部分一致 ×
完全一致 × × ×
  • 部分一致検索は、検索語を単語分けした後の各単語が、検索対象文字列に含まれている場合にヒットします。
    検索語を単語分けした後の各単語の一部が、検索対象文字列に含まれている場合は、ヒットする場合もヒットしない場合もあります。

  • Google検索で、普通の検索と同じと思います。

  • 完全一致検索は、検索語の全部が、検索対象文字列に含まれている場合にヒットします。

  • Google検索で、検索語を「"」(ダブルクォーテーション)で囲んだ場合と同じイメージです。

Elasticsearchでの実装方法

Elasticsearchは文字列のフィールドタイプとして、textkeywordの二つがあります。
textは、文字列を分析して単語ごとに分けて保存したものです。
keywordは、文字列を単語分けせずそのままの状態で保存しています。

部分一致検索は、textに対して、full text queryの一つであるmatchクエリを用いることによって実現できます。
完全一致検索は、keywordに対して、term-level queryの一つのwildcardクエリを用いることによって実現できます。

フィールドタイプ クエリ
部分一致 text match
完全一致 keyword wildcard

textとkeywordの二つのデータを持つ方法

Elasticsearchでは、マルチフィールドでmappingを定義することにより、一つの文字列に対して、複数のフィールドタイプのデータを持たせることができます(公式リファレンス)

mapping.py
# -*- coding: utf-8 -*-
from elasticsearch import Elasticsearch

es = Elasticsearch("localhost:9200")

mapping = {
    "mappings" : {
        "properties" : {
            "content": {
                "type":"text",
                "analyzer": "kuromoji",  # アナライザーはkuromoji
                "fields": {
                    "keyword": {
                        "type": "keyword",
                        "ignore_above": 8191 # デフォルトでは256
                    }
                }
            }
        }
    }
}

es.indices.create(index='myes', body=mapping)

これで、"myes"のデータとして、メインにtextタイプのデータ、サブにkeywordタイプのデータを持つことができます。
textタイプのデータには"content"、keywordタイプのデータには"content.keyword"でアクセスできます。
ここで、サブとして持つkeywordタイプのデータは、デフォルトでは文字列が256文字以上の場合、作成されません。
256文字以上のデータを取り扱う場合は、明示的に"ignore_above"を設定する必要があります。
上では、"ignore_above": 8191としています。
Elasticsearchの内部で使用されているLuceneというプログラムのkeywordタイプのデータの最大容量が32766バイトであり、UTF-8が1文字で最大4バイトのため、32766/4=8191としています。
また、日本語analyzerとしてkuromojiを入れています。

インデックスの登録

サンプルの文字列(300字強)を登録します。
(某ドラマ原作 ^^/)

index.py
# -*- coding: utf-8 -*-
from elasticsearch import Elasticsearch

es = Elasticsearch("localhost:9200")

sample = u"東京中央銀行大阪西支店の融資課長・半沢直樹のもとにとある案件が持ち込まれる。\
大手IT企業ジャッカルが、業績低迷中の美術系出版社・仙波工藝社を買収したいというのだ。 \
大阪営業本部による強引な買収工作に抵抗する半沢だったが、やがて背後にひそむ秘密の存在に気づく。\
有名な絵に隠された「謎」を解いたとき、半沢がたどりついた驚愕の真実とは――。\
探偵半沢が絵画に込められた謎を解く、江戸川乱歩賞出身の池井戸潤、真骨頂ミステリー!\
『半沢直樹1 オレたちバブル入行組』の前日譚となるシリーズ原点にして、「やられたら、倍返し!」\
あの突き上げる爽快感とともに、明かされる真実に胸が熱くなる、\
7月19日放送開始ドラマ「半沢直樹」シリーズ待望の最新原作小説が、ついに登場。"

book = {"content":sample}
es.index(index="myes", doc_type="_doc", id=1, body=book)

部分一致検索

contentに対して、matchクエリで検索します。

search_p.py
# -*- coding: utf-8 -*-
from elasticsearch import Elasticsearch

es = Elasticsearch("localhost:9200")

rq = {
    "query" : {
        "match": {
            "content": "東京中央銀行"
        }
    }
}

result = es.search(index="myes", body=rq)
print(result)

検索語と検索結果は以下の通り。

検索語 検索結果
東京中央銀行
東京の中央銀行
三井住友銀行 ×

matchクエリでは、検索語を単語ごとに分解してから検索します。
検索語「東京の中央銀行」は、「東京」、「中央」、「銀行」に単語分けしてから、同じく単語分けされたtextタイプの検索対象データを検索するため、検索がヒットします。
検索語「三井住友銀行」も、「銀行」が検索対象データに含まれるためヒットする可能性がありましたが、今回はヒットしませんでした。

完全一致検索

content.keywordに対して、wildcardクエリで検索します。
検索語の前後に「*」(wildcard)を付けます。
「*」は0文字以上の任意の文字と一致します。

search_f.py
# -*- coding: utf-8 -*-
from elasticsearch import Elasticsearch

es = Elasticsearch("localhost:9200")

rq = {
    "query" : {
        "wildcard": {
            "content.keyword": "*東京中央銀行*"
        }
    }
}

result = es.search(index="myes", body=rq)
print(result)

検索語と検索結果は以下の通り。

検索語 検索結果
*東京中央銀行*
*東京の中央銀行* ×
*三井住友銀行* ×

wildcardクエリ(term-level query)では、検索語をそのまま検索対象の文字列のデータと比較します。
検索対象の文字列には、「東京の中央銀行」、「三井住友銀行」は含まれていないため、検索がヒットしません。

まとめ

Elasticsearchで部分一致検索と完全一致検索の両方を実現しようとしたところ、簡単にtextとkeywordの二つのデータを持つことができるとわかり、「なんて便利なデータ構造なんだ!」と感心しました。
しかしながら、サブとして持つkeywordタイプのデータではデフォルトで"ignore_above": 256になることに気づかず少々ハマりました。
後で知ったのですが、wildcardクエリに最適化された、wildcardフィールドタイプというものもあるようなので、keywordの代わりにこちらを使用した方が良いかもしれません。

参考

初心者のためのElasticsearchその1
初心者のためのElasticsearchその2 -いろいろな検索-

42
30
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?