分布式搜索引擎-Es01
这完全是我自己想出来的,我太开心啦,哈哈哈:1.概述:elasticsearch是一个开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能比如:elastic stack(ELK)是以elasticsearch为核心的技术栈,包括beats、Logstash、kibana、elasticsearch,这是这个技术栈的基本架构,数据可视化,数据抓取的技术栈不一定要依靠这三个,但数据
1.概述:
elasticsearch是一个开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能 比如:elastic stack(ELK)是以elasticsearch为核心的技术栈,
包括beats、Logstash、kibana、elasticsearch,这是这个技术栈的基本架构,数据可视化,数据抓取的技术栈不一定要依靠这三个,但数据检索es是必不可少的。
2.倒排索引:
倒排索引的概念是基于MySQL这样的正向索引而言的。 2.1.正向索引 正向索引:例如给mysql表中的id创建索引,如果是根据id查询,那么直接走索引,查询速度非常快。但如果是基于title做模糊查询,索引会失效,只能是逐行扫描数据,流程如下:逐行扫描,也就是全表扫描,随着数据量增加,其查询效率也会越来越低。当数据量达到数百万时,就是一场灾难。 2.2.倒排索引 倒排索引中有两个非常重要的概念: - 文档(`Document`,类比于mysql中的一条数据):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息 - 词条(`Term`):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条 创建倒排索引是对正向索引的一种特殊处理,流程如下:
- 将每一个文档的数据利用算法分词,得到一个个词条 - 创建表,每行数据包括词条、词条所在文档id、位置等信息 - 因为词条唯一性,可以给词条创建索引,例如hash表结构索引 至此这就相当于创建了倒排索引 - 然后给文档id创建索引,方便拿着id去找文档 基于倒排索引的搜索流程如下(以搜索"华为手机"为例):
1)用户输入条件`"华为手机"`进行搜索。 2)对用户输入内容分词,得到词条:`华为`、`手机`。 3)拿着词条在倒排索引中查找,可以得到包含词条的文档id:1、2、3。 4)拿着文档id到正向索引中查找具体文档。 虽然要先查询词条索引,再查询id正向索引,但是无论是词条、还是文档id都建立了索引,查询速度非常快!无需全表扫描。 2.3对比: 为什么一个叫做正向索引,一个叫做倒排索引呢? 正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程。而倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程。 2.4优缺点: 正向索引: - 优点: - 可以给多个字段创建索引 - 根据索引字段搜索、排序速度非常快 - 缺点: - 根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。 倒排索引: - 优点: - 根据词条搜索、模糊搜索时,速度非常快 - 缺点: - 只能给词条创建索引,而不是字段 - 无法根据字段做排序
3.es中的概念,对比mysql

3.1.文档和字段 elasticsearch是面向文档(Document)存储的,文档相当于mysql中的一行数据。文档数据会被序列化为json格式后存储在elasticsearch中:一个文档中往往包含很多的字段(Field),类似于数据库中的列,比如这里的id,title等都是一个字段。
3.2.索引和映射 索引(Index),就是相同类型的文档的集合,类似mysql中的数据库。比如:数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束(唯一约束,主键约束等)。 3.3互相补充 Mysql:擅长事务类型操作,可以确保数据的安全和一致性,Elasticsearch:擅长海量数据的搜索、分析、计算。因此在企业中,往往是两者结合使用: - 对安全性要求较高的写操作,使用mysql实现 - 对查询性能要求较高的搜索需求,使用elasticsearch实现 - 两者再基于某种方式,实现数据的同步,保证一致性(这里使用MQ)

