ES101系列05 | 深入搜索与多字段查询

62 阅读7分钟

本篇文章会深入 ElasticSearch 中的查询功能,包括 Term 查询、全文查询、搜索相关性和多字段查询等,都含有 Demo 方便大家测试。

Term 搜索与全文搜索

对比维度Term 搜索全文搜索
查询类型精确匹配模糊匹配
分析过程不分词,直接匹配索引中的词项(Term)分词处理,匹配词条(Token)
适用字段类型keyword、数字、日期等精确值字段text 类型字段
使用场景过滤(Filter)、聚合(Aggregation)自由文本搜索(如搜索框输入)
性能特点高效,适合大数据集和实时过滤相对耗时,依赖分词和相关性计算
查询语法{"term": {"field": "value"}}{"match": {"field": "user_input"}}
倒排索引使用直接定位词项的文档列表通过词条组合计算相关性得分

Term 搜索

示例

1、插入数据

POST /products/_bulk
{ "index": { "_id": 1 }}
{ "productID" : "XHDK-A-1293-#fJ3","desc":"iPhone" }
{ "index": { "_id": 2 }}
{ "productID" : "KDKE-B-9947-#kL5","desc":"iPad" }
{ "index": { "_id": 3 }}
{ "productID" : "JODL-X-1937-#pV7","desc":"MBP" }

2、Term 查询

POST /products/_search
{
  "query": {
    "term": {
      "desc": {
        // "value": "iPhone"
        // "value": "iphone"
      }
    }
  }
}

使用 iPhone 不能搜索出结果,而小写的 iphone 可以。因为是精确查询,而原始数据在分词后的结果为 iphone

POST /products/_search
{
  "query": {
    "term": {
      "desc.keyword": {
        // "value": "iPhone"
        // "value": "iphone"
      }
    }
  }
}

此时使用 keyword 类型能成功获取数据。

3、Constant Score 转为 Filter

POST /products/_search
{
  "explain": true,
  "query": {
    "constant_score": {
      "filter": {
        "term": {
          "productID.keyword": "XHDK-A-1293-#fJ3"
        }
      }
    }
  }
}
  • 将 Query 转为 Filter,忽略 TF-IDF 计算,避免相关性算分的开销。
  • Filter 可以有效利用缓存。

全文搜索

Match / Match Phrase / Query String

  • 索引和搜索时都会进行分词,查询字符串先传递到一个合适的分词器,然后生成一个供查询的列表。
  • 查询会对每个词项逐个查询再将结果进行合并,并为每个文档生成一个算分。
场景推荐查询类型
普通全文搜索match
精确短语匹配match_phrase
高级搜索(用户输入带逻辑)query_string
用户输入框 + 安全性优先match / multi_match,避免 query_string

结构化搜索

结构化数据是指具有固定格式和明确字段的数据,每个字段都有特定的类型(如字符串、数字、日期等),并且数据是可预测、易于解析的。结构化搜索即对结构化数据进行搜索。

示例

1、插入数据

DELETE products
POST /products/_bulk
{ "index": { "_id": 1 }}
{ "price" : 10,"avaliable":true,"date":"2018-01-01", "productID" : "XHDK-A-1293-#fJ3" }
{ "index": { "_id": 2 }}
{ "price" : 20,"avaliable":true,"date":"2019-01-01", "productID" : "KDKE-B-9947-#kL5" }
{ "index": { "_id": 3 }}
{ "price" : 30,"avaliable":true, "productID" : "JODL-X-1937-#pV7" }
{ "index": { "_id": 4 }}
{ "price" : 30,"avaliable":false, "productID" : "QQPX-R-3956-#aD8" }

2、Bool

POST products/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "term": {
          "avaliable": true
        }
      }
    }
  }
}

3、数字

POST products/_search
{
  "query": {
    "term": {
      "price": 30
    }
  }
}

4、Range

POST products/_search
{
    "query" : {
        "constant_score" : {
            "filter" : {
                "range" : {
                    "price" : {
                        "gte" : 20,
                        "lte"  : 30
                    }
                }
            }
        }
    }
}

搜索相关性

  • 搜索的相关性算分,描述了一个文档和查询语句匹配的程度。ES 会对每个匹配查询条件的结果进行算分 _score。
  • 打分的本质是排序,需要把最符合用户需求的文档排在前面。ES5 之前,默认的相关性算分采用 TF-IDF,现在采用 BM 25。

词频 TF

  • TF 即是词在一篇文档中出现的频率。
  • 度量一条查询和结果文档相关性的简单方法:将搜索中的每一个词 TF 相加。
  • Stop Word 不应考虑,类似 「the」、「的」。

逆文档频率 IDF

  • DF:检索词在所有文档中出现的评率。
  • Inverse Document Frequency:log(全部文档数/检索词出现过的文档总数)log(全部文档数/检索词出现过的文档总数)
  • TF-IDF 的本质就是将 TF 求和变成了加权求和:TF(X)IDF(X)TF(X)*IDF(X)

BM 25

与 TF-IDF 相比,当一个词的 TF 无限增加时,BM 25 算分会趋于一个稳定值。

Boosting

