Elasticsearch核心技术与实践三

827 阅读17分钟

四、深入搜索

1. 基于词项和基于全文的搜索

1.1 基于 Term 的查询

  • Term的重要性
    • Term是表达语义的最小单位。搜索和利用统计语言模型进行自然语言处理都需要处理Term
  • 特点
    • Term Level Query: Term Query / Range Query / Exists Query / Prefix Query / Wildcard Query
    • ES 中,Term查询,对输入不做分词。会将输入作为一个整体,在倒排索引中查找准确的词项,并且使用相关度算分公式为每个包含该词项的文档进行相关度算分 - 例如Apple Store
    • 可以通过 Constant Score 将查询转换成一个 Filtering, 避免算分,并利用缓存,提高性能

1.2 Term查询的例子

1.2.1 插入数据

# Term查询的例子,并思考
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"}

GET /products

1.2.2 例1

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

发现什么结果都查不出来,这是什么原因呢?
是因为我们使用的term查询,es并不会对我们输入的条件做任何的处理,就是说我们搜索的条件就是一个带大写的"iPhone",而es在做数据索引的时候,会对这个text类型的数据进行默认分词的处理,并且转了小写,这就是为什么我们取不到数据的原因(因为是小写啊)

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

这样查就能查出数据了 image

1.2.3 例2

POST /products/_search
{
  "query": {
    "term": {
      "productID": {
        "value": "XHDK-A-1293-#fJ3"
      }
    }
  }
}

这也什么都没有查到,这是什么原因呢?通过之前的分析我们知道,term查询不会对输入的结果进行分词转小写处理,我们来看下es"XHDK-A-1293-#fJ3"这个是如何处理的

POST /_analyze
{
  "analyzer": "standard",
  "text": ["XHDK-A-1293-#fJ3"]
}

上面的结果是

{
  "tokens" : [
    {
      "token" : "xhdk",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "<ALPHANUM>",
      "position" : 0
    },
    {
      "token" : "a",
      "start_offset" : 5,
      "end_offset" : 6,
      "type" : "<ALPHANUM>",
      "position" : 1
    },
    {
      "token" : "1293",
      "start_offset" : 7,
      "end_offset" : 11,
      "type" : "<NUM>",
      "position" : 2
    },
    {
      "token" : "fj3",
      "start_offset" : 13,
      "end_offset" : 16,
      "type" : "<ALPHANUM>",
      "position" : 3
    }
  ]
}

我们可以这样查

POST /products/_search
{
  "query": {
    "term": {
      "productID": {
        "value": "xhdk" 
      }
    }
  }
}

小写的xhdk这个可以匹配分词后的内容,所以可以查到结果,那我们如何精确匹配呢?

"XHDK-A-1293-#fJ3"这个进行了分词并且转小写的处理,我们拿着"XHDK-A-1293-#fJ3"这个原本的值去查肯定查不到,我们得通过keyword去查才行

POST /products/_search
{
  "query": {
    "term": {
      "productID.keyword": {
        "value": "XHDK-A-1293-#fJ3"
      }
    }
  }
}

Term查询不会做分词
如果想要完全匹配,可以采用es中的一个多字段属性,它默认会把text类型的字段增加一个keyword的字段,通过keyword字段就可以进行完全匹配了

1.3 复合查询-Constant Score转为Filter

Term查询还是会返回相应的算分结果的,那如果我们想跳过算分结果该如何做呢?

  • Query转为Filter,忽略TF-IDF计算,避免相关性算分的开销
  • Filter可以有效利用缓存

1.4 基于全文的查询

  • 基于全文的查找
    • Match Query / Match Phrase Query / Query String Query
  • 特点
    • 索引和搜索时都会进行分词,查询字符串先传递到一个合适的分词器,然后生成一个供查询的词项列表
    • 查询时候,先会对输入的查询进行分词,然后每个词项逐个进行底层的查询,最终结果进行合并。并为每个文档生成一个算分。例如查"Matrix reloaded",会查到包括Matrix或者reload的所有结果

image image image

1.5 Match Query查询过程

image

1.6 总结

  • 基于词项的查找 vs 基于全文的查找
  • 通过字段Mapping控制字段的分词
    • Text vs Keyword
  • 通过参数控制查询的 Precision & Recall
  • 复合查询 - Constant Score查询
    • 即便是对Keyword进行Term查询,同样会进行算分
    • 可以将查询转为Filtering,取消相关性算分的环节,以提升性能

2. 结构化搜索

2.1 结构化数据

image

