Term API:精确搜索

231 阅读10分钟

在使用 Match API 全文搜索的时候,系统会对检索内容进行分词,然后再对每一个词项进行检索。Term APIMatch API 不同,它会将输入的内容作为一个整体来进行检索,并且使用相关性算分对包含整个检索内容的文档进行相关性算分。

常用的 Term API 如下

  • Term Query,返回在指定字段中准确包含了检索内容的文档。
  • Terms Query,跟 Term Query 类似,不过可以同时检索多个词项的功能。
  • Range Query,范围查询。
  • Exist Query,返回在指定字段上有值的文档,一般用于过滤没有值的文档。
  • Prefix Query,返回在指定字段中包含指定前缀的文档。
  • Wildcard Query,通配符查询。

还是以订单为模型,介绍 Term API

准备数据

PUT order
{
  "mappings": {
    "properties": {
        "id": {
          "type": "keyword"
        },
        "name": {
          "type": "text"
        },
        "sub_order_no": {
          "type": "keyword"
        },
        "order_no": {
          "type": "keyword"
        },
        "price":{
          "type": "double"
        },
        "createTime":{
          "type": "date"
        }
      }
  },
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 1
  }
}

## 简单造几条数据
PUT /order/_doc/d17be93b-648f-44b7-993f-98e2ae329306
{
    "id": "d17be93b-648f-44b7-993f-98e2ae329306",
    "name": "iphone13",
    "sub_order_no": [
        "3b9ebe09-99d7-4c79-85a4-e99a721c24b5",
        "86a38185-b100-4713-9a73-36c17f22fba4",
        "8887aed3-ac31-4826-9b1b-0b02e549b4fb",
        "47c735dd-735a-4a3e-a427-2b35f95c1ec3",
        "7ca1fcd4-faf9-44ac-b524-3e2844701dc9"
    ],
    "order_no": "d17be93b-648f-44b7-993f-98e2ae329306",
    "price": 2634.6998447085434,
    "createTime": 1688211961508
}

PUT /order/_doc/47c735dd-735a-4a3e-a427-2b35f95c1ec3
{
    "id": "47c735dd-735a-4a3e-a427-2b35f95c1ec3",
    "name": "xiaomi13",
    "sub_order_no": [],
    "order_no": "47c735dd-735a-4a3e-a427-2b35f95c1ec3",
    "price": 23.50361213313873,
    "createTime": 1688211961508
}

PUT /order/_doc/8887aed3-ac31-4826-9b1b-0b02e549b4fb
{
    "id": "8887aed3-ac31-4826-9b1b-0b02e549b4fb",
    "name": "huawei mate 50",
    "sub_order_no": [],
    "order_no": "8887aed3-ac31-4826-9b1b-0b02e549b4fb",
    "price": 21.132645156267703,
    "createTime": 1688211961508
}

PUT /order/_doc/86a38185-b100-4713-9a73-36c17f22fba4
{
    "id": "86a38185-b100-4713-9a73-36c17f22fba4",
    "name": "sanxing",
    "sub_order_no": [],
    "order_no": "86a38185-b100-4713-9a73-36c17f22fba4",
    "price": 35.66395720954454,
    "createTime": 1688211961508
}

PUT /order/_doc/7ca1fcd4-faf9-44ac-b524-3e2844701dc9
{
    "id": "7ca1fcd4-faf9-44ac-b524-3e2844701dc9",
    "name": "vivo",
    "sub_order_no": [],
    "order_no": "7ca1fcd4-faf9-44ac-b524-3e2844701dc9",
    "price": 18.91113695347623,
    "createTime": 1688211961508
}

PUT /order/_doc/3b9ebe09-99d7-4c79-85a4-e99a721c24b5
{
    "id": "3b9ebe09-99d7-4c79-85a4-e99a721c24b5",
    "name": "oppo phone is very good",
    "sub_order_no": [],
    "order_no": "3b9ebe09-99d7-4c79-85a4-e99a721c24b5",
    "price": 9.281517420473545,
    "createTime": 1688211961508
}

1.Term Query API

1.1 Term Query

Term Query API 返回在指定字段中准确包含了检索内容的文档,可以通过该API查询精确值的字段,如订单ID,订单流水号,订单价格等等。

For Example:

GET /order/_search
{
  "query": {
    "term": {
      "order_no": {
        "value": "3b9ebe09-99d7-4c79-85a4-e99a721c24b5"
      }
    }
  }
}

