Function_score queryでのスコアリング時に単語が分割されてしまう事象について

<実現したいこと>
titleフィールドに「消費税」という単語が含まれていれば、5ポイント、
questionフィールドに「消費税」という単語が含まれていれば、4ポイント、
answerフィールドに「消費税」という単語が含まれていれば、3ポイントを付与し、
その合計をスコアリングの値とし("score_mode": "sum")、スコアの降順で表示したいです。
※Elasticsearchのデフォルトのスコアリングは無視する設定にしています("boost_mode": "replace")。
function_score_queryの公式ドキュメント

<問題を発見した経緯>
titleとquestionに「消費税」という単語が含まれている場合、
スコアは9.0になるはず(titleに含まれている:5ポイント+questionに含まれている:4ポイント=9ポイント)ですが、実行結果は12.0になっています。
explainAPIでデバッグをしてみたところ、どうやら「消費税」を「消」「費」「税」と分割していることがわかりました。
("description" : "match filter: title:消 title:費 title:税")

※title、question、answerフィールドは全てtext型で、kuromojiプラグインによる形態素解析をしています。これが原因のような気もするのですが...
・mapping定義

"title" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }

<解決したいこと>
上記事象、スコアリング時に言葉が分割される問題を解決したいです。


以下、実行クエリです。
[すべてkibanaコンソール上で実行]

・multi_matchでの検索とfunction_score_queryを同時に実行するクエリ

GET /sample_table/_search?pretty=true
{
  "query": {
    "function_score": {
      "query": {"multi_match": {
      "fields": ["title","question","answer"],
      "query": "消費税"
    }},
      "functions": [
        {
          "filter": { "match": { "title": "消費税" } },
          "weight": 5
        },
        {
          "filter": { "match": { "question": "消費税" } },
          "weight": 4
        },
        {
          "filter": { "match": { "answer": "消費税" } },
          "weight": 3
        }
      ],
      "max_boost": 100,
      "score_mode": "sum",
      "boost_mode": "replace",
      "min_score": 0
    }
  }
  ,"size": 1500
  ,"_source": ["title","question","answer"]
}

・explainAPIの実行クエリ

GET /sample_table/_explain/QCu5jH8BunS87XDCQfYF
{
  "query": {
    "function_score": {
      "query": {"multi_match": {
      "fields": ["title","question","answer"],
      "query": "消費税"
    }},
      "functions": [
        {
          "filter": { "match": { "title": "消費税" } },
          "weight": 5
        },
        {
          "filter": { "match": { "question": "消費税" } },
          "weight": 4
        },
        {
          "filter": { "match": { "answer": "消費税" } },
          "weight": 3
        }
      ],
      "score_mode": "sum",
      "boost_mode": "replace"
    }
  }
}

・explainAPIの実行結果

{
  "_index" : "sample_table",
  "_type" : "_doc",
  "_id" : "QCu5jH8BunS87XDCQfYF",
  "matched" : true,
  "explanation" : {
    "value" : 12.0,
    "description" : "min of:",
    "details" : [
      {
        "value" : 12.0,
        "description" : "function score, score mode [sum]",
        "details" : [
          {
            "value" : 5.0,
            "description" : "function score, product of:",
            "details" : [
              {
                "value" : 1.0,
                "description" : "match filter: title:消 title:費 title:税",
                "details" : [ ]
              },
              {
                "value" : 5.0,
                "description" : "product of:",
                "details" : [
                  {
                    "value" : 1.0,
                    "description" : "constant score 1.0 - no function provided",
                    "details" : [ ]
                  },
                  {
                    "value" : 5.0,
                    "description" : "weight",
                    "details" : [ ]
                  }
                ]
              }
            ]
          },
          {
            "value" : 4.0,
            "description" : "function score, product of:",
            "details" : [
              {
                "value" : 1.0,
                "description" : "match filter: question:消 question:費 question:税",
                "details" : [ ]
              },
              {
                "value" : 4.0,
                "description" : "product of:",
                "details" : [
                  {
                    "value" : 1.0,
                    "description" : "constant score 1.0 - no function provided",
                    "details" : [ ]
                  },
                  {
                    "value" : 4.0,
                    "description" : "weight",
                    "details" : [ ]
                  }
                ]
              }
            ]
          },
          {
            "value" : 3.0,
            "description" : "function score, product of:",
            "details" : [
              {
                "value" : 1.0,
                "description" : "match filter: answer:消 answer:費 answer:税",
                "details" : [ ]
              },
              {
                "value" : 3.0,
                "description" : "product of:",
                "details" : [
                  {
                    "value" : 1.0,
                    "description" : "constant score 1.0 - no function provided",
                    "details" : [ ]
                  },
                  {
                    "value" : 3.0,
                    "description" : "weight",
                    "details" : [ ]
                  }
                ]
              }
            ]
          }
        ]
      },
      {
        "value" : 3.4028235E38,
        "description" : "maxBoost",
        "details" : [ ]
      }
    ]
  }
}

