ElasticsearchのIntervalsクエリで単語間の位置を考慮した検索を行う

こんにちは、検索チームのよこやまです。

最近単語の位置を考慮した検索について調べる機会があり、ElasticsearchのIntervalsクエリを確認していたため、調べた一部をご紹介できればと思います。
Intervalsクエリは意外と使用している例が見つかりづらく、パッと見挙動も分かりづらいため、ここでご紹介した内容がお役に立てれば幸いです。

※ Intervalsクエリは「単語の位置」ではなく「トークンの位置」を考慮しますが、この記事では以下の前提で述べるように英文に対してstandard analyzerを適用するため、単語とトークンを同じものとして表記します

前提

  • Elasticsearch 7.14
  • 以下のmappingを使用します
{
  "mappings": {
    "properties": {
      "contents": {
        "type": "text",
        "analyzer": "standard"
      }
    }
  }
}
  • 以下のドキュメントに対して検索することを想定します
{
  "contents": "VQ (VisasQ) is a leading global expert network service headquartered in Japan."
}

Query stringクエリで近接検索(Proximity searches)を行う

Intervalsクエリを取り上げる前に、 Query stringクエリでも近接検索を行えることを確認します。

公式ドキュメント Query string query: Proximity searches

以下の query_string を利用したクエリが対象のドキュメントにマッチします。

{
  "query": {
    "query_string": {
      "query": "\"global network\"~1",
      "fields": [
        "contents"
      ]
    }
  }
}

対象のドキュメントでは globalnetwork は1単語分離れていますが、クエリ内でフレーズクエリに対して ~1と指定しているためヒットします。

また、 Query stringクエリの近接検索は単語の編集距離を見ているため、単語の順番が逆でもその分指定する最大編集距離を大きく指定してあげるとヒットさせることができます。

Intervalsクエリによる絞り込み

公式ドキュメント Intervals query

Intervalsクエリを使用すると、 Query stringクエリよりも複雑な条件で近接検索を行うことができます。

まずは単純な例から見ていきます。

単語同士が近い場合にヒットさせる

{
  "query": {
    "intervals": {
      "contents": {
        "match": {
          "query": "global network",
          "max_gaps": 1
        }
      }
    }
  }
}

こちらは先述したQuery stringクエリと似た挙動をするクエリです。

max_gaps で単語間の最大距離※を指定することができます。

また、この querynetwork global のように順序を逆にしてもヒットします。

上記クエリでは指定していない orderedパラメータを true にすると単語の順序を固定して検索を行うことができます。

※ ここで言う距離は「単語の位置の差」のことであり、編集距離やベクトル間の距離ではありません

単語間の距離に対して別の条件を加える

filterパラメータを使用することで単語間の距離とはまた違った条件を加えることができます。

公式ドキュメント Intervals query: filter rule parameters

filter パラメータで指定できる条件の一つ、 containing 条件を例に取ります。

{
  "query": {
    "intervals": {
      "contents": {
        "match": {
          "query": "global network",
          "max_gaps": 1,
          "filter": {
            "containing": {
              "match": {
                "query": "expert"
              }
            }
          }
        }
      }
    }
  }
}

このクエリは「 globalnetwork の間に expert がある」ドキュメントにヒットします。

もし対象のドキュメントに含まれる文字列が global expert network ではなく global professional network などであった場合は上記クエリはヒットしません。

他にも before 条件では「filterの条件よりも前に intervals.[フィールド名].match.query で指定した内容がある」場合、 overlap 条件では「filter条件でヒットした箇所と、intervals.[フィールド名].match.queryの条件でヒットした箇所が少しでもかぶっている」場合、などの指定ができます。

他の条件については公式ドキュメントを御覧ください。

Intervalsクエリを組み合わせる

all_of any_of パラメータを使用することで、複雑な条件を指定することができます。

all_of

まずは all_of を指定したクエリから見ていきましょう。

