Edge NGram Token Filterを使用した場合のhighlightについて


(kako) #1

Edge NGram Token Filterを使用しています。検索にてHITした箇所にhighlightをつけたいのですが、想定した箇所とは異なる部分がhighlightされており、どうすればよいのか、アドバイスをいただければ幸いです。

Elasticsearch 6.3.0

●インデックスのmapping

PUT /my_index
{
  "settings" : {
    "index": {
      "analysis": {
        "filter":{
          "edgeNGramFilter":{
            "type" : "edgeNGram",
            "max_gram" : 50
          }
        },
        "analyzer": {
          "jp_analyzer": {
            "tokenizer": "kuromoji_tokenizer",
            "filter": [
              "kuromoji_stemmer",
              "edgeNGramFilter"
            ]
          },
          "jp_search_analyzer": {
            "tokenizer": "kuromoji_tokenizer",
            "filter": [
              "kuromoji_stemmer"
            ]
          }
        }
      }
    }
  },
  "mappings" : {
    "my_type" : {
      "properties" : {
        "code" : {
          "type" : "keyword"
        },
        "contents" : {
          "type" : "text",
          "analyzer" : "jp_analyzer",
          "search_analyzer" : "jp_search_analyzer"
        }
      }
    }
  }
}

●ドキュメント

POST _bulk
{ "index" : { "_index" : "my_index", "_type" : "my_type", "_id" : "1" } }
{ "contents": "本日は晴天なり。お隣の花壇に、秋桜の花が咲き乱れる。", "code": "1-1" }

●クエリ

GET my_index/_search
{
  "_source": false, 
  "query": {
    "match": {
      "contents": "秋"
    }
  },
  "highlight": {
    "fields": {
      "contents" : {}
    }
  }
}

●結果

{
  "took": 17,
  "timed_out": false,
  "_shards": {
    ・・・
  },
  "hits": {
    "total": 1,
    "max_score": 0.73617005,
    "hits": [
      {
        "_index": "my_index",
        "_type": "my_type",
        "_id": "1",
        "_score": 0.34992102,
        "highlight": {
          "contents": [
            "お隣の花壇に、<em>秋桜</em>の花が咲き乱れる。"
          ]
        }
      }
    ]
  }
}

結果として

"お隣の花壇に、<em>秋桜</em>の花が咲き乱れる。"

となりますが、検索したワードが「秋」の為

"お隣の花壇に、<em>秋</em>桜の花が咲き乱れる。"

となるのが理想です。

tokenizerの結果としての「秋桜」がhighlightされているのであろう、という事象は理解できるのですが、ではどうすれば「秋」だけがhighlightされるのかが判りません。
調べてみたところ、Token FilterではなくTokenizerでEdge NGram を使用すればうまくいく、ということがわかりましたが、対象とする文字列が日本語の文章であるため、Tokenizerはkuromojiでないと単語の区切りができないのでは?と考えております。

前方一致検索をしたい為、Edge NGramを使用しておりますが、Edge NGramにこだわってはおりません。(ただ、Edge NGramを使用せずにPrefix Queryも試してみましたが、結果は同じでした。)

どうぞよろしくお願いいたします。


(Sunggyu Kei Rhie) #2

こんばんは。

ハイライトはanalyzerで作られたTermが反映されるようなので、kuromoji_tokenizerにしてしまうと、どっちにしろtokenの分け方が形態素になってしまうと思うので、それをtoken filterでedgeNGramにして1文字ずつにしてもhighlightされないようです。

なので、もう最初から区切ったほうがいいので、kuromoji_tokenizerを使わず、最初からngramでやっちゃいます。

PUT /my_index
{
  "settings" : {
    "index": {
      "analysis": {
        "tokenizer":{
          "ngram_tokenizer":{
            "type" : "nGram"
          }
        },
        "analyzer": {
          "jp_analyzer": {
            "tokenizer": "ngram_tokenizer"
          },
          "jp_search_analyzer": {
            "tokenizer": "kuromoji_tokenizer",
            "filter": [
              "kuromoji_stemmer"
            ]
          }
        }
      }
    }
  },
  "mappings" : {
    "my_type" : {
      "properties" : {
        "code" : {
          "type" : "keyword"
        },
        "contents" : {
          "type" : "text",
          "analyzer" : "jp_analyzer",
          "search_analyzer" : "jp_search_analyzer"
        }
      }
    }
  }
}

これでいけます。

試してみると

{
  "took": 13,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 0.29000834,
    "hits": [
      {
        "_index": "my_index2",
        "_type": "my_type",
        "_id": "1",
        "_score": 0.29000834,
        "_source": {
          "contents": "本日は晴天なり。お隣の花壇に、秋桜の花が咲き乱れる。",
          "code": "1-1"
        },
        "highlight": {
          "contents": [
            "お隣の花壇に、<em>秋</em>桜の花が咲き乱れる。"
          ]
        }
      }
    ]
  }
}

少し厄介ですが、これでなんとかいけそうですね。


(Sunggyu Kei Rhie) #3

と思いましたけど
単純にanalyzerがngramになってるだけで適用されたものなので、結局検索がngramになってしまってだめですねー

うーん
もう少し調べます


(Sunggyu Kei Rhie) #4

少し考えてみましたが、ハイライトがanalyzerで指定したanalyzerによってtokenizeされたもの(token_filterの影響は受けないようなので)、index自体を分けたほうがいいなと思いました。
ちょっとトリッキーではありますが、
以下のようにすると要件は満たせます。