2.2 ES中的结构化搜索

  • 布尔、时间、日期和数字这类结构化数据:有精确的格式,我们可以对这些格式进行逻辑操作。包括比较数字或时间的范围,或判定两个值的大小
  • 结构化的文本可以做精确匹配或者部分匹配
    • Term查询/Prefix前缀查询
  • 结构化结果只有"是"或"否"两个值
    • 根据场景需要,可以决定结构化搜索是否需要打分

2.3 例子

2.3.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":"XHDK-A-1293-#fJ3"}
{"index":{"_id":4}}
{"price":10,"avaliable":false,"productID":"XHDK-A-1293-#fJ3"}

GET products/_mapping

2.3.2 对布尔值 term 查询,有算分

# 对布尔值 term 查询,有算分
POST /products/_search
{
  "profile": "true",
  "query": {
    "term": {
      "avaliable": true
    }
  }
}

2.3.3 对布尔值 term 查询,通过constant score 转成 filtering,没有算分

# 对布尔值 term 查询,通过constant score 转成 filtering,没有算分
POST products/_search
{
  "profile": "true",
  "explain": true,
  "query": {
    "constant_score": {
      "filter": {
        "term": {
          "avaliable": true
        }
      }
    }
  }
}

2.3.4 数字Range查询

# 数字Range查询
GET products/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "range": {
          "price": {
            "gte": 20,
            "lte": 30
          }
        }
      }
    }
  }
}

2.3.4 日期range

# 日期range查询
GET products/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "range": {
          "date": {
            "gte": "now-1y"
          }
        }
      }
    }
  }
}

now-1y:表示的意思是now减去1年(now表示现在,y表示年,1y就表示1年),上面的意思是date大于一年前

字段字段描述
y
M
w
d
H/h小时
m分钟
s

2.3.5 Exists 查询,查询文档中不包含某个字段的文档

# Exists 查询,查询文档中不包含某个字段的文档
GET /products/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "exists": {
          "field": "date"
        }
      }
    }
  }
}

2.3.6 多值字段查询

POST /movies/_bulk
{"index":{"_id":1}}
{"title":"Father of the Bridge Part II","year":1995,"gener":"Comedy"}
{"index":{"_id":2}}
{"title":"Dave","year":1993,"gener":["Comedy","Romance"]}
2.3.6.1 处理多值字段,term查询是包含而不是等于
# 处理多值字段,term查询是包含而不是等于
GET /movies/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "term": {
          "gener.keyword": "Comedy"
        }
      }
    }
  }
}

返回了包含"Comedy"的所有文档,那如果我们想在多值字段中精确匹配呢(意思就是只包含这一个值)?我们该怎么做
解决方案:增加一个 genre_count字段进行计数。会在组合bool query给出解决方法

2.3.6.2 多值字段,term查询如果精确匹配(这里如果不懂,可以先跳过)
{ "tags" : ["search"], "tag_count" : 1 }
{ "tags" : ["search", "open_source"], "tag_count" : 2 }

GET /my_index/my_type/_search
{
    "query": {
        "constant_score" : {
            "filter" : {
                 "bool" : {
                    "must" : [
                        { "term" : { "tags" : "search" } }, 
                        { "term" : { "tag_count" : 1 } }
                    ]
                }
            }
        }
    }
}

2.3 总结

  • 结构化数据 & 结构化搜索
    • 如果不需要算分,可以通过 Constant Score,将查询转为Filtering
  • 范围查询和 Date Math
  • 使用Exit查询处理非空NULL
  • 精确值 & 多值字段的精确查找
    • Term查询是包含,不是完全相等。针对多值字段查询要尤其注意

3. 搜索的相关性打分

3.1 相关性和相关性算分

image

3.2 词频(TF)

image

3.3 逆文档频率 IDF

image

3.4 TF-IDF 的概念

image

3.5 Lucene 中的 TF-IDF 评分公式

image

3.6 BM25

image

3.7 定制相似度(Similarity)

image

3.8 通过Explain API 查看TF-IDF

image

3.9 Boosting Relevance

image

3.10 总结

  • 什么是相关性 & 相关性算分介绍
    • TF-IDF/BM25
  • 在Elasticsearch中定制相关度算法的参数
  • ES中可以对索引,字段分别设置Boosting参数

4. Query&Filtering与多字符串多字段查询

4.1 Query Context & Filter Context

