elastic search实战小demo:https://github.com/simonsfan/springboot-quartz-demo,分支:feature_es
之前在 Elastic Search之Search API(Query DSL)、Elasticsearch之索引和文档API 文章中讲到过elastic search(以下简称es)的以下常用的原生api,本篇开始讲述如何结合java开发使用es api进行索引的CRUD及复杂查询。我这里以springboot中使用为例方便测试,首先需要引入maven依赖:
org.elasticsearch.client
transport
5.6.1
org.apache.logging.log4j
log4j-core
2.7
springboot配置文件application.yml中加如下配置,注意9300是es与java交互的端口,不是es与http交互的端口号9200:
elasticsearch:
cluster:
name: elasticsearch
host: 127.0.0.1
port: 9300
建立测试用到的movie_index索引并且新增两条测试数据:
PUT /movie_index
{
"mappings": {
"info": {
"properties": {
"actors": {
"type": "text"
},
"alias": {
"type": "text"
},
"directors": {
"type": "text"
},
"introduction": {
"type": "text"
},
"label": {
"type": "text"
},
"name": {
"type": "text"
},
"score": {
"type": "float"
},
"release": {
"type": "date"
},
"area": {
"type": "keyword"
}
}
}
}
}
POST /movie_index/info
{
"name": "毒液",
"alias": "毒液",
"actors": "汤姆·哈迪 米歇尔·威廉姆斯 伍迪·哈里森 里兹·阿迈德 珍妮·斯蕾特",
"directors": "鲁本·弗雷斯彻",
"score": 9,
"area": "美国",
"label": "2018年 美国 电影 动作 好莱坞 VIP电影 VIP尊享 动作 新片",
"release": "2019",
"introduction": "017年3月17日,索尼宣布将为蜘蛛侠的死对头“毒液”(Venom)打造外传电影,并计划于2018年10月5日上映。《毒液》被视作蜘蛛侠系列的外传,将由《超凡蜘蛛侠2》的编剧艾里克斯·库兹曼(《木乃伊》)执导,《蜘蛛侠:英雄归来》的制片人马修·托马齐以及漫威影业前CEO阿维·阿拉德担任制片,由丹特·哈珀(《明日边缘》)编剧。他们表示,此片与汤姆·赫兰德主演的蜘蛛侠三部曲没什么关系,是一个独立的外传。关于此片的更多细节并未透露。 2017年3月28日,索尼确认《毒液》将以R级的形式进行开发。 2017年5月,确认英国演员汤姆·哈迪将出演漫威蜘蛛侠衍生片《毒液》,将扮演自由摄影师Eddie Brock 。 2017年6月,制片人艾米·帕斯卡尔证实影片将和漫威电影宇宙连接,作为附属电影,并且有机会让汤姆·赫兰德回归饰演蜘蛛侠。"
}
POST /movie_index/info
{
"name": "我不是药神",
"alias": "我不是药神",
"actors": "徐峥 王传君 周一围 谭卓 章宇",
"directors": "文牧野",
"score": 9.2,
"area": "内地",
"label": "国内院线 VIP电影 剧情 VIP尊享 院线 喜剧 喜剧 新片 剧情",
"release": "2018",
"introduction": "普通中年男子程勇(徐峥 饰)经营着一家保健品店,失意又失婚。不速之客吕受益(王传君 饰)的到来,让他开辟了一条去印度买药做“代购”的新事业,虽然困难重重,但他在这条“买药之路”上发现了商机,一发不可收拾地做起了治疗慢粒白血病的印度仿制药独家代理商。赚钱的同时,他也认识了几个病患及家属,为救女儿被迫做舞女的思慧(谭卓 饰)、说一口流利“神父腔”英语的刘牧师(杨新鸣 饰),以及脾气暴烈的“黄毛”(章宇 饰),几个人合伙做起了生意,利润倍增的同时也危机四伏。程勇昔日的小舅子曹警官(周一围 饰)奉命调查仿制药的源头,假药贩子张长林(王砚辉 饰)和瑞士正牌医药代表(李乃文 饰)也对其虎视眈眈,生意逐渐变成了一场关于救赎的拉锯战。"
}
首先注入elastic search的客户端TransportClient:
@Configuration
public class ElasticSearchConfig {
@Value("${elasticsearch.host}")
private String esHost;
@Value("${elasticsearch.port}")
private int esPort;
@Value("${elasticsearch.cluster.name}")
private String esName;
@Bean
public TransportClient esClient() throws UnknownHostException {
TransportClient client = null;
try {
Settings settings = Settings.builder()
.put("client.transport.sniff", true)
.put("cluster.name", this.esName)
.build();
InetSocketTransportAddress master = new InetSocketTransportAddress(InetAddress.getByName(esHost), esPort);
client = new PreBuiltTransportClient(settings).addTransportAddress(master);
} catch (Exception e) {
e.printStackTrace();
}
return client;
}
}
后续需要使用es客户端TransportClient 时直接使用@Autowired注入即可。
主要体现为文档的增删改查、批量操作等
Index API
index api表现为新增文档(如果索引不存在会直接新建),参数需要指定索引名称、类型、json数据三个参数,然后根据返回IndexResponse的状态判断是否新增成功:
@Autowired
private TransportClient transportClient;
@Autowired
private ObjectMapper objectMapper;
/**
* 新增文档
*
* @param indexTemplate
* @return
*/
@Override
public String addIndex(MovieIndexTemplate indexTemplate) {
if (indexTemplate == null) {
return ResultUtil.fail();
}
try {
IndexRequestBuilder indexRequestBuilder = transportClient.prepareIndex(INDEX, TYPE).setSource(objectMapper.writeValueAsBytes(indexTemplate), XContentType.JSON);
IndexResponse indexResponse = indexRequestBuilder.get();
if (indexResponse.status() == RestStatus.CREATED) {
logger.info("name={},新增文档成功", indexTemplate.getName());
return ResultUtil.success();
}
} catch (JsonProcessingException e) {
logger.error("");
}
return ResultUtil.fail();
}
@Autowired
private MovieSearchService movieSearchService;
@Test
public void testAddDocument() {
MovieIndexTemplate indexTemplate = new MovieIndexTemplate();
indexTemplate.setName("喜剧之王");
indexTemplate.setAlias("喜剧之王");
indexTemplate.setActors("周星驰,莫文蔚,张柏芝,吴孟达,林子善,田启文");
indexTemplate.setDirectors("周星驰 李力持");
indexTemplate.setIntroduction("《喜剧之王》是星辉海外有限公司出品的一部喜剧电影,由李力持、周星驰执导,周星驰、 莫文蔚、张柏芝等主演。该片于1999年2月13日在香港上映。影片讲述对喜剧情有独钟的尹天仇与舞女柳飘飘逐渐产生感情,之后在杜娟儿的帮助下,尹天仇终于获得机会演主角,但又陷入与柳飘飘、杜娟儿的三角恋漩涡之中");
indexTemplate.setArea("内地");
indexTemplate.setScore(9.4f);
indexTemplate.setLabel("喜剧 爱情");
indexTemplate.setRelease("1999");
movieSearchService.addIndex(indexTemplate);
}
Get API
get api用于获取指定的文档数据,接收索引名称、类型、id三个参数:
@Override
public String getIndex(String id) {
GetResponse getResponse = transportClient.prepareGet(INDEX, TYPE, id).get();
String result = getResponse.getSourceAsString();
logger.info("get api result ={},", result);
if (StringUtils.isEmpty(result)) {
return ResultUtil.fail();
}
return ResultUtil.success(result);
}
@Test
public void getIndex(){
String id = "hTCYWmgBBHn7EfncQOq6";
movieSearchService.getIndex(id);
}
get api只支持一个id,但是你可以使用multi get api来一次性传入多个id,并返回多个对应的结果集:
//multi getindex
public void getIndex2(String[] keyword) {
MultiGetResponse multiGetItemResponse = transportClient.prepareMultiGet()
.add(INDEX, TYPE, keyword[0])
.add(INDEX, TYPE, keyword[1])
.get();
for (MultiGetItemResponse getItemResponse : multiGetItemResponse) {
GetResponse response = getItemResponse.getResponse();
String json = response.getSourceAsString();
}
}
Delete API
delete api用于删除指定的文档数据,同get api类似,也是接收索引名称、类型、id三个参数:
@Override
public String deleteIndex(String id) {
DeleteRequestBuilder deleteRequestBuilder = transportClient.prepareDelete(INDEX, TYPE, id);
DeleteResponse deleteResponse = deleteRequestBuilder.get();
if (deleteResponse.status() == RestStatus.OK) {
logger.info("");
return ResultUtil.success();
}
return ResultUtil.fail();
}
@Test
public void deleteIndex(){
String id = "hTCYWmgBBHn7EfncQOq6";
movieSearchService.deleteIndex(id);
}
Delete By Query API
这个删除操作根据筛选条件来删除指定数据,比单纯的根据id进行删除灵活一些,例如下面的根据电影name字段匹配删除:
@Override
public String deleteByQueryAction(String keyword) {
BulkByScrollResponse bulkByScrollResponse = DeleteByQueryAction.INSTANCE.newRequestBuilder(transportClient).filter(QueryBuilders.matchQuery(MovieSearch.NAME, keyword)).source(INDEX).get();
long deleted = bulkByScrollResponse.getDeleted();
logger.info("根据条件keyword={}删除result={}", keyword, deleted);
if (deleted < 1) {
logger.info("根据条件keyword={}删除失败", keyword);
return ResultUtil.fail();
}
logger.info("根据条件keyword={}删除成功", keyword);
return ResultUtil.success();
}
@Test
public void deleteByQueryAction(){
String name = "喜剧之王";
movieSearchService.deleteByQueryAction(name);
}
由于这个delete by query操作可能执行较长时间,因此你可以设置异步方式执行替代直接get,并设置这个delete by query操作的监听器,在监听器里面处理成功和失败的具体逻辑:
//异步执行delete by query
public void deleteByQueryAction1(String keyword) {
DeleteByQueryAction.INSTANCE.newRequestBuilder(transportClient).filter(QueryBuilders.matchQuery(MovieSearch.NAME, keyword)).source(INDEX).execute(new ActionListener() {
@Override
public void onResponse(BulkByScrollResponse bulkByScrollResponse) {
long deleted = bulkByScrollResponse.getDeleted();
logger.info("根据条件keyword={}删除result={}", keyword, deleted);
if (deleted < 1) {
logger.info("根据条件keyword={}删除失败", keyword);
}
logger.info("根据条件keyword={}删除成功", keyword);
}
@Override
public void onFailure(Exception e) {
// Hanlder the exception……
}
});
}
Bulk API
bulk api支持批量操作,例如一个请求里面同时包含删除、新增功能:
//批量bulk操作
@Override
public void buldOption(MovieIndexTemplate indexTemplate, String id) throws JsonProcessingException {
BulkRequestBuilder bulkRequestBuilder = transportClient.prepareBulk();
bulkRequestBuilder.add(transportClient.prepareDelete(INDEX, TYPE, id));
bulkRequestBuilder.add(transportClient.prepareIndex(INDEX, TYPE).setSource(objectMapper.writeValueAsBytes(indexTemplate), XContentType.JSON));
BulkResponse bulkItemResponses = bulkRequestBuilder.get();
RestStatus status = bulkItemResponses.status();
logger.info("bulk option result status={}", status);
}
Update API和Reindex API这里不讲,用的不多,由于在elastic search中不推荐对Index进行修改,而应该直接删除再新增的方式,所以update和reindex api用的并不多。
之前在Elastic Search之Search API(Query DSL)、字段类查询、复合查询 一文中说到了es的原生Query DSL API,分为字段类查询和复合查询,字段类查询又可以分为单词类查询和全文匹配。
全文匹配:包括match、match_phrase、query_string、simple_query_string等查询语句;
//Match Query
MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery(MovieSearch.NAME, "毒液");
//Muitl Match Query
MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("", MovieSearch.NAME, MovieSearch.AREA, MovieSearch.INTRODUCTION, MovieSearch.ACTORS, MovieSearch.DIRECTORS);
//Common Terms Query
CommonTermsQueryBuilder commonTermsQueryBuilder = QueryBuilders.commonTermsQuery(MovieSearch.NAME, "毒液");
//Query String Query
QueryStringQueryBuilder queryStringQueryBuilder = QueryBuilders.queryStringQuery("毒液").field(MovieSearch.NAME).defaultOperator(Operator.OR);
//Simple Query String Query
SimpleQueryStringBuilder simpleQueryStringBuilder = QueryBuilders.simpleQueryStringQuery("毒液").field(MovieSearch.NAME);
//Term Query
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery(MovieSearch.NAME, "毒液");
//Terms Query
TermsQueryBuilder termsQueryBuilder = QueryBuilders.termsQuery(MovieSearch.NAME, "毒液", "我不是药神");
//Range Query 筛选评分在7~10分之间的数据集,includeLower(false)表示from是gt,反之;includeUpper(false)表示to是lt,反之
QueryBuilders.rangeQuery(MovieSearch.SCORE).from(7).to(10).includeLower(false).includeUpper(false);
复合查询是es中用的最多的,常见的是Bool查询,包括must、should、filter、must_not,在Elastic Search之Search API(Query DSL)、字段类查询、复合查询 一文中也有说到过其原生api。下面是综合使用Bool查询示例code:
public void boolDsl() {
//Bool Query
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
//电影名称必须包含我不是药神经过分词后的文本,比如我、不、是、药、神
boolQueryBuilder.must(QueryBuilders.matchQuery(MovieSearch.NAME, "我不是药神"));
//排除导演是张三的电影信息
boolQueryBuilder.mustNot(QueryBuilders.termQuery(MovieSearch.DIRECTORS, "张三"));
//别名中应该包含药神经过分词后的文本,比如药、神
boolQueryBuilder.should(QueryBuilders.matchQuery(MovieSearch.ALIAS, "药神"));
//评分必须大于9(因为es对filter会有智能缓存,推荐使用)
boolQueryBuilder.filter(QueryBuilders.rangeQuery(MovieSearch.SCORE).gt(9));
//name、actors、introduction、alias、label 多字段匹配"药神",或的关系
boolQueryBuilder.filter(QueryBuilders.multiMatchQuery("药神", MovieSearch.NAME, MovieSearch.ACTORS, MovieSearch.INTRODUCTION, MovieSearch.ALIAS, MovieSearch.LABEL));
String[] includes = {MovieSearch.NAME, MovieSearch.ALIAS, MovieSearch.SCORE, MovieSearch.ACTORS, MovieSearch.DIRECTORS, MovieSearch.INTRODUCTION};
SearchRequestBuilder searchRequestBuilder = transportClient.prepareSearch(INDEX).setTypes(TYPE).setQuery(boolQueryBuilder).addSort(MovieSearch.SCORE, SortOrder.DESC).setFrom(0).setSize(10).setFetchSource(includes, null);
SearchResponse searchResponse = searchRequestBuilder.get();
if (!RestStatus.OK.equals(searchResponse.status())) {
return;
}
for (SearchHit searchHit : searchResponse.getHits()) {
String name = (String) searchHit.getSource().get(MovieSearch.NAME);
//TODO
}
}
根据这些基础api做了个基于es搜索引擎的demo项目,功能比较简单,就是可以根据用户输入的文本去es中实时搜索数据集,效果如下图:
项目github:https://github.com/simonsfan/springboot-quartz-demo,分支:feature_es。要注意:由于这个项目之前是用来展示 "使用quartz实现定制化定时任务" 功能的,所以需要kafka服务,并且要安装elastic search服务。项目中有很多问题需要完善,有兴趣的小伙伴可以自己完善下。有问题欢迎留言交流!
下一篇: Elastic Search与mysql数据同步方案
参考资料: es官网https://www.elastic.co/guide/en/elasticsearch/client/java-api/6.3/index.html