目录
1. pom.xml配置
2. application.yml文件配置
3. entity文件配置
4. repository配置
5. controller调用
6. repository自定义方法
7. 其它查询方法--controller调用
ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java开发的,并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。
我们建立一个网站或应用程序,并要添加搜索功能,但是想要完成搜索工作的创建是非常困难的。我们希望搜索解决方案要运行速度快,我们希望能有一个零配置和一个完全免费的搜索模式,我们希望能够简单地使用JSON通过HTTP来索引数据,我们希望我们的搜索服务器始终可用,我们希望能够从一台开始并扩展到数百台,我们要实时搜索,我们要简单的多租户,我们希望建立一个云的解决方案。因此我们利用Elasticsearch来解决所有这些问题及可能出现的更多其它问题。
ES的基本知识:
mysql数据库可以分为:数据库(database) ->表(table) -> 行(row)->列(column)
es相应也分为: 索引(index)->类型(type)->文档(document)->字段(field)
具体的对应规则如下:
Relational DB -> Database-> Table -> Row -> Column
Elasticsearch -> Index -> Type -> Document-> Field
注:
关于type
6.*版本中type已过时, 并且当前版本只能建一个type
7.*版本将删除type, 所以在开发过程中弱化type的概念
1. pom.xml配置
<modelVersion>4.0.0</modelVersion>
<!-- 定义公共资源版本 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<!-- elasticsearch -->
<!-- springboot-data-elasticsearch 提供了面向对象的方式操作elasticsearch -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
</dependencies>
2. application.yml文件配置
server:
port: 18015
max-http-header-size: 8192
redis:
host: 10.101.15.59
spring:
application:
name: spring-boot-elasticsearch-actuator #ServiceId
# ******************* elasticsearch ******************* #
data:
elasticsearch:
cluster-name: my-application
cluster-nodes: ${redis.host}:9300
properties: {}
elasticsearch:
rest:
uris: ["http://${redis.host}:9200"] # 不配置该配置会报错: Elasticsearch health check failed
3. entity文件配置
entity中注解说明
Spring Data通过注解来声明字段的映射属性,有下面的三个注解:
@Document 作用在类,标记实体类为文档对象,一般有两个属性
indexName:对应索引库名称
type:对应在索引库中的类型
shards:分片数量,默认5
replicas:副本数量,默认1
@Id 作用在成员变量,标记一个字段作为id主键
@Field 作用在成员变量,标记为文档的字段,并指定字段映射属性:
type:字段类型,是枚举:FieldType,可以是text、long、short、date、integer、object等
text:存储数据时候,会自动分词,并生成索引
keyword:存储数据时候,不会分词建立索引
Numerical:数值类型,分两类
基本数据类型:long、interger、short、byte、double、float、half_float
浮点数的高精度类型:scaled_float
需要指定一个精度因子,比如10或100。
elasticsearch会把真实值乘以这个因子后存储,取出时再还原。
Date:日期类型
elasticsearch可以对日期格式化为字符串存储,
但是建议我们存储为毫秒值,存储为long,节省空间。
index:是否索引,布尔类型,默认是true
store:是否存储,布尔类型,默认是false
analyzer:分词器名称,这里的ik_max_word即使用ik分词器
entity示例
package org.fiend.entity;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
/**
* @author langpf 2019/2/27
*/
// indexName索引名称 可以理解为数据库名 必须为小写,
// 不然会报org.elasticsearch.indices.InvalidIndexNameException异常
// type类型 可以理解为表名
@Document(indexName = "item", type = "docs", shards = 1, replicas = 0)
public class Item {
/**
* 注: 该注解是必填项
* @Description: @Id注解必须是springframework包下的
* org.springframework.data.annotation.Id
* @Author:
*/
@Id
private Long id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String title; //标题
@Field(type = FieldType.Keyword)
private String category;// 分类
@Field(type = FieldType.Keyword)
private String brand; // 品牌
@Field(type = FieldType.Double)
private Double price; // 价格
@Field(index = false, type = FieldType.Keyword)
private String images; // 图片地址
public Item(Long id, String title, String category,
String brand, Double price, String images) {
this.id = id;
this.title = title;
this.category = category;
this.brand = brand;
this.price = price;
this.images = images;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public String getImages() {
return images;
}
public void setImages(String images) {
this.images = images;
}
}
4. repository配置
package org.fiend.repository;
import org.fiend.entity.Item;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author langpf 2019/2/27
*/
@Component
// <ItemEntity, Long>: ItemEntity的@Id所注解的数据项的数据类型, 必须与Long一致,
// 即ItemEntity的@Id如果是String类型, 这里的Long就变为String
public interface ItemRepository extends ElasticsearchRepository<Item, Long> {
/**
* @Description:根据价格区间查询
* @Param price1
* @Param price2
* @Author:
*/
List<Item> findByPriceBetween(double price1, double price2);
}
5. controller调用
ElasticsearchTemplate用来对索引的创建,删除,以及数据的插入,查询(包括按页查询)等。
repository用来实现对数据的CRUD操作
package org.fiend.controller;
import com.alibaba.fastjson.JSONObject;
import org.fiend.entity.Item;
import org.fiend.repository.ItemRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author Administrator
*/
@Controller
@RequestMapping(value = "item")
public class ItemController {
Logger log = LoggerFactory.getLogger(getClass());
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
@Autowired
private ItemRepository itemRepository;
/**
* 创建索引
*/
@GetMapping("createIndex")
public JSONObject createIndex(Object obj) {
JSONObject json = new JSONObject();
boolean result = elasticsearchTemplate.createIndex(obj.getClass());
json.put("result", result);
return json;
}
@GetMapping("insert")
public String insert() {
Item item = new Item(1L, "小米手机7", " 手机",
"小米", 3499.00, "http://image.baidu.com/13123.jpg");
itemRepository.save(item);
return "success";
}
//http://localhost:8888/delete?id=1525415333329
@GetMapping("delete")
public String delete(long id) {
itemRepository.deleteById(id);
return "success";
}
/**
* elasticsearch中本没有修改,它的修改原理是该是先删除在新增
* 修改和新增是同一个接口,区分的依据就是id。
*/
//http://localhost:8888/update?id=1525417362754&name=修改&description=修改
@GetMapping("update")
public String update(Long id, String title, String category, String brand, Double price, String images) {
Item item = new Item(id, title, category, brand, price, images);
itemRepository.save(item);
return "success";
}
/**
* 查找所有
*/
@GetMapping("findAll")
public List<Item> findAll() {
// 查找所有
// Iterable<Item> list = this.itemRepository.findAll();
// 对某字段排序查找所有 Sort.by("price").descending() 降序
// Sort.by("price").ascending(): 升序
Iterable<Item> iterable = itemRepository.findAll(Sort.by("price").ascending();
List<Item> list = Lists.newArrayList(iterable);
return list;
}
}
6. repository自定义方法
Spring Data 的另一个强大功能,是根据方法名称自动实现功能。
比如:你的方法名叫做:findByTitle,那么它就知道你是根据title查询,然后自动帮你完成,无需写实现类。
当然,方法名称要符合一定的约定:
Keyword | Sample |
And | findNameAndPrice |
Or | findByNameOrPrice |
Is | findByName |
Not | findByNameNot |
Between | findByPriceBetween |
LessThanEqual | findByPriceLessThan |
GreaterThanEqual | findByPriceGreaterThan |
Before | findByPriceBefore |
After | findByPriceAfter |
Like | findByNameLike |
StartingWith | findByNameStartingWith |
EndingWith | findByNameEndingWith |
Contains/Containing | findByNameContaining |
In | findByNameIn(Collection<String>names) |
NotIn | findByNameNotIn(Collection<String>names) |
Near | findByStoreNear |
True | findByAvailableTrue |
False | findByAvailableFalse |
OrderBy | findByAvailableTrueOrderByNameDesc |
例如,按照价格区间查询,定义这样的一个方法:
public interface ItemRepository extends ElasticsearchRepository<Item,Long> {
/**
* @Description:根据价格区间查询
* @Param price1
* @Param price2
* @Author:
*/
List<Item> findByPriceBetween(double price1, double price2);
}
添加一些测试数据:
@Autowired
private ItemRepository itemRepository;
/**
* @Description:准备测试数据
* @Author:
*/
@Test
public void insertList() {
List<Item> list = new ArrayList<>();
list.add(new Item(1L, "小米手机7", "手机", "小米", 3299.00, "http://image.baidu.com/13123.jpg"));
list.add(new Item(2L, "坚果手机R1", "手机", "锤子", 3699.00, "http://image.baidu.com/13123.jpg"));
list.add(new Item(3L, "华为META10", "手机", "华为", 4499.00, "http://image.baidu.com/13123.jpg"));
list.add(new Item(4L, "小米Mix2S", "手机", "小米", 4299.00, "http://image.baidu.com/13123.jpg"));
list.add(new Item(5L, "荣耀V10", "手机", "华为", 2799.00, "http://image.baidu.com/13123.jpg"));
// 接收对象集合,实现批量新增
itemRepository.saveAll(list);
}
7. 其它查询方法--controller调用
/**
* 自定义查询: matchQuery
*/
@GetMapping("matchQuery")
public Page<Item> matchQuery() {
// 构建查询条件
// NativeSearchQueryBuilder:Spring提供的一个查询条件构建器, 帮助构建json格式的请求体
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 添加基本分词查询
// QueryBuilders.matchQuery(“title”, “小米手机”):利用QueryBuilders来生成一个查询。
// QueryBuilders提供了大量的静态方法, 用于生成各种不同类型的查询:
queryBuilder.withQuery(QueryBuilders.matchQuery("title", "小米手机"));
// 搜索, 获取结果
// Page<item>:默认是分页查询, 因此返回的是一个分页的结果对象, 包含属性:
// totalElements:总条数
// totalPages:总页数
// Iterator:迭代器, 本身实现了Iterator接口, 因此可直接迭代得到当前页的数据
Page<Item> page = this.itemRepository.search(queryBuilder.build());
for (Item item : page) {
System.out.println(item);
}
// 总条数
long total = page.getTotalElements();
System.out.println("total = " + total);
// Iterator<Item> iterable = page.iterator();
return page;
}
/**
* 自定义查询: termQuery
* termQuery: 功能更强大, 除了匹配字符串以外, 还可以匹配 int/long/double/float/....
*/
@GetMapping("termQuery")
public Page<Item> termQuery() {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
builder.withQuery(QueryBuilders.termQuery("price", 998.0));
// 查找
Page<Item> page = this.itemRepository.search(builder.build());
for (Item item : page) {
System.out.println(item);
}
return page;
}
/**
* 自定义查询: fuzzyQuery -- 模糊查询
*/
@GetMapping("fuzzyQuery")
public Page<Item> fuzzyQuery() {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
builder.withQuery(QueryBuilders.fuzzyQuery("title", "faceoooo"));
Page<Item> page = this.itemRepository.search(builder.build());
for (Item item : page) {
System.out.println(item + "");
}
return page;
}
/**
* 分页查询
*/
@GetMapping("searchByPage")
public Page<Item> searchByPage() {
// 构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 添加基本分词查询
queryBuilder.withQuery(QueryBuilders.termQuery("category", "手机"));
// 分页:
int pageNum = 0;
int size = 2;
queryBuilder.withPageable(PageRequest.of(pageNum, size));
// 搜索, 获取结果
Page<Item> page = this.itemRepository.search(queryBuilder.build());
// 总条数
long total = page.getTotalElements();
System.out.println("总条数 = " + total);
// 总页数
System.out.println("总页数 = " + page.getTotalPages());
// 当前页
System.out.println("当前页:" + page.getNumber());
// 每页大小
System.out.println("每页大小:" + page.getSize());
for (Item item : page) {
System.out.println(item);
}
return page;
}
/**
* 排序查询
*/
@GetMapping("searchAndSort")
public Page<Item> searchAndSort() {
// 构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 添加基本分词查询
queryBuilder.withQuery(QueryBuilders.termQuery("category", "手机"));
// 排序
queryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.ASC));
// 搜索, 获取结果
Page<Item> page = this.itemRepository.search(queryBuilder.build());
// 总条数
long totalNum = page.getTotalElements();
System.out.println("总条数: " + totalNum);
// 总页数
System.out.println("总页数 = " + page.getTotalPages());
// 当前页
System.out.println("当前页: " + page.getNumber());
// 每页大小
System.out.println("每页大小:" + page.getSize());
for (Item item : page) {
System.out.println(item);
}
return page;
}
// 聚合查询:
// 比较常用的一些度量聚合方式:
// Avg Aggregation:求平均值
// Max Aggregation:求最大值
// Min Aggregation:求最小值
// Percentiles Aggregation:求百分比
// Stats Aggregation:同时返回avg、max、min、sum、count等
// Sum Aggregation:求和
// Top hits Aggregation:求前几
// Value Count Aggregation:求总数
// ……
/**
* Elasticsearch中的聚合, 包含多种类型, 最常用的两种, 一个叫桶, 一个叫度量:
* 聚合查询: 聚合为桶 -- 查询
* aggregation bucket 查询
*/
@GetMapping("aggBucketSearch")
public AggregatedPage<Item> aggBucketSearch() {
// 构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 不查询任何结果
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null));
// 1、添加一个新的聚合, 聚合类型为terms, 聚合名称为brands, 聚合字段为brand
// AggregationBuilders:聚合的构建工厂类, 所有聚合都由这个类来构建
queryBuilder.addAggregation(AggregationBuilders.terms("brands").field("brand"));
// 2、查询, 需要把结果强转为AggregatedPage类型
AggregatedPage<Item> aggPage = (AggregatedPage<Item>)
this.itemRepository.search(queryBuilder.build());
// 3 解析
// 3.1 从结果中取出名为brands的那个聚合,
// 因为是利用String类型字段来进行的term聚合, 所以结果要强转为StringTerm类型
StringTerms agg = (StringTerms) aggPage.getAggregation("brands");
// 3.2、获取桶
List<StringTerms.Bucket> buckets = agg.getBuckets();
// 3.3、遍历
for (StringTerms.Bucket bucket : buckets) {
// 3.4、获取桶中的key, 即品牌名称
System.out.println(bucket.getKeyAsString());
// 3.5、获取桶中的文档数量
System.out.println(bucket.getDocCount());
}
return aggPage;
}
// (1)统计某个字段的数量
// ValueCountBuilder vcb= AggregationBuilders.count("count_uid").field("uid");
// (2)去重统计某个字段的数量(有少量误差)
// CardinalityBuilder cb= AggregationBuilders.cardinality("distinct_count_uid").field("uid");
// (3)聚合过滤
// FilterAggregationBuilder fab= AggregationBuilders.filter("uid_filter")
// .filter(QueryBuilders.queryStringQuery("uid:001"));
// (4)按某个字段分组
// TermsBuilder tb= AggregationBuilders.terms("group_name").field("name");
// (5)求和
// SumBuilder sumBuilder= AggregationBuilders.sum("sum_price").field("price");
// (6)求平均
// AvgBuilder ab= AggregationBuilders.avg("avg_price").field("price");
// (7)求最大值
// MaxBuilder mb= AggregationBuilders.max("max_price").field("price");
// (8)求最小值
// MinBuilder min= AggregationBuilders.min("min_price").field("price");
// (9)按日期间隔分组
// DateHistogramBuilder dhb= AggregationBuilders.dateHistogram("dh").field("date");
// (10)获取聚合里面的结果
// TopHitsBuilder thb= AggregationBuilders.topHits("top_result");
// (11)嵌套的聚合
// NestedBuilder nb= AggregationBuilders.nested("negsted_path").path("quests");
// (12)反转嵌套
// AggregationBuilders.reverseNested("res_negsted").path("kps ");
/**
* 聚合查询: 嵌套聚合, 求平均值
*/
@GetMapping("aggSubSearch")
public AggregatedPage<Item> aggSubSearch() {
// 构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 不查询任何结果
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null));
// 1、添加一个新的聚合, 聚合类型为terms, 聚合名称为brands, 聚合字段为brand
queryBuilder.addAggregation(AggregationBuilders.terms("brands").field("brand")
// 在品牌聚合桶内进行嵌套聚合, 求平均值
.subAggregation(AggregationBuilders.avg("priceAvg").field("price")));
// 2、查询,需要把结果强转为AggregatedPage类型
AggregatedPage<Item> aggPage = (AggregatedPage<Item>)
this.itemRepository.search(queryBuilder.build());
// 3 解析
// 3.1 从结果中取出名为brands的那个聚合,
// 因为是利用String类型字段来进行的term聚合, 所以结果要强转为StringTerm类型
StringTerms agg = (StringTerms) aggPage.getAggregation("brands");
// 3.2、获取桶
List<StringTerms.Bucket> buckets = agg.getBuckets();
// 3.3、遍历
for (StringTerms.Bucket bucket : buckets) {
// 3.4、获取桶中的key, 即品牌名称 3.5、获取桶中的文档数量
System.out.println(bucket.getKeyAsString() + ", 共" + bucket.getDocCount() + "台");
// 3.6.获取子聚合结果:
InternalAvg avg = (InternalAvg) bucket.getAggregations().asMap().get("priceAvg");
System.out.println("平均售价:" + avg.getValue());
}
return aggPage;
}