{
  "query": {
    "intervals": {
      "contents": {
        "all_of": {
          "max_gaps": 10,
          "ordered": true,
          "intervals": [
            {
              "match": {
                "query": "global network",
                "max_gaps": 1
              }
            },
            {
              "match": {
                "query": "headquartered Japan",
                "max_gaps": 1
              }
            }
          ]
        }
      }
    }
  }
}

徐々にクエリが複雑になってきました。

このクエリは「最初に global network (1単語離れていることは許容) という文字列があり、そこから10単語以内の箇所に headquartered Japan (1単語離れていることは許容)という文字列がある」というドキュメントにヒットします。

all_of を使用することで複数のIntervalsクエリを組み合わせつつ、そのクエリの順序や距離を指定することできます。

any_of

all_of は内部で指定した全てのIntervalsクエリの条件にヒットすることを求めますが、 any_of はこれをORにすることができます。

{
  "query": {
    "intervals": {
      "contents": {
        "all_of": {
          "max_gaps": 10,
          "ordered": true,
          "intervals": [
            {
              "match": {
                "query": "global network",
                "max_gaps": 1
              }
            },
            {
              "any_of": {
                "intervals": [
                  {
                    "match": {
                      "query": "headquartered Japan",
                      "max_gaps": 1
                    }
                  },
                  {
                    "match": {
                      "query": "headquartered Singapore",
                      "max_gaps": 1
                    }
                  }
                ]
              }
            }
          ]
        }
      }
    }
  }
}

ネストがかなり深くなってきました。このクエリは all_of で例に挙げたクエリの拡張です。

all_of で例に挙げたクエリは 「(略)headquartered Japan (1単語離れていることは許容)という文字列がある」ドキュメントにヒットしましたが、このクエリの場合は headquartered Japan だけでなくheadquartered Singapore (1単語離れ)にもヒットさせることができます。

all_of と異なり any_ofmax_gapsordered が指定できません。

その他

ここまで Intervalsクエリを使った絞り込みについて一部の機能を取り上げました。

他にも match の代わりに prefix wildcard fuzzy を使用することができたり、 filter 条件にnotを使用できたりなど複数の機能があります。

また、Intervalsクエリではクエリ実行時間を短くできるよう(線形時間で伸びるよう)、常に最小間隔の文字列群に対してマッチします。これは初見だと想定外な挙動にも見えるため、使用する前に公式ドキュメントの Minimization の項目を見ておくことを推奨します。

Intervalsクエリのスコアリング

最後にIntervalsクエリでドキュメントがヒットした場合のスコアリングについて確認します。

下記の単純なIntervalsクエリを想定します。

{
  "query": {
    "intervals": {
      "contents": {
        "match": {
          "query": "global network",
          "max_gaps": 1
        }
      }
    }
  }
}

このクエリを冒頭に記載したドキュメントに対してexplainをかけると、以下のようなスコアを確認できます。

// 一部略
"explanation": {
  "value": 0.3333333,
  "description": "Saturation function on interval frequency, computed as w * S / (S + k) from:",
  "details": [
    {
      "value": 1,
      "description": "w, weight of this function",
      "details": []
    },
    {
      "value": 1,
      "description": "k, pivot feature value that would give a score contribution equal to w/2",
      "details": []
    },
    {
      "value": 0.5,
      "description": "S, the sloppy frequency of the interval query contents:MAXGAPS/1(UNORDERED(global,network))",
      "details": []
    }
  ]
}

S の値はヒット時の単語間の距離に影響を受け、Intervalsクエリはヒット時に単語間の距離が短いほどスコアが高くなります。

よって上記のクエリでは、 global expert network という文字列を含むドキュメントよりも、 global network という文字列を含むドキュメントのほうがスコアが高くなります。

おわりに

今回はElasticsearchのIntervalsクエリについて取り上げました。

フレーズクエリにある程度単語の曖昧性をもたせて検索をさせたいときなど、Intervalsクエリを使うことを考えてみるのも良いかもしれません。

エンジニアを募集しています

ビザスクでは、エンジニアとして働きたい方を募集しています。
ご興味のある方は下記よりお気軽にご連絡ください。