本文在上一章节springboot集成es基础上完成全文检索接口编写
完整代码github地址
代码使用了commons包的一些工具类,添加依赖
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.8version>
dependency>
<dependency>
<groupId>commons-collectionsgroupId>
<artifactId>commons-collectionsartifactId>
<version>3.2.2version>
<scope>compilescope>
dependency>
package app.vo;
import lombok.Data;
/**
* @author faith.huan 2019-07-21 11:01
*/
@Data
public class EsPageVO {
/**
* 搜索评分
*/
private Float score;
/**
* 文件名
*/
private String fileName;
/**
* 标题
*/
private String title;
/**
* 内容
*/
private String content;
/**
* 原文地址
*/
private String url;
/**
* 爬取时间
*/
private String crawlDate;
/**
* 写入ES时间
*/
private String toEsDate;
}
/**
* 高亮字段
*/
private final HighlightBuilder.Field[] highlightFields = new HighlightBuilder.Field[]{
new HighlightBuilder.Field("content").fragmentSize(500).numOfFragments(1).noMatchSize(500).preTags("").postTags(""),
new HighlightBuilder.Field("title").fragmentSize(150).numOfFragments(1).noMatchSize(150).preTags("").postTags("")
};
因为content字段内容很长,我们在查询是筛选掉content
字段,不进行查询该字段
/**
* 显示字段筛选,不显示content字段
*/
private final SourceFilter sourceFilter = new FetchSourceFilterBuilder().withExcludes("content").build();
根据关键词和查询类型(phrase|term)构建查询
/**
* 根据关键词和检索类型构建查询
*
* @param keyword 关键词
* @param type 查询类型
* @return QueryBuilder
*/
private QueryBuilder buildKeywordQuery(String keyword, String type) {
if ("phrase".equals(type)) {
// 使用短语匹配查询
log.debug("matchPhraseQuery,keyword:{}", keyword);
BoolQueryBuilder builder = QueryBuilders.boolQuery();
builder.should().add(QueryBuilders.matchPhraseQuery("title", keyword));
builder.should().add(QueryBuilders.matchPhraseQuery("content", keyword));
return builder;
} else {
// 使用分词查询
log.debug("multiMatchQuery,keyword:{}", keyword);
return QueryBuilders.multiMatchQuery(keyword, "content", "title");
}
}
/**
* 全文检索
*
* @param keyword 关键词
* @param type 检索类型 phrase|term
* @param page 页码
* @param pageSize 一页条数
*/
public AggregatedPage<EsPageVO> fullTextSearch(String keyword, String type,
int page, int pageSize) {
try {
QueryBuilder queryBuilder = buildKeywordQuery(keyword, type);
SearchQuery searchQuery = new NativeSearchQueryBuilder().withIndices(INDEX_NAME).withTypes(DOC_TYPE)
.withQuery(queryBuilder)
// 设置字段筛选
.withSourceFilter(sourceFilter)
// 设置高亮字段
.withHighlightFields(highlightFields)
// 设置分页
.withPageable(PageRequest.of(page, pageSize)).build();
return elasticsearchRestTemplate.queryForPage(searchQuery, EsPageVO.class,new SearchResultMapper() {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
List<EsPageVO> list = new ArrayList<>();
SearchHits hits = response.getHits();
for (SearchHit searchHit : hits) {
EsPageVO doc = new EsPageVO();
Map<String, Object> sourceMap = searchHit.getSourceAsMap();
doc.setScore(searchHit.getScore());
doc.setFileName(MapUtils.getString(sourceMap, "fileName"));
doc.setUrl(MapUtils.getString(sourceMap, "url"));
doc.setCrawlDate(MapUtils.getString(sourceMap, "crawlDate"));
doc.setToEsDate(MapUtils.getString(sourceMap, "toEsDate"));
// 高亮字段处理
Map<String, HighlightField> highlightFields = searchHit.getHighlightFields();
// 标题高亮
if (highlightFields.containsKey("title")) {
Text[] titles = highlightFields.get("title").getFragments();
doc.setTitle(titles[0].string());
} else {
log.warn("未找到标题高亮内容");
doc.setTitle(MapUtils.getString(sourceMap, "title"));
}
// 正文高亮
if (highlightFields.containsKey("content")) {
Text[] contents = highlightFields.get("content").getFragments();
doc.setContent(contents[0].string());
} else {
log.warn("未找到正文高亮内容");
doc.setContent("无正文内容");
}
list.add(doc);
}
return new AggregatedPageImpl<T>((List<T>) list, pageable, hits.getTotalHits(), hits.getMaxScore());
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
return null;
}
});
} catch (Exception e) {
log.error("高级查询发生异常", e);
return null;
}
}
package app.web;
import app.service.SearchService;
import app.vo.EsPageVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.web.bind.annotation.*;
/**
* 全文检索Controller
*
* @author faith.huan 2019-10-21 21:29:26
*/
@RestController
@RequestMapping("/search")
@Slf4j
public class SearchController {
private final SearchService searchService;
public SearchController(SearchService searchService) {
this.searchService = searchService;
}
/**
* 全文检索方法
*
* @param keyword 关键字
* @param type 检索类型
* @param page 页码
* @param pageSize 一页条数
*/
@CrossOrigin
@PostMapping("/fullTextSearch")
public AggregatedPage<EsPageVO> fullTextSearch(@RequestParam(value = "keyword", required = false, defaultValue = "") String keyword,
@RequestParam(value = "type", required = false, defaultValue = "") String type,
@RequestParam(value = "page", required = false, defaultValue = "0") int page,
@RequestParam(value = "pageSize", required = false, defaultValue = "10") int pageSize
) {
log.info("keyword:{}, type:{},page:{},pageSize:{}", keyword, type, page, pageSize);
return searchService.fullTextSearch(keyword, type, page, pageSize);
}
}
我使用的是谷歌插件RESTer
进行的测试,你也可以选择postman
等其他测试rest接口的工具。
设置关键词为权威指南
,查询类型为phrase
,点击send进行测试
返回json,只查询到一个文档,文档包含权威指南
这个短语
{
"content": [
{
"score": 12.266467,
"fileName": "f927fb4b4850e60ff42bcbd4d9d7bb96.json",
"title": "如何读这本书",
"content": "这本权威指南不仅会帮助你学习 Elasticsearch,而且希望能够带你接触一些更深入、更有趣的话题,如 、 、 和 ,这些虽然不是必要的阅读却能让你深入理解其内在机制。 本书的第一部分应该按章节顺序阅读,因为每一章建立在上一章的基础上(尽管你也可以浏览刚才提到的章节)。 后续各章节如 和 相对独立,你可以按需选择性参阅。",
"url": "https://www.elastic.co/guide/cn/elasticsearch/guide/current/_how_to_read_this_book.html",
"crawlDate": "2019-10-20T09:56:54.924",
"toEsDate": "2019-10-20T10:27:47.135+0800"
}
],
"pageable": {
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"offset": 0,
"pageSize": 10,
"pageNumber": 0,
"paged": true,
"unpaged": false
},
"facets": [],
"aggregations": null,
"scrollId": null,
"maxScore": 12.266467,
"totalElements": 1,
"totalPages": 1,
"number": 0,
"size": 10,
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"numberOfElements": 1,
"first": true,
"last": true,
"empty": false
}
关键词仍然用权威指南
,查询类型改为term
,点击send进行测试
查询返回json,查询到三个文档,其中第二、三个文档只包含指南
关键字
{
"content": [
{
"score": 12.266468,
"fileName": "f927fb4b4850e60ff42bcbd4d9d7bb96.json",
"title": "如何读这本书",
"content": "这本权威指南不仅会帮助你学习 Elasticsearch,而且希望能够带你接触一些更深入、更有趣的话题,如 、 、 和 ,这些虽然不是必要的阅读却能让你深入理解其内在机制。 本书的第一部分应该按章节顺序阅读,因为每一章建立在上一章的基础上(尽管你也可以浏览刚才提到的章节)。 后续各章节如 和 相对独立,你可以按需选择性参阅。",
"url": "https://www.elastic.co/guide/cn/elasticsearch/guide/current/_how_to_read_this_book.html",
"crawlDate": "2019-10-20T09:56:54.924",
"toEsDate": "2019-10-20T10:27:47.135+0800"
},
{
"score": 6.866167,
"fileName": "6e937165ca8899fab217ff535cb3335b.json",
"title": "部署",
"content": "这一章不是在生产中运行集群的详尽指南,但是它涵盖了集群上线之前需要考虑的关键事项。 主要包括三个方面:",
"url": "https://www.elastic.co/guide/cn/elasticsearch/guide/current/deploy.html",
"crawlDate": "2019-10-20T09:57:07.270",
"toEsDate": "2019-10-20T10:27:47.063+0800"
},
{
"score": 5.19139,
"fileName": "dff9ac1115ab6ffdd6141c8b2b7ce91e.json",
"title": "前言",
"content": "无论你是需要全文搜索,还是结构化数据的实时统计,或者两者结合,这本指南都能帮助你了解其中最基本的概念, 从最基本的操作开始学习 Elasticsearch。之后,我们还会逐渐开始探索更加高级的搜索技术,不断提升搜索体验来满足你的需求。 Elasticsearch 不仅仅只是全文搜索,我们还将介绍结构化搜索、数据分析、复杂的人类语言处理、地理位置和对象间关联关系等。 我们还将探讨为了充分利用 Elasticsearch 的水平伸缩性,应当如何建立数据模型,以及在生产环境中如何配置和监控你的集群。",
"url": "https://www.elastic.co/guide/cn/elasticsearch/guide/current/preface.html",
"crawlDate": "2019-10-20T09:56:55.336",
"toEsDate": "2019-10-20T10:27:47.120+0800"
}
],
"pageable": {
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"offset": 0,
"pageSize": 10,
"pageNumber": 0,
"paged": true,
"unpaged": false
},
"facets": [],
"aggregations": null,
"scrollId": null,
"maxScore": 12.266468,
"totalElements": 3,
"totalPages": 1,
"number": 0,
"size": 10,
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"numberOfElements": 3,
"first": true,
"last": true,
"empty": false
}
```
至此全文检索接口编写、测试结束。