一、简介
1、什么是 Elasticsearch
Elasticsearch 是一个开源的高度可扩展的全文搜索和分析引擎,拥有查询近实时的超强性能。
2、搜索为什么不用MySQL而用es
我们本文案例是一个迷你商品搜索系统,为什么不考虑使用MySQL来实现搜索功能呢?原因如下:
- MySQL默认使用innodb引擎,底层采用b+树的方式来实现,而Es底层使用倒排索引的方式实现,使用倒排索引支持各种维度的分词,可以掌控不同粒度的搜索需求。(MYSQL8版本也支持了全文检索,使用倒排索引实现,有兴趣可以去看看两者的差别)
- 如果使用MySQL的
%key%
的模糊匹配来与es的搜索进行比较,在8万数据量时他们的耗时已经达到40:1左右,毫无疑问在速度方面es完胜。
二、使用
1、ES客户端选型
elasticsearch-rest-high-level-client
这是官方推荐的客户端,支持最新的es,其实使用起来也很便利,因为是官方推荐所以在特性的操作上肯定优于前者。而且该客户端与TransportClient不同,不存在并发瓶颈的问题,官方首推,必为精品!
2、搭建
pom.xml
<properties>
<es.version>7.3.2</es.version>
</properties>
<!-- high client-->
<dependencies>
<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>
</dependencies>
配置文件
es-config.properties
es.host=localhost
es.port=9200
es.token=es-token
es.charset=UTF-8
es.scheme=http
es.client.connectTimeOut=5000
es.client.socketTimeout=15000
配置实体类
@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常用业务操作类
@Service
public class RestHighLevelClientService {
@Qualifier("restHighLevelClient")
@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);
}
/**
* 删除索引
*
* @param indexNames
* @return
* @throws IOException
*/
public AcknowledgedResponse deleteIndex(String... indexNames) throws IOException {
DeleteIndexRequest request = new DeleteIndexRequest(indexNames);
return client.indices().delete(request, RequestOptions.DEFAULT);
}
/**
* 判断 index 是否存在
*
* @param indexName
* @return
* @throws IOException
*/
public boolean indexExists(String indexName) throws IOException {
GetIndexRequest request = new GetIndexRequest(indexName);
return client.indices().exists(request, RequestOptions.DEFAULT);
}
/**
* 简单模糊匹配 默认分页为 0,10
*
* @param field
* @param key
* @param page
* @param size
* @param indexNames
* @return
* @throws IOException
*/
public SearchResponse search(String field, String key, int page, int size, String... indexNames) throws IOException {
SearchRequest request = new SearchRequest(indexNames);
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(new MatchQueryBuilder(field, key)).from(page).size(size);
request.source(builder);
return client.search(request, RequestOptions.DEFAULT);
}
/**
* 简单模糊匹配 默认分页为 0,10
*
* @param field
* @param key
* @param indexNames
* @return
* @throws IOException
*/
public SearchResponse search(String field, String key, String... indexNames) throws IOException {
SearchRequest request = new SearchRequest(indexNames);
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(new MatchQueryBuilder(field, key));
request.source(builder);
return client.search(request, RequestOptions.DEFAULT);
}
/**
* term 查询 精准匹配
*
* @param field
* @param key
* @param page
* @param size
* @param indexNames
* @return
* @throws IOException
*/
public SearchResponse termSearch(String field, String key, int page, int size, String... indexNames) throws IOException {
SearchRequest request = new SearchRequest(indexNames);
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.termsQuery(field, key)).from(page).size(size);
request.source(builder);
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.toString(), 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中文分词器)、采用什么分词方式。
分词技巧:
- 索引时最小分词,搜索时最大分词,例如”Java知音”索引时分词包含Java、知音、音、知等,最小粒度分词可以让我们匹配更多的检索需求,但是我们搜索时应该设置最大分词,用“Java”和“知音”去匹配索引库,得到的结果更贴近我们的目的,
- 对分词字段同时也设置keyword,便于后续排查错误时可以精确匹配搜索,快速定位。
扩展
1、商品搜索权重扩展
我们可以利用多种收费方式智能为不同店家提供增加权重,增加曝光度适应自身的营销策略。同时我们经常发现淘宝搜索前列的商品许多为我们之前查看过的商品,这是通过记录用户行为,跑模型等方式智能为这些商品增加权重。
2、分词扩展
也许因为某些商品的特殊性,我们可以自定义扩展分词字典,更精准、人性化的搜索。
3、高亮功能
es提供highlight高亮功能,我们在淘宝上看到的商品展示中对搜索关键字高亮,就是通过这种方式来实现。
附录:
项目地址:https://gitee.com/zukxupu/little-tools/tree/master/es-search-demo