我们看到很多的系统都支持多个字段的查询,搜索引擎一般也提供基于时间价格等过滤条件,那么ES也是支持的,下面就来介绍ES的高级查询;

  • ES高级搜索的功能:支持多项文本输入,针对多个字段进行搜索
  • ES中,有QueryFilter两种不同的Context(Context就是上下文,后面会介绍)
    • Query Context: 使用Query Context的查询,搜索结果会进行相关性算分
    • Filter Context: 使用Filter Context的查询,结果不会进行算分,从而可以利用缓存(Cache),获得更好的性能

4.2 条件组合

假设我们现在要完成如下查询:

  • 假设搜索电影评论中包含了Guitar,用户打分高于3分,同时上映日期要在1993与2000年之间;

这个搜索包含了3段逻辑,分别都是针对不同的字段,评论字段要包含Guitar,用户评分要大于3,上映日期需要在给定的范围,同时包含这三段逻辑,且要有一个好的性能,我们该如何做呢?

这就需要用到es中的复合查询:bool Query

4.3 bool 查询

  • 一个bool查询,是一个或者多个查询子句的组合
    • 总共包括4种子句。其中2种会影响算分,2种不影响算分;
  • 相关性并不只是全文本检索的专利。也适用于 yes|no 的子句,匹配的子句越多,相关性评分越高。如果多条查询子句被合并为一条复合查询语句,比如bool查询,则每个查询子句计算得出的评分会被合并到总的相关性评分种。
子句描述
must必须匹配。贡献算分
should选择性匹配。贡献算分
must_notFilter Context查询子句,必须不能匹配
filterFilter Context必须匹配,但是不贡献算分

4.3.1 bool查询语法

image

  • bool查询种子查询可以任意顺序出现
  • 可以嵌套多个查询
  • 如果你的bool查询种,没有must条件,should种必须至少满足一条查询

从这里我们回过头去看 2.3.6.2 小节,就很简单了

4.3.2 bool查询嵌套

image

  • 上面就实现了一个should_not的逻辑(虽然没有should_not,但是我们可以这样实现)

4.3.3 bool查询语句的结构,会对相关度算分产生影响

image

  • 同一级下的竞争字段,具有相同的权重;
  • 通过嵌套bool查询,可以改变对算分的影响;
4.3.3.1 控制字段Boosting
  • Boosting是控制相关度的一种手段
    • Boosting可以用在索引,字段或查询子条件种
  • 参数boost的含义
    • boost > 1 时,打分的相关度相对性提升;
    • 当 0 < boost < 1时,打分的权重相对性降低;
    • boost < 0 时,贡献负分;
  • 插入数据
# 控制字段`Boosting`
POST /blogs/_bulk
{"index":{"_id":1}}
{"title":"Apple iPad","content":"Apple iPad,Apple iPad"}
{"index":{"_id":2}}
{"title":"Apple iPad,Apple iPad","content":"Apple iPad"}
  • 查询1(title字段的boost值比较高)
POST blogs/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "title": {
              "query": "apple, ipad",
              "boost": 1.1
            }
          }
        },
        {
          "match": {
            "content": {
              "query": "apple, ipad",
              "boost": 1
            }
          }
        }
      ]
    }
  }
}

image

因为title字段的boost的值比较高所以它的权重就比较大,所以文档2显示在最前面,因为文档2种title的值含有两个apple ipad

  • 查询2(content字段的boost值比较高)
POST blogs/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "title": {
              "query": "apple, ipad",
              "boost": 1
            }
          }
        },
        {
          "match": {
            "content": {
              "query": "apple, ipad",
              "boost": 2
            }
          }
        }
      ]
    }
  }
}

image

4.4 总结

  • Query Context vs Filter Query
  • Bool Query:跟多的组合条件(这里类似sql种where后面跟多个条件)
  • 查询结构与相关性算分
  • 如何控制查询的精准度
    • Boosting & Boosting Query(这里的例子没有记录,视频种有)

5. 单字符串多字段查询:Dis Max Query

5.1 单字符串查询的实例

image

我们对上面的文档进行单字符串多字段查询,就是将Brown fox这个单个字符串拿到多个字段种去匹配,上面的例子种是在titlebody种匹配。

我们来分析一下文档中的内容:

  • title
    • 文档1中只出现了Brown
  • body
    • 文档1中出现了Brown
    • Brown fox在文档2中全部出现,并且保持和查询一致的顺序,目测相关性最高
# 单字符串查询的实例
POST /blogs/_bulk
{"index":{"_id":1}}
{"title":"Quick brown rabbits","content":"Brown rabbits are commonly seen"}
{"index":{"_id":2}}
{"title":"Keppping pets healthy","content":"My quick brown fox eats rabbits on a regular basis"}

