一、引言
1.1 数据库查询为什么还要ElasticSearch?
数据库一般只适合保存搜索结构化的数据,对于非结构化的数据( 比如文章内容),只能通过like%%模糊查询,但是在大量的数据面前,like%%有两个弊端:
1)搜索效率会很差,因为是做一个全表扫描(like%%会让索引失效)
2)搜索没办法通过相关度匹配排序(可能返回的是用户不关心的结果)
ElasticSearch就可以解决这些问题
1.2 什么是全文检索?
全文检索 将非结构化数据中的一部分信息提取出来,重新组织,使其变得具有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。这部分从非结构化数据中提取出的然后重新组织的信息,我们称之索引。
例如字典的拼音表和部首检字表就相当于字典的索引,通过查找拼音表或者部首检字表就可以快速的查找到我们要查的字。
这种先建立索引,再对索引进行搜索的过程就叫全文检索(Full-text Search)。
1.3 全文检索的流程
1.4 构建索引的过程
索引过程,对要搜索的原始内容进行索引构建一个索引库,索引过程包括:
获得文档→创建文档→分析文档→索引文档。
1.4.1 获得原始文档
原始文档是指要索引和搜索的内容。原始内容包括互联网上的网页、数据库中的数据、磁盘上的文件等。
1.4.2 创建文档对象(Document)
获取原始文档的目的是为了索引,在索引前需要将原始内容创建成文档(Document),文档中包括一个一个的域(Field),域中存储内容。
1.4.3 分析文档(分词)
将原始内容创建为包含域(Field)的文档(document),需要再对域中的内容进行分析,分析的过程是经过对原始文档提取单词、将字母转为小写、去除标点符号、去除停用词等过程生成最终的语汇单元。
1.4.4 创建索引
创建索引是对语汇单元索引,通过词语找文档,这种索引的结构叫倒排索引结构。
1.5 倒排索引
1.5.1 正向索引
简单来说,正向索引就是根据文件ID找到该文件的内容,在文件内容中匹配搜索关键字,这种方法是顺序扫描方法,数据量大、搜索慢。
1.5.2 反向(倒排)索引
倒排索引和正向索引刚好相反,是根据内容(词语)找文档
二、ElasticSearch
2.1 ElasticSearch简介
ElasticSearch基本概念
2.1.1 索引库(index)
索引库是ElasticSearch存放数据的地方,可以理解为关系型数据库中的一个数据库。事实上,我们的数据被存储和索引在分片(shards)中,索引只是一个把一个或多个分片分组在一起的逻辑空间。
2.1.2 映射类型(type)
映射类型用于区分同一个索引下不同的数据类型,相当于关系型数据库中的表。
注意:在 6.0 的index下是无法创建多个type,并且会在 7.0 中完全移除。
2.1.3 文档(documents)
文档是ElasticSearch中存储的实体,类比关系型数据库,每个文档相当于数据库表中的一行数据。
2.1.4 字段(fields)
文档由字段组成,相当于关系数据库中列的属性。
2.1.5 分片与副本
如果一个索引具有很大的数据量,它的数据量可能会超出单个节点的容量限制(硬盘容量),而且单个节点数据量过大,执行性能也会随之下降,每个搜索请求的执行效率都会降低。 为了解决上述问题, Elasticsearch 提出了分片的概念,索引将划分成多份,称为分片。每个分片都可以创建对应的副本,以便保证服务的高可用性。
2.2 ElasticSearch的安装
1)准备ElasticSearch的docker-compose.yml文件
version: '3.1'
services:
elasticsearch:
image: elasticsearch:6.8.5
restart: always
container_name: elasticsearch
ports:
- 9200:9200
- 9300:9300
environment:
discovery.type: single-node
volumes:
- ./es/data:/usr/share/elasticsearch/data:rw
- ./es/logs:/usr/share/elasticsearch/logs:rw
- ./es/plugins:/usr/share/elasticsearch/plugins
- config:/usr/share/elasticsearch/config
kibana:
image: kibana:6.8.5
container_name: kibana
restart: always
environment:
SERVER_NAME: kibana
ELASTICSEARCH_URL: http://193.168.195.135:9200
ports:
- 5601:5601
volumes:
config:
注意:第23行必须写elasticsearch所在机器的ip地址,不能写127.0.0.1
2)执行docker-compose up -d 命令启动容器
docker-compose up -d
注意:第一次创建容器会失败,需要给.es文件夹赋予权限,执行chmod 777 -R ./es命令,然后重启容器
3)安装中文分词器
进入elasticsearch容器,执行中文分词器相关安装命令
docker exec -it elasticsearch bash
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.8.5/elasticsearch-analysis-ik-6.8.5.zip
注意:可以访问
https://github.com/medcl/elasticsearch-analysis-ik/releases
查找和当前es匹配的版本
4)手动安装中文分词器
如果第3步安装超时失败,可以尝试进行手动安装,如果第三步成功则跳过该步骤
4.1)手动下载对应的IK分词器版本https://github.com/medcl/elasticsearch-analysis-ik/releases
4.2)直接将分词器压缩包上传到容器的plugins路径(上传到宿主机的容器卷路径即可)
4.3)解压分词器
unzip elasticsearch-analysis-ik-6.8.5.zip -d ./ik-analyze
5)重启容器
docker-compose restart
6)访问kibana服务
7)测试IK分词器
POST _analyze
{
"analyzer":"ik_smart",
"text":"歼10系列战斗机"
}
POST _analyze
{
"analyzer":"ik_max_word",
"text":"歼10系列战斗机"
}
2.3 索引库(Index)相关操作
2.3.1 概述
ElasticSearch采用Rest风格的API,因此其API就是一次Http请求
请求分为: PUT POST GET DELETE
GET:查询数据
PUT:插入数据
POST:更新数据,实际上很多情况下 es 不是很清晰你到底要作什么,有些时候POST也可用于新增或者查询
DELETE: 删除数据
2.3.2 新增索引库语法
PUT /索引库名称
{
"settings":{
"number_of_shards": 3, #分片的数量
"number_of_replicas": 2 #副本的数量
}
}
settings:表示索引库的设置 number_of_shards:表示分片的数量 number_of_replicas:副本数量
2.3.3 查询索引信息
GET /索引库名称
2.3.4 判断索引库是否存在
HEAD /索引库名称
2.3.5 删除索引库
DELETE /索引库名称
注意
1)索引库 类似于 MySQL中数据库的概念
2)如果创建索引不指定settings,默认会有5个分片,1个副本
2.4 映射类型(type)相关操作
2.4.1 新增映射类型语法
PUT /索引库名/_mapping/类型名称
{
"properties": {
"字段名1": {
"type": "类型",
"index": true,
"store": true,
"analyzer": "分词器"
},
"字段名2": {
"type": "类型",
"index": true,
"store": true,
"analyzer": "分词器"
}
}
}
type:类型,可以是text、long、date、integer、object、keyword(表示关键字,不能被分词)
index:是否参与索引,默认为true
store:是否参与存储,默认为false
analyzer:分词器,可选 “ik_max_word”或者“ik_smart”,表示使用ik分词器
2.4.2 查看映射类型信息
GET /索引库名称/_mapping/类型名称
2.4.3 字段属性详解
type
String类型,又分两种: text:可分词,不可参与聚合 keyword:不可分词,数据会作为完整字段进行匹配,可以参与聚合
Numerical:数值类型,分两类 基本数据类型:long、interger、short、byte、double、flfloat、half_flfloat 浮点数的高精度类型:scaled_float,需要指定一个精度因子,比如10或100,elasticsearch会把真实值乘以这个因子后存储,取出时再还原
Date:日期类型 elasticsearch可以对日期格式化为字符串存储,但是建议我们存储为毫秒值,存储为long,节省空间
boolean: 设置字段类型为boolean后,可以填入的值为:true、false、"true"、"false"
binary: binary类型接受base64编码的字符串
geo_point: 地理点类型用于存储地理位置的经纬度对
更多类型参考:https://www.elastic.co/guide/en/elasticsearch/reference/6.5/mapping-types.html
index
index影响字段的索引情况。
true:字段会被索引,则可以用来进行搜索。默认值就是true
false:字段不会被索引,不能用来搜索
index的默认值就是true,也就是说你不进行任何配置,所有字段都会被索引。但是有些字段是我们不希望被索引的,比如商品的图片信息,就需要手动设置index为false。
store
是否将数据进行额外存储。在学习lucene和solr时,我们知道如果一个字段的store设置为false,那么在文档列表中就不会有这个字段的值,用户的搜索结果中不会显示出来。
但是在Elasticsearch中,即便store设置为false,也可以搜索到结果。
原因是Elasticsearch在创建文档索引时,会将文档中的原始数据备份,保存到一个叫做 _source 的属性中。而且我们可以通过过滤 _source 来选择哪些要显示,哪些不显示。而如果设置store为true,就会在 _source 以外额外存储一份数据,多余,因此一般我们都会将store设置为false,事实上,store的默认值就是false。
analyzer
定义的是该字段的分析器,默认的分析器是 standard 标准分析器,这个地方可定义为自定义的分析器。
比如IK分词器为: ik_max_word 或者 ik_smart
boost
激励因子。这个与lucene中一样,我们可以通过指定一个boost值来控制每个查询子句的相对权重。
该值默认为1。一个大于1的boost会增加该查询子句的相对权重
比如:
GET /_search {
"query": {
"bool": {
"must": {
"match": {
"content": {
"query": "full text search",
"operator": "and"
}
}
},
"should": [{
"match": {
"content": {
"query": "Elasticsearch",
"boost": 3
}
}
},
{
"match": {
"content": {
"query": "Lucene",
"boost": 2
}
}
}
]
}
}
}
注意
1)映射类型(type) 类似于 MySQL数据库中表的概念
2)从ElasticSearch 6.x之后,一个Index下只能有一个type
2.5 文档相关(document)操作
2.5.1 添加文档
#指定id的添加方式
PUT /索引库名/类型名称/id #id需要自己指定
{
"field1":"value1",
"field2":"value2",
...
}
#自动生成id的添加方式
POST /索引库名/类型名称 #使用POST无需指定id
{
"field1":"value1",
"field2":"value2",
...
}
#批量添加文档
PUT /索引库名称/类型名称/_bulk
{"index":{"_id":id值1}}
{"field1":"value1", "field2":"value2"...}
{"index":{"_id":id值2}}
{"field1":"value1", "field2":"value2"...}
....
2.5.2 更新文档
#全局更新,会将所有字段更新,没有指定的字段会自动删除
PUT /索引库名/类型名称/id #需要更新的id,id必须存在,如果不存在就变成了添加
{
"field1":"value1",
"field2":"value2",
...
}
#局部更新,只更新需要更新的字段
POST /索引库名/类型名称/id/_update
{
"doc":{
"field1": "新的value"
}
}
2.5.3 删除文档
DELETE /索引库名/类型名称/id
2.5.4 查询文档
#查询索引库全部数据
GET /索引库名称/_search
#根据id查询
GET /索引库名称/类型名称/id
#批量查询
GET /_mget
{
"docs": [
{
"_index": "索引库名称1",
"_type": "映射类型1",
"_id":"查询文档id1"
},
{
"_index": "索引库名称2",
"_type": "映射类型2",
"_id":"查询文档id2"
}
]
}
注意
1)文档(document)类似于 数据库中表的一条记录
2)当添加的文档中,设置的field,而type中没有时,type会自动的添加该field的映射记录,
这是elasticsearch的自动映射功能
三、SpringBoot操作ElasticSearch(elasticsearch-rest-high-level-client)
3.1 配置ElasticSearch
1)添加依赖
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>6.8.5</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>6.8.5</version>
</dependency>
2)配置application.yml
spring:
elasticsearch:
rest:
uris: http://192.168.195.135:9200
3)在需要的地方注入RestHighLevelClient对象
@Autowired
private RestHighLevelClient restHighLevelClient;
3.2 使用SpringBoot操作索引库(Index)
@Autowired
private RestHighLevelClient client;
/**
* 创建索引
* @param indexName
* @return
*/
@Override
public boolean createIndex(String indexName) {
CreateIndexRequest indexRequest = new CreateIndexRequest(indexName);
//设置索引库的相关属性
Settings settings = Settings.builder()
.put("number_of_shards", 1)//设置分片数量
.put("number_of_replicas", 0)//设置副本数量
.build();
indexRequest.settings(settings);
try {
CreateIndexResponse response = client.indices().create(indexRequest, RequestOptions.DEFAULT);
//返回结果
return response.isAcknowledged();
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
/**
* 判断索引是否存在
* @param indexName
* @return
*/
@Override
public boolean isExistsIndex(String indexName) {
GetIndexRequest getIndexRequest = new GetIndexRequest(indexName);
try {
boolean exists = client.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
return exists;
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
/**
* 删除索引
* @param indexName
* @return
*/
@Override
public boolean deleteIndex(String indexName) {
DeleteIndexRequest request = new DeleteIndexRequest(indexName);
try {
AcknowledgedResponse response = client.indices().delete(request, RequestOptions.DEFAULT);
return response.isAcknowledged();
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
3.3 使用SpringBoot操作映射类型(type)
/**
* 添加映射
* PUT /partform_hotal/_mapping/hotal
* {
* "properties": {
* "hotalName":{
* "type": "text",
* "analyzer": "ik_max_word"
* },
* "hotalImage":{
* "type": "keyword",
* "index": false
* },
* "type":{
* "type": "integer"
* },
* "hotalInfo":{
* "type":"text",
* "analyzer": "ik_max_word"
* },
* "keyword":{
* "type":"text",
* "analyzer": "ik_max_word"
* },
* "location":{
* "type": "geo_point"
* },
* "star":{
* "type": "integer"
* },
* "brand":{
* "type": "text",
* "analyzer": "ik_max_word"
* },
* "address":{
* "type": "keyword"
* },
* "openTime":{
* "type": "date",
* "format": "yyyy-MM-dd"
* },
* "cityname":{
* "type": "keyword"
* },
* "regid":{
* "type": "text",
* "analyzer": "ik_max_word"
* }
* }
* }
*
*
* @return
*/
@Override
public boolean createMapping(String index) {
PutMappingRequest putMappingRequest = new PutMappingRequest(index);
try {
XContentBuilder builder = JsonXContent.contentBuilder();
builder
.startObject()
.startObject("properties")
.startObject("hotalName")
.field("type", "text")
.field("analyzer", "ik_max_word")
.endObject()
.startObject("hotalImage")
.field("type", "keyword")
.field("index", "false")
.endObject()
.startObject("type")
.field("type", "integer")
.endObject()
.startObject("hotalInfo")
.field("type", "text")
.field("analyzer", "ik_max_word")
.endObject()
.startObject("keyword")
.field("type", "text")
.field("analyzer", "ik_max_word")
.endObject()
.startObject("location")
.field("type", "geo_point")
.endObject()
.startObject("star")
.field("type", "integer")
.endObject()
.startObject("brand")
.field("type", "text")
.field("analyzer", "ik_max_word")
.endObject()
.startObject("address")
.field("type", "text")
.field("analyzer", "ik_max_word")
.endObject()
.startObject("openTime")
.field("type", "date")
.field("format", "yyyy-MM-dd")
.endObject()
.startObject("cityname")
.field("type", "keyword")
.endObject()
.startObject("regid")
.field("type", "text")
.field("analyzer", "ik_max_word")
.endObject()
.endObject().endObject();
//设置到Request对象中
putMappingRequest.source(builder);
client.indices().putMapping(putMappingRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
3.4 使用SpringBoot操作文档(document)
新增文档
/**
* 给索引库添加文档
* @param indexName
* @param hotal
* @return
*/
@Override
public boolean insertDco(String indexName, Hotal hotal) {
String json = JSON.toJSONString(hotal);
System.out.println(json);
IndexRequest indexRequest = new IndexRequest(indexName, "_doc")
.id(hotal.getId() + "")
.source(json, XContentType.JSON);
try {
IndexResponse index = client.index(indexRequest, RequestOptions.DEFAULT);
long seqNo = index.getSeqNo();
String lowercase = index.getResult().getLowercase();
int status = index.status().getStatus();
System.out.println("状态:" + status);
System.out.println("返回:" + lowercase);
System.out.println("序号:" + seqNo);
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
删除文档
/**
* 根据ID删除
* @param indexName
* @param id
* @return
*/
@Override
public boolean deleteDco(String indexName, Integer id) {
DeleteRequest deleteRequest = new DeleteRequest(indexName, "_doc", id + "");
try {
DeleteResponse resp = client.delete(deleteRequest, RequestOptions.DEFAULT);
int status = resp.status().getStatus();
System.out.println("结果:" + status);
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
更新文档
/**
* 根据id修改信息
* @param indexName
* @param hotal
* @return
*/
@Override
public boolean updateDco(String indexName, Hotal hotal) {
String json = JSON.toJSONString(hotal);
System.out.println(json);
// Map map = new HashMap();
// map.put("hotalInfo", "xxxx");
UpdateRequest updateRequest = new UpdateRequest(indexName, "_doc", hotal.getId() + "");
updateRequest.doc(json, XContentType.JSON);
try {
UpdateResponse response = client.update(updateRequest, RequestOptions.DEFAULT);
int status = response.status().getStatus();
System.out.println("状态:" + status);
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
四、基本查询
4.1 term、terms查询
什么是term查询?
term是代表完全匹配,也就是精确查询,搜索前不会再对搜索词进行分词,所以我们的搜索词必须是文档分词集合中的一个。 比如文档内容为:"美的微波炉",被分词为"美的"和"微波炉",term搜索的关键字必须为"美的"或者"微波炉"才能搜索出这个文档,搜索"美的微波炉"搜索不出来
语法
#term查询
GET /索引库/映射类型/_search
{
"query": {
"term": {
"字段名称": {
"value": "搜索关键字"
}
}
}
}
#terms查询 - 可以同时查询多个关键词
GET /索引库/映射类型/_search
{
"query":{
"terms": {
"字段名称": [
"关键字1","关键字2"...
]
}
}
}
注意:terms查询 多个关键字之间是或者的关系,也就是说只要符合一个关键字的文档就会被查询出来
4.2 match查询
什么是match查询?
match 查询是高层查询,它们了解字段映射的信息:
1.如果查询 日期(date) 或 整数(integer) 字段,它们会将查询字符串分别作为日期或整数对待。
2.如果查询一个( not_analyzed )未分词的精确值字符串字段, 它们会将整个查询字符串作为单个词项对待。
3.但如果要查询一个( analyzed )已分析的全文字段, 它们会先将查询字符串传递到一个合适的分析器,然后生成一个供查询的词项列表。 一旦组成了词项列表,这个查询会对每个词项逐一执行底层的查询,再将结果合并,然后为每个文档生成一个最终的相关度评分。 match查询其实底层是多个term查询,最后将term的结果合并。
语法
#match_all查询 - 查询指定库的指定类型的所有文档
GET /索引库/映射类型/_search
{
"query": {
"match_all": {}
}
}
#match查询 - 根据关键字查询
GET /索引库/映射类型/_search
{
"query": {
"match": {
"字段名称": "搜索关键字"
}
}
}
#布尔match查询
GET /索引库/映射类型/_search
{
"query": {
"match": {
"字段名称": {
"query": "搜索关键字",
"operator": "OR或者AND"
}
}
}
}
注意:operator值为
and表示关键词分词后的结果,必须全部匹配上
or表示需要一个分词匹配上即可,默认为or
#mulit_match查询 - 可以查询多个字段
GET /索引库/映射类型/_search
{
"query": {
"multi_match": {
"query": "搜索关键字",
"fields": ["字段名称1^2.0", "字段名称2^0.5"],
"operator": "or"
}
}
}
注意:
^2.0表示这个字段在搜索中的权重,值越高权重越大,可以不设置。
#match_phrase查询 - 短语查询
GET /索引库/映射类型/_search
{
"query": {
"match_phrase": {
"字段名称": "关键词1 关键词2"
}
}
}
注意:
match_phrase查询,只会匹配关键词1 和关键词2 挨在一起的文档,如果两个关键词分开太远的文档是不会匹配上的
4.3 ids查询
什么ids查询?
ids查询是一类简单的查询,它过滤返回的文档只包含其中指定标识符的文档,
该查询默认指定作用在“_id”上面。
语法
GET /索引库/映射类型/_search
{
"query": {
"ids": {
"values": ["1","3","6"...]
}
}
}
4.4 prefix前缀查询
什么是prefix查询?
前缀查询,可以使我们找到某个字段以给定前缀开头的文档。最典型的使用场景,一般是在文本框录入的时候的联想功能
语法
GET /索引库/映射类型/_search
{
"query": {
"prefix": {
"字段名称": {
"value": "前缀"
}
}
}
}
注意:前缀查询并不是和搜索字段的内容前缀匹配,而是和搜索字段的所有分词的前缀匹配,匹配上一个分词后,就会查询出该文档,建议和keyword类型的字段结合使用
4.5 fuzzy查询
什么是fuzzy查询?
fuzzy(模糊)查询是一种模糊查询,term 查询的模糊等价。
语法
GET /索引库/映射类型/_search
{
"query": {
"fuzzy": {
"字段名称": {
"value": "关键词",
"fuzziness": "2"
}
}
}
}
注意:
1、fuzzy搜索以后,会自动尝试将你的搜索文本进行纠错,然后去跟文本进行匹配
2、fuzziness属性表示关键词最多纠正的次数, 比如空条 -> 空调,需要纠正一次,fuzziness需要设置为1
3、prefix_length属性表示不能被 “模糊化” 的初始字符数。 大部分的拼写错误发生在词的结尾,而不是词的开始。 例如通过将prefix_length 设置为 3 ,你可能够显著降低匹配的词项数量。(前面3个字不能出错,否则查不到)
4.6 wildcard查询
什么是wildcard查询?
wildcard(通配符)查询意为通配符查询
GET /索引库/映射类型/_search
{
"query": {
"wildcard": {
"字段名称": {
"value": "关键词? *"
}
}
}
}
注意:
*表示匹配0或者多个字符
?表示匹配一个字符
wildcard查询不注意查询性能,应尽可能避免使用。
4.7 range查询
什么range查询?
range查询既范围查询,可以对某个字段进行范围匹配
GET /索引库/映射类型/_search
{
"query": {
"range": {
"字段名称": {
"gte": 0,
"lte": 2000,
}
}
}
}
4.8 regexp查询
什么是regexp查询?
正则表达式查询,wildcard和regexp查询的工作方式和prefix查询完全一样。它们也需要遍历倒排索引中的词条列表来找到所有的匹配词条,然后逐个词条地收集对应的文档ID。它们和prefix查询的唯一区别在于它们能够支持更加复杂的模式。
语法
GET /索引库/映射类型/_search
{
"query": {
"regexp": {
"字段名称": "正则表达式"
}
}
}
注意:
1、prefix(前缀),wildcard(通配符)以及regexp(正则)查询基于词条进行操作。如果你在一个analyzed字段上使用了它们,它们会检查字段中的每个词条,而不是整个字段。
2、对一个含有很多不同词条的字段运行这类查询是非常消耗资源的。应该避免使用一个以通配符开头的模式(比如,*foo)
4.9 使用JavaAPI实现以上查询
查询的基础结构,通过不同的QueryBuilder对象,可以实现不同的查询
@Override
public List<Hotal> queryHotals(String indexName, QueryBuilder queryBuilder) {
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(queryBuilder);
SearchRequest searchRequest = new SearchRequest(indexName);
searchRequest.source(searchSourceBuilder);
try {
SearchResponse search = client.search(searchRequest, RequestOptions.DEFAULT);
SearchHits hits = search.getHits();
//循环结果
for (SearchHit hit : hits) {
System.out.println("------------------------------------------");
Map<String, DocumentField> fields = hit.getFields();
for (Map.Entry<String, DocumentField> entry : fields.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue().getValue());
}
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
//term
TermQueryBuilder termQueryBuilder =
QueryBuilders.termQuery("hotalName", "连锁");
//terms
TermsQueryBuilder termsQueryBuilder =
QueryBuilders.termsQuery("hotalName", "7天", "连锁");
//match
MatchQueryBuilder matchQueryBuilder =
QueryBuilders.matchQuery("hotalName", "爱丽丝");
//match查询
MultiMatchQueryBuilder matchBuilder = QueryBuilders.multiMatchQuery("大中华")
.field("hotalName", 1.0f)
.field("hotalInfo", 2.0f);
//matchall
MatchAllQueryBuilder matchAllQueryBuilder =
QueryBuilders.matchAllQuery();
//Ids
IdsQueryBuilder idsQueryBuilder =
QueryBuilders.idsQuery().addIds("2","3");
//prefix
PrefixQueryBuilder prefixQueryBuilder =
QueryBuilders.prefixQuery("hotalName", "连");
//fuzzy
FuzzyQueryBuilder fuzzyQueryBuilder = QueryBuilders.fuzzyQuery("regid", "平三区")
.fuzziness(Fuzziness.TWO)
.prefixLength(0);
//wildcard
WildcardQueryBuilder wildcardQueryBuilder =
QueryBuilders.wildcardQuery("hotalName", "爱丽*");
//range
RangeQueryBuilder rangeQuery =
QueryBuilders.rangeQuery("star").gte(0).lt(3);
//regexp
RegexpQueryBuilder regexQuery =
QueryBuilders.regexpQuery("hotalName", "\\S{0,}[0-9]{1}.*");
五、复合查询
5.1 bool查询
bool 过滤器。 这是个 复合过滤器(compound fifilter) ,它可以接受多个其他过滤器作为参数,并将这些过滤器结合成各式各样的布尔(逻辑)组合。
语法
GET /索引库/映射类型/_search
{
"query": {
"bool": {
"should": [
{
"term": {
"content": {
"value": "性价比"
}
}
},{
"match": {
"title": "微波炉"
}
}
],
"must": [
{
"match": {
"content": "格力造"
}
}
],
"must_not": [
{
"range": {
"price": {
"gte": 300,
"lte": 3000
}
}
}
],
"filter": {
"match": {
"title": "美的"
}
}
}
}
}
例子
#bool查询 - 将多个基本查询组合在一起
GET /hotal_index/_search
{
"query": {
"bool": {
"should": [
{
"term": {
"brand": {
"value": "7天"
}
}
}
],
"must": [
{
"match": {
"hotalName": "连锁"
}
},{
"range": {
"price": {
"gte": 500,
"lte": 3000
}
}
}
],
##minimum_should_match:1
"filter": {
"multi_match": {
"query": "大中华",
"fields": ["hotalName^2.0", "hotalInfo"]
}
}
}
}
}
属性含义
must: 返回的文档必须满足must子句的条件,并且参与计算分值,与 AND 等价
must_not:所有的语句都 不能(must not) 匹配,与 NOT 等价
should: 返回的文档可能满足should子句的条件。在一个Bool查询中,如果没有must或者filter,有一个或 者多个should子句,那么只要满足一个就可以返回,与 OR 等价
minimum_should_match:用来指定should至少需要匹配几个语句
filter:返回的文档必须满足filter子句的条件。但是不会像Must一样,参与计算分值
注意
如果查询中没有must语句,那么至少要匹配一个should语句
5.1.1 什么是filter?
filter vs query
filter ,仅仅只是按照搜索条件过滤出需要的数据而已,不计算任何相关度分数,对相关度没有任何影响;
query ,会去计算每个document相对于搜索条件的相关度,并按照相关度进行排序;
一般来说,如果你是在进行搜索,需要将最匹配搜索条件的数据先返回,那么用query;如果你只是要根据一些条件筛选出一部分数据,不关注其排序,那么用filter; 除非是你的这些搜索条件,你希望越符合这些搜索条件的document越排在前面返回,那么这些搜索条件要放在query中;如果你不希望一些搜索条件来影响你的document排序,那么就放在filter中即可;
filter和query的性能对比
filter ,不需要计算相关度分数,不需要按照相关度分数进行排序,同时还有内置的自动cache最常使用filter的数据
query ,相反,要计算相关度分数,按照分数进行排序,而且无法cache结果
所以filter查询性能会高于query
5.3 boosting查询
什么是boosting查询?
该查询用于将两个查询封装在一起,并降低其中一个查询所返回文档的分值。它接受一个positive查询和一个negative查询。只有匹配了positive查询的文档才会被包含到结果集中,但是同时匹配了negative查询的文档会被降低其相关度,通过将文档原本的score和negative_boost参数进行相乘来得到新的score。因此,negative_boost参数必须小于1.0
(positive作为查询条件,如果查询条件同时符合negative条件,则会降低分值)
"negative_boost": 会把原来的分值乘以negative_boost的值作为最后的分值
如果negative_boost>1:提高分值
如果negative_boost<1:降低分值
运用场景
例如,在互联网上搜索"苹果"也许会返回,水果或者各种食谱的结果。但是用户可能只想搜索到苹果手机等电子产品,当然我们可以通过排除“水果 乔木 维生素”和这类单词,结合bool查询中的must_not子句,将结果范围缩小到只剩苹果手机,但是这种做法难免会排除掉那些真的想搜索水果的用户,这时可以通过boosting查询,通过降低“水果 乔木 维生素”等关键词的评分,让苹果等电子产品的排名靠前
语法
GET /索引库/映射类型/_search
{
"query": {
"boosting": {
"positive": {
"match": {
"title": "性价比"
}
},
"negative": {
"match": {
"content": "性价比"
}
},
"negative_boost": 0.1
}
}
}
5.4 使用JavaAPI实现以上查询
//bool
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery()
.must(....)
.mustNot(...)
.should(...)
.filter(...)
.minimumShouldMatch(1);
//boosting
BoostingQueryBuilder boostingQueryBuilder = QueryBuilders
.boostingQuery(..., ...)
.negativeBoost(0.2f);
例子:
六、排序
ElasticSearch默认会有一套相关性分数计算,分数越高,说明文档相关性越大,也就越会排在前面。除了相关性排序之外,开发者也可以通过自己的需要,通过某些规则设置查询文档的排序
如果进行手动排序,则评分为null
语法
GET /索引库/映射类型/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"排序字段1": {
"order": "asc"
}
},
{
"排序字段2":{
"order": "desc"
}
}
]
}
例子:
#手动排序
GET /hotal_index/_search
{
"query": {
"match": {
"hotalName": "品牌连锁酒店"
}
},
"sort": [
{
"price": {
"order": "asc"
}
}
]
}
//创建查询构建器
QueryBuilder queryBuilder = .........
..................
//执行查询
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder
.query(queryBuilder)
//设置排序
.sort("star", SortOrder.DESC)
.sort("type", SortOrder.ASC);
七、高亮
什么是高亮?
许多应用都倾向于在每个搜索结果中 高亮 显示搜索的关键词,比如字体的加粗,改变字体的颜色等.以便让用户知道为何该文档符合查询条件。在 Elasticsearch 中检索出高亮片段也很容易。高亮显示需要一个字段的实际内容。 如果该字段没有被存储(映射mapping没有将存储设置为 true),则加载实际的source,并从source中提取相关的字段。
语法
GET /索引库/映射类型/_search
{
"query": {
....
},
"highlight": {
"fields": {
"待高亮字段1": {},
"待高亮字段2": {}
},
"post_tags": ["</font>"],
"pre_tags": ["<font color='red'>"],
//功能:搜索的摘要显示
"number_of_fragments": 5,
"fragment_size": 3
}
}
例子:
摘要显示
参数含义
number_of_fragments: fragment 是指一段连续的文字。返回结果最多可以包含几段不连续的文字。
默认是5。
fragment_size: 某字段的值,长度是1万,但是我们一般不会在页面展示这么长,可能只是展示一部分。设置要显示出来的fragment文本判断的长度,默认是100
noMatchSize: 搜索出来的这个文档这个字段已经显示出高亮的情况,可是其它字段并没有任何显示,设置这个属性可以显示出来。
pre_tags: 标记 highlight 的开始标签。
post_tags: 标记 highlight 的结束标签。
JavaAPI
//设置高亮信息
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder
.field("title", 100)
.field("content", 100)
.preTags("<font color='red'>")
.postTags("</font>");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder
.query(queryBuilder)
.highlighter(highlightBuilder)
.sort("star", SortOrder.DESC)
.sort("type", SortOrder.ASC);
........
//获得高亮结果
Map<String, HighlightField> highlightFields
= hit.getHighlightFields();
for (Map.Entry<String, HighlightField> entry : highlightFields.entrySet()) {
System.out.println(entry.getKey() + "--"
+ entry.getValue().getFragments()[0].string());
}
八、地理位置搜索
地理位置在ElasticSearch中的字段类型geo-point
8.1 地理位置类型的相关操作
#创建映射类型:
PUT /soufang/_mapping/house
{
"properties": {
"name": {
"type": "text"
},
"location": {
"type": "geo_point"
}
}
}
#添加坐标点数据:
PUT /soufang/house/1
{
"name": "市民中心",
"location": {
"lat": 22.54737, #lat代表纬度
"lon": 114.067531 #lon代表经度
}
}
8.2 通过geo_distance过滤器搜索坐标
geo_distance:地理距离过滤器( geo_distance )以给定位置为圆心画一个圆,来找出那些地理坐标落在其中的文档
GET /soufang/house/_search
{
"query": {
"geo_distance":{
"location": {
"lat": 22.551013,
"lon": 114.065432
},
"distance": "1km",
"distance_type": "arc"
}
}
}
distance:中心点的半径距离
distance_type:两点间的距离计算的精度算法
arc - 最慢但是最精确是弧形(arc)计算方式,这种方式把世界当作是球体来处理
plane - 平面(plane)计算方式,把地球当成是平坦的。 这种方式快一些但是精度略逊
8.3 通过geo_bounding_box过滤器搜索坐标
geo_bounding_box: 查找某个长方形区域内的位置
GET /soufang/house/_search
{
"query": {
"geo_bounding_box":{
"location":{
"top_left": {
"lat": 22.628427,
"lon": 114.009234
},
"bottom_right": {
"lat": 22.521103,
"lon": 114.148939
}
}
}
}
}
top_left:代表矩形左上角
bottom_right:代表矩形右下角
8.4 通过geo_polygon过滤器搜索坐标
geo_polygon:查找位于多边形内的地点
GET /soufang/house/_search
{
"query": {
"geo_polygon": {
"location":{
"points": [
[113.908911, 22.613748],
[114.056952,22.634298],
[114.031368,22.575843],
[114.097196,22.500803],
[113.9,22.493591]
]
}
}
}
}
8.5 过滤结果通过距离排序
query:是查询这个区域的house
sort:这个区域的house为location里的坐标为零点,按距离排序返回数据
GET /soufang/house/_search
{
"query": {
"geo_distance":{
"location": {
"lat": 22.551013,
"lon": 114.065432
},
"distance": "1km",
"distance_type": "arc"
}
},
"sort": [
{
"_geo_distance": {
"order": "asc",
"location": {
"lat": 22.551013,
"lon": 114.065432
},
"unit": "km",
"distance_type": "arc"
}
}
]
}
unit:以 公里(km)为单位,将距离设置到每个返回结果的 sort 键中
8.6 JavaApi的执行方式
//geo_distance查找方式
QueryBuilders.geoDistanceQuery("location")
.point(22.55243, 114.044335)
.distance(2.8, DistanceUnit.KILOMETERS)
//geo_bounding_box查找方式
QueryBuilders.geoBoundingBoxQuery("location")
.setCorners(
new GeoPoint(22.628427, 114.009234),
new GeoPoint(22.521103, 114.148939))
//geo_polygon查找方式
List<GeoPoint> points = new ArrayList<>();
points.add(new GeoPoint(22.613748, 113.908911));
points.add(new GeoPoint(22.634298, 114.056952));
points.add(new GeoPoint(22.575843,114.031368));
points.add(new GeoPoint(22.500803,114.097196));
points.add(new GeoPoint(22.493591,113.9));
QueryBuilders.geoPolygonQuery("location", points)
//根据距离排序
SortBuilders
.geoDistanceSort("location", 22.586737, 113.960829)
.order(SortOrder.DESC)
.unit(DistanceUnit.KILOMETERS)
九、function_score自定义文档相关性
9.1 什么是function_score?
在使用ES进行全文搜索时,搜索结果默认会以文档的相关度进行排序,而这个 "文档的相关度",是可以通过 function_score 自己定义的,也就是说我们可以透过使用function_score,来控制 "怎么样的文档相关度更高" 这件事
9.2 文档相关度评分默认大概规则
1、关键词词频越高,评分越高
2、关键词在所有文档中出现的频率越高,评分越低
3、搜索的关键词与目标文档中分词匹配个数越多,评分越高
4、匹配的字段权重越高,评分越高
9.3 function_score的基本用法
9.3.1 function_score提供的加强_score的函数
1、weight:设置权重提升值,可以用于任何查询
2、field_value_factor: 将某个字段的值乘上old_score
3、random_score : 为每个用户都使用一个不同的随机评分对结果排序,但对某一具体用户来说,看到的顺序始终是一致的
4、衰减函数 (linear、exp、guass) : 以某个字段的值为基准,距离某个值越近得分越高
5、script_score : 当需求超出以上范围时,可以用自定义脚本完全控制评分计算,不过因为还要额外维护脚本不好维护,因此尽量使用ES提供的评分函数,需求真的无法满足再使用script_score
9.3.2 function_score其他辅助的参数
boost_mode
决定 old_score 和 加强score 如何合并
可选值:
multiply(默认) : new_score = old_score * 加强score
sum : new_score = old_score + 加强score
min : old_score 和 加强score 取较小值,new_score = min(old_score, 加强score)
max : old_score 和 加强score 取较大值,new_score = max(old_score, 加强score)
replace : 加强score直接替换掉old_score,new_score = 加强score
score_mode
决定functions里面的加强score们怎么合并,会先合并加强score们成一个总加强score,再使用总加强score去和old_score做合并,换言之就是会先执行score_mode,再执行boost_mode
可选值:
multiply (默认):将所有加强score相乘
sum:求和
avg:取平均值
first : 使加强首个函数(可以有filter,也可以没有)的结果作为最终结果
max:取最大值
min:取最小值
max_boost
限制加强函数的最大效果,就是限制加强score最大能多少,但要注意不会限制old_score
9.3.3 function_score语法
单加强函数语法
GET /索引库/映射类型/_search
{
"query": {
"function_score": {
//主查询,查询完后这裡自己会有一个评分,就是old_score
"query": {.....},
//在old_score的基础上,给他加强其他字段的评分,这裡会产生一个加强score,如果只有一个加强function时,直接将加强函数名写在query下面就可以了
"field_value_factor": {...},
//指定用哪种方式结合old_score和加强score成为new_score
"boost_mode": "multiply",
//限制加强score的最高分,但是不会限制old_score
"max_boost": 1.5
}
}
}
多加强函数语法
GET /索引库/映射类型/_search
{
"query": {
"function_score": {
"query": {.....},
"functions": [
//可以有多个加强函数(或是filter+加强函数),每一个加强函数会产生一个加强score,因此functions会有多个加强score
{ "field_value_factor": ... },
{ "gauss": ... },
{ "filter": {...}, "weight": ... }
],
//决定加强score们怎么合并
"score_mode": "sum",
//决定总加强score怎么和old_score合并
"boost_mode": "multiply"
}
}
}
weight加强函数用法
GET /shop/goods/_search
{
"query": {
"function_score": {
"query": {
"match_all": {}
},
"functions": [
{"filter": {
"range": {
"price": {
"gte": 1000,
"lte": 3000
}
}
}, "weight": 3}
],
"boost_mode": "sum"
}
}
}
解析:查询所有文档,如果某个文档的价格在1000~3000范围内,文档评分就会*3,并且new_score会和old_score相加得到最终评分
random_score加强函数使用案例
GET /shop/goods/_search
{
"query": {
"function_score": {
"query": {
"match_all": {}
},
"functions": [
{"random_score": {
"seed": 2
}}
]
}
}
}
解析:不同的用户,可以设置不同的seed值(比如用户的id号),实现随机排序的效果,但是对同一个用户排序结果又是恒定的
GET /shop/goods/_search
{
"query": {
"function_score": {
"query": {
"match_all": {}
},
"functions": [
{"field_value_factor": {
"field": "price"
}}
]
}
}
}
解析:查询所有文档,并且将所有文档的old_score,乘以本身的价格,得到new_score,默认将new_score * old_score,得到最终评分
9.3.4 衰减函数评分
什么是衰减函数?
以某一个范围为基准,距离这个范围越远,评分越低。 比如以100为基准,那么大于100,或者小于100评分都将变得越来越低。
为什么需要衰减函数?
在一次搜索中,某个条件并不一定是线性增长或者递减来影响最终结果评分的。比如搜索商品,并不是价格越低就意味着越好,用户就会越感兴趣,往往可能维系在一个价格区间的用户会更感兴趣一些。比如原来做过一个调查,如果买车大概会买什么价位的,最后有40%的人选择的是10~15w之间的车型。所以我们会发现,价格这个因素,对用户来说并不是越高越好,同时也不意味着越低越好,而衰减函数就是为了对这一类的数据进行评分的
衰减函数的分类
linear、exp 和 gauss,三种衰减函数的差别只在于衰减曲线的形状,在DSL的语法上的用法完全一样
linear : 线性函数是条直线,一旦直线与横轴相交,所有其他值的评分都是0
exp : 指数函数是先剧烈衰减然后变缓
gauss(最常用) : 高斯函数则是钟形的,他的衰减速率是先缓慢,然后变快,最后又放缓
field_value_factor加强函数使用案例
衰减函数的支持参数
origin : 中心点,或是字段可能的最佳值,落在原点(origin)上的文档评分_score为满分1.0,支持数值、时间 以及 "经纬度地理座标点"等类型字段 _
offset : 从 origin 为中心,为他设置一个偏移量offset覆盖一个范围,在此范围内所有的评分_score也都是和origin一样满分1.0
scale : 衰减率,即是一个文档从origin下落时,_score改变的速度
衰减函数案例
GET /mytest/doc/_search
{
"query": {
"function_score": {
"functions": [
//第一个gauss加强函数,决定距离的衰减率
{
"gauss": {
"location": {
"origin": { //origin点设成酒店的经纬度座标
"lat": 51.5,
"lon": 0.12
},
"offset": "2km", //距离中心点2km以内都是满分1.0,2km外开始衰减
"scale": "3km" //衰减率
}
}
},
//第二个gauss加强函数,决定价格的衰减率,因为用户对价格更敏感,所以给了这个gauss 加强函数2倍的权重
{
"gauss": {
"price": {
"origin": "50",
"offset": "50",
"scale": "20"
}
},
"weight": 2
}
]
}
}
}
JavaAPI设置评分
/**
* 自定义评分
*/
@Test
public void functionScore() throws IOException {
List<FunctionScoreQueryBuilder.FilterFunctionBuilder> list
= new ArrayList<>();
list.add(new FunctionScoreQueryBuilder.
FilterFunctionBuilder(ScoreFunctionBuilders.
gaussDecayFunction("location", new GeoPoint(22.586203, 114.031687), "6km", "5km")));
SearchRequest searchRequest = new SearchRequest("soufang").types("house");
searchRequest.source().query(
QueryBuilders.functionScoreQuery(QueryBuilders.matchAllQuery(), list.toArray(new FunctionScoreQueryBuilder.FilterFunctionBuilder[0]))
.boostMode(CombineFunction.REPLACE));
SearchResponse response =
restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
System.out.println("查询结果:" + hit.getSourceAsString() + " 评分:" + hit.getScore());
}
}
例子:
注意:如果price用“scaled_float”会报错。需要改成double
十、分页
任何搜索都可以加两个参数“from”/"size",相当于数据库的limit ?,?
总条数在结果中的“total”参数中,可以自己运算出分页数
十一、ElasticSearch集群搭建
1、创建基本目录/usr/local/es-cluster
2、在master/conf/elasticsearch.yml添加如下内容
bootstrap.memory_lock: false
cluster.name: "es-cluster"
node.name: es-master
node.master: true
node.data: false
network.host: 0.0.0.0
http.port: 9200
transport.tcp.port: 9300
discovery.zen.ping.unicast.hosts: ["es-master:9300"]
discovery.zen.minimum_master_nodes: 1
path.logs: /usr/share/elasticsearch/logs
http.cors.enabled: true
http.cors.allow-origin: "*"
xpack.security.audit.enabled: true
3、在node1&node2/conf/elasticsearch.yml添加如下内容
cluster.name: "es-cluster"
node.name: node1 #这里注意替换
node.master: false
node.data: true
network.host: 0.0.0.0
http.port: 9202
transport.tcp.port: 9302
discovery.zen.ping.unicast.hosts: ["es-master:9300"]
path.logs: /usr/share/elasticsearch/logs
4、编写docker-compose.yml
version: '3.1'
services:
es-master:
image: elasticsearch:6.8.5
container_name: es-master
restart: always
volumes:
- ./master/data:/usr/share/elasticsearch/data:rw
- ./master/conf/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
- ./master/logs:/user/share/elasticsearch/logs:rw
ports:
- 9200:9200
- 9300:9300
networks:
- es-network
es-node1:
image: elasticsearch:6.8.5
container_name: es-node1
restart: always
networks:
- es-network
volumes:
- ./node1/data:/usr/share/elasticsearch/data:rw
- ./node1/conf/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
- ./node1/logs:/user/share/elasticsearch/logs:rw
es-node2:
image: elasticsearch:6.8.5
container_name: es-node2
restart: always
networks:
- es-network
volumes:
- ./node2/data:/usr/share/elasticsearch/data:rw
- ./node2/conf/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
- ./node2/logs:/user/share/elasticsearch/logs:rw
es-head:
image: mobz/elasticsearch-head:5
container_name: es-head
restart: always
ports:
- 9100:9100
networks:
- es-network
kibana:
image: kibana:6.8.5
restart: always
container_name: kibana
environment:
SERVER_NAME: kibana
ELASTICSEARCH_URL: http://192.168.195.135:9200
ports:
- 5601:5601
networks:
- es-network
networks:
es-network:
5、启动docker-compose.yml
#授权
chmod 777 -R master node1 node2
#启动
docker-compose up -d
安装中遇到问题
问题:max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]
解决:在宿主机执行sysctl -w vm.max_map_count=262144,重启docker容器
十二、综合使用
12.1、案例1、
代码实现
/**
* 通过查询条件,执行相应的查询
* @param searchParams
* @return
* @throws IOException
*/
@Override
public List<Hotal> query(SearchParams searchParams) throws IOException {
//构建查询构建器
//城市查询
TermQueryBuilder cityQuery = QueryBuilders.termQuery("cityname", searchParams.getCityName());
//通过关键词匹配多个字段
QueryBuilder keywordQuery = null;
if (StringUtils.isNotEmpty(searchParams.getKeyword())) {
//用户关键字不为空
keywordQuery = QueryBuilders
.multiMatchQuery(searchParams.getKeyword())
.field("hotalName").boost(2)
.field("brand").boost(2)
.field("regid")
.field("keyword")
.field("hotalInfo");
} else {
keywordQuery = cityQuery;
}
//通过价格限制条件查询
RangeQueryBuilder priceQuery = QueryBuilders.rangeQuery("price")
.gte(searchParams.getMinPirce() != null ? searchParams.getMinPirce().doubleValue() : 0)
.lte(searchParams.getMaxPirce() != null ? searchParams.getMaxPirce().doubleValue() : Integer.MAX_VALUE);
//bool查询将以上两个查询整合
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
.must(keywordQuery)
.must(priceQuery);
//降级查询 - 符合这个条件的文档,评分会降低
//bool对城市查询取非
BoolQueryBuilder boolQuery2 = QueryBuilders.boolQuery().mustNot(cityQuery);
//使用boosting查询将以上两个bool查询组合起来
BoostingQueryBuilder execQuery = QueryBuilders.boostingQuery(
boolQuery,
boolQuery2
).negativeBoost(0.2f);
return execQuery;
}