Elasticsearch简介
Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎,能够解决不断涌现出的各种用例。作为 Elastic Stack 的核心,Elasticsearch 会集中存储您的数据,让您飞快完成搜索,微调相关性,进行强大的分析,并轻松缩放规模。
Elasticsearch的安装
下载安装包
下载Elasticsearch7.17.3版本的zip包,并解压到指定目录,下载地址:https://www.elastic.co/cn/downloads/past-releases/elasticsearch-7-17-3
安装中文分词器
安装中文分词器,注意下载与Elasticsearch对应的版本,下载地址
https://github.com/medcl/elasticsearch-analysis-ik/releases
下载完成后解压
下载完成后解压到Elasticsearch的plugins目录下
运行bin目录下的elasticsearch.bat启动Elasticsearch服务。
访问 http://localhost:9200/ 出现这个界面,表示启动成功
安装Kibana
作为Elasticsearch 的客户端访问
下载Kibana,作为访问Elasticsearch的客户端,请下载7.17.3版本的zip包,并解压到指定目录,下载地址:https://www.elastic.co/cn/downloads/past-releases/kibana-7-17-3
运行bin目录下的kibana.bat,启动Kibana服务;
打开Kibana的用户界面,访问地址:http://localhost:5601
相关概念
● Near Realtime(近实时):Elasticsearch是一个近乎实时的搜索平台,这意味着从索引文档到可搜索文档之间只有一个轻微的延迟(通常是一秒钟)。
● Cluster(集群):群集是一个或多个节点的集合,它们一起保存整个数据,并提供跨所有节点的联合索引和搜索功能。每个集群都有自己的唯一集群名称,节点通过名称加入集群。
● Node(节点):节点是指属于集群的单个Elasticsearch实例,存储数据并参与集群的索引和搜索功能。可以将节点配置为按集群名称加入特定集群,默认情况下,每个节点都设置为加入一个名为elasticsearch的群集。
● Index(索引):索引是一些具有相似特征的文档集合,类似于MySql中数据库的概念。
● Type(类型):类型是索引的逻辑类别分区,通常,为具有一组公共字段的文档类型,类似MySql中表的概念。注意:在Elasticsearch 6.0.0及更高的版本中,一个索引只能包含一个类型。
● Document(文档):文档是可被索引的基本信息单位,以JSON形式表示,类似于MySql中行记录的概念。
● Shards(分片):当索引存储大量数据时,可能会超出单个节点的硬件限制,为了解决这个问题,Elasticsearch提供了将索引细分为分片的概念。分片机制赋予了索引水平扩容的能力、并允许跨分片分发和并行化操作,从而提高性能和吞吐量。
● Replicas(副本):在可能出现故障的网络环境中,需要有一个故障切换机制,Elasticsearch提供了将索引的分片复制为一个或多个副本的功能,副本在某些节点失效的情况下提供高可用性。
简单操作
通过Kibana的Dev Tools功能,我们可以操作Elasticsearch;
索引操作
创建索引并查看
bash
PUT /customer
GET /_cat/indices?v
删除索引并查看
cpp
DELETE /customer
GET /_cat/indices?v
类型操作
首先要导入一批数据
cpp
https://github.com/macrozheng/mall-learning/blob/teach/document/json/accounts.json
cpp
POST /bank/account/_bulk
查看文档类型的操作
cpp
GET /bank/_mapping
在索引中添加文档
cpp
PUT /customer/doc/1
{
"name": "John Doe"
}
查看索引中的文档
GET /customer/doc/1
修改索引中的文档
cpp
POST /customer/doc/1/_update
{
"doc": { "name": "Jane Doe" }
}
删除索引中的文档
cpp
DELETE /customer/doc/1
数据搜索
查询表达式(Query DSL)是一种非常灵活又富有表现力的查询语言,Elasticsearch使用它可以以简单的JSON接口来实现丰富的搜索功能,下面的搜索操作都将使用它。
数据导入
首先要导入一批数据
cpp
https://github.com/macrozheng/mall-learning/blob/teach/document/json/accounts.json
cpp
POST /bank/account/_bulk
搜索入门
最简单的搜索,使用match_all来表示,例如搜索全部
cpp
GET /bank/_search
{
"query": { "match_all": {} }
}
分页搜索,from表示偏移量,从0开始,size表示每页显示的数量
cpp
GET /bank/_search
{
"query": { "match_all": {} },
"from": 0,
"size": 10
}
搜索排序,使用sort表示,例如按balance字段降序排列;
cpp
GET /bank/_search
{
"query": { "match_all": {} },
"sort": { "balance": { "order": "desc" } }
}
搜索并返回指定字段内容,使用_source表示,例如只返回account_number和balance两个字段内容:
cpp
GET /bank/_search
{
"query": { "match_all": {} },
"_source": ["account_number", "balance"]
}
条件搜索
条件搜索,使用match表示匹配条件,例如搜索出account_number为20的文档
cpp
GET /bank/_search
{
"query": {
"match": {
"account_number": 20
}
}
}
短语匹配搜索,使用match_phrase表示,例如搜索address字段中同时包含mill和lane的文档:
cpp
GET /bank/_search
{
"query": {
"match_phrase": {
"address": "mill lane"
}
}
}
组合搜索
组合搜索,使用bool来进行组合,must表示同时满足,例如搜索address字段中同时包含mill和lane的文档;
cpp
GET /bank/_search
{
"query": {
"bool": {
"must": [
{ "match": { "address": "mill" } },
{ "match": { "address": "lane" } }
]
}
}
}
组合搜索,should表示满足其中任意一个,搜索address字段中包含mill或者lane的文档;
cpp
GET /bank/_search
{
"query": {
"bool": {
"should": [
{ "match": { "address": "mill" } },
{ "match": { "address": "lane" } }
]
}
}
}
组合搜索,must_not表示同时不满足,例如搜索address字段中不包含mill且不包含lane的文档;
cpp
GET /bank/_search
{
"query": {
"bool": {
"must_not": [
{ "match": { "address": "mill" } },
{ "match": { "address": "lane" } }
]
}
}
}
组合搜索,组合must和must_not,例如搜索age字段等于40且state字段不包含ID的文档;
cpp
GET /bank/_search
{
"query": {
"bool": {
"must": [
{ "match": { "age": "40" } }
],
"must_not": [
{ "match": { "state": "ID" } }
]
}
}
}
过滤搜索
搜索过滤,使用filter来表示,例如过滤出balance字段在20000~30000的文档;
cpp
GET /bank/_search
{
"query": {
"bool": {
"must": { "match_all": {} },
"filter": {
"range": {
"balance": {
"gte": 20000,
"lte": 30000
}
}
}
}
}
}
参考资料https://www.elastic.co/guide/en/elasticsearch/reference/7.17/getting-started.html
Spring Data Elasticsearch
Spring Data Elasticsearch是Spring提供的一种以Spring Data风格来操作数据存储的方式,它可以避免编写大量的样板代码。
常用注解
其中常用的FieldType类型有如下几种:
cpp
public enum FieldType {
Auto("auto"), //自动判断字段类型
Text("text"), //会进行分词并建了索引的字符类型
Keyword("keyword"), //不会进行分词建立索引的类型
Long("long"), //
Integer("integer"), //
Short("short"), //
Byte("byte"), //
Double("double"), //
Float("float"), //
Date("date"), //
Boolean("boolean"), //
Object("object"), //
Nested("nested"), //嵌套对象类型
Ip("ip"), //
}
SpringData 方式的数据操作
- 继承ElasticsearchRepository接口可以获得常用的数据操作方法;
可以使用衍生查询,在接口中直接指定查询方法名称便可查询,无需进行实现,如商品表中有商品名称、标题和关键字,直接定义以下查询,就可以对这三个字段进行全文搜索。
cpp
/**
* @description 商品ES操作类
*/
public interface EsProductRepository extends ElasticsearchRepository<EsProduct, Long> {
/**
* 搜索查询
*
* @param name 商品名称
* @param subTitle 商品标题
* @param keywords 商品关键字
* @param page 分页信息
* @return
*/
Page<EsProduct> findByNameOrSubTitleOrKeywords(String name, String subTitle, String keywords, Pageable page);
}
通过@Query注解可以使用Elasticsearch的原生DSL语句进行查询;
cpp
/**
* @description 商品ES操作类
*/
public interface EsProductRepository extends ElasticsearchRepository<EsProduct, Long> {
@Query("{"bool" : {"must" : {"field" : {"name" : " ? 0"}}}}")
Page<EsProduct> findByName(String name, Pageable pageable);
}
整合Elasticsearch商品搜索
整理依赖配置
- 在pom.xml中添加相关依赖;
xml
<!--Elasticsearch相关依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
- 修改application.yml配置文件,在spring节点下添加Elasticsearch相关配置;
yml
spring:
data:
elasticsearch:
repositories:
enabled: true # 开启ES仓库配置,自动为仓库接口生成实现类
elasticsearch:
uris: http://localhost:9200 # ES的连接地址及端口号
实现商品搜索功能
添加商品文档对象EsProduct
不需要中文分词的字段设置成Keyword类型,需要中文分词的设置成Text类型,并设置分词器为ik_max_word;
cpp
/**
* @description 搜索商品的信息
*/
@Data
@EqualsAndHashCode
@Document(indexName = "pms")
@Setting(shards = 1,replicas = 0)
public class EsProduct implements Serializable {
private static final long serialVersionUID = -1L;
@Id
private Long id;
@Field(type = FieldType.Keyword)
private String productSn;
private Long brandId;
@Field(type = FieldType.Keyword)
private String brandName;
private Long productCategoryId;
@Field(type = FieldType.Keyword)
private String productCategoryName;
private String pic;
@Field(analyzer = "ik_max_word",type = FieldType.Text)
private String name;
@Field(analyzer = "ik_max_word",type = FieldType.Text)
private String subTitle;
@Field(analyzer = "ik_max_word",type = FieldType.Text)
private String keywords;
private BigDecimal price;
private Integer sale;
private Integer newStatus;
private Integer recommandStatus;
private Integer stock;
private Integer promotionType;
private Integer sort;
@Field(type =FieldType.Nested)
private List<EsProductAttributeValue> attrValueList;
}
继承ElasticsearchRepository接口
这样就拥有了一些基本的Elasticsearch数据操作方法,同时定义了一个衍生查询方法;
cpp
/**
* @description 商品ES操作类
*/
public interface EsProductRepository extends ElasticsearchRepository<EsProduct, Long> {
/**
* 搜索查询
*
* @param name 商品名称
* @param subTitle 商品标题
* @param keywords 商品关键字
* @param page 分页信息
* @return
*/
Page<EsProduct> findByNameOrSubTitleOrKeywords(String name, String subTitle, String keywords, Pageable page);
}
添加EsProductService,定义好ES的操作方法
cpp
/**
* @description 商品搜索管理Service
*/
public interface EsProductService {
/**
* 从数据库中导入所有商品到ES
*/
int importAll();
/**
* 根据id删除商品
*/
void delete(Long id);
/**
* 根据id创建商品
*/
EsProduct create(Long id);
/**
* 批量删除商品
*/
void delete(List<Long> ids);
/**
* 根据关键字搜索名称或者副标题
*/
Page<EsProduct> search(String keyword, Integer pageNum, Integer pageSize);
}
添加EsProductService接口的实现类EsProductServiceImpl;
cpp
/**
* @description 搜索商品管理Service实现类
*/
@Service
public class EsProductServiceImpl implements EsProductService {
private static final Logger LOGGER = LoggerFactory.getLogger(EsProductServiceImpl.class);
@Autowired
private EsProductDao productDao;
@Autowired
private EsProductRepository productRepository;
@Override
public int importAll() {
List<EsProduct> esProductList = productDao.getAllEsProductList(null);
Iterable<EsProduct> esProductIterable = productRepository.saveAll(esProductList);
Iterator<EsProduct> iterator = esProductIterable.iterator();
int result = 0;
while (iterator.hasNext()) {
result++;
iterator.next();
}
return result;
}
@Override
public void delete(Long id) {
productRepository.deleteById(id);
}
@Override
public EsProduct create(Long id) {
EsProduct result = null;
List<EsProduct> esProductList = productDao.getAllEsProductList(id);
if (esProductList.size() > 0) {
EsProduct esProduct = esProductList.get(0);
result = productRepository.save(esProduct);
}
return result;
}
@Override
public void delete(List<Long> ids) {
if (!CollectionUtils.isEmpty(ids)) {
List<EsProduct> esProductList = new ArrayList<>();
for (Long id : ids) {
EsProduct esProduct = new EsProduct();
esProduct.setId(id);
esProductList.add(esProduct);
}
productRepository.deleteAll(esProductList);
}
}
@Override
public Page<EsProduct> search(String keyword, Integer pageNum, Integer pageSize) {
Pageable pageable = PageRequest.of(pageNum, pageSize);
return productRepository.findByNameOrSubTitleOrKeywords(keyword, keyword, keyword, pageable);
}
}
添加EsProductController定义接口。
cpp
/**
* @description 搜索商品管理Controller
*/
@Controller
@Api(tags = "EsProductController")
@Tag(name = "EsProductController", description = "搜索商品管理")
@RequestMapping("/esProduct")
public class EsProductController {
@Autowired
private EsProductService esProductService;
@ApiOperation(value = "导入所有数据库中商品到ES")
@RequestMapping(value = "/importAll", method = RequestMethod.POST)
@ResponseBody
public CommonResult<Integer> importAllList() {
int count = esProductService.importAll();
return CommonResult.success(count);
}
@ApiOperation(value = "根据id删除商品")
@RequestMapping(value = "/delete/{id}", method = RequestMethod.GET)
@ResponseBody
public CommonResult<Object> delete(@PathVariable Long id) {
esProductService.delete(id);
return CommonResult.success(null);
}
@ApiOperation(value = "根据id批量删除商品")
@RequestMapping(value = "/delete/batch", method = RequestMethod.POST)
@ResponseBody
public CommonResult<Object> delete(@RequestParam("ids") List<Long> ids) {
esProductService.delete(ids);
return CommonResult.success(null);
}
@ApiOperation(value = "根据id创建商品")
@RequestMapping(value = "/create/{id}", method = RequestMethod.POST)
@ResponseBody
public CommonResult<EsProduct> create(@PathVariable Long id) {
EsProduct esProduct = esProductService.create(id);
if (esProduct != null) {
return CommonResult.success(esProduct);
} else {
return CommonResult.failed();
}
}
@ApiOperation(value = "简单搜索")
@RequestMapping(value = "/search/simple", method = RequestMethod.GET)
@ResponseBody
public CommonResult<CommonPage<EsProduct>> search(@RequestParam(required = false) String keyword,
@RequestParam(required = false, defaultValue = "0") Integer pageNum,
@RequestParam(required = false, defaultValue = "5") Integer pageSize) {
Page<EsProduct> esProductPage = esProductService.search(keyword, pageNum, pageSize);
return CommonResult.success(CommonPage.restPage(esProductPage));
}
}