# 查询1
POST /blogs/_search
{
  "query": {
    "bool": {
      "should": [
        {"match": {"title": "Brown fox"}},
        {"match": {"content": "Brown fox"}}
      ]
    }
  }
}

image

奇怪,我们明明分析文档2的相关度应该更高才是,为什么文档1却在前面,且比文档2的算分要高?

5.2 bool查询的should查询的算分过程

  • 查询should语句中的两个查询
  • 加和两个查询的评分
  • 乘以匹配语句的总数
  • 除以所有语句的总数

分析:文档1中的titlecontent都包含了我们查询的关键字所以对于should的两个子查询都会匹配到,文档2虽然精准包含了查询的关键字,但是它只出现在content中并没有出现在title中,should查询的子查询只有一个能匹配到,所以文档1的打分就比文档2要高,这就是原因。

5.3 Disjunction Max Query查询

  • 上例中,titlecontent相互竞争
    • 不应该将分数简单叠加,而是应该找到单个最佳匹配的字段的评分
  • Disjunction Max Query
    • 将任何与任一查询匹配的文档作为结果返回。采用字段上最匹配的评分最终评分返回
POST /blogs/_search
{
  "query": {
    "dis_max": {
      "queries": [
        {"match": {"title": "Quick fox"}},
        {"match": {"content": "Quick fox"}}
      ]
    }
  }
}

image

当我们使用Disjunction Max Query来查询时,因为Disjunction Max Query采用字段上最匹配的评分最终评分返回,倘若两个文档都是不完全匹配的话,那么他们的算分也是一样的,这中情况该怎么处理呢?

POST /blogs/_bulk
{"index":{"_id":1}}
{"title":"Quick brown rabbits","content":"Brown rabbits are commonly seen"}
{"index":{"_id":2}}
{"title":"Keppping pets healthy","content":"My quick brown fox eats rabbits on a regular basis"}

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

上述两个文档中都没有完全匹配关键字Quick pets,那么他们的算分按理应该也是一样的,我们来看看 image

5.3.1 Tie Breaker参数

  • Tie Breaker是一个介于 0-1 之间的浮点数。0代表使用最佳匹配;1代表所有语句同等重要;
  • Disjunction Max Query会获得最佳匹配语句的评分_score
  • 将其它匹配语句的评分与Tie Breaker相乘
  • 对以上评分求和并规范化
POST /blogs/_search
{
  "query": {
    "dis_max": {
      "queries": [
        {"match": {"title": "Quick pets"}},
        {"match": {"content": "Quick pets"}}
      ],
      "tie_breaker": 0.1
    }
  }
}

image

6. 单字符串多字段查询:Multi Match

6.1 单字符串多字段查询的三种场景

  • 最佳字段(Best Fields)
    • 当字段之间相互竞争,又相互关联。例如titlebody这样的字段(上一节有提到这个)。评分来自最匹配字段
  • 多数字段(Most Fields)
    • 处理英文内容时:一种常见的手段是,在主字段(Engish Analyzer),抽取词干,加入同义词,以匹配更多的文档。相同的文本,加入子字段(Standard Analyzer),以提供更加精确的匹配。其他字段作为匹配文档提高相关度的信号。匹配字段越多越好
  • 混合字段(Cross Field)
    • 对于某些实体,例如人名,地址,图书信息。需要在多个字段中确定信息,单个字段只能作为整体的一部分。希望在任何这些列出的字段中找到尽可能多的词

6.2 Multi Match Query语法格式

image

  • Best Fields是默认类型,可以不用指定
  • Minimum should match等参数可以传递到生成的query

6.3 Multi Match中的most field案例

6.3.1 定义索引,插入数据

DELETE title
PUT /titles
{
  "mappings": {
    "properties": {
      "title":{
        "type": "text",
        "analyzer": "english"
      }
    }
  }
}

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

6.3.2 采用普通的match查询

GET titles/_search
{
  "query": {
    "match": {
      "title": "barking dogs"
    }
  }
}

image

我们分析了文档内容,显然发现第二个文档的相关性更高,但是采用普通的match查询,我们发现第一个文档排在前面,这是为什么呢?因为我们在设置mapping时采用英文分词器,而且第一篇文档的长度短(这里可以百度下),所以第一个文档排在了前面,对于这种情况,我们得做一些优化。

6.3.3 重新定义Mapping,并插入数据

