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建索,分别是url
、title
、text
、declareTime
,他们的数据类型分别是keyword、text、text、Date;其中Date的格式为xxxx-xx-xx
其中text类型均采用ik_max_word
保证最大限度的分词,为了实现搜索提示,可以定义title的第二个type为completion,方便自动补全。综上,新建索引的操作如下
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策略以作为高亮显示和按时间范围搜索的基础。
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插件保证安全性的,连接的时候要创建使用许可证才能成功连接
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
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(); // 关闭资源
}
新建索引
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;
}
}
删除索引
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();
}
}
添加文档
public ResultEntry add(ResultEntry entry) {
try {
client.index(i -> i
.index(EsUtil.index).document(entry));
} catch (IOException e) {
return null;
}
return entry;
}
全文检索
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);
}
按时间范围全文检索
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);
}