上面的SQL查询订单流水号为 "3b9ebe09-99d7-4c79-85a4-e99a721c24b5" 的文档。

避免将 Term Query 用在 text 类型的字段上。

GET /order/_search
{
  "query": {
    "term": {
      "name": {
        "value": "Oppo"
      }
    }
  }
}

上面的SQL检索订单标题含有 "Oppo" 的文档,其实我们是有一个文档标题中含有 "oppo" 的,但是上面SQL查询的结果却没有匹配上。这是因为基于Term的查询是不会对检索内容进行分词的,输入的文本会作为一个整体进行查询。但是索引数据的时候是会进行分词并且转化为小写的(这个和分词器有关),所以上面的SQL查询结果是空,一个文档都没有匹配上。如果要对text类型的字段进行搜索应该使用 Match API 而不是 Term API

1.2 Terms Query

Terms Query 可以同时检索多个词项。下面的SQL就是查同时查询订单流水号是 "d17be93b-648f-44b7-993f-98e2ae329306" 或者 "8887aed3-ac31-4826-9b1b-0b02e549b4fb" 的文档。

GET /order/_search
{
  "query": {
    "terms": {
      "order_no": [
        "d17be93b-648f-44b7-993f-98e2ae329306",
        "8887aed3-ac31-4826-9b1b-0b02e549b4fb"
      ]
    }
  }
}

1.3 Range Query

Range Query API 可以查询字段值符合某个范围的文档数据。例如查询在某个时间范围内创建的订单。

GET order/_search
{
  "query": {
    "range": {
      "createTime": {
        "gte": 1688211961108,
        "lte": 1688211961508
      }
    }
  }
}

大小的比较有以下四个值:

  • gt:表示大于
  • gte: 表示大于或者等于
  • lt: 表示小于
  • lte: 表示小于或者等于

1.4 Exist Query

Exist Query API 用来查询那些在指定字段上有值的文档,通常可以使用这个 API 来做文档过滤。

那什么样的值才被认为是空值呢?一个字段的值为空可能是由于下面这个几种原因导致的:

  1. 字段的 JSON 值为 null 或者 [],如果一个字段压根不存在于文档的 _source 里,也被认为是空的。
  2. 一个字段在 Mapping 定义的时候设置了 "index" : false。
  3. 一个字段的值的长度超出了 Mapping 里这个字段设置的 ignore_above 时。
  4. 当字段的值不合规,并且 Mapping 中这个字段设置了 ignore_malformed 时。
POST order/_search
{
  "query": {
    "exists": {
      "field": "price"
    }
  }
}

1.5 Prefix Query

Prefix Query 可以查询在指定字段中包含特定前缀的文档。

GET order/_search
{
  "query": {
    "prefix": {
      "name": {
        "value": "iphone"
      }
    }
  }
}

对于上面的SQL,使用了 Prefix Query 查询含有 "iphone" 前缀的文档,如果订单名字中含有 "iphone" 开头的词语的文档将会被匹配上。

需要注意的是,text 类型的字段会被分词,成为一个个的 term,所以这里的前缀匹配是匹配这些分词后term!

1.6 Wildcard Query

Wildcard Query 允许使用通配符表达式进行匹配。Wildcard Query 支持两个通配符:

  • ?,使用 ? 来匹配任意字符。
  • *,使用 * 来匹配 0 或多个字符。
GET order/_search
{
  "query": {
    "wildcard": {
      "name": "xiaomi*"
    }
  }
}

Prefix QueryWildcard Query 在进行查询的时候需要扫描倒排索引中的词项列表才能找到全部匹配的词项,然后再获取匹配词项对应的文档 ID。所以使用 Wildcard Query API 的时候需要注意性能问题,要尽量避免使用左通配匹配模式,如 "*13"、".*13"。

实际开发中我们会经常使用这些 API 来对结构化数据进行查询、过滤,这个时候一般不需要对数据进行相关性评分,所以可以跳过评分阶段来提高搜索的性能。

1.7 Ids Query

实际开发中我们经常会有一个需求,根据一批 ID 查询文档,这样的需求其实Es已经提供了现成的API。

这里的ID指的是文档的 _id 字段。

GET order/_search
{
  "query": {
    "ids" : {
      "values" : ["1", "4", "100"]
    }
  }
}

1.8 Fuzzy Query

Fuzzy Query 首先会将根据可编辑距离创建搜索词项的所有可能变化或拓展的集合,然后该查询返回每一个扩展的精确匹配。