Boosting 是控制相关度的一种手段。

  • 当 boost > 1 时打分的相关度相对性提升。
  • 当 0 < boost < 1 时打分的权重相对性降低。
  • 当 boost < 0 时,贡献负分。

多字段多字符串查询

bool 查询

一个 bool 查询是一个或者多个查询子句的组合。

子句描述
must必须匹配,贡献算分。
should选择性匹配,贡献算分。
must_notFilter Context 查询字句,必须不能匹配。
filterFilter Context 必须匹配,不贡献算分。
示例

1、bool 查询

POST /products/_search
{
  "query": {
    "bool" : {
      "must" : {
        "term" : { "price" : "30" }
      },
      "filter": {
        "term" : { "avaliable" : "true" }
      },
      "must_not" : {
        "range" : {
          "price" : { "lte" : 10 }
        }
      },
      "should" : [
        { "term" : { "productID.keyword" : "JODL-X-1937-#pV7" } },
        { "term" : { "productID.keyword" : "XHDK-A-1293-#fJ3" } }
      ],
      "minimum_should_match" :1
    }
  }
}
  • 子查询可以任意顺序出现。
  • 可以嵌套多个查询。
  • 如果 bool 查询中没有 must 条件,那么 should 中必须至少满足一条查询。

2、boost 控制查询分数

POST /news/_bulk  
{ "index": { "_id": 1 }}  
{ "content":"Apple Mac" }  
{ "index": { "_id": 2 }}  
{ "content":"Apple iPad" }  
{ "index": { "_id": 3 }}  
{ "content":"Apple employee like Apple Pie and Apple Juice" }

POST news/_search
{
  "query": {
    "boosting": {
      "positive": {
        "match": {
          "content": "apple"
        }
      },
      "negative": {
        "match": {
          "content": "pie"
        }
      },
      "negative_boost": 0.5
    }
  }
}

此时会将苹果产品放前边,而 id 为 3 的显示在最后。

多字段单字符串查询

Disjunction Max Query

将任何与任一查询匹配的文档作为结果返回。采用字段上最匹配的评分最终评分返回。

示例

1、插入数据

PUT /blogs/_doc/1
{
    "title": "Quick brown rabbits",
    "body":  "Brown rabbits are commonly seen."
}

PUT /blogs/_doc/2
{
    "title": "Keeping pets healthy",
    "body":  "My quick brown fox eats rabbits on a regular basis."
}

2、bool 测试

POST /blogs/_search
{
    "query": {
        "bool": {
            "should": [
                { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
            ]
        }
    }
}

结果可以看到 1 号文档在前,是因为 bool 会对两个进行加和平均,不符合直觉。

2、dis_max 测试

POST blogs/_search
{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
            ]
        }
    }
}

此时 2 号文档在前,符合直觉。

POST blogs/_search
{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Quick pets" }},
                { "match": { "body":  "Quick pets" }}
            ]
        }
    }
}

结果还是 1 号文档在前,同时分数相同。因为最高分数的 quick 都只出现一次。但 2 号文档中有 pet ,直觉上应该 2 号文档更高,但 dis_max 只取最大的一条。

POST blogs/_search
{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Quick pets" }},
                { "match": { "body":  "Quick pets" }}
            ],
            "tie_breaker": 0.2
        }
    }
}

加入 tie_breaker 后会把最匹配一条之外的分数与其值相乘,这样 2 号文档就能更高,符合直觉。

MultiMatch

multi_match 是 Elasticsearch 中一种用于在多个字段中执行全文搜索的查询方式。它扩展了 match 查询,允许你在多个字段上同时进行匹配。

类型描述使用场景
best_fields匹配最佳字段(默认)标题或正文关键词搜索
most_fields多字段尽量都匹配多语言字段匹配
cross_fields字段合并为整体匹配姓名拆分(first_name + last_name)
phrase短语匹配查找完整短语
phrase_prefix短语前缀匹配自动补全
bool_prefix前缀词项布尔匹配高效前缀搜索

示例

1、插入数据

PUT /titles
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "english",
        "fields": {"std": {"type": "text","analyzer": "standard"}}
      }
    }
  }
}

POST titles/_bulk
{ "index": { "_id": 1 }}
{ "title": "My dog barks" }
{ "index": { "_id": 2 }}
{ "title": "I see a lot of barking dogs on the road " }

2、使用 most_fields

GET /titles/_search
{
   "query": {
        "multi_match": {
            "query":  "barking dogs",
            "type":   "most_fields",
            "fields": [ "title", "title.std" ]
        }
    }
}

结果显示 2 号文档在前,符合直觉。若不使用 MultiMatch 则会 1 号文档在前,因为 English 分词器会把 barking dogs 分为 bark 和 dog,1 号文档分数更高。

写在最后

这是该系列的第五篇,主要讲解 ElasticSearch 中的查询功能,包括 Term 查询、全文查询、搜索相关性和多字段查询等,可以自己去到 Kibana 的 Dev Tool 实战操作,未来会持续更新该系列,欢迎关注👏🏻。

同时欢迎关注公众号:LanTech指南。不定时分享职场思考、独立开发日志、大厂方法论和后端经验❤️

参考

  1. github.com/onebirdrock…
  2. www.elastic.co/elasticsear…