どなたか原因及び対処法をご教示頂けますと幸いです。よろしくお願いいたします。

ご指摘の通りtokenizerによる挙動と思われます。multi-matchクエリでどうなのか確証がないのですが、matchクエリにおいてデフォルトはOR検索です。したがって、「消 OR 費 OR 税」となっていることが考えられます。
ただANDにすると離れて出現している場合もヒットしてしまいますので、これも完璧ではないですが、N-gram tokenizerを用いた上で、"operator": ANDにしてみてはどうでしょうか。厳密な部分一致が必要であれば、wildcard typeとregex queryを用いる方法もあるかもしれません。

ご質問に記載のマッピング定義だと、 kuromoji の analyzer 指定がないのでデフォルトの standard analyzer が動いているのではないでしょうか。こちらで kuromoji を使ってテストしたところ、 title と question に消費税が入ったドキュメントのスコアは期待通り 9.0 となりました。

なお、 kuromoji で分割されたトークンは '消費' と '税' でした。このため、 '税' の一文字が入っていれば match だとスコアが加算されてしまいます。 match_phrase クエリに変更すれば消費税のみを対象とできます。
テストデータの二件目は answer に '税' が含まれるため match クエリだとスコアが 12.0 になります。match_phrase だと 9.0 になります。

テストに利用したのは以下です:

PUT discuss-302662
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "kuromoji"
      },
      "question": {
        "type": "text",
        "analyzer": "kuromoji"
      },
      "answer": {
        "type": "text",
        "analyzer": "kuromoji"
      }
    }
  }
}

POST discuss-302662/_bulk
{"index": {}}
{"title": "消費税、また増税の可能性", "question": "消費税がまた上がるのですか?", "answer": "なんとも言えません"}
{"index": {}}
{"title": "消費税、また増税の可能性", "question": "消費税がまた上がるのですか?", "answer": "なんとも言えません税"}

GET /discuss-302662/_search?pretty=true&explain=true
{
  "query": {
    "function_score": {
      "query": {"multi_match": {
      "fields": ["title","question","answer"],
      "query": "消費税"
    }},
      "functions": [
        {
          "filter": { "match_phrase": { "title": "消費税" } },
          "weight": 5
        },
        {
          "filter": { "match_phrase": { "question": "消費税" } },
          "weight": 4
        },
        {
          "filter": { "match_phrase": { "answer": "消費税" } },
          "weight": 3
        }
      ],
      "max_boost": 100,
      "score_mode": "sum",
      "boost_mode": "replace",
      "min_score": 0
    }
  }
  ,"size": 1500
  ,"_source": ["title","question","answer"]
}

Tomo_M さま

回答を下さったにも関わらず、ご返信が遅くなり申し訳ございません。

  1. tokenizerにより検索キーワードが "消 費 税"と分割される
  2. 分割したキーワードを使用して、"消 OR 費 OR 税" とOR検索をしているということですね。

ご意見を頂戴し、大変助かりました。ありがとうございました。

1 Like

Koji_Kawamura さま

kuromojiのAnalyzer指定の記載が漏れていました。
マッピング定義は全てのフィールドで、Standard analyzerを使用しております。

match_phraseクエリを使用し、語順通りに検索する ≒ 検索キーワードが分割されずにスコアリングされるということですね。

match_phraseクエリを使用して実行したところ、期待通りの結果が得られました。

ご教示頂き、誠にありがとうございました。