共计 9454 个字符,预计需要花费 24 分钟才能阅读完成。
这期内容当中丸趣 TV 小编将会给大家带来有关如何使用 elasticsearch 搭建自己的搜索系统,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。
什么是 elasticsearch#
Elasticsearch 是一个开源的高度可扩展的全文搜索和分析引擎,拥有查询近实时的超强性能。
大名鼎鼎的 Lucene 搜索引擎被广泛用于搜索领域,但是操作复杂繁琐,总是让开发者敬而远之。而 Elasticsearch 将 Lucene 作为其核心来实现所有索引和搜索的功能,通过简单的 RESTful 语法来隐藏掉 Lucene 的复杂性,从而让全文搜索变得简单
ES 在 Lucene 基础上,提供了一些分布式的实现:集群,分片,复制等。
搜索为什么不用 MySQL 而用 es#
我们本文案例是一个迷你商品搜索系统,为什么不考虑使用 MySQL 来实现搜索功能呢?原因如下:
MySQL 默认使用 innodb 引擎,底层采用 b + 树的方式来实现,而 Es 底层使用倒排索引的方式实现,使用倒排索引支持各种维度的分词,可以掌控不同粒度的搜索需求。(MYSQL8 版本也支持了全文检索,使用倒排索引实现,有兴趣可以去看看两者的差别)
如果使用 MySQL 的 %key% 的模糊匹配来与 es 的搜索进行比较,在 8 万数据量时他们的耗时已经达到 40:1 左右,毫无疑问在速度方面 es 完胜。
es 在大厂中的应用情况 #
es 运用最广泛的是 elk 组合来对日志进行搜索分析
58 安全部门、京东订单中心几乎全采用 es 来完成相关信息的存储与检索
es 在 tob 的项目中也用于各种检索与分析
在 c 端产品中,企业通常自己基于 Lucene 封装自己的搜索系统,为了适配公司营销战略、推荐系统等会有更多定制化的搜索需求
es 客户端选型 #spring-boot-starter-data-elasticsearch#
我相信你看到的网上各类公开课视频或者小项目均推荐使用这款 springboot 整合过的 es 客户端,但是我们要 say no!
此图是引入的最新版本的依赖,我们可以看到它所使用的 es-high-client 也为 6.8.7,而 es7.x 版本都已经更新很久了,这里许多新特性都无法使用,所以版本滞后是他最大的问题。而且它的底层也是 highclient,我们操作 highclient 可以更灵活。我呆过的两个公司均未采用此客户端。
elasticsearch-rest-high-level-client#
这是官方推荐的客户端,支持最新的 es,其实使用起来也很便利,因为是官方推荐所以在特性的操作上肯定优于前者。而且该客户端与 TransportClient 不同,不存在并发瓶颈的问题,官方首推,必为精品!
搭建自己的迷你搜索系统 #
引入 es 相关依赖,除此之外需引入 springboot-web 依赖、jackson 依赖以及 lombok 依赖等。
Copy properties
es.version 7.3.2 /es.version
/properties
!-- high client--
dependency
groupId org.elasticsearch.client /groupId
artifactId elasticsearch-rest-high-level-client /artifactId
version ${es.version} /version
exclusions
exclusion
groupId org.elasticsearch.client /groupId
artifactId elasticsearch-rest-client /artifactId
/exclusion
exclusion
groupId org.elasticsearch /groupId
artifactId elasticsearch /artifactId
/exclusion
/exclusions
/dependency
dependency
groupId org.elasticsearch /groupId
artifactId elasticsearch /artifactId
version ${es.version} /version
/dependency
!--rest low client high client 以来低版本 client 所以需要引入 --
dependency
groupId org.elasticsearch.client /groupId
artifactId elasticsearch-rest-client /artifactId
version ${es.version} /version
/dependency
es 配置文件 es-config.properties
Copyes.host=localhost
es.port=9200
es.token=es-token
es.charset=UTF-8
es.scheme=http
es.client.connectTimeOut=5000
es.client.socketTimeout=15000
封装 RestHighLevelClient
Copy@Configuration@PropertySource(classpath:es-config.properties)public class RestHighLevelClientConfig { @Value( ${es.host} ) private String host; @Value(${es.port} ) private int port; @Value(${es.scheme} ) private String scheme; @Value(${es.token} ) private String token; @Value(${es.charset} ) private String charSet; @Value(${es.client.connectTimeOut} ) private int connectTimeOut; @Value(${es.client.socketTimeout} ) private int socketTimeout; @Bean
public RestClientBuilder restClientBuilder() { RestClientBuilder restClientBuilder = RestClient.builder( new HttpHost(host, port, scheme)
);
Header[] defaultHeaders = new Header[]{ new BasicHeader( Accept , */*), new BasicHeader(Charset , charSet), // 设置 token 是为了安全 网关可以验证 token 来决定是否发起请求 我们这里只做象征性配置
new BasicHeader(E_TOKEN , token)
};
restClientBuilder.setDefaultHeaders(defaultHeaders);
restClientBuilder.setFailureListener(new RestClient.FailureListener(){ @Override
public void onFailure(Node node) {
System.out.println( 监听某个 es 节点失败
}
});
restClientBuilder.setRequestConfigCallback(builder -
builder.setConnectTimeout(connectTimeOut).setSocketTimeout(socketTimeout)); return restClientBuilder;
} @Bean
public RestHighLevelClient restHighLevelClient(RestClientBuilder restClientBuilder) { return new RestHighLevelClient(restClientBuilder);
}
}
封装 es 常用操作
es 搜索系统封装源码
Copy@Servicepublic class RestHighLevelClientService {
@Autowired
private RestHighLevelClient client; @Autowired
private ObjectMapper mapper; /**
* 创建索引
* @param indexName
* @param settings
* @param mapping
* @return
* @throws IOException
*/
public CreateIndexResponse createIndex(String indexName, String settings, String mapping) throws IOException { CreateIndexRequest request = new CreateIndexRequest(indexName); if (null != settings ! .equals(settings)) { request.settings(settings, XContentType.JSON);
} if (null != mapping ! .equals(mapping)) { request.mapping(mapping, XContentType.JSON);
} return client.indices().create(request, RequestOptions.DEFAULT);
} /**
* 判断 index 是否存在
*/
public boolean indexExists(String indexName) throws IOException { GetIndexRequest request = new GetIndexRequest(indexName); return client.indices().exists(request, RequestOptions.DEFAULT);
}
/**
* 搜索
*/
public SearchResponse search(String field, String key, String rangeField, String
from, String to,String termField, String termVal,
String ... indexNames) throws IOException{ SearchRequest request = new SearchRequest(indexNames);
SearchSourceBuilder builder = new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
boolQueryBuilder.must(new MatchQueryBuilder(field, key)).must(new RangeQueryBuilder(rangeField).from(from).to(to)).must(new TermQueryBuilder(termField, termVal));
builder.query(boolQueryBuilder);
request.source(builder);
log.info([ 搜索语句为:{}] ,request.source().toString()); return client.search(request, RequestOptions.DEFAULT);
} /**
* 批量导入
* @param indexName
* @param isAutoId 使用自动 id 还是使用传入对象的 id
* @param source
* @return
* @throws IOException
*/
public BulkResponse importAll(String indexName, boolean isAutoId, String source) throws IOException{ if (0 == source.length()){ //todo 抛出异常 导入数据为空
}
BulkRequest request = new BulkRequest();
JsonNode jsonNode = mapper.readTree(source); if (jsonNode.isArray()) { for (JsonNode node : jsonNode) { if (isAutoId) { request.add(new IndexRequest(indexName).source(node.asText(), XContentType.JSON));
} else { request.add(new IndexRequest(indexName)
.id(node.get( id).asText())
.source(node.asText(), XContentType.JSON));
}
}
} return client.bulk(request, RequestOptions.DEFAULT);
}
创建索引,这里的 settings 是设置索引是否设置复制节点、设置分片个数,mappings 就和数据库中的表结构一样,用来指定各个字段的类型,同时也可以设置字段是否分词(我们这里使用 ik 中文分词器)、采用什么分词方式。
Copy @Test
public void createIdx() throws IOException { String settings = + {\n + \ number_of_shards\ : \ 2\ ,\n + \ number_of_replicas\ : \ 0\ \n + }
String mappings = + {\n + \ properties\ : {\n + \ itemId\ : {\n + \ type\ : \ keyword\ ,\n + \ ignore_above\ : 64\n + },\n + \ urlId\ : {\n + \ type\ : \ keyword\ ,\n + \ ignore_above\ : 64\n + },\n + \ sellAddress\ : {\n + \ type\ : \ text\ ,\n + \ analyzer\ : \ ik_max_word\ , \n + \ search_analyzer\ : \ ik_smart\ ,\n + \ fields\ : {\n + \ keyword\ : {\ ignore_above\ : 256, \ type\ : \ keyword\}\n + }\n + },\n + \ courierFee\ : {\n + \ type\ : \ text\n + },\n + \ promotions\ : {\n + \ type\ : \ text\ ,\n + \ analyzer\ : \ ik_max_word\ , \n + \ search_analyzer\ : \ ik_smart\ ,\n + \ fields\ : {\n + \ keyword\ : {\ ignore_above\ : 256, \ type\ : \ keyword\}\n + }\n + },\n + \ originalPrice\ : {\n + \ type\ : \ keyword\ ,\n + \ ignore_above\ : 64\n + },\n + \ startTime\ : {\n + \ type\ : \ date\ ,\n + \ format\ : \ yyyy-MM-dd HH:mm:ss\ \n + },\n + \ endTime\ : {\n + \ type\ : \ date\ ,\n + \ format\ : \ yyyy-MM-dd HH:mm:ss\ \n + },\n + \ title\ : {\n + \ type\ : \ text\ ,\n + \ analyzer\ : \ ik_max_word\ , \n + \ search_analyzer\ : \ ik_smart\ ,\n + \ fields\ : {\n + \ keyword\ : {\ ignore_above\ : 256, \ type\ : \ keyword\}\n + }\n + },\n + \ serviceGuarantee\ : {\n + \ type\ : \ text\ ,\n + \ analyzer\ : \ ik_max_word\ , \n + \ search_analyzer\ : \ ik_smart\ ,\n + \ fields\ : {\n + \ keyword\ : {\ ignore_above\ : 256, \ type\ : \ keyword\}\n + }\n + },\n + \ venue\ : {\n + \ type\ : \ text\ ,\n + \ analyzer\ : \ ik_max_word\ , \n + \ search_analyzer\ : \ ik_smart\ ,\n + \ fields\ : {\n + \ keyword\ : {\ ignore_above\ : 256, \ type\ : \ keyword\}\n + }\n + },\n + \ currentPrice\ : {\n + \ type\ : \ keyword\ ,\n + \ ignore_above\ : 64\n + }\n + }\n + }
clientService.createIndex(idx_item , settings, mappings);
}
分词技巧:
索引时最小分词,搜索时最大分词,例如 Java 知音 索引时分词包含 Java、知音、音、知等,最小粒度分词可以让我们匹配更多的检索需求,但是我们搜索时应该设置最大分词,用“Java”和“知音”去匹配索引库,得到的结果更贴近我们的目的,
对分词字段同时也设置 keyword,便于后续排查错误时可以精确匹配搜索,快速定位。
我们向 es 导入十万条淘宝双 11 活动数据作为我们的样本数据,数据结构如下所示
Copy{_id : https://detail.tmall.com/item.htm?id=538528948719\u0026skuId=3216546934499 , 卖家地址 : 上海 , 快递费 : 运费: 0.00 元 , 优惠活动 : 满 199 减 10, 满 299 减 30, 满 499 减 60, 可跨店 , 商品 ID : 538528948719 , 原价 : 2290.00 , 活动开始时间 : 2016-11-11 00:00:00 , 活动结束时间 : 2016-11-11 23:59:59 , 标题 : 【天猫海外直营】 ReFa CARAT RAY 黎珐 双球滚轮波光美容仪 , 服务保障 : 正品保证; 赠运费险; 极速退款; 七天退换 , 会场 : 进口尖货 , 现价 : 1950.00}
调用上面封装的批量导入方法进行导入
Copy @Test
public void importAll() throws IOException { clientService.importAll( idx_item , true, itemService.getItemsJson());
}
我们调用封装的搜索方法进行搜索,搜索产地为武汉、价格在 11-149 之间的相关酒产品,这与我们淘宝中设置筛选条件搜索商品操作一致。
Copy @Test
public void search() throws IOException {
SearchResponse search = clientService.search( title , 酒 , currentPrice , 11 , 149 , sellAddress , 武汉
SearchHits hits = search.getHits();
SearchHit[] hits1 = hits.getHits(); for (SearchHit documentFields : hits1) { System.out.println( documentFields.getSourceAsString());
}
}
我们得到以下搜索结果,其中_score 为某一项的得分,商品就是按照它来排序。
Copy { _index : idx_item , _type : _doc , _id : Rw3G7HEBDGgXwwHKFPCb , _score : 10.995819, _source : { itemId : 525033055044 , urlId : https://detail.tmall.com/item.htm?id=525033055044 skuId=def , sellAddress : 湖北武汉 , courierFee : 快递: 0.00 , promotions : 满 199 减 10, 满 299 减 30, 满 499 减 60, 可跨店 , originalPrice : 3768.00 , startTime : 2016-11-01 00:00:00 , endTime : 2016-11-11 23:59:59 , title : 酒嗨酒 西班牙原瓶原装进口红酒蒙德干红葡萄酒 6 只装整箱送酒具 , serviceGuarantee : 破损包退; 正品保证; 公益宝贝; 不支持 7 天退换; 极速退款 , venue : 食品主会场 , currentPrice : 151.00
}
}
扩展性思考 #
商品搜索权重扩展,我们可以利用多种收费方式智能为不同店家提供增加权重,增加曝光度适应自身的营销策略。同时我们经常发现淘宝搜索前列的商品许多为我们之前查看过的商品,这是通过记录用户行为,跑模型等方式智能为这些商品增加权重。
分词扩展,也许因为某些商品的特殊性,我们可以自定义扩展分词字典,更精准、人性化的搜索。
高亮功能,es 提供 highlight 高亮功能,我们在淘宝上看到的商品展示中对搜索关键字高亮,就是通过这种方式来实现。
高亮使用方式
上述就是丸趣 TV 小编为大家分享的如何使用 elasticsearch 搭建自己的搜索系统了,如果刚好有类似的疑惑,不妨参照上述分析进行理解。如果想知道更多相关知识,欢迎关注丸趣 TV 行业资讯频道。