DELETE titles
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"}
  1. 分析我们的mapping定义
  2. 加入了子字段std,并且子字段类型是text,且采用standard分词器
  3. 采用english分词器,会按照英文语法分词,而采用standard分词器,不会针对英文语法分词,这样可以保证数据的精度

6.3.4 采用Multi Query查询

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

image

6.3.5 Multi Query字段权重

  • 用广度匹配字段title包括尽可能多的文档——以提升召回率——同时又使用字段title.std作为信号,将相关度更高的文档置于结果顶部
  • 每个字段对于最终评分的贡献可以通过自定义值boost来控制,比如,使title字段更为重要,这样同时也降低了其他信号字段的作用
GET /titles/_search
{
  "query": {
    "multi_match": {
      "query": "barking dogs",
      "type": "most_fields", 
      "fields": ["title^10","title.std"]
    }
  }
}

6.4 Multi Match中的cross field(跨字段搜索)案例

image

  1. 当我们要在多个字段中查询的时候,我们可能会想到使用most fields来实现
  2. 没错,most fields可以一定程度上满足我们的需求,但是有些特殊情况它是不能满足的,比如:我们想要查询的数据同时出现在所有的字段中,most fields就不能满足,我们使用"operator":"and"也不能满足(这里我也有些傻傻分不清楚,看下后面的例子吧,这个要想深入理解的话得有具体的场景分析,可以自行百度),我们可以使用copy_to的方式来解决(之前提到过),但是需要额外的存储空间;

image

这个时候我们可以使用cross_fields来实现

6.4.1 插入数据

# cross_fields 案例
PUT address/_doc/1
{
  "street": "5 Poland Street",
  "city" : "London",
  "country": "United Kingdom",
  "postcode": "W1V 3DG"
}

6.4.2 使用most_fields来查询

POST address/_search
{
  "query": {
    "multi_match": {
      "query": "Poland Street W1V",
      "type": "most_fields",
      "fields": ["street","city","country","postcode"]
    }
  }
}

可以满足我们的需求 image

如果我们想所有的字段都出现查询的结果,我们可以使用"operator": "and"most_fields

POST address/_search
{
  "query": {
    "multi_match": {
      "query": "Poland Street W1V",
      "type": "most_fields",
      "operator": "and", 
      "fields": ["street","city","country","postcode"]
    }
  }
}

image

但是,如果我们想期望查询文本中所有的词都在文档中出现,而又不介意在文档中哪些字段中出现,我们可以使用corss_fields+and

POST address/_search
{
  "query": {
    "multi_match": {
      "query": "Poland Street W1V",
      "type": "cross_fields",
      "operator": "and", 
      "fields": ["street","city","country","postcode"]
    }
  }
}

image

6.5 区分按字段为中心的查询、词条为中心的查询

www.cnblogs.com/jiangtao121…

  1. best_fields
    • 适用于多字段查询且查询相同文本;
    • 得分取其中一个字段的最高分。
    • 可通过tie_breaker(取值0~1)将低得分字段的分数引入的最终得分中。
    • best_fields可与dis_max查询互换。ES内部转换为dis_max查询operator(此查询中慎用)
    • minimum_should_match 作用于每个字段的子查询内部中。
例如:

"query":"complete conan doyle"

"field":["title","author","characters"]

"type":"best_fields"

"operator":"and"

等价于:

(+title:complete +title:conan +title:doyle) | (+autorh:complete +author:conan +autore:doyle) | (+characters:complete +characters:conan +characters:doyle)
  1. corss_fields
    • 适用于期望查询文本中所有的词都在文档中出现,而又不介意在文档中哪些字段中出现。
    • operator作用于子查询与子查询之间的连接中
    • 应用场景:信息被索引时分割到不同字段中,如住址,姓、名。多数情况下opertaotr使用and
上述查询等价于:

+(title:complete author:complete charactors:complete) +(title:conan author:conan charators:conan) +(title:doyle author:doyle charactor:doyle)
  1. most_fields
    • 适用于检索多处包含相同文本,但是恩本分析处理方式不同的文档。
    • 多数情况下operator使用or,ES内部转化为bool查询
    • 应用场景:多语言处理

7. Search TemplateIndex Alias查询

7.1 Search Template:解耦程序和搜索DSL

image

  • 将查询参数化,这样大家就可以各司其职了,你写你的业务逻辑,我优化我的DSL

7.2 Index Alias实现零停机运维

image

  • 我们可以为索引创建别名
  • 我们每天会创建一些新的索引,但是读写的时候,我们是希望他们从一个Index里面读出来,这样我们就可以用到别名了