编辑距离指的是将一个单词转换为另一个单词需要单个字符的变化次数,这些变化包括:

  • 变化一个字符:[box -> fox]
  • 移除一个字符:[black -> lack]
  • 插入一个字符:[sic -> sick]
  • 调换两个相邻的字符:[act -> cat]

For Example:

GET order/_search
{
  "query": {
    "fuzzy": {
      "name": {
        "value": "xi"
      }
    }
  }
}

另外该API还包含一些高级的参数:

GET order/_search
{
  "query": {
    "fuzzy": {
      "name": {
        "value": "xi",
        "fuzziness": "AUTO",
        "max_expansions": 50,
        "prefix_length": 0,
        "transpositions": true,
        "rewrite": "constant_score"
      }
    }
  }
}
  • value:指定的是你要搜索的内容。
  • fuzziness:可选字段,匹配允许的最大编辑距离。
  • max_expansions:可选字段,创建的最大变化数,默认是50。

避免在max_expansions参数中使用高值,尤其是当prefix_length参数值为0时。max_expansions参数中的高值可能会导致性能下降,因为要检查大量的变化。

  • prefix_length:可选字段,创建扩展时保持不变的起始字符数,默认值为0。
  • transpositions:表示默认是否包括两个相邻字符的换位(ab → ba)。默认为true。
  • rewrite:用于重写查询的方法。有关有效值和更多信息,请参见rewrite参数。

1.9 Terms set Query

指定查询字段中至少要包含几个搜索词项,满足条件的文档才会被Es返回。比如对于订单类型包含 ["家电","电子产品","智能家居"...]几种类型,可以使用 Terms set Query 查询返回至少匹配其中两种类型的文档。

如何使用 Terms set Query

  1. 首先我们来创建一个索引。

实际开发中,我们一般需要在索引中包含一个数值字段映射,以便使用terms_set查询。此数值字段包含返回文档所需的匹配项的数量。

这个索引包含三个字段:

  • name:keyword类型的名字。
  • programming_languages:keyword类型的编程语言。
  • required_matches:表示需要命中几条才返回该文档。
PUT /job-candidates
{
  "mappings": {
    "properties": {
      "name": {
        "type": "keyword"
      },
      "programming_languages": {
        "type": "keyword"
      },
      "required_matches": {
        "type": "long"
      }
    }
  }
}
  1. 接下来我们来添加两条数据。
PUT /job-candidates/_doc/1?refresh
{
  "name": "Jane Smith",
  "programming_languages": [ "c++", "java" ],
  "required_matches": 2
}

PUT /job-candidates/_doc/2?refresh
{
  "name": "Jason Response",
  "programming_languages": [ "java", "php" ],
  "required_matches": 2
}
  1. 现在我们可以进行查询了。

下面SQL的查询条件:在输入的几个词项里面满足文档字段required_matches里面最小匹配数的文档。

GET /job-candidates/_search
{
  "query": {
    "terms_set": {
      "programming_languages": {
        "terms": [ "c++", "java", "php" ],
        "minimum_should_match_field": "required_matches"
      }
    }
  }
}

minimum_should_match_fieldminimum_should_match_script 参数作为可选参数,接下来我们来看一下 minimum_should_match_script 参数。

这个参数的含义:包含返回文档所需的匹配项数量的自定义脚本。

下面这条SQL的意思:取 terms字段里面值的个数 和 文档上required_matches字段值 的最小值作为匹配数,返回匹配的文档。

GET /job-candidates/_search
{
  "query": {
    "terms_set": {
      "programming_languages": {
        "terms": [ "c++", "java", "php" ],
        "minimum_should_match_script": {
          "source": "Math.min(params.num_terms, doc['required_matches'].value)"
        },
        "boost": 1.0
      }
    }
  }
}

2.结构化搜索 Term Level Query API

结构化搜索指的是对结构化的数据进行搜索。那什么是结构化数据呢?简单点说就是高度组织、格式化的数据,例如日期、颜色、地区编码、价格等等。

像订单的创建时间、价格这些都是有精确的格式的,我们可以对这些数据进行逻辑操作,例如判断价格的范围等。一般我们会对结构化数据进行精确匹配,而精确匹配的结果为布尔值,这个时候可以考虑跳过相关性算分的步骤,从而提高搜索的性能。

使用 Constant Score 可以将 query 转化为 filter,可以忽略相关性算分的环节,并且 filter 可以有效利用缓存,从而提高查询的性能。

GET /order/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "range": {
          "price": {
            "gte": 20,
            "lte": 100
          }
        }
      }
    }
  }
}