4.安装
4.1安装es和kibana,ik分词器
看一下代码,
首先得新建一个网络,用于es软件的互相通信:
docker network creat es-net
然后运行的时候--network加入即可。
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network es-net \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.12.1
命令解释:
- `-e "ES_JAVA_OPTS=-Xms512m -Xmx512m"`:内存大小,指定最大运行内存
- `-e "discovery.type=single-node"`:非集群模式
- `-v es-data:/usr/share/elasticsearch/data`:挂载逻辑卷,绑定es的数据目录
- `-v es-logs:/usr/share/elasticsearch/logs`:挂载逻辑卷,绑定es的日志目录
- `-v es-plugins:/usr/share/elasticsearch/plugins`:挂载逻辑卷,绑定es的插件目录(分词器可以放在这里)
- `--privileged`:授予逻辑卷访问权
- `--network es-net` :加入一个名为es-net的网络中(在此网络中的容器可以以名字直接连接)
- `-p 9200:9200`:端口映射配置,供其他用户访问
- `-p 9300:9300:这是 Elasticsearch 集群内部节点之间进行通信的 tcp端
kibana可以给我们提供一个elasticsearch的可视化界面。安装:
运行docker命令,部署kibana
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601 \
kibana:7.12.1
--network es-net` :加入一个名为es-net的网络中,与elasticsearch在同一个网络中,能够直接访问 --e ELASTICSEARCH_HOSTS=http://es:9200":设置elasticsearch的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名直接访问elasticsearch - `-p 5601:5601`:端口映射配置
现在直接访问宿主机ip加5601就可以访问kibana,从而直接可以写DSl语句了,然后把ik分词器放到配置的那个数据卷目录下,这就完成了。
4.2ik分词器
分词器的作用:创建倒排索引时对文档分词,用户搜索时,对输入的内容分词当然分词器有很多,后面我们会详细讲这里先用一种支持中文的分词器 IK分词器包含两种模式: ik_smart:最少切分(最大粒度切分),ik_max_word:最细切分比如:


