0、团队项目博客

1、主要使用的技术及开发工具

  • Elasticsearch 7.17.3
  • REST API
  • Elasticsearch java API Client 7.17.3
  • Kibana 7.17.3
  • Jackson 2.12.3

2、Elasticsearch简介

Elasticsearch 是一个分布式的免费开源搜索和分析引擎,适用于包括文本、数字、地理空间、结构化和非结构化数据等在内的所有类型的数据。可用于应用程序搜索,网站搜索等诸多使用场景。

使用ES,我们可以快速的开发一个自己的搜索引擎,ES会帮我们在庞大的数据中建索

3、mapping的设计思路

有四个数据要存进ES建索,分别是urltitletextdeclareTime,他们的数据类型分别是keyword、text、text、date;其中date的格式为xxxx-xx-xx 其中text类型均采用ik_max_word保证最大限度的分词,为了实现搜索提示,可以定义title的第二个type为completion,方便自动补全。综上,新建索引的操作如下

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
PUT /link-repo2
{
"mappings": {
"properties": {
"url":{"type": "keyword"},
"title":{
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"suggest": {
"type": "completion",
"analyzer": "ik_max_word"
}
}
},
"text":{
"type": "text",
"analyzer": "ik_max_word"
},
"declareTime": {
"type": "date"
}
}
}
}

4、ES的搜索策略

4.1 Rest操作

在此课程设计中,我主要采用match分词搜索,分别在title和text中搜索,并附带相关的highlight和filler策略以作为高亮显示和按时间范围搜索的基础。

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
GET link-repo2/_search
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "软银机器人杯”2019中国机器人技能大赛:我院学子获1项季军、1项二等奖",
"fields": ["title", "text"],
"analyzer": "ik_smart"
}
}
],
"filter": [
{
"range": {
"declareTime": {
"gte": "2022-01-01"
}
}
}
]
}
},
"highlight": {
"pre_tags": "<span class=\"hit-result\">",
"post_tags": "</span>",
"fields": {
"title": {},
"text": {}
}
},
"from": 0,
"size": 10
}

4.2 全文检索功能

multi_match表示多字段的分词搜索,query为搜索内容,fields为搜索字段,analyzer表示搜索文本的分词器。

4.3 按时间范围检索功能

在bool块里,通过must和filter的组合,must实现分词搜索,filter实现过滤时间;range块中的field为字段,gte表示大于等于。这样就可实现时间范围检索

4.4 高亮检索

highlight,搜索中关注title和text,当有匹配分词出现,就套上pre_tags和post_tags标签,“高亮”出来。这样就可以实现高亮查询的功能

4.5 分页搜索

from和size的组合,可以实现分页功能

5、使用Elasticsearch java API Client连接ES

团队项目的ES是有使用xpack插件保证安全性的,连接的时候要创建使用许可证才能成功连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static ElasticsearchClient getConnect() {
// 创建许可证
final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(
AuthScope.ANY, new UsernamePasswordCredentials(USERNAME, PASSWORD));
// 导入许可证
RestClientBuilder builder = RestClient.builder(new HttpHost(URL, PORT))
.setHttpClientConfigCallback(httpAsyncClientBuilder -> httpAsyncClientBuilder
.setDefaultCredentialsProvider(credentialsProvider));
// 建立连接
restClient = builder.build();
transport = new RestClientTransport(
restClient, new JacksonJsonpMapper());
return new ElasticsearchClient(transport);
}

6、代码实现

Search.java

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package es;

import bean.ResultEntry;

import java.io.Reader;
import java.util.List;

