ElasticsearchRestTemplate 是 Spring 封装 ES 客户端的一些原生 api 模板,方便实现一些查询,和 ElasticsearchTemplate 一样,但是目前 spring 推荐使用前者,是一种更高级的 REST 风格 api。
废话不多说,先上一个 demo,
@RunWith(SpringRunner.class)
@SpringBootTest
public class EsArticleControllerTest {
@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;
@Test
public void test1() {
NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
//查询条件
.withQuery(QueryBuilders.queryStringQuery("浦东开发开放").defaultField("title"))
//分页
.withPageable(PageRequest.of(0, 5))
//排序
.withSort(SortBuilders.fieldSort("id").order(SortOrder.DESC))
//高亮字段显示
.withHighlightFields(new HighlightBuilder.Field("浦东"))
.build();
List articleEntities = elasticsearchRestTemplate.queryForList(nativeSearchQuery, ArticleEntity.class);
articleEntities.forEach(item -> System.out.println(item.toString()));
}
}
这个方法是根据指定的 title 模糊查询一个列表,其中用到了几个关键类,说明一下:
elasticsearchRestTemplate.queryForList是查询一个列表,用的就是ElasticsearchRestTemplate的一个对象实例;
NativeSearchQuery :是springdata中的查询条件;
NativeSearchQueryBuilder :用于建造一个NativeSearchQuery查询对象;
QueryBuilders :设置查询条件,是ES中的类;
SortBuilders :设置排序条件;
HighlightBuilder :设置高亮显示;
下面分类具体介绍下。
这是一个原生的查询条件类,用来和 ES 的一些原生查询方法进行搭配,实现一些比较复杂的查询。
下面是 NativeSearchQuery 的一些内部属性,基本上都是 ES 的一些内部对象:
//查询条件,查询的时候,会考虑关键词的匹配度,并按照分值进行排序
private QueryBuilder query;
//查询条件,查询的时候,不考虑匹配程度以及排序这些事情
private QueryBuilder filter;
//排序条件的builder
private List sorts;
private final List scriptFields = new ArrayList<>();
private CollapseBuilder collapseBuilder;
private List facets;
private List aggregations;
//高亮显示的builder
private HighlightBuilder highlightBuilder;
private HighlightBuilder.Field[] highlightFields;
private List indicesBoost;
上述属性的值,就像 demo 的写的,通过 NativeSearchQueryBuilder 进行构建即可,最终作为 elasticsearchRestTemplate 的查询条件入参。
QueryBuilders 是 ES 中的查询条件构造器。下面结合一些具体的查询场景,分析其常用方法。
ES 中已经有 title 为 “*** 关心浦东开发开放” 的数据;
ik_smart 分词结果:
{
"tokens": [
{
"token": "***",
"start_offset": 3,
"end_offset": 6,
"type": "CN_WORD",
"position": 1
},
{
"token": "关心",
"start_offset": 6,
"end_offset": 8,
"type": "CN_WORD",
"position": 2
},
{
"token": "浦东",
"start_offset": 8,
"end_offset": 10,
"type": "CN_WORD",
"position": 3
},
{
"token": "开发",
"start_offset": 10,
"end_offset": 12,
"type": "CN_WORD",
"position": 4
},
{
"token": "开放",
"start_offset": 12,
"end_offset": 14,
"type": "CN_WORD",
"position": 5
}
]
}
精确,指的是查询关键字(或者关键字分词后),必须与目标分词结果完全匹配。
1. 指定字符串作为关键词查询,关键词支持分词
// 查询 title 字段中,包含 ” 开发”、“开放 "这个字符串的 document;相当于把" 浦东开发开放 " 分词了,再查询;
QueryBuilders.queryStringQuery(“开发开放”).defaultField(“title”);
// 不指定 feild,查询范围为所有 feild
QueryBuilders.queryStringQuery(“青春”);
// 指定多个 feild
QueryBuilders.queryStringQuery(“青春”).field(“title”).field(“content”);
2. 以关键字 “开发开放”,关键字不支持分词
QueryBuilders.termQuery(“title”, “开发开放”)
QueryBuilders.termsQuery(“fieldName”, “fieldlValue1”,“fieldlValue2…”)
3. 以关键字 “开发开放”,关键字支持分词
QueryBuilders.matchQuery(“title”, “开发开放”)
QueryBuilders.multiMatchQuery(“fieldlValue”, “fieldName1”, “fieldName2”, “fieldName3”)
模糊,是指查询关键字与目标关键字可以模糊匹配。
1. 左右模糊查询,其中 fuzziness 的参数作用是在查询时,es 动态的将查询关键词前后增加或者删除一个词,然后进行匹配
QueryBuilders.fuzzyQuery(“title”, “开发开放”).fuzziness(Fuzziness.ONE)
2. 前缀查询,查询 title 中以 “开发开放” 为前缀的 document;
QueryBuilders.prefixQuery(“title”, “开发开放”)
3. 通配符查询,支持 * 和?,?表示单个字符;注意不建议将通配符作为前缀,否则导致查询很慢
QueryBuilders.wildcardQuery(“title”, “开 * 放”)
QueryBuilders.wildcardQuery(“title”, “开?放”)
注意,
在分词的情况下,针对 fuzzyQuery、prefixQuery、wildcardQuery 不支持分词查询,即使有这种 doucment 数据,也不一定能查出来,因为分词后,不一定有 “开发开放” 这个词;
查询总结:
开发开放 放 开
queryStringQuery 目标中含有开发、开放、开发开放的 无 无
matchQuery 同 queryStringQuery 无 无
termQuery 无结果,因为它不支持分词 无 无
prefixQuery 无结果,因为它不支持分词 无 有,目标分词中以” 开 “开头的
fuzzyQuery 无结果,但是与 fuzziness 参数有关系 无 无
wildcardQuery 开发开放_无结果 开_,有 放 *,无
// 闭区间查询
QueryBuilders.rangeQuery(“fieldName”).from(“fieldValue1”).to(“fieldValue2”);
// 开区间查询,默认是 true,也就是包含
QueryBuilders.rangeQuery(“fieldName”).from(“fieldValue1”).to(“fieldValue2”).includeUpper(false).includeLower(false);
// 大于
QueryBuilders.rangeQuery(“fieldName”).gt(“fieldValue”);
// 大于等于
QueryBuilders.rangeQuery(“fieldName”).gte(“fieldValue”);
// 小于
QueryBuilders.rangeQuery(“fieldName”).lt(“fieldValue”);
// 小于等于
QueryBuilders.rangeQuery(“fieldName”).lte(“fieldValue”);
QueryBuilders.boolQuery()
QueryBuilders.boolQuery().must();// 文档必须完全匹配条件,相当于 and
QueryBuilders.boolQuery().mustNot();// 文档必须不匹配条件,相当于 not
QueryBuilders.boolQuery().should();// 至少满足一个条件,这个文档就符合 should,相当于 or
具体 demo 如下:
public void testBoolQuery() {
NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.boolQuery()
.should(QueryBuilders.termQuery(“title”, “开发”))
.should(QueryBuilders.termQuery(“title”, “青春”))
.mustNot(QueryBuilders.termQuery(“title”, “潮头”))
)
.withSort(SortBuilders.fieldSort(“id”).order(SortOrder.DESC))
.withPageable(PageRequest.of(0, 50))
.build();
List articleEntities = elasticsearchRestTemplate.queryForList(nativeSearchQuery, ArticleEntity.class);
articleEntities.forEach(item -> System.out.println(item.toString()));
}
以上是查询 title 分词中,包含 “开发” 或者 “青春”,但不能包含“潮头” 的 document;
也可以多个 must 组合。
上述 demo 中,我们使用了排序条件:
// 按照 id 字段降序
.withSort(SortBuilders.fieldSort(“id”).order(SortOrder.DESC))
注意排序时,有个坑,就是在以 id 排序时,比如降序,结果可能并不是我们想要的。因为根据 id 排序,es 实际上会根据_id 进行排序,但是_id 是 string 类型的,排序后的结果会与整型不一致。
建议:
在创建 es 的索引 mapping 时,将 es 的 id 和业务的 id 分开,比如业务 id 叫做 myId:
@Id
@Field(type = FieldType.Long, store = true)
private Long myId;
@Field(type = FieldType.Text, store = true, analyzer = "ik_smart")
private String title;
@Field(type = FieldType.Text, store = true, analyzer = "ik_smart")
private String content;
这样,后续排序可以使用 myId 进行排序。
使用如下方式分页:
@Test
public void testPage() {
NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchQuery("title", "青春"))
.withSort(SortBuilders.fieldSort("myId").order(SortOrder.DESC))
.withPageable(PageRequest.of(0, 50))
.build();
AggregatedPage<ArticleEntity> page = elasticsearchRestTemplate.queryForPage(nativeSearchQuery, ArticleEntity.class);
List<ArticleEntity> articleEntities = page.getContent();
articleEntities.forEach(item -> System.out.println(item.toString()));
}
注意,如果不指定分页参数,es 默认只显示 10 条。
查询 title 字段中的关键字,并高亮显示:
@Test
public void test() {
String preTag = "";
String postTag = "";
NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchQuery("title", "开发"))
.withPageable(PageRequest.of(0, 50))
.withSort(SortBuilders.fieldSort("id").order(SortOrder.DESC))
.withHighlightFields(new HighlightBuilder.Field("title").preTags(preTag).postTags(postTag))
.build();
AggregatedPage<ArticleEntity> page = elasticsearchRestTemplate.queryForPage(nativeSearchQuery, ArticleEntity.class,
new SearchResultMapper() {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
List<ArticleEntity> chunk = new ArrayList<>();
for (SearchHit searchHit : response.getHits()) {
if (response.getHits().getHits().length <= 0) {
return null;
}
ArticleEntity article = new ArticleEntity();
article.setMyId(Long.valueOf(searchHit.getSourceAsMap().get("id").toString()));
article.setContent(searchHit.getSourceAsMap().get("content").toString());
HighlightField title = searchHit.getHighlightFields().get("title");
if (title != null) {
article.setTitle(title.fragments()[0].toString());
}
chunk.add(article);
}
if (chunk.size() > 0) {
return new AggregatedPageImpl<>((List<T>) chunk);
}
return null;
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
return null;
}
});
List<ArticleEntity> articleEntities = page.getContent();
articleEntities.forEach(item -> System.out.println(item.toString()));
}
结果:
title = 勇立潮头——*** 关心浦东开发开放 40, content = 外交部:望