但是这样就存在一些问题,首先就是如何添加新词,比如现在的网络用语,我们需要添加进去,只需要修改ik分词器的文件,然后重启即可:
1)打开IK分词器config目录: 2)在IKAnalyzer.cfg.xml配置文件内容添加:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 *** 添加扩展词典-->
<entry key="ext_dict">ext.dic</entry>
</properties>
<entry key="ext_dict">ext.dic</entry>这个标签就是指定那个文件是新增词。 3)新建一个 ext.dic,写上拓展词即可。
然后就是禁用词,和拓展词配置方法一样:
IK分词器也提供了强大的停用词功能,让我们在索引时就直接忽略当前的停用词汇表中的内容。 1)IKAnalyzer.cfg.xml配置文件内容添加:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典-->
<entry key="ext_dict">ext.dic</entry>
<!--用户可以在这里配置自己的扩展停止词字典 *** 添加停用词词典-->
<entry key="ext_stopwords">stopword.dic</entry>
</properties>
3)在 stopword.dic 添加停用词即可。
5.Kibana中利用DSL操作es(我们平常是利用Java操作的,所以这里只要会看就行,最好记住)
5.1操作索引库
索引库就类似数据库表,mapping映射就类似表的结构。我们要向es中存储数据,必须先创建“库”和“表,要创建库,就要类似于mysql知道约束,所以先看映射,常见的mapping属性包括:
- type:字段数据类型,常见的简单类型有:
- 字符串:text(可分词的文本,要配合分词器)、keyword(精确值,例如:品牌、国家、ip地址)
- 数值:long、integer、short、byte、double、float、
- 布尔:boolean
- 日期:date
- 对象:object
- index:是否创建索引,默认为true
- analyzer:使用哪种分词器
- properties:该字段的子字段
现在来看对于索引库的CRUD:
C:基本语法: - 请求方式:PUT - 请求路径:/索引库名,可以自定义,里面都有引号,多个映射和字段逗号隔开.
PUT /索引库名称
{
"mappings": {
"properties": {
"字段名":{
"type": "text",
"analyzer": "ik_smart"
},
"字段名2":{
"type": "keyword",
"index": "false"
},
"字段名3":{
"properties": {
"子字段": {
"type": "keyword"
}
}
},
***
}
}
}
R: 基本语法: - 请求方式:GET/索引库名
U:es不支持映射的改动所以不能U,虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。
PUT /索引库名/_mapping
{
"properties": {
"新字段名":{
"type": "integer"
}
}
}
D:语法:格式: DELETE /索引库名
PUT /myes
{
"mappings": {
"properties": {
"key1":{
"type": "text",
"analyzer": "ik_smart"
},
"key2":{
"type": "keyword"
}
}
}
}
GET /myes
DELETE /myes
5.2操作文档(CRUD)
文档操作有哪些?
- 创建文档:POST /{索引库名}/_doc/文档id { json文档 }
- 查询文档:GET /{索引库名}/_doc/文档id
- 删除文档:DELETE /{索引库名}/_doc/文档id
- 修改文档:
- 全量修改:PUT /{索引库名}/_doc/文档id { json文档 }
- 增量修改:POST /{索引库名}/_update/文档id { "doc": {字段}}
全量修改的原理就是先删除数据库,然后再创建一个新的,如果没有就直接创建一个新的,所以可以把创建也认为是全量修改,从而简化业务。
PUT /myes/_doc/1
{
"key1":"我是第一个",
"key2":"我是第二个"
}
GET /myes/_doc/1
DELETE /myes/_doc/1
5.3RestAPI
ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。其中的Java Rest Client又包括两种: - Java Low Level Rest Client - Java High Level Rest Client 这里讲的的是Java HighLevel Rest Client客户端API,看一个实战
关键在于映射应该怎么样设计,应该考虑的有几点:
- 字段名 - 字段数据类型 - 是否参与搜索 - 是否需要分词 - 如果分词,分词器是什么? 其中: - 字段名、字段数据类型,可以参考数据表结构的名称和类型 - 是否参与搜索要分析业务来判断,例如图片地址,就无需参与搜索 - 是否分词呢要看内容,内容如果是一个整体就无需分词,反之则要分词 - 分词器,我们可以统一使用ik_max_word
PUT /hotel
{
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"name":{
"type": "text",
"analyzer": "ik_max_word",
"copy_to": "all"
},
"address":{
"type": "keyword",
"index": false
},
"price":{
"type": "integer"
},
"score":{
"type": "integer"
},
"brand":{
"type": "keyword",
"copy_to": "all"
},
"city":{
"type": "keyword",
"copy_to": "all"
},
"starName":{
"type": "keyword"
},
"business":{
"type": "keyword"
},
"location":{
"type": "geo_point"
},
"pic":{
"type": "keyword",
"index": false
},
"all":{
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
其中有几个细节:地理坐标类型
"location":{ "type": "geo_point" }插入的时候应该是这种格式的:
"location": "31.230416,121.473701" // 纬度在前,经度在后然后就是all这个类型字段:组合字段,其目的是将多字段的值 利用copy_to合并,提供给用户搜索,因为当查询涉及到的索引很多时,肯定不如一个索引(类似于联合索引),所以就写了这个字段,然后如果那个字段要加入直接指定"copy_to"即可,一个反直觉的事情就是,你看一个字段是keyword类型,不能分词,但是copy到all里面,all是text类型,竟然可以分词了,其实这并不冲突:
. 各字段的角色分工
keyword类型字段(如brand、city)
- 不分词,作为一个完整的精确值存储和索引。
- 适合做:精确过滤(term 查询)、聚合(aggregation)、排序(sort)。
- 它们的
copy_to: "all"只是把自己的原始值复制一份到all字段里,自己本身的类型和行为不受影响。
all字段(text 类型)
- 接收来自多个
keyword字段的原始值。- 作为一个
text字段,会用ik_max_word分词器对这些拼接起来的文本进行分词,生成倒排索引。- 适合做:全文检索(match 查询),让用户可以在品牌、城市等多个维度里同时搜索。
其实就是两个独立的东西,互不干扰
5.1使用RestAPI:
在elasticsearch提供的API中,与elasticsearch一切交互都封装在一个名为RestHighLevelClient的类中,必须先完成这个对象的初始化,建立与elasticsearch的连接。分为三步:
1)引入es的RestHighLevelClient依赖:
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
2)因为SpringBoot默认的ES版本是7.6.2,所以我们需要覆盖默认的ES版本:
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>
3)初始化RestHighLevelClient:我们可以配置一个bean实现自动注入(当然可以读取一个配置类)
@Bean
public RestHighLevelClient restHighLevelClient() {
return new RestHighLevelClient(
RestClient.builder(
HttpHost.create("http://192.168.150.101:9200")
)
);
然后就是学习一下利用API完成索引和文档的操作:
JavaRestClient操作elasticsearch的流程基本类似。核心是client.indices()方法来获取索引库的操作对象。 索引库操作的基本步骤: - 初始化RestHighLevelClient - 创建XxxIndexRequest。XXX是Create、Get、Delete - 准备DSL( Create时需要,其它是无参) - 发送请求。调用RestHighLevelClient#indices().xxx()方法,xxx是create、exists、delete
这是一个整体框架,所有对于es的操作框架都是这三步,第一步创建请求对象,根据操作不同需要的对象不一样,可以先写第三步的代码,然后ctrl+p提示需要什么对象就创建什么对象,然后第二部是第一步对象,.source就是代表那个下面的大的json文档,有的请求有请求体就写,没有就不需要第二步,支持链式编程。

5.2操作索引库
第三步的注入对象.indices就是index的复数,就是索引库的意思,返回的对象可以操作索引库。

需要的参数是一个什么类型的请求,就new一个,比如:
@Test
void text01() throws IOException {
//1.准备请求对象
CreateIndexRequest hotel = new CreateIndexRequest("hotel");
//2.准备请求体
hotel.source(CreatIndex.MAPPING_TEMPLATE, XContentType.JSON);
//3.发送请求
client.indices().create(hotel, RequestOptions.DEFAULT);
}
其他的API就是一样的:
@Test
void testDeleteHotelIndex() throws IOException {
// 1.创建Request对象
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
// 2.发送请求
client.indices().delete(request, RequestOptions.DEFAULT);
}
判断索引库是不是存在,其实本质就是GET请求,但是分装的不是get:
@Test
void testExistsHotelIndex() throws IOException {
// 1.创建Request对象
GetIndexRequest request = new GetIndexRequest("hotel");
// 2.发送请求
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
// 3.输出
System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");
}
5.3操作文档:
其实基本框架是一样的,只不过多了一步就是:解析结果罢了,其实也很简单,就是拿到响应结果,然后一步步的去拆解结果得到数据即可。而且是注入对象直接加操作了。
第一步当然就是创建一个实体类了,对应索引库的类型。值得注意的是,索引库要求的地理位置是"纬度,经度",这和数据库实体类的有一个差别就是数据库的是分开的两个属性,所以要进行以下转化,通过构造函数转化即可
@Data
@NoArgsConstructor
public class HotelDoc {
private Long id;
private String name;
private String address;
private Integer price;
private Integer score;
private String brand;
private String city;
private String starName;
private String business;
private String location;
private String pic;
public HotelDoc(Hotel hotel) {
this.id = hotel.getId();
this.name = hotel.getName();
this.address = hotel.getAddress();
this.price = hotel.getPrice();
this.score = hotel.getScore();
this.brand = hotel.getBrand();
this.city = hotel.getCity();
this.starName = hotel.getStarName();
this.business = hotel.getBusiness();
this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
this.pic = hotel.getPic();
}
}
this.location = hotel.getLatitude() + ", " + hotel.getLongitude();这个就是构造函数传入一个数据库实体类,再进行改编。
来看新增操作:这里不是create而是index这个比较特殊,其他都是见名知意。
@Test
void testAddDocument() throws IOException {
// 1.根据id查询酒店数据
Hotel hotel = hotelService.getById(61083L);
// 2.转换为文档类型
HotelDoc hotelDoc = new HotelDoc(hotel);
// 3.将HotelDoc转json
String json = JSON.toJSONString(hotelDoc);
// 1.准备Request对象
IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());
// 2.准备Json文档
request.source(json, XContentType.JSON);
// 3.发送请求
client.index(request, RequestOptions.DEFAULT);
}
String json = JSON.toJSONString(hotelDoc);就是把一个对象转化为json对象,前面学过JsonUtils.toJsonString,都是可以的,还有对应的JSON.parseObject(json, 目标类.class),JsonUtils.toBean(json, 目标类.class)。
然后查看操作:
@Test
void test02() throws Exception {
// 1.准备Request对象
GetRequest request = new GetRequest("hotel").id(String.valueOf(61083L));
// 2.发送请求61083L
GetResponse response = client.get(request, RequestOptions.DEFAULT);
//3.处理响应
String json = response.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println(hotelDoc);
}
这三部都一样,只不过多了解析,这里演示一下如何解析:这是dsl查询出来的结果:

