为什么要在项目中使用
搜索功能会产生巨大的资源消耗,沉重的数据库加载会拖垮整个应用性能,所以我们将搜索功能转移到一个外部的搜索服务器,减轻数据库的压力。es是基于Lucene(全文检索引擎工具包)的高性能搜索服务。使用于高并发的搜索应用程序。
- 在项目中使用 Spring Data ElasticSearch
- 简化ES的java客户端开发,底层封装了ES的客户端官方API
倒排索引
倒排索引:是搜索引擎的核心。由于正排索引的检索耗时太长,所以一般我们使用的搜索引擎都为倒排索引,是以word(关键字)作为索引。其实就是建立词语和文档的对应关系,词语哪篇文档中出现,在哪个位置出现,出现了几次。
详见:https://blog.csdn.net/zhanggaokai/article/details/76563206
ElasticSearch的优点
- 近乎实时的存储、检索数据。
- 扩展性好,可拓展至上百台服务器。
- 处理数据量大。
对比solr
solr | Es |
---|---|
利用zookeeper进行分布式管理 | 自带分布式协调管理功能 |
支持的格式多 | JSON格式 |
自动功能多 | 需通过第三方插件 |
适用于传统搜索应用 | 使用于实时搜索应用 |
为什么不使用solr
- solr在传统的搜索应用中性能高于ES,但在处理实时搜索时的效率低于ES;
- 对已有数据搜索时,solr更快,但在建立索引时,solr会产生IO阻塞,查询性能差;
- 随着数据量的增加,solr的搜索效率会减低,ES不变。
关系型数据库和ES的对应关系
关系型数据库 | 数据库 | 表(table) | 行(rows) | 列(columns) |
---|---|---|---|---|
ES服务 | 索引库 | 类型(types) | 文档(document) | 字段(fields) |
安装部署ElasticSearch,集成分词器(略)
- 参考网络文档
依赖jar包
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>3.1.3.RELEASE</version>
</dependency>
SpringDataElasticSearch注解
-
@Document : 将实体类映射成一篇文档
iddexName 指定索引库名称
type 指定索引库的类型(类似数据库中的表) -
@Id : 主键
-
@Field : 将数据库表中的列转化成文档中的Field
store: 是否存储 true存储|false不存储(默认为false)
index: 是否建立索引true建立|false不建立(默认为true)
type: 指定该Field的数据类型
analyzer: 指定建立索引时的分词器
searchAnalyzer: 指定搜索时的分词器
copyTo: 复制到哪个Field(将多Field复制到一个Field,方便指定搜索条件)
pattern: 日期类型,需要指定日期格式器
Spring整合ES
- 创建数据表对应实体类,用注解方式进行映射(略)
- 配置SpringDataElasticsearch.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:elasticsearch="http://www.springframework.org/schema/data/elasticsearch"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/elasticsearch
http://www.springframework.org/schema/data/elasticsearch/spring-elasticsearch.xsd">
<!-- 2.配置Elasticsearch客户端连接对象
cluster-nodes: 集群中的节点,如果有多个节点用逗号隔开
cluster-name: 集群的名称
-->
<elasticsearch:transport-client id="client" cluster-nodes="192.168.12.131:9300"
cluster-name="elasticsearch"/>
<!-- 1.配置ElasticsearchTemplate模板对象 -->
<bean id="elasticsearchTemplate"
class="org.springframework.data.elasticsearch.core.ElasticsearchTemplate">
<!-- 设置客户端连接对象 -->
<constructor-arg name="client" ref="client"/>
</bean>
<!-- 3.配置Elasticsearch数据访问接口,采用包扫描 -->
<elasticsearch:repositories base-package="cn.uda.es.dao"/>
</beans>
- 创建数据访问接口
//需继承ElasticsearchRepository
public interface ManDao extends ElasticsearchRepository<实体类名称,主键>{
}
基本使用(CRUD)
- CRUD使用数据访问接口
- 搜索使用elasticsearchTemplate对象(已在配置文件中创建)
添加/修改:save(对象);
public void testSave(){
Man man= new Man();
man.setName("UDA");
man.setAge(24);
// 添加或修改类型
itemDao.save(man);
}
- 注:无需先创建索引库,添加映射,在进行save操作时会进行判断,如果没有索引库和映射会自动添加。
批量添加:ManDao .saveAll(对象集合)
public void testBatchSave(){
List<Man> manList = new ArrayList<>();
for (long i = 1; i <= 1000; i++) {
Man man= new man();
man.setId(i);
man.setName("UDA" + i);
}
// 批量添加或修改类型
ManDao.saveAll(manList);
}
删除
// 根据主键ID删除:deleteById("id");
@Test
public void testDeleteById(){
manDao.deleteById(1);
}
//根据条件删除:delete(对象);
@Test
public void testDeleteByItem(){
Man man = new man();
man.setId(2);
manDao.delete(man);
}
//删除全部:deleteAll();
@Test
public void testDeleteAll(){
manDao.deleteAll();
}
查询
//根据ID查询:findById("Id");
public void testFindById(){
Man man = manDao.findById(1).get();
System.out.println(man.getId() + " " + man.getName());
}
//查询全部:findAll();
public void testFindAll(){
Iterable<man> mans = manDao.findAll();
for (Item item : items) {
System.out.println(item.getId() + "\t" + item.getTitle());
}
}
//分页查询:findAll(Pageable);
public void testFindByPage(){
// 创建分页参数封装对象(当前页面,每页大小)
Pageable pageable = PageRequest.of(0, 5);
Page<Man> page = imanDao.findAll(pageable);
List<Man> items = page.getContent();
for (Man man: mans) {
System.out.println(man.getId());
}
}
使用ES进行搜索
- 默认十条数据一页
- 使用NativeSearchQuery原生搜索查询对象
创建搜索查询对象SearchQuery - wildcard 不分词
- matchQuery分词
public void testWildcardQuery(){
// 创建搜索查询对象 (wildcardQuery: 不分词
SearchQuery query = new NativeSearchQuery(QueryBuilders.wildcardQuery("title", "华 为"));
// 分页搜索,得到合计分页对象
AggregatedPage<Item> page = esTemplate.queryForPage(query, Item.class);
System.out.println("总记录数: " + page.getTotalElements());
System.out.println("总页数: " + page.getTotalPages());
// 获取分页数据
List<Item> items = page.getContent();
for (Item item : items) {
System.out.println(item.getId() + "\t" + item.getTitle());
}
}
/** 匹配分页搜索 */
public void testMatchQuery(){
/**
* 创建搜索查询对象 (matchQuery: 分词)
* keywords 搜索fileds
* 华为小米 搜索关键字
*/
SearchQuery query = new NativeSearchQuery(QueryBuilders.matchQuery("keywords", "华为小米"));
// 创建分页参数封装对象
Pageable pageable = PageRequest.of(0, 5);
// 设置分页
query.setPageable(pageable);
// 分页搜索
AggregatedPage<Item> page = esTemplate.queryForPage(query, Item.class);
System.out.println("总记录数: " + page.getTotalElements());
System.out.println("总页数: " + page.getTotalPages());
// 获取分页数据
List<Item> items = page.getContent();
for (Item item : items) {
System.out.println(item.getId() + "\t" + item.getTitle());
}
复制fields
嵌套field
- 只能使用Map集合进行封装
项目中使用步骤
- 查询数据库查询所有商品数据
- 创建实体类,加上注解
- 导入依赖jar包
- 配置文件
- 编写数据访问接口
项目中controller编写
- 在controller中不使用原生搜索条件对象NativeSearchQuery创建搜索查询对象SearchQuery 。
- 使用原生搜索条件构建对象NativeSearchQueryBuilder创建搜索查询对象SearchQuery ,方便添加查询条件。
- wildcard 不分词
- matchQuery 分词
- matchAllQuery 查询全部(分词)
- multiMatchQuery 设置高亮时使用
/** 商品搜索服务接口实现类 */
@Service(interfaceName = "com.pinyougou.service.ItemSearchService")
public class ItemSearchServiceImpl implements ItemSearchService {
@Autowired
private ElasticsearchTemplate esTemplate;
@Autowired
private EsItemDao esItemDao;
/** 搜索方法 */
@Override
public Map<String, Object> search(Map<String,Object> params) {
/** 获取查询关键字 */
String keywords = (String)params.get("keywords");
// 创建原生的搜索条件构建对象
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
// 设置默认搜索全部
builder.withQuery(QueryBuilders.matchAllQuery());
/** 判断检索关键字是否为空 */
if (StringUtils.isNoneBlank(keywords)) {
// 设置根据条件匹配查询: keywords 复制Field
builder.withQuery(QueryBuilders .matchQuery("keywords", keywords));
}
// 构建搜索查询对象
SearchQuery query = builder.build();
// 分页查询,得到合计分页对象
AggregatedPage<EsItem> page = esTemplate
.queryForPage(query, EsItem.class);
// 定义Map集合封装返回数据
Map<String,Object> data = new HashMap<>();
// 设置总记录数
data.put("total", page.getTotalElements());
// 设置分页结果
data.put("rows", page.getContent());
return data;
}
}
高亮字段显示
高亮设置
- 创建高亮字段对象
HighlightBuilder.Field - 设置高亮字段(使用原生搜索查询构建对象设置高亮字段)
builder.withHighlightFields(高亮字段对象); - 在合计分页对象AggregatePage中转化搜索结果SearchResultMapper
AggregatedPage<EsItem> page = esTemplate.queryForPage(query, EsItem.class, new SearchResultMapper() {...}
- 高亮字段设置不能使用复制field
/** 搜索方法 */
@Override
public Map<String, Object> search(Map<String,Object> params) {
/** 获取查询关键字 */
String keywords = (String)params.get("keywords");
// 创建原生的搜索条件构建对象
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
// 设置默认搜索全部
builder.withQuery(QueryBuilders.matchAllQuery());
/** 判断检索关键字是否为空 */
if (StringUtils.isNoneBlank(keywords)) {
// 设置根据多条件匹配查询:
builder.withQuery(QueryBuilders.multiMatchQuery(keywords,
"title", "category", "brand", "seller"));
// 创建高亮字段对象
HighlightBuilder.Field field = new HighlightBuilder
.Field("title") // 设置需要高亮的字段
.preTags("<font color='red'>") // 设置高亮前缀
.postTags("</font>") // 设置高亮后缀
.fragmentSize(50); // 设置文本截断
// 设置高亮字段对象
builder.withHighlightFields(field);
}
// 构建搜索查询对象
SearchQuery query = builder.build();
// 分页查询,得到合计分页对象
AggregatedPage<EsItem> page = esTemplate.queryForPage(query, EsItem.class,
new SearchResultMapper() { // 搜索结果转化
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse sr,
Class<T> aClass, Pageable pageable) {
// 定义List集合封装分页结果
List<T> content = new ArrayList<>();
// 循环封装分页结果
for (SearchHit hit : sr.getHits()) {
// 获取搜索的文档json字符串,转化成EsItem对象
EsItem esItem = JSON.parseObject(hit.getSourceAsString(), EsItem.class);
// 获取标题高亮对象
HighlightField highlightTitle = hit.getHighlightFields().get("title");
// 判断是否有标题高亮对象
if (highlightTitle != null){
// 获取标题高亮内容字符串
String title = highlightTitle.getFragments()[0].toString();
// 设置标题高亮内容
esItem.setTitle(title);
}
// 添加到新的集合
content.add((T)esItem);
}
return new AggregatedPageImpl<T>(content,
pageable, sr.getHits().getTotalHits());
}
});
// 定义Map集合封装返回数据
Map<String,Object> data = new HashMap<>();
// 设置总记录数
data.put("total", page.getTotalElements());
// 设置分页结果
data.put("rows", page.getContent());
return data;
}
- 前端数据使用v-html即可完成高亮显示
组合查询对象
- 原生搜索查询对象添加过滤查询builder.withFilter(组合查询构建对象);
- 组合查询构建对象 BoolQueryBuilder(must必要条件)
- 范围查询构建对象 RangQueryBuilder
public Map<String, Object> search(Map<String,Object> params) {
/** 获取查询关键字 */
String keywords = (String)params.get("keywords");
......
/** 判断检索关键字是否为空 */
if (StringUtils.isNoneBlank(keywords)) {
// 设置根据多条件匹配查询
// 创建高亮字段对象
......
}
/** ############# 过滤查询 ########### */
// 1. 创建组合查询构建对象,封装多个过滤条件
BoolQueryBuilder boolBuilder = QueryBuilders.boolQuery();
// 2. 组合多个过滤条件(必须的方式组合)
// 2.1 按商品分类过滤
String category = (String)params.get("category");
if (StringUtils.isNoneBlank(category)){
// 词条查询
boolBuilder.must(QueryBuilders.termQuery("category", category));
}
// 2.2 按商品品牌过滤
String brand = (String)params.get("brand");
if (StringUtils.isNoneBlank(brand)){
// 词条查询
boolBuilder.must(QueryBuilders.termQuery("brand", brand));
}
// 2.3 按商品规格过滤
Map<String,String> specMap = (Map<String, String>)params.get("spec");
if (specMap != null && specMap.size() > 0){
// 迭代Map集合
for (String key : specMap.keySet()) {
// 嵌套Field的名称
String field = "spec." + key + ".keyword";
// 嵌套查询
boolBuilder.must(QueryBuilders.nestedQuery("spec",
QueryBuilders.termQuery(field, specMap.get(key)),
ScoreMode.Max));
}
}
// 2.4 按商品价格区间过滤
String price = (String)params.get("price");
if (StringUtils.isNoneBlank(price)) {
// 得到价格区间数组
String[] priceArr = price.split("-");
// 范围查询构建对象
RangeQueryBuilder rqBuilder = QueryBuilders.rangeQuery("price");
// 如果价格结束为星号
if ("*".equals(priceArr[1])){
// 大于起始价格 3000-*
rqBuilder.gt(priceArr[0]);
}else{
// 从起始价格 到 结束价格
rqBuilder.from(priceArr[0]).to(priceArr[1]);
}
// 组合范围查询
boolBuilder.must(rqBuilder);
}
// 3. 原生搜索查询对象添加过滤查询
builder.withFilter(boolBuilder);
// 构建搜索查询对象
SearchQuery query = builder.build();
// 分页查询,得到合计分页对象
......
return data;
}
搜索分页
分页使用搜索查询对象
- 搜索分页使用搜索查询对象
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder(); - 创建搜索查询对象
SearchQuery query = builder.build(); - 添加分页条件
query.setPageable(PageRequest.of(curPage - 1, 20));
/** ############# 分页查询 ########### */
// 1. 获取当前页面
Integer curPage = (Integer) params.get("page");
if (curPage == null){
curPage = 1;
}
// 2. 设置分页对象 (注意,页码从0开始)
query.setPageable(PageRequest.of(curPage - 1, 20));
// 分页查询,得到合计分页对象
AggregatedPage<EsItem> page = esTemplate.queryForPage(query,
EsItem.class,new SearchResultMapper() {......});
// 定义Map集合封装返回数据
Map<String,Object> data = new HashMap<>();
// 设置总记录数
data.put("total", page.getTotalElements());
// 设置分页结果
data.put("rows", page.getContent());
// 设置总页数
data.put("totalPages", page.getTotalPages());
return data;
搜索排序
搜索排序使用搜索查询对象
- 创建排序对象
Sort sort = new Sort(升序(枚举)|降序(枚举)); - 搜索查询对象添加排序
query.addSort(sort);
// 1. 获取排序参数
String sortValue = (String) params.get("sortValue"); // ASC DESC
String sortField = (String) params.get("sortField"); // 排序字段
if(StringUtils.isNoneBlank(sortValue)&& StringUtils.isNoneBlank(sortField)){
// 2. 创建排序对象
Sort sort = new Sort("ASC".equalsIgnoreCase(sortValue) ?
Sort.Direction.ASC : Sort.Direction.DESC, sortField);
// 3. 设置添加排序
query.addSort(sort);
}
...