分布式搜索(1)
分布式搜索(1)
Elasticsearch
一个开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能
概念
elastic stack(ELK): 是以elasticsearch为核心的技术栈,包括beats、Logstash、kibana、elasticsearch
正向索引:根据id索引来查找文档
倒排索引:根据搜索的词条找id再找文档
安装es
创建一个网络, 让es和kibana容器互联
1 |
|
拉取镜像(体积比较大,下载时间可能比较长)
1
docker pull elasticsearch:7.12.1
运行
1
2
3
4
5
6
7
8
9
10
11docker 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 "cluster.name=es-docker-cluster"
:设置集群名称-e "http.host=0.0.0.0"
:监听的地址,可以外网访问-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
:端口映射配置
安装kibana
kibana可以给我们提供一个elasticsearch的可视化界面
拉取镜像(一定要和es版本一样)
1
docker pull kibana:7.12.1
运行
1
2
3
4
5
6docker 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
:端口映射配置
安装ik
es内置的分词器不支持中文。ik分词器是一个标准的中文分词器
在线安装(速度慢)
1
2
3
4
5
6
7
8
9
10# 进入容器内部
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离线安装
1
2
3
4
5
6
7# 查看es插件目录挂载的数据卷
docker volume inspect es-plugins
# 将ik分词器上传到数据卷内
# 重启容器
docker restart es测试
1
2
3
4
5GET /_analyze
{
"analyzer": "ik_max_word",
"text": "上海自来水来自上海"
}
IK分词器包含两种模式:
ik_smart
:最少切分ik_max_word
:最细切分
扩展词词典
IK分词器提供了扩展词汇的功能
ik/config目录下的IKAnalyzer.cfg.xml配置文件可以添加扩展词典
1
2
3
4
5
6
7<?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>创建ext_dict,在文件中添加词汇,并放到ik/config里
1
2
3# ext_dict文件
哈哈哈哈哈哈
传智播客重启es
1
docker restart es
ES基本操作
索引库操作
类似mysql的表
mapping是对索引库中文档的约束
常见的mapping属性包括:
- type:字段数据类型,常见的简单类型有:
- 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
- 数值:long、integer、short、byte、double、float、
- 布尔:boolean
- 日期:date
- 对象:object
- index:是否创建索引,默认为true
- analyzer:使用哪种分词器
- properties:该字段的子字段
示例:
1 |
|
文档操作
类似mysql的数据
示例:
1 |
|
RESTAPI
ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。官方文档地址:https://www.elastic.co/guide/en/elasticsearch/client/index.html
Java Rest Client又包括两种:
- Java Low Level Rest Client
- Java High Level Rest Client
设计mapping映射要考虑的信息包括:
- 字段名
- 字段数据类型
- 是否参与搜索
- 是否需要分词
- 如果分词,分词器是什么?
创建客户端
通过客户端的api操作es
导入依赖
1
2
3
4
5
6
7
8
9
10
11<properties>
<java.version>1.8</java.version>
<!--版本要和es的版本一致-->
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>
<!--RestHighLevelClient依赖-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>通过new的方式创建客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25package cn.itcast.hotel;
public class HotelIndexTest {
private RestHighLevelClient client;
@Test
public void test() {
System.out.println(client);
}
//初始化
@BeforeEach
void setup() {
client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.229.128:9200")
));
}
//结束后销毁
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
}
创建索引库
准备创建语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54package cn.itcast.hotel.constants;
public class ESConstants {
public static String MAPPING_TEMPLATE = "{\n" +
" \"mappings\": {\n" +
" \"properties\": {\n" +
" \"id\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"name\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"address\":{\n" +
" \"type\": \"keyword\",\n" +
" \"index\": false\n" +
" },\n" +
" \"price\":{\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"score\":{\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"brand\":{\n" +
" \"type\": \"keyword\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"city\":{\n" +
" \"type\": \"keyword\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"starName\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"business\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"location\":{\n" +
" \"type\": \"geo_point\"\n" +
" },\n" +
" \"pic\":{\n" +
" \"type\": \"keyword\",\n" +
" \"index\": false\n" +
" },\n" +
" \"all\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
}通过client发送创建索引库请求
1
2
3
4
5
6
7
8
9
10
11
12/**
* 创建索引库
*/
@Test
public void createIndex() throws IOException {
//创建request请求对象
CreateIndexRequest createIndexRequest = new CreateIndexRequest("hotel");
//设置请求参数
createIndexRequest.source(MAPPING_TEMPLATE, XContentType.JSON);
//返送请求
client.indices().create(createIndexRequest, RequestOptions.DEFAULT);
}
删除索引库
通过client发送删除引库请求
1
2
3
4
5
6
7
8
9
10/**
* 删除索引库
*/
@Test
public void deleteIndex() throws IOException {
//创建request请求对象
DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("hotel");
//发送请求
client.indices().delete(deleteIndexRequest, RequestOptions.DEFAULT);
}
查询索引库
通过client发送查询引库请求
1
2
3
4
5
6
7
8
9
10
11/**
* 查询索引库
*/
@Test
public void getIndex() throws IOException {
//创建request请求对象
GetIndexRequest getIndexRequest = new GetIndexRequest("hotel");
//发送请求
boolean exists = client.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
System.out.println(exists ? "索引库存在" : "索引库不存在");
}
新增文档
通过client发送新增文档请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43package cn.itcast.hotel;
@SpringBootTest
public class HotelDocumentTest {
private RestHighLevelClient client;
@Autowired
private IHotelService iHotelService;
/**
* 新增文档
* @throws IOException
*/
@Test
public void createDoc() throws IOException {
//从数据库中获取hotel对象
Hotel hotel = iHotelService.getById(61083L);
//数据库中的对象属性和索引库中不一致,要转换一下
HotelDoc hotelDoc = new HotelDoc(hotel);
//创建request请求
IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());
//设置请求参数
request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
//发送请求
client.index(request, RequestOptions.DEFAULT);
}
@BeforeEach
void setup() {
client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.229.128:9200")
));
}
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
}
查询文档
通过client发送查询文档请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/**
* 查询文档
* @throws IOException
*/
@Test
public void GetDoc() throws IOException {
//创建请求
GetRequest request = new GetRequest("hotel", "61083");
//发送请求,获得响应结果
GetResponse response = client.get(request, RequestOptions.DEFAULT);
//将响应结构转为对象
String json = response.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println(hotelDoc);
}
修改文档
通过client发送修改文档请求,全量修改和新增操作一样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/**
* 更新文档(局部修改)
* @throws IOException
*/
@Test
public void UpdateDoc() throws IOException {
//创建请求
UpdateRequest request = new UpdateRequest("hotel", "61083");
//设置请求参数
request.doc(
"price", "999"
);
//发送请求
client.update(request, RequestOptions.DEFAULT);
}
删除文档
通过client发送删除文档请求
1
2
3
4
5
6
7
8
9
10
11/**
* 删除文档
* @throws IOException
*/
@Test
public void deleteDoc() throws IOException {
//创建请求
DeleteRequest request = new DeleteRequest("hotel", "61083");
//发送请求
client.delete(request, RequestOptions.DEFAULT);
}
批量新增文档
通过client发送批量新增文档请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/**
* 批量新增文档
* @throws IOException
*/
@Test
public void bulkCreateDoc() throws IOException {
//创建请求
BulkRequest request = new BulkRequest();
//查询数据库中所有hotel信息
List<Hotel> hotels = iHotelService.list();
//将hotel转换为hotelDoc,再封装到请求中
for (Hotel hotel : hotels) {
HotelDoc hotelDoc = new HotelDoc(hotel);
request.add(new IndexRequest("hotel")
.id(hotel.getId().toString())
.source(JSON.toJSONString(hotelDoc),XContentType.JSON)
);
}
//发送请求
client.bulk(request, RequestOptions.DEFAULT);
}在kibana中用
GET /hotel/_search
可查看所有文档
DSL查询语法
常见查询类型:
- 查询所有
- 全文检索查询: 对搜索条件分词得到词条,然后通过词条来查询相关字段
- 精准查询:不会对搜索条件分词
- 地理查询:给定坐标来查询
- 复合查询:将其它简单查询组合起来,实现更复杂的搜索逻辑
通用查询语法
1 |
|
查询所有
match_all
实例
1
2
3
4
5
6
7# 查询hotel索引库中所有信息
GET /hotel/_search
{
"query": {
"match_all":{}
}
}
全文检索查询
match: 单字段查询
multi_match: 多字段查询
match实例
1
2
3
4
5
6
7
8
9# 对内容进行分词,然后查询"all"字段
GET /hotel/_search
{
"query": {
"match": {
"all": "外滩 如家"
}
}
}multi_match实例
1
2
3
4
5
6
7
8
9
10# 对内容进行分词,然后查询"brand", "name", "business"三个字段
GET /hotel/_search
{
"query": {
"multi_match": {
"query": "外滩 如家",
"fields": ["brand", "name", "business"]
}
}
}
精准查询
term:根据词条精确值查询
range:根据值的范围查询
term实例
1
2
3
4
5
6
7
8
9
10
11# 查询"city"字段为"上海"的信息
GET /hotel/_search
{
"query": {
"term": {
"city": {
"value": "上海"
}
}
}
}range实例
1
2
3
4
5
6
7
8
9
10
11
12
13# 查询"price"字段值为[200, 300]之间的信息
# gte: greater then equals , lte: less then equals
GET /hotel/_search
{
"query": {
"range": {
"price": {
"gte": 200,
"lte": 300
}
}
}
}
地理查询
geo_distance:指定坐标和半径来查询范围内的数据
geo_bounding_box:指定两个坐标,查询形成的矩形内的数据
geo_distance实例
1
2
3
4
5
6
7
8
9
10# 以"31.21,121.5" 为点做半径为15km的圆
GET /hotel/_search
{
"query": {
"geo_distance":{
"distance": "15km",
"location": "31.21,121.5"
}
}
}geo_bounding_box实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17GET /hotel/_search
{
"query": {
"geo_bounding_box": {
"location": {
"top_left": {
"lat": 31.1,
"lon": 121.5
},
"bottom_right": {
"lat": 30.9,
"lon": 121.7
}
}
}
}
}
复合查询
function score:算分函数查询,可以控制文档相关性算分,控制文档排名
bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索
function score
elasticsearch会根据词条和文档的相关度做打分,算法由两种:
TF-IDF算法
BM25算法,elasticsearch5.1版本后采用的算法
function score实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24# 复合查询
GET /hotel/_search
{
"query": {
"function_score": {
"query": { //原始查询条件
"match": {
"all": "外滩"
}
},
"functions": [
{
"filter": { //过滤查到的信息
"term": {
"brand": "如家"
}
},
"weight": 1
}
]
, "boost_mode": "sum" //sum模式为_score = 原始分数 + weight
}
}
}
bool query
布尔查询是一个或多个查询子句的组合,每一个子句就是一个子查询。子查询的组合方式有:
- must:必须匹配每个子查询,类似“与”
- should:选择性匹配子查询,类似“或”
- must_not:必须不匹配,不参与算分,类似“非”
- filter:必须匹配,不参与算分
bool query实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35# 名字中包含"如家", 价格400以上,范围10km之内
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "如家"
}
}
],
"must_not": [
{
"range": {
"price": {
"gt": 400
}
}
}
],
"filter": [
{
"geo_distance": {
"distance": "10km",
"location": {
"lat": 31.21,
"lon": 121.5
}
}
}
]
}
}
}
搜索结果处理
排序
sort
实例1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# 按分数降序,价格升序排序
GET /hotel/_search
{
"query": {
"match_all": {}
}
, "sort": [
{
"score": "desc"
},
{
"price": "asc"
}
]
}实例2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19# 按距离升序排序,单位"km"
GET /hotel/_search
{
"query": {
"match_all": {}
}
, "sort": [
{
"_geo_distance": {
"location": {
"lat": 31,
"lon": 121
},
"order": "asc",
"unit": "km"
}
}
]
}
分页
from、size
深度分页问题:
elasticsearch内部分页时,要查990 ~ 1000的这10条,必须先查询 0~1000条,然后截取其中的990 ~ 1000的这10条。当es集群时,若要查前1000的文档,就得先查询所有节点的前1000,再将这些文档重新排名后选出前1000,对于节点有很大压力。
因此elasticsearch会禁止from+ size 超过10000的请求。
实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# 从0号开始查询20条数据
GET /hotel/_search
{
"query": {
"match_all": {}
},
"from": 0, # 起始值,默认0
"size": 20, # 希望获取的文档总数
"sort": [
{
"price": "asc"
}
]
}
高亮
highlight
实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15GET /hotel/_search
{
"query": {
"match": {
"all": "如家"
}
},
"highlight": {
"fields": {
"name": {
"require_field_match": "false"
}
}
}
}注意:
- 高亮是对关键字高亮,因此搜索条件必须带有关键字,而不能是范围这样的查询。
- 默认情况下,高亮的字段,必须与搜索指定的字段一致,否则无法高亮
- 如果要对非搜索字段高亮,则需要添加一个属性:required_field_match=false
RestAPI查询
不一样的地方就QueryBuilders的查询条件
查询所有
- matchAllQuery实例
1 |
|
解析响应结果,就是逐层解析JSON字符串,流程如下:
searchHits
:通过response.getHits()获取,就是JSON中的最外层的hits,代表命中的结果SearchHits#getTotalHits().value
:获取总条数信息SearchHits#getHits()
:获取SearchHit数组,也就是文档数组SearchHit#getSourceAsString()
:获取文档结果中的_source,也就是原始的json文档数据
全文检索查询
matchQuery实例
1
2
3
4
5
6
7
8
9
10
11
12@Test
public void testMatch() throws IOException {
//创建request请求对象
SearchRequest request = new SearchRequest("hotel");
//组织DSL参数
request.source().query(QueryBuilders.matchQuery("all", "如家"));
//发送查询请求,获取结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//解析结果
handResponse(response);
}multiMatchQuery实例
1
2
3
4
5
6
7
8
9
10
11
12
13@Test
public void testMultiMatch() throws IOException {
//创建request请求对象
SearchRequest request = new SearchRequest("hotel");
//组织DSL参数
request.source().query(QueryBuilders.multiMatchQuery("如家",
"name", "business"));
//发送查询请求,获取结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//解析结果
handResponse(response);
}
精准查询
termQuery实例
1
2
3
4
5
6
7
8
9
10
11
12@Test
public void testTerm() throws IOException {
//创建request请求对象
SearchRequest request = new SearchRequest("hotel");
//组织DSL参数
request.source().query(QueryBuilders.termQuery("city", "上海"));
//发送查询请求,获取结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//解析结果
handResponse(response);
}rangeQuery实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14@Test
public void testTerm() throws IOException {
//创建request请求对象
SearchRequest request = new SearchRequest("hotel");
//组织DSL参数
request.source().query(QueryBuilders.rangeQuery("price")
.gte(100)
.lte(150));
//发送查询请求,获取结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//解析结果
handResponse(response);
}
复合查询
boolQuery实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14@Test
public void testBool() throws IOException {
//创建request请求对象
SearchRequest request = new SearchRequest("hotel");
//组织DSL参数
request.source().query(QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("city", "上海"))
.filter(QueryBuilders.rangeQuery("price").lte(200)));
//发送查询请求,获取结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//解析结果
handResponse(response);
}
排序
sort实例
1
2
3
4
5
6
7
8
9
10
11
12@Test
public void testSort() throws IOException {
//创建request请求对象
SearchRequest request = new SearchRequest("hotel");
//组织DSL参数
request.source().sort("price", SortOrder.DESC);
//发送查询请求,获取结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//解析结果
handResponse(response);
}
分页
from、size实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14@Test
public void testPage() throws IOException {
int page = 1, size = 4;
//创建request请求对象
SearchRequest request = new SearchRequest("hotel");
//组织DSL参数
request.source().from((page - 1) * size).size(size);
//发送查询请求,获取结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//解析结果
handResponse(response);
}
高亮
highlighter实例
1
2
3
4
5
6
7
8
9
10
11
12
13@Test
public void testHighlight() throws IOException {
//创建request请求对象
SearchRequest request = new SearchRequest("hotel");
//组织DSL参数
request.source().query(QueryBuilders.matchQuery("all", "如家"))
.highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
//发送查询请求,获取结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//解析结果
handleResponse(response);
}结果处理时得获取Highlight里的值