而get得到的结果就是响应体,其实就是最外边的括号,所以String json = response.getSourceAsString();就是在得到真实的数据。然后转化为对象解析即可。
删就不看了很简单,看以下改,改的话分为全量和增量修改,全量修改和新增API完全一样,不看了,看一下增量修改:比如这个业务。
批量处理BulkRequest,其本质就是将多个普通的CRUD请求组合在一起发送。比如批量插入删除等:基本语法是这样:
框架还是一样,只不过第二步变成了批量加入(add),然后里面是以前的第一步和第二部框架,只不过利用了链式编程。所以批量导入hotel数据:
@Test void test04() throws Exception { //查询数据库中数据 List<Hotel> list = hotelService.listBy(); // 1.准备Request对象 BulkRequest bulkRequest = new BulkRequest(); for (Hotel hotel : list) { HotelDoc hotelDoc = new HotelDoc(hotel); // 3.准备请求体 IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString()); request.source(JSON.toJSONString(hotelDoc), XContentType.JSON); bulkRequest.add(request); } // 2.发送请求61083L client.bulk(bulkRequest, RequestOptions.DEFAULT); }
6.Es的DSL数据搜索:
es的一般查询有:
查询所有:查询出所有数据例如:match_all 全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如: - match(单字段) - multi_match(多字段) 精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。例如: - range - term 地理(geo)查询:根据经纬度查询。例如: - geo_distance(距离查询) - geo_bounding_box(矩形范围查询) 复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如: - bool - function_score 查询的语法基本是这样的:
GET /indexName/_search
{
"query": {
"查询类型": {
"查询条件": "条件值"
}
}
}
比如查询所有:
GET /hotel/_search
{
"query": {
"match_all": {}
}
}
6.1全文检索查询:
6.1.1全文检索查询的基本流程如下:
- 对用户搜索的内容做分词,得到词条(因为是拿着词条去匹配,因此参与搜索的字段也必须是可分词的text类型的字段)
- 根据词条去倒排索引库中匹配,得到文档id
- 根据文档id找到文档,返回给用户
比较常用的场景包括:
- 商城的输入框搜索
- 百度输入框搜索
比如:以下代码就会先对外滩如家进行分词,然后去索引库中进行匹配,然后得到文档,这里是匹配copy到all的字段。


因为将brand、name、business值都利用copy_to复制到了all字段中。因此你根据三个字段搜索,和根据all字段搜索效果一样了。但是,搜索字段越多,对查询性能影响越大,因此建议采用copy_to,然后单字段查询的方式。
6.2精准查询
精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。
常见的有: - term:根据词条精确值查询 - range:根据值的范围查询

加e就是表示多了等于

6.3地理位置查询:
所谓的地理坐标查询,其实就是根据经纬度查询,常见的使用场景包括: - 携程:搜索我附近的酒店 - 滴滴:搜索我附近的出租车 - 微信:搜索我附近的人 - 黑马点评实现按距离排序
6.3.1.矩形范围查询 矩形范围查询,也就是geo_bounding_box查询,查询坐标落在某个矩形范围的所有文档: 查询时,需要指定矩形的左上、右下两个点的坐标,然后画出一个矩形,落在该矩形内的都是符合条件的点。
{
"query": {
"geo_bounding_box": {
"FIELD": {
"top_left": { // 左上点
"lat": 31.1,
"lon": 121.5
},
"bottom_right": { // 右下点
"lat": 30.9,
"lon": 121.7
}
}
}
}
}
6.3.2.附近查询 附近查询,也叫做距离查询(geo_distance):查询到指定中心点小于某个距离值的所有文档。 换句话来说,在地图上找一个点作为圆心,以指定距离为半径,画一个圆,落在圆内的坐标都算符合条件,这才是业务中经常用到的:
GET /hotel/_search
{
"query": {
"geo_distance":{
"distance":"15km",
"location":"31.21,121.5"
}
}
}
第一个distance是表示距离,第二个"Field":坐标表示是在说明中心。
6.4复合查询
复合(compound)查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑。常见的有两种: - fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名 - bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索
6.4.1.相关性算分
当match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。
例如,我们搜索 "虹桥如家",结果如下:
```json
[
{
"_score" : 17.850193,
"_source" : {
"name" : "虹桥如家酒店真不错",
}
},
{
"_score" : 12.259849,
"_source" : {
"name" : "外滩如家酒店真不错",
}
},
{
"_score" : 11.91091,
"_source" : {
"name" : "迪士尼如家酒店真不错",
}
}
]
谁分高谁就是在前边。
在elasticsearch中,早期使用的打分算法是TF-IDF算法,公式如下
在后来的5.1版本升级中,elasticsearch将算法改进为BM25算法,公式如下:
TF-IDF算法有一各缺陷,就是词条频率越高,文档得分也会越高,单个词条对文档影响较大。而BM25则会让单个词条的算分有一个上限,曲线更加平滑:
但是实际开发中肯定要自己控制打分吧,谁给米了谁就考前,所以要控制打分机制,就需要利用elasticsearch中的function score 查询了。
6.4.2fuction score查询
基本语法:太多了还没有提示,看懂就行了
这就是复合查询,就是在先查询出query的情况下,利用functions定义过滤条件(里面又是一个查询)和得到一个新的算分函数,这个会对过滤之后的数据进行重新算分,然后可以指定算分的方法。看一个复杂一点的:

6.4.3布尔查询
布尔查询是一个或多个查询子句的组合,每一个子句就是一个子查询。子查询的组合方式有: - must:必须匹配每个子查询,类似“与” - should:选择性匹配子查询,类似“或” - must_not:必须不匹配,**不参与算分**,类似“非” - filter:必须匹配,**不参与算分*
注意如果不需要参与算分的业务应该使用后两者,因为算分要消耗性能。比如一个业务:
这种多条件查询时,建议这样做: - 搜索框的关键字搜索,是全文检索查询,使用must查询,参与算分 - 其它过滤条件,采用filter查询。不参与算分
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{"term": {"city": "上海" }}
],
"should": [
{"term": {"brand": "皇冠假日" }},
{"term": {"brand": "华美达" }}
],
"must_not": [
{ "range": { "price": { "lte": 500 } }}
],
"filter": [
{ "range": {"score": { "gte": 45 } }},
{"geo_distance":{
"distance":"5km",
"location":"31.25,125.1"
}}
]
}
}
}
每个都是一个列表,列表里面是子查询。
6.5对查询结果的处理:
6.5.1排序
排序大体可以分为两种:普通字段排序和地理位置排序(看见了吧哈哈哈,终于可以解决点评中地理位置排序了)
elasticsearch默认是根据相关度算分来排序,但是也支持自定义方式对搜索[结果排序],就相当于mysql中的orderby排在查询结果后面,可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。 普通字段排序:keyword、数值、日期类型排序的语法基本一致。 GET /indexName/_search { "query": { "match_all": {} }, "sort": [ { "FIELD": "desc" // 排序字段、排序方式ASC、DESC } ] } 排序条件是一个数组,也就是可以写多个排序条件。按照声明的顺序,当第一个条件相等时,再按照第二个条件排序,以此类推,不写了很简单,注意是和query平级。 地理坐标排序!!!重要 GET /indexName/_search { "query": { "match_all": {} }, "sort": [ { "_geo_distance" : { "FIELD" : "纬度,经度", // 文档中geo_point类型的字段名、目标坐标点 "order" : "asc", // 排序方式 "unit" : "km" // 排序的距离单位 } } ] }里面location换了一种写法,其实写成"location":"31,121"也可以。
这里写一个小的知识点,也是课程中没有涉及到的(可以作为项目的一个亮点我感觉,我太牛逼了):排序的话很明显算分就没有用了,那么如果我还是想让排序完,某些酒店置顶呢?就是在查询里面pinned中的ids数组里写上要置顶的顺序,其他的数据放在organic里,organic里写法和之前一样。
GET /hotel/_search { "query": { "pinned": { "ids": [ "hotel_003", // 第1个置顶 "hotel_001", // 第2个置顶 "hotel_005" // 第3个置顶 ], "organic": { "query": { "match_all": {} }, "sort": [ { "_geo_distance": { "location": { "lat": 31.034661, "lon": 121.612282 }, "order": "asc", "unit": "km" } } ] } } } }但还是有一个问题,这种写法支持:静态指定 ID + 控制 ID 顺序 + 其余文档自定义排序,但是 不支持:动态检索(如全文检索)、动态算分(如
function_score)、按字段规则筛选置顶文档,这本质是为了性能和确定性:ID 是文档的唯一标识,ES 能直接通过倒排索引快速定位,置顶操作几乎无性能损耗;那如何完成动态置顶需求呢(就是在置顶的那些东西里面动态调整)?这完全是我自己想出来的,我太开心啦,哈哈哈:
我的思路是,首先分开置顶和非置顶,那如何分开呢,当然就我们自定义分数的查询:function_score了。我们可以给非置顶的得分置0,这样一般情况下置顶得分都是高于0的,这样就分开了。。从而排序的话先根据分数排序,这样所有置顶的都排完了(这就是动态置顶排序),然后非置顶的是0都一样,再按照自己想排序的规则排序(比如距离呀,什么的)。是不是很esay,这里的核心就是得分置0。所以第一部分就是拆开:
GET /hotel/_search { "query": { "bool": { "should": [ // ========== 第一部分:动态置顶的酒店(得分>0) ========== { "function_score": { // 1. 筛选置顶候选:全文检索 "query": { "match": { "name": { "query": "皇冠假日 上海 豪华", // 要置顶的检索关键词 "operator": "or" // 包含任意关键词即可 } } }, // 2. 置顶酒店内部排序规则(按权重高低排) "functions": [ // 规则1:品牌精准匹配,权重10(优先级最高) { "filter": { "term": { "brand": "皇冠假日" } }, "weight": 10 }, // 规则2:评分≥4.8,权重5(优先级次之) { "filter": { "range": { "score": { "gte": 48 } } }, "weight": 5 }, // 规则3:价格<1000,权重2(优先级再次之) { "filter": { "range": { "price": { "lt": 1000 } } }, "weight": 2 } ], // 3. 得分合并规则 "score_mode": "sum" // 多个函数权重求和 "boost_mode": "multiply", // 函数权重 × 基础检索得分 } }, // ========== 第二部分:非置顶酒店(得分固定为0) ========== { "constant_score": { "filter": { "bool": { "must_not": [ // 排除和置顶候选相同的文档(避免重复) { "match": { "name": { "query": "皇冠假日 上海 豪华", "operator": "or" } } } ] } }, "boost": 0 // 非置顶酒店得分强制为0(核心配置) } } ] } } }细节1:以前写的应该是"name":"皇冠假日 上海 豪华",这其实是以下的简写,or表示的是有其中一个分词即可,and表示必须这些分词都包含,要想完成更复杂的业务逻辑就得详细写。
细节2:以前之所以只有boost_mode是因为以前只写了一个过滤条件,现在写了好几个条件,如果有一个酒店满足所有的条件,那么它的第二个得分就是score_mode控制的,boost_mode控制的是最终得分。
细节3:这是一个新的算分复合查询,顾名思义就是常数算分,直接给过滤条件的数据赋值为0。还有就是利用must_not里面写成和第一部分一样的来排除置顶。
这样就查到了两部分数据,一部分就是正分的置顶数据,而且按照我们的意思给分了,一部分就是全为0的非置顶数据。然后就很简单了,排序即可:
"sort": [ // 维度1:优先按得分降序(置顶酒店>0,非置顶=0 → 置顶永远在前) { "_score": { "order": "desc" } }, // 维度2:同得分的文档(仅非置顶),按距离由近到远排序 { "_geo_distance": { "location": { "lat": 31.034661, "lon": 121.612282 }, // 目标坐标 "order": "asc", "unit": "km" // 距离单位:公里 } } ]细节:
这个_score就是代表按照得分排,这个字段里面会包含第一步查询出来的得分。这是一个新的语法,之前我们说的只能根据字段排,因为排序的本意不就是不按照它的得分排嘛,所以这个不常用。
总体就是:其他业务的时候直接复制这个进行,里面的细节你可以改一下。太牛逼了我
GET /hotel/_search { "query": { "bool": { "should": [ { "function_score": { "query": { "match": { "name": "云南" } }, "functions": [ { "filter": { "range": { "score": { "gte": 48 } } }, "weight": 5 }, { "filter": { "range": { "price": { "lt": 1000 } } }, "weight": 2 } ], "score_mode": "sum", "boost_mode": "multiply" } }, { "constant_score": { "filter": { "bool": { "must_not": [ { "match": { "name": "云南" } } ] } }, "boost": 0 } } ] } }, "sort": [ { "_score": { "order": "desc" } }, { "_geo_distance": { "location": { "lat": 31.034661, "lon": 121.612282 }, "order": "asc", "unit": "km" } } ] }这是结果,看见没sort列表里第一个得分是0,说明不属于置顶的,第二个得分就是地理位置远近。都符合
6.5.2分页:分页查询语法很简单,from和size就类似于limit的参数一样。
GET /hotel/_search
{
"query": {
"match_all": {}
},
"from": 0, // 分页开始的位置,默认为0
"size": 10, // 期望获取的文档总数
}
而如果是深度查询呢?
比如第990~第1000条数据:elasticsearch内部分页时,必须先查询 0~1000条,然后截取其中的990 ~ 1000的这10条:比如查询TOP1000,如果es是单点模式,这并无太大影响。但是elasticsearch将来一定是集群,例如我集群有5个节点,我要查询TOP1000的数据,并不是每个节点查询200条就可以了。因为节点A的TOP200,在另一个节点可能排到10000名以外了。因此要想获取整个集群的TOP1000,必须先查询出每个节点的TOP1000,汇总结果后,重新排名,,重新截取TOP1000,这样的查询量是很巨大的。当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力,因此elasticsearch会禁止from+ size 超过10000的请求。那如何深度分页呢?其实我们可以直接从源头上禁止,就不允许这样分页就行了,如果不得不分页呢?针对深度分页,ES提供了两种解决方案: - search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。 - scroll:原理将排序后的文档id形成快照,保存在内存。官方已经不推荐使用。 总结: - `from + size`: - 优点:支持随机翻页 - 缺点:深度分页问题,默认查询上限(from + size)是10000 - 场景:百度、京东、谷歌、淘宝这样的随机翻页搜索 - `after search`: - 优点:没有查询上限(单次查询的size不超过10000) - 缺点:只能向后逐页查询,不支持随机翻页 - 场景:没有随机翻页需求的搜索,例如手机向下滚动翻页 - `scroll`: - 优点:没有查询上限(单次查询的size不超过10000) - 缺点:会有额外内存消耗,并且搜索结果是非实时的 - 场景:海量数据的获取和迁移。从ES7.1开始不推荐,建议用 after search方案
6.5.3高亮:
高亮是因为我们给前端返回的Java直接就带了<em>标签,如何前端写一个css样式即可,因为这样才能达到动态的效果。

样例:
GET /hotel/_search
{
"query": {
"match": {
"FIELD": "TEXT" // 查询条件,高亮一定要使用全文检索查询
}
},
"highlight": {
"fields": { // 指定要高亮的字段
"FIELD": {
"pre_tags": "<em>", // 用来标记高亮字段的前置标签
"post_tags": "</em>" // 用来标记高亮字段的后置标签
}
}
}
}
注意的点:
- 高亮是对关键字高亮,因此搜索条件必须带有关键字,而不能是范围这样的查询,而且必须是全文检索查询。 - 默认情况下,高亮的字段,必须与搜索指定的字段一致,否则无法高亮,如果要对非搜索字段高亮,则需要添加一个属性:required_field_match=false,比如下面的收缩字段是all,但是要对name字段进行高亮,所以不一致,所以要改属性。

更多推荐
elastic stack(ELK)是以elasticsearch为核心的技术栈,
包括beats、Logstash、kibana、elasticsearch,这是这个技术栈的基本架构,数据可视化,数据抓取的技术栈不一定要依靠这三个,但数据检索es是必不可少的。
逐行扫描,也就是全表扫描,随着数据量增加,其查询效率也会越来越低。当数据量达到数百万时,就是一场灾难。
2.2.倒排索引
倒排索引中有两个非常重要的概念:
- 文档(`Document`,类比于mysql中的一条数据):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息
- 词条(`Term`):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条
创建倒排索引是对正向索引的一种特殊处理,流程如下:
- 将每一个文档的数据利用算法分词,得到一个个词条
- 创建表,每行数据包括词条、词条所在文档id、位置等信息
- 因为词条唯一性,可以给词条创建索引,例如hash表结构索引
1)用户输入条件`"华为手机"`进行搜索。
2)对用户输入内容分词,得到词条:`华为`、`手机`。
3)拿着词条在倒排索引中查找,可以得到包含词条的文档id:1、2、3。
4)拿着文档id到正向索引中查找具体文档。
虽然要先查询词条索引,再查询id正向索引,但是无论是词条、还是文档id都建立了索引,查询速度非常快!无需全表扫描。
2.3对比:
为什么一个叫做正向索引,一个叫做倒排索引呢?
正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是
一个文档中往往包含很多的字段(Field),类似于数据库中的列,比如这里的id,title等都是一个字段。
数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束(唯一约束,主键约束等)。
3.3互相补充
Mysql:擅长事务类型操作,可以确保数据的安全和一致性,Elasticsearch:擅长海量数据的搜索、分析、计算。因此在企业中,往往是两者结合使用:
- 对安全性要求较高的写操作,使用mysql实现
- 对查询性能要求较高的搜索需求,使用elasticsearch实现
- 两者再基于某种方式,实现数据的同步,保证一致性(这里使用MQ)



里面location换了一种写法,其实写成"location":"31,121"也可以。



所有评论(0)