/**
* @author 开架大飞机
* @description 搜索引擎功能接口
* @date: 2022/12/17
*/
public interface Search {
/**
* 获取搜素结果数量
* @return 搜索结果数量,类型为long
*/
long getSearchCount();

/**
* 新建索引
* @param reader 一个Reader类型的字符串,ResultFul风格的索引类型信息
* @return 新建成功返回true,失败返回false
*/
boolean newIndex(Reader reader);

/**
* 删除索引,目标为ESUtil的index
* @return 删除成功返回true,失败返回false
*/
boolean deleteIndex();

/**
* 传入一个条目,将条目插入到对应的Search中
* @param entry 被插入的条目
* @return 插入成功返回该条目,否则返回null
*/
ResultEntry add(ResultEntry entry);

/**
* 全文检索,根据text查找, 待优化
* @param searchText 待搜索文本
* @return 搜索结果放在List集合中
*/
List<ResultEntry> search(String searchText);

/**
* 全文检索,先对searchText进行分词,结果按匹配度评分降序返回对应的页数内容
* @param searchText 待搜索文本
* @param page 页数
* @return page页的searchText的搜索结果
*/
List<ResultEntry> search(String searchText, int page);

/**
* 全文检索,对searchText分词,返回在最早发布日期后、对应的页数的结果
* @param searchText 待搜索文本
* @param page 页数
* @param beginDate 最早发布日期
* @return 对应的搜索结果
*/
List<ResultEntry> search(String searchText, int page, String beginDate);

/**
* 全文检索,对searchText进行分词,返回在最早发布日期和最晚发布日期间对应页数的结果
* @param searchText 待搜索文本
* @param page 页数
* @param beginDate 最早发布日期
* @param endDate 最晚发布日期
* @return 对应的搜索结果
*/
List<ResultEntry> search(String searchText, int page, String beginDate, String endDate);

/**
* 获取搜索建议,传入前缀,返回匹配前缀的字符串集合
* @param prefix 字符串前缀
* @return 匹配的字符串集合
*/
List<String> getSearchSuggest(String prefix);
/**
* 释放Search的资源
*/
void close(); // 关闭资源
}

新建索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public boolean newIndex(Reader reader) {
CreateIndexRequest createIndexRequest = new CreateIndexRequest.Builder()
.withJson(reader)
.index(EsUtil.index)
.build();

CreateIndexResponse response = null;
try {
response = client.indices().create(createIndexRequest);
} catch (Exception e) {
// 创建索引引发的异常
}
if(response != null) {
return Objects.requireNonNullElse(response.acknowledged(), false);
} else {
return false;
}
}

删除索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public boolean deleteIndex() {
DeleteIndexResponse deleteIndexResponse = null;
try {
deleteIndexResponse = client.indices().delete(d -> d
.index(EsUtil.index));
} catch (Exception e) {
// 删除索引引发的所有异常
}
if(deleteIndexResponse == null) {
return false;
} else {
return deleteIndexResponse.acknowledged();
}
}

添加文档

1
2
3
4
5
6
7
8
9
public ResultEntry add(ResultEntry entry) {
try {
client.index(i -> i
.index(EsUtil.index).document(entry));
} catch (IOException e) {
return null;
}
return entry;
}

全文检索

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
public List<ResultEntry> search(String searchText, int page) {
// 页数从0开始编号
int value = (page - 1) * 10;
SearchResponse<ResultEntry> search = null;

try {
search = client.search(s -> s
.index(EsUtil.index)
.query(q -> q
.multiMatch(m -> m
.query(searchText)
.fields("title", "text")
.analyzer("ik_smart")))
.highlight(h -> h
.preTags("<span class=\"hit-result\">")
.postTags("</span>")
.fields("title", builder -> builder)
.fields("text", builder -> builder))
.from(value)
.size(10)
, ResultEntry.class);
} catch (IOException e) {
e.printStackTrace();
}

return dealSearchResponse(search);
}

按时间范围全文检索

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
public List<ResultEntry> search(String searchText, int page, String beginDate) {
// 页数从0开始编号
int value = (page - 1) * 10;
JsonData jsonBeginDate = JsonData.of(beginDate);
SearchResponse<ResultEntry> search = null;
try {
search = client.search(s -> s
.index(EsUtil.index)
.query(q -> q
.bool(b -> b
.must(b1 -> b1
.multiMatch(b2 -> b2
.query(searchText)
.fields("title", "text")
.analyzer("ik_smart")))
.filter(b3 -> b3
.range(b4 -> b4
.field("declareTime")
.gte(jsonBeginDate)))))
.highlight(h -> h
.preTags("<span class=\"hit-result\">")
.postTags("</span>")
.fields("title", builder -> builder)
.fields("text", builder -> builder))
.from(value)
.size(10)
, ResultEntry.class);
} catch (IOException e) {
e.printStackTrace();
}

return dealSearchResponse(search);
}

7、参考链接

官方新版java API文档

新版java api 操作示例

ES英文文档

ES中文文档

ES设置账号密码

Java连接ES有账号密码的情况