Mapping

PUT /my_index2
{
  "settings" : {
    "index": {
      "analysis": {
        "filter":{
          "edgeNGramFilter":{
            "type" : "edgeNGram",
            "max_gram" : 50
          }
        },
        "tokenizer":{
          "ngram_tokenizer":{
            "type" : "nGram"
          }
        },
        
        "analyzer": {
          "jp_analyzer": {
            "tokenizer": "kuromoji_tokenizer",
            "filter": [
              "kuromoji_stemmer",
              "edgeNGramFilter"
            ]
          },
          "highlight_analyzer": {
            "tokenizer": "ngram_tokenizer"
          },
          "jp_search_analyzer": {
            "tokenizer": "kuromoji_tokenizer",
            "filter": [
              "kuromoji_stemmer"
            ]
          }
        }
      }
    }
  },
  "mappings" : {
    "my_type" : {
      "properties" : {
        "code" : {
          "type" : "keyword"
        },
        "contents" : {
          "type" : "text",
          "analyzer" : "jp_analyzer",
          "search_analyzer" : "jp_search_analyzer"
        },
        "contents_highlight" : {
          "type" : "text",
          "analyzer" : "highlight_analyzer"
        }
      }
    }
  }
}

Query

GET my_index2/_search
{
  "_source": false,
  "query": {
    "match": {
      "contents": "秋"
    }
  },
  "highlight": {
    "fields": {
      "contents_highlight" : {
        "highlight_query": {
          "match": {
            "contents_highlight": "秋"
          }
        }
      }
    }
  }
}

Result

{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 0.34992102,
    "hits": [
      {
        "_index": "my_index2",
        "_type": "my_type",
        "_id": "1",
        "_score": 0.34992102,
        "highlight": {
          "contents_highlight": [
            "お隣の花壇に、<em>秋</em>桜の花が咲き乱れる。"
          ]
        }
      }
    ]
  }
}

今はこれくらいしか思いつかないですね、すみません!


(kako) #5

ご回答ありがとうございます。

少し考えてみましたが、ハイライトがanalyzerで指定したanalyzerによってtokenizeされたもの(token_filterの影響は受けないようなので)、index自体を分けたほうがいいなと思いました。

highlight_queryを知らなかったので、matchクエリと highlight_queryで別々のfieldを対象としてanalyzerを変える、というアプローチがあるのか!と、とても勉強になりました。

ご提示いただいた方法で検証いたしました。大体において要件を満たしていたのですが、highlight_queryにてnGramしたものをmatchで探しに行くと、ノイズがひっかかってしまいました。(「隣人」をhighlightで囲いたいのに、「隣の客」の「隣」もHITしてしまう。)
こちらを避ける為、match_phraseでの検索に修正いたしました。

また、contentsとcontents_highlightで、同じデータを登録するのが面倒に思えたので、copy_toを使用するようmappingを修正しました。
結果、下記にて、イメージどおりの検索が行えるようになりました。ありがとうございます。

●mapping

PUT /my_index3
{
  "settings" : {
    "index": {
      "analysis": {
        "filter":{
          "edgeNGramFilter":{
            "type" : "edgeNGram",
            "max_gram" : 50
          }
        },
        "tokenizer":{
          "ngram_tokenizer":{
            "type" : "nGram"
          }
        },
        "analyzer": {
          "jp_analyzer": {
            "tokenizer": "kuromoji_tokenizer",
            "filter": [
              "kuromoji_stemmer",
              "edgeNGramFilter"
            ]
          },
          "highlight_analyzer": {
            "tokenizer": "ngram_tokenizer"
          },
          "jp_search_analyzer": {
            "tokenizer": "kuromoji_tokenizer",
            "filter": [
              "kuromoji_stemmer"
            ]
          }
        }
      }
    }
  },
  "mappings" : {
    "my_type" : {
      "properties" : {
        "code" : {
          "type" : "keyword"
        },
        "contents" : {
          "type" : "text",
          "analyzer" : "jp_analyzer",
          "search_analyzer" : "jp_search_analyzer",
          "copy_to": "contents_highlight"
        },
        "contents_highlight" : {
          "type" : "text",
          "analyzer" : "highlight_analyzer",
          "store": true
        }
      }
    }
  }
}

●ドキュメント

POST _bulk
{ "index" : { "_index" : "my_index3", "_type" : "my_type", "_id" : "1" } }
{ "contents": "本日は晴天なり。お隣の花壇に、秋桜の花が咲き乱れる。", "code": "1-1" }
{ "index" : { "_index" : "my_index3", "_type" : "my_type", "_id" : "2" } }
{ "contents": "隣の客が隣の人の柿食う。その隣人とはまた違う。", "code": "1-2" } 

●クエリ

GET my_index3/_search
{
  "_source": false,
  "query": {
    "match": {
      "contents": "隣人"
    }
  },
  "highlight": {
    "fields": {
      "contents_highlight" : {
        "highlight_query": {
          "match_phrase": {
            "contents_highlight": "隣人"
          }
        }
      }
    }
  }
}

●結果

{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 0.3187269,
    "hits": [
      {
        "_index": "my_index3",
        "_type": "my_type",
        "_id": "2",
        "_score": 0.3187269,
        "highlight": {
          "contents_highlight": [
            "その<em>隣</em><em>人</em>とはまた違う。"
          ]
        }
      }
    ]
  }
}