分布式搜索ES
Elastic Search简介
什么是elasticsearch?
elasticsearch是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容
elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK),被广泛应用在日志数据分析、实时监控等领域
数据可视化
- Kibana
存储、搜索、分析数据
- elasticsearch是elastic stack的核心
数据抓取
- Logstash、Beats
ElasticSearch的底层是Lucene技术(apache组织提供)
- Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发,官网:https://lucene.apache.org/
- Lucene的优势:易扩展,高性能(基于倒排索引)
- Lucene的缺点:只限于Java语言开发,学习曲线陡峭,不支持水平扩展
Elastic Search的发展
2004年Shay Banon基于Lucene开发了Compass
2010年Shay Banon重写了Compass,取名为Elasticsearch
官网地址:https://www.elastic.co/cn/
官方教程:https://www.elastic.co/guide/cn/index.html
相比与Lucene,elasticsearch具备下列优势
- 支持分布式,可水平扩展
- 提供Restful接口,可被任何语言调用
较热门的搜索引擎技术:
- Elasticsearch 开源的分布式搜索引擎
- Splunk 商业项目
- Solr Apache的开源搜索引擎
倒排索引
正向索引和倒排索引
正向索引(根据文档找到词)
传统数据库(如mysql)采用正向索引,例如给下表(tb_goods)中的id创建索引:
id | title | price |
---|---|---|
1 | 小米手机 | 3499 |
2 | 华为手机 | 4999 |
3 | 华为小米充电器 | 49 |
4 | 小米手环 | 49 |
… | … | … |
搜索 手机
select * from tb_goods where title like '%手机%'
逐条数据扫描,判断是否包含手机
,如果包含则存入结果集中
倒排索引(根据词找文档)
词条(term) | 文档id |
---|---|
小米 | 1,3,4 |
手机 | 1,2 |
华为 | 2,3 |
充电器 | 3 |
手环 | 4 |
elasticsearch采用倒排索引
- 文档(document):每条数据就是一个文档
- 词条(term):文档按照语义分成的词语 (词条不会重复创建,具有唯一性)
使用倒排索引搜索华为手机
分词,得到
华为
,手机
两个词条根据词条去词条列表查询文档id
得到每个词条所在文档id:
华为:2,3
手机:1,2
根据文档id查询文档,得到id为1,2,3的文档,存入结果集中(2的关联度较高会放在前面)
文档
elasticsearch是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息
文档数据会被序列化为JSON格式后存储在elasticsearch中
索引(Index)
相同类型的文档的集合,如商品索引,用户索引,订单索引(类似数据库表的概念)
映射(mapping):索引中文档的字段约束信息,类似表的结构约束
概念对比
MySQL | Elasticsearch | 说明 |
---|---|---|
Table | Index | 索引(index),就是文档的集合,类似数据库的表(table) |
Row | Document | 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式 |
Column | Field | 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column) |
Schema | Mapping | Mapping(映射)是索引中文档的约束,例如字段类型约束,类似数据库的表结构(Schema) |
SQL | DSL | DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD |
架构对比
- Mysql:擅长事务类型操作,可以确保数据的安全和一致性(ACID原则)
- Elasticsearch:擅长海量数据的搜索,分析,计算
ES承担了搜索操作,Mysql承担了写操作,mysql可以将数据同步到ES中(起到了互补的效果)
安装ES
部署单点es
1、创建网络
因为我们还要部署kibana容器,因此需要让es和kibana容器互联,这里先创建一个网络:
docker network create es-net
2、加载镜像
这里我们采用elasticsearch的1.12.1版本的镜像,这个镜像体积非常大,接近1G,不建议自己pull
使用镜像tar包,上传到虚拟机使用命令加载即可
docker load -i es.tar
同理kibana的tar包也是如此
3、运行
运行docker命令,部署单点es:
-e参数说明
- 配置JVM内存大小(es由java实现)
- 配置运行模式
-v参数说明
- 数据保存挂载目录
- 插件拓展目录
--network
: 加入网络
--privileged
:授予逻辑卷访问权
-p参数说明
9200端口:http端口,提供用户访问
9300端口:容器之间各个节点互联端口
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
输入192.168.119.88:9200
查看访问结果
部署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
:端口映射配置
kibana版本需要和es版本保持一致
浏览器访问192.168.119.88:5601
查看图形化界面
使用Kibana提供的Dev Tools来发送DSL语句(本质上就是发送一个RESTFUL的请求到es当中)
测试es是否连接
GET /
分词器
es在创建倒排索引时需要对文档分词,在搜索时,需要对用户输入内容分词,但默认的分词规则对中文处理并不友好,我们在kibana的DevTools中测试
POST /_analyze
{
"analyzer":"standard",
"text":"学习分布式搜索引擎ElasticSearch"
}
语法说明:
POST:请求方式
/_analyze:请求路径,这里省略了http://192.168.119.88:9200
,由kibana帮我们补充
请求参数,json风格:
- analyzer:分词器类型,这里使用的是默认的standard分词器
- text:要分词的内容
IK分词器
安装分词器
处理中文分词,一般会使用IK分词器
网址:https://github.com/medcl/elasticsearch-analysis-ik
安装IK分词器
在线拉取
# 进入容器内部
docker exec -it elasticsearch /bin/bash
# 在线下载并安装
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
#退出
exit
#重启容器
docker restart elasticsearch
离线安装
安装插件需要知道elasticsearch的plugins目录位置,而我们用了数据卷挂载,因此需要查看elasticsearch的数据卷目录,通过下面命令查看
docker volume inspect es-plugins
显示结果:
[
{
"CreatedAt": "2022-05-06T10:06:34+08:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/es-plugins/_data",
"Name": "es-plugins",
"Options": null,
"Scope": "local"
}
]
说明plugins目录被挂载到了:/var/lib/docker/volumes/es-plugins/_data
这个目录中
重启容器
#重启容器
docker restart es
#查看es日志
docker logs -f es
测试IK分词器
IK分词器包含两种模式:
ik_smart
:最少切分(粗粒度切分,分词较少)ik_max_word
:最细粒度切分(分词较多,带来的内存消耗大)
字典个性化设置
分词器都会依赖于一个字典来进行分词,我们可以为字典添加拓展词汇和停用词汇
ik分词器-拓展词库
要拓展ik分词器的词库,需要修改ik分词器目录下config目录中的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"></entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
指定拓展词所在文件名(与当前目录同级)
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">ext.dic</entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords">stopword.dic</entry>
创建ext.dic,添加扩展词即可,一行一个词语,注意必须都是UTF-8编码格式
分词器的作用
- 创建倒排索引时对文档分词
- 用户搜索时,对输入的内容分词
DSL-索引库操作
mapping属性
mapping是对索引库中文档的约束,常见的mapping属性包括:
type 字段数据类型,常见类型有
- 字符串:text(可分词的文本),keyword(精确值,例如:品牌,国家,ip地址)是否可以拆分
- 数值:long,integer,short,byte,double,float
- 布尔:boolean
- 日期:date
- 对象:object(内部有子字段)
在es中没有数组,但是允许某个字段有多个值
index:是否创建倒排索引,默认为true(是否参与倒排索引)
analyzer:使用哪种分词器(结合text类型使用)
properties:该字段的子字段
{
"age":21,
"weight":53.2,
"isMarried":false,
"info":"简介内容",
"email":"os467@qq.com",
"score":[99.1,99.5,98.9],
"name":{
"firstName":"三",
"lastName":"张"
}
}
创建索引库
ES中通过Restful请求操作索引库、文档,请求内容用DSL语句来表示
创建索引库和mapping的DSL语法如下:
PUT /索引库名称
{
"mappings":{
"properties":{
"字段名1":{
"type":"text",
"analyzer":"ik_smart"
},
"字段名2":{
"type":"keyword",
"index":"false"
},
"字段名3":{
"properties":{
"子字段":{
"type":"keyword"
}
}
},
//...略
}
}
}
创建索引库
#创建索引库
PUT /os467
{
"mappings": {
"properties": {
"info":{
"type": "text",
"analyzer": "ik_smart"
},
"email":{
"type": "keyword",
"index": false
},
"name":{
"type": "object",
"properties": {
"firstName":{
"type": "keyword"
},
"lastName":{
"type": "keyword"
}
}
}
}
}
}
查询、删除索引库
#查询索引库
GET /索引库名称
#删除索引库
DELETE /索引库名称
修改索引库
禁止修改原有字段
禁止修改索引库原有字段,因为mapping约束定义好后,es就会创建倒排索引库,如果修改原字段会对倒排索引库产生影响
添加新字段
索引库和mapping一旦创建无法修改,但可以添加新字段(新字段名不能和旧字段名重复)
PUT /索引库名称/_mapping
{
"properties":{
"新字段名":{
"type":"integer"
}
}
}
测试
#修改索引库,添加新字段
PUT /os467/_mapping
{
"properties":{
"age":{
"type":"integer"
}
}
}
DSL-文档操作
新增文档
每次写操作都会导致文档版本增加
DSL语法:
如果不加id,会由ES随机生成id
POST /索引库名称/_doc/文档id
{
"字段1":"值1",
"字段2":"值2",
"字段3":{
"子字段1":"值3",
"子字段2":"值4"
},
//...略
}
测试
#插入文档
POST /os467/_doc/1
{
"info":"os467的个人简介内容",
"email":"os467@qq.com",
"name":{
"firstName":"三",
"lastName":"张"
}
}
查询文档
GET /索引库名称/_doc/文档id
测试
#查询文档
GET /os467/_doc/1
删除文档
DELETE /索引库名称/_doc/文档id
测试
#删除文档
DELETE /os467/_doc/1
修改文档
方式一:全量修改,会删除旧文档,添加新文档(如果id不存在则新增)
PUT /索引库名称/_doc/文档id
{
"字段1":"值1",
"字段2":"值2"
//...略
}
测试
#全量修改文档
PUT /os467/_doc/1
{
"info":"os467的个人简介内容",
"email":"zhangsan@qq.com",
"name":{
"firstName":"三",
"lastName":"张"
}
}
方式二:增量修改(局部修改),修改指定字段
POST /索引库名/_update/文档id
{
"doc":{
"字段名":"新的值"
}
}
测试
#局部修改文档字段
POST /os467/_update/1
{
"doc":{
"email":"os467@qq.com"
}
}
RestClient操作索引库
以后在java开发中,我们会使用java代码来实现操作es引擎,因此我们使用es官方提供的RestClient
什么是RestClient?
ES官方提供了各种不同语言的客户端,用来操作ES,这些客户端的本质就是组装DSL语句,通过http请求发送给ES
官方文档:https://www.elastic.co/guide/en/elasticsearch/client/index.html
利用JavaRestClient实现创建、删除索引库,判断索引库是否存在
mapping要考虑的问题:
字段名、数据类型、是否参与搜索、是否分词、如果分词,分词器是什么
ES中支持两种地理坐标数据类型:
geo_point:由维度(latitude)和经度(longitude)确定的一个点,例如
“116.397128, 39.916527”
geo_shape:有多个geo_point组成的复杂几何图形,例如一条直线
“LINESTRING(-74.19231902148438, -56.6090593036236)”
字段拷贝
当我们需要对几个相关字段进行查询的时候,我们可以使用copy_to工具来高效的创建一个新的字段来存储我们需要查询的词
字段拷贝可以使用copy_to属性将当前字段拷贝到指定字段,实例:
"all":{
"type": "text",
"analyzer": "ik_max_word"
},
"brand":{
"type": "keyword",
"copy_to": "all"
}
示例
# 酒店的mapping
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"
},
"starName":{
"type": "keyword"
},
"business":{
"type": "keyword",
"copy_to": "all"
},
"location":{
"type": "geo_point"
},
"pic":{
"type": "keyword",
"index": false
},
"all":{
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
初始化JavaRestClient
引入es的RestHighLevelClient依赖坐标
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
因为SpringBoot默认的ES版本是7.6.2,所以我们需要覆盖默认的ES版本
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>
初始化RestHighLevelClient
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.119.88:9200")
));
测试
package cn.itcast.hotel;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import java.io.IOException;
public class HotelIndexTest {
private RestHighLevelClient client;
@BeforeEach
void setUp(){
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.119.88:9200")
));
}
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
}
Java索引库操作
创建索引库
步骤:
1、准备request对象,指定索引库名称
2、指定DSL,以及格式为JSON
3、获取到索引库操作对象,调用创建索引库的方法
测试
@Test
void testCreateHotelIndex(){
//1、创建Request对象,需要传入索引库名称
CreateIndexRequest request = new CreateIndexRequest("hotel");
//2、请求参数:DSL语句,MAPPING_TEMPLATE是静态常量字符串,内容是创建索引库的DSL语句
request.source(MAPPING_TEMPLATE, XContentType.JSON);
try {
//发起请求
//indices返回的对象中包含索引库操作的所有方法,索引库操作对象
client.indices().create(request, RequestOptions.DEFAULT);
} catch (IOException e) {
e.printStackTrace();
}
}
删除索引库
@Test
void testDeleteHotelIndex(){
//1、创建Request对象
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
try {
//2、发送请求
client.indices().delete(request,RequestOptions.DEFAULT);
} catch (IOException e) {
e.printStackTrace();
}
}
判断索引库是否存在
@Test
void testExistsHotelIndex(){
//1、创建Request对象
GetIndexRequest request = new GetIndexRequest("hotel");
try {
//2、发送请求
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
//3、输出
System.out.println(exists ? "索引库存在":"索引库不存在");
} catch (IOException e) {
e.printStackTrace();
}
}
Java文档操作
添加新的文档
IndexRequest
IndexRequest("hotel")
:指定索引库的名称id()
:需要接收字符串类型的参数,一般使用toString()
转换index()
:用于操作索引库文档的方法,这里用于添加文档信息
@Test
void testAddDocument(){
//根据id查询酒店数据,使用mybatisPlus查询到结果
Hotel hotel = hotelService.getById(61083L);
//转换为索引库需要的文档类型,主要是location字段的差异
HotelDoc hotelDoc = new HotelDoc(hotel);
//1、创建request对象
IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
//2、准备JSON文件,调用fastjson的api将hotelDoc数据转换为json串
request.source(JSON.toJSONString(hotelDoc),XContentType.JSON);
//3、发送请求
try {
//文档新增
client.index(request,RequestOptions.DEFAULT);
} catch (IOException e) {
e.printStackTrace();
}
}
查询文档数据
GetRequest
getSourceAsString()
:得到响应对象资源
@Test
void testGetDocumentById(){
//1、准备request
GetRequest request = new GetRequest("hotel", "61083");
//2、发送请求,得到响应
GetResponse response = null;
try {
response = client.get(request, RequestOptions.DEFAULT);
} catch (IOException e) {
e.printStackTrace();
}
//3、解析响应结果
String json = response.getSourceAsString();
//4、反序列化JSON串
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println(hotelDoc);
}
更新文档数据
修改文档数据有两种方式:
方式一:全局更新,再次写入id一样的文档,就会删除旧文档,添加新文档
方式二:局部更新,只更新部分字段
测试局部更新
UpdateRequest
@Test
void testUpdateDocumentById() throws IOException{
//1、创建request对象
UpdateRequest request = new UpdateRequest("hotel","61083");
//2、准备参数,每两个参数为一对key,value
request.doc(
"price",952,
"starName","四钻"
);
//3、更新文档
client.update(request,RequestOptions.DEFAULT);
}
删除文档数据
DeleteRequest
@Test
void testDeleteDocumentById(){
//1、创建request对象
DeleteRequest request = new DeleteRequest("hotel","61083");
//2、发送请求
try {
client.delete(request,RequestOptions.DEFAULT);
} catch (IOException e) {
e.printStackTrace();
}
}
Bulk批量处理
批量添加可以将多个请求放入一个bulk请求中做批量添加的处理
@Test
void testBulkRequest(){
//批量获取到酒店数据
List<Hotel> hotels = hotelService.list();
//1、创建Bulk请求
BulkRequest request = new BulkRequest();
//2、准备参数,添加多个新增Request
for (Hotel hotel : hotels) {
//转换为文档类型HotelDoc
HotelDoc hotelDoc = new HotelDoc(hotel);
//创建新增文档的Request对象
request.add(new IndexRequest("hotel")
.id(hotelDoc.getId().toString())
.source(JSON.toJSONString(hotelDoc),XContentType.JSON));
}
//3、发送请求
try {
client.bulk(request,RequestOptions.DEFAULT);
} catch (IOException e) {
e.printStackTrace();
}
}
ES搜索功能
DSL-查询文档
DSL Query的分类
Elasticsearch提供了基于JSON的DSL(Domain Specific Language)来定义查询
- 查询所有:查询所有数据(有分页限制),一般测试用:match_all
- 全文检索(full text)查询:利用分词器对用户输入的内容分词,然后去倒排索引库中匹配
- match_query
- multi_match_query
- 精确查询:根据精确词条值查找数据,一般是查找keyword,数值,日期,boolean等类型字段
- ids
- range
- term
- 地理(geo)查询:根据经纬度查询
- geo_distance
- geo_bounding_box
- 复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件
- bool
- function_score
DSL Query基本语法
查询的基本语法如下:
GET /indexName/_search
{
"query":{
"查询类型":{
"查询条件":"条件值"
}
}
}
查询所有
#查询所有
GET /hotel/_search
{
"query": {
"match_all": {}
}
}
全文检索查询
全文检索查询,会对用户输入内容分词,常用于搜索框搜索:
match查询:全文检索查询的一种,会对用户输入内容分词,然后去倒排索引库检索
语法:
- FIELD:指定查询的字段名称
- TEXT:用户查询的内容
GET /indexName/_search { "query":{ "match":{ "FIELD":"TEXT" } } }
测试:
GET /hotel/_search { "query": { "match": { "all": "如家外滩" } } }
multi_match查询:与match查询类似,只不过允许同时查询多个字段
语法:
- query:用户输入的内容
- fields:指定参与查询的多个字段
GET /indexName/_search { "query":{ "multi_match":{ "query":"TEXT", "fields":["FIELD1","FIELD2"] } } }
测试:
GET hotel/_search { "query": { "multi_match": { "query": "外滩如家", "fields": ["brand","name","business"] } } }
和match查询all字段内容效果差不多,但是查询字段越多性能越差,因此推荐使用all字段结合copy_to的方法
精确查询
精确查询一般是查找keyword、数值、日期、boolean等类型字段,所以不会对搜索条件分词,常见的有:
term查询:根据词条精确值查询
语法:
- FIELD:指定查询的字段名称
- value:精确查询的内容
GET /indexName/_search { "query":{ "term":{ "FIELD":{ "value":"VALUE" } } } }
测试:
#term查询 GET hotel/_search { "query": { "term": { "city": { "value": "上海" } } } }
range查询:根据值的范围查询,可以是数值,日期的范围
语法:
- FIELD:指定查询的字段名称
- gte:指定大于等于某个值 (gt:大于)
- lte:指定小于等于某个值 (lt:小于)
GET /indexName/_search { "query":{ "range":{ "FIELD":{ "gte":10, "lte":20 } } } }
测试:
#range查询 GET hotel/_search { "query": { "range": { "price": { "gte": 100, "lte": 300 } } } }
地理查询
根据经纬度查询,常见的使用场景包括
携程:搜索我附近的酒店
滴滴:搜索我附近的出租车
geo_bounding_box查询:查询geo_point值落在某个矩形范围的所有文档
语法:
- FIELD:文档中指定为geo_point的字段
GET /indexName/_search { "query":{ "geo_bounding_box":{ "FIELD":{ "top_left":{ "lat":31.1, "lon":121.5 }, "bottom_right":{ "lat":30.9, "lon":121.7 } } } } }
geo_distance查询:查询到指定中心点小于某个距离值的所有文档
语法:
- distance:到中心点的距离范围
- FIELD:指定的中心点
GET /indexName/_search { "query":{ "geo_distance":{ "distance":"15km", "FIELD":"31.21,121.5" } } }
测试:
GET /hotel/_search { "query": { "geo_distance":{ "distance":"15km", "location":"31.21,121.5" } } }
复合查询
复合(compound)查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑,例如:
function_score查询:算分函数查询,可以控制文档相关性算分,控制文档排名,例如百度竞价
当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列
ES中的相关性打分算法:
$$
TF(词条频率) = \frac {词条出现次数}{文档中词条总数}
$$
$$
TF-IDF算法
\
\
IDF(逆文档频率) = log(\frac {文档总数}{包含词条的文档总数}) \
score = \sum_i^n TF(词条频率) * IDF(逆文档频率)
$$
TF-IDF是一种统计方法,用以评估一字词对于一个文件集或一个语料库中的其中一份文件的重要程度,字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降
$$
BM25算法
\
Score(Q,d) = \sum_i^nlog(1 + \frac {N-n+0.5}{n+0.5})· (\frac {f_i}{fi+k_1·(1-b+b·\frac{dl}{avgdl})})
$$
在es5.0后使用此算法,BM25算法不会受词频影响较大,在传统TF算法中,词频越高,得分会无限增加,但BM25会趋近于水平,曲线更加平滑
语法:
query:原始查询条件,搜索文档并根据相关性打分(
query score
)filter:过滤条件,符合条件的文档才会被重新算分
weight:算分函数,算分函数的结果称为
function score
,将来会与query score
运算,得到新算分,常见的算分函数有:- weight:给一个常量值,作为函数结果(
function score
) - field_value_factor:用文档中的某个字段值作为函数结果
- random_score:随机生成一个值,作为函数结果
- script_score:自定义计算公式,公式结果作为函数结果
- weight:给一个常量值,作为函数结果(
boost_mode:加权模式,定义
function score
与query score
的运算方式,包括:- multiply:两者相乘(默认)
- replace:用
function score
替换query score
- 其它:
sum
,avg
,max
,min
```json
GET /hotel/_search
{
“query”:{
“function_score”:{
“query”:{“match”:{“all”:”外滩”}},
“functions”:[
{
“filter”:{“term”:{“id”:”1”}},
“weight”:10
}
],
“boost_mode”:”multiply”
}
}
}
**测试:**
```json
GET /hotel/_search
{
"query":{
"function_score":{
"query":{"match":{
"all":"外滩"
}},
"functions":[
{
"filter":{
"term":{
"brand":"如家"
}
},
"weight":10
}
],
"boost_mode":"sum"
}
}
}
Boolean Query查询:布尔查询是一个或多个查询子句的组合,子查询的组合方式有:
- must:必须匹配每个子查询,类似与(一般指定的是关键字)
- should:选择性匹配子查询,类似或
- must_not:必须不匹配,不参与算分,类似非
- filter:必须匹配,不参与算分(可以减少使用BM25算法带来的性能消耗)
语法:
```json
GET /hotel/_search
{
“query”:{
“bool”:{
“must”:[
{“term”:{“FIELD”:”TEXT”}}
],
“should”:[
{“term”:{“FIELD”:”TEXT”}},
{“term”:{“FIELD”:”TEXT”}}
],
“must_not”:[
{“range”:{“price”:{“lte”:500}}}
],
“filter”:[
{“range”:{“score”:{“gte”:45}}}
]
}
}
}
**测试:**
需求:搜索名字包含“如家“,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店
```json
GET /hotel/_search
{
"query":{
"bool":{
"must":[
{
"match":{"name":"如家酒店"}
}
],
"must_not": [
{
"range": {"price": {"gte": 500}}
}
],
"filter":[
{
"geo_distance":{"distance":"10km","location":"31.21,121.5"}
}
]
}
}
}
搜索结果处理
排序
elasticsearch支持对搜索结果排序,默认是根据相关度算分(_score)来排序,可以排序字段类型有:keyword类型、数值类型、地理坐标类型,日期类型等
sort参数: 指定字段和排序的方法
GET /indexName/_search
{
"query":{
"match_all":{}
},
"sort":[
{
"FIELD":"desc" //排序字段和排序方式ASC、DESC
}
]
}
需求:查询酒店信息,先按评分降序,再按价格升序
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"score":"desc"
},
{
"price": "asc"
}
]
}
需求:对酒店数据按照到指定位置坐标的距离升序排序
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_geo_distance": {
"location": {
"lat": 31.034661,
"lon": 121.612282
},
"order": "asc",
"unit": "km"
}
}
]
}
分页
elasticsearch 默认情况下只返回前十条数据,而如果要查询更多数据就需要修改分页参数了
elasticsearch中通过修改from,size参数来控制要返回的分页结果:
from参数:分页起始位置
size参数:分页单位
GET /hotel/_search
{
"query":{
"match_all":{}
},
"from":0, //分页开始的位置,默认为0
"size":10, //分页单位
"sort":[
{"price":"asc"}
]
}
优点:支持随机翻页
缺点:深度分页问题,默认查询上限(from+size)是 10000
场景:百度、京东、谷歌、淘宝这样的随机翻页搜索
深度分页问题
ES是分布式的,所以会面临深度分页问题
例如:按price排序后,获取from = 990,size = 10 的数据:
1、首先在每个数据分片上都排序并查询前1000条文档
2、然后将所有节点的结果聚合,在内存中重新排序选出1000条文档
3、最后从这1000条中,选取从990开始的10条文档
如果搜索的页数过深,或者结果集(from+size)越大,对内存和CPU的消耗也越高,因此ES设定结果集查询的上限是10000(一般从业务层面杜绝了深度分页,例如百度允许的查询页数上限是70页)
深度分页解决方案
针对深度分页,ES提供了两种解决方案官方文档:
search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据(官方推荐)
优点:没有查询上限(单次查询的size不超过10000,多次可以)
缺点:只能向后逐页查询,不支持随机翻页
场景:没有随机翻页需求的搜索,例如手机向下滚动翻页
scroll:原理是将排序数据形成快照,保存在内存(官方已经不推荐使用)
优点:没有查询上限(单次查询的size不超过10000)
缺点:会有额外内存消耗,并且搜索结果是非实时的
场景:海量数据的获取和迁移,从ES7.1开始不推荐,建议用search_after方案
高亮
把搜索结果中的关键字突出显示
服务端提前给关键字添加标签
es在返回搜索结果的时候应该给这些关键字加上标签以便于后期前端添加样式
默认情况下,ES搜索字段必须与高亮字段一致
语法:
- FIELD:指定要高亮的字段
- pre_tags、post_tags:前置标签和后置标签,默认为
em
- require_field_match:查询字段是否要和高亮字段匹配
GET /hotel/_search { "query":{ "match":{ "FIELD":"TEXT" } }, "highlight":{ "fields":{ "FIELD":{ //指定要高亮的字段 "pre_tags":"<em>", //用来标记高亮字段的前置标签 "post_tags":"</em>" //用来标记高亮字段的后置标签 } } } }
测试:
GET /hotel/_search { "query": { "match": { "all":"如家" } }, "highlight": { "fields": { "name": { "require_field_match": "false" } } } }
RestClient查询文档
快速入门
测试matchAll查询API
步骤:
1、创建SearchRequest对象
2、准备Request.source(),也就是DSL
QueryBuilders来构建查询条件
传入Request.source()的query()方法
3、发生请求得到结果
4、解析结果(参考JSON结果,从外到内,逐层解析)
API:
- source():封装了大量功能,查询,排序等
- QueryBuilders:封装了match,boolquery,range等查询
@SpringBootTest
public class HotelSearchTest {
@Autowired
private IHotelService hotelService;
private RestHighLevelClient client;
@BeforeEach
void setUp(){
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.119.88:9200")
));
}
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
@Test
void testMatchAll() throws IOException{
//1、准备Request
SearchRequest request = new SearchRequest("hotel");
//2、组织DSL参数
request.source().query(QueryBuilders.matchAllQuery());
//3、发生请求,得到响应
SearchResponse response = client.search(request,RequestOptions.DEFAULT);
//打印JSON串
//System.out.println(response);
//4、解析响应
SearchHits searchHits = response.getHits();
//4.1、获取总条数
long total = searchHits.getTotalHits().value;
System.out.println("共搜索到"+ total + "条数据");
//4.2、文档数组
SearchHit[] hits = searchHits.getHits();
//4.3、遍历
for (SearchHit hit : hits) {
//获取文档source
String json = hit.getSourceAsString();
//反序列化对象
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println("hotelDoc = "+ hotelDoc);
}
}
}
全文检索查询
全文检索查询的match和multi_match查询与match_all的API基本一致,差别就是查询条件,也就是query的部分
//参数1:指定字段,参数2:查询的内容
QueryBuilders.matchQuery("all","如家");
//多字段查询,参数1:查询的内容,参数2,3...:指定字段
QueryBuilders.multiMatchQuery("如家","name","business")
精确查询
精确查询常见的有term查询和range查询,同样利用QueryBuilders实现
//词条查询
QueryBuilders.termQuery("city","杭州");
//范围查询
QueryBuilders.rangeQuery("price").gte(100).lte(150);
复合查询-boolean query
//创建布尔查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//添加must条件
boolQuery.must(QueryBuilders.termQuery("city","杭州"));
//添加filter条件
boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250));
测试布尔查询
@Test
void testBooleanQuery() throws IOException{
//1、准备Request
SearchRequest request = new SearchRequest("hotel");
//2、组织DSL参数
request.source().query();
//2.1准备BooleanQuery
BoolQueryBuilder booleanQuery = QueryBuilders.boolQuery();
//2.2添加term查询条件
booleanQuery.must(QueryBuilders.termQuery("city","上海"));
//2.3添加range查询条件
booleanQuery.filter(QueryBuilders.rangeQuery("price").lte(250));
//构建查询请求
request.source().query(booleanQuery);
//3、发生请求,得到响应
SearchResponse response = client.search(request,RequestOptions.DEFAULT);
//打印JSON串
//System.out.println(response);
//解析响应
HandleResponse(response);
}
排序和分页
搜索结果的排序和分页是与query同级的参数,对应的API如下:
@Test
void testOrderAndPage() throws IOException {
//页码,分页单位
int page = 1,size = 5;
SearchRequest request = new SearchRequest("hotel");
//查询
request.source().query(QueryBuilders.matchAllQuery());
//分页
request.source().from((page - 1) * size).size(size);
//价格排序
request.source().sort("price", SortOrder.ASC);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
handleResponse(response);
}
根据地理坐标排序
request.source().sort(SortBuilders.geoDistanceSort("location",new GeoPoint(location))
.unit(DistanceUnit.KILOMETERS)
.order(SortOrder.ASC)
);
高亮显示
高亮API包括请求DSL构建和结果解析两部分
@Test
void testHighLight() throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source().query(QueryBuilders.matchQuery("all","如家"));
request.source().highlighter(new HighlightBuilder()
//需要高亮的字段名
.field("name")
//是否需要与查询字段匹配
.requireFieldMatch(false)
);
SearchResponse response = client.search(request,RequestOptions.DEFAULT);
SearchHit[] hits = response.getHits().getHits();
for (SearchHit hit : hits) {
HotelDoc hotelDoc = JSON.parseObject(hit.getSourceAsString(), HotelDoc.class);
//获取高亮结果属性集
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if(!CollectionUtils.isEmpty(highlightFields)){
//根据字段获取高亮结果集
HighlightField highlightField = highlightFields.get("name");
if (highlightField != null){
//获取高亮结果
Text[] fragments = highlightField.getFragments();
//获取高亮值
String name = fragments[0].string();
//覆盖非高亮结果
hotelDoc.setName(name);
}
}
System.out.println("hotelDoc = "+ hotelDoc);
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以邮件至 1300452403@qq.com