Elasticsearch也是基于Lucene的全文检索库,本质也是存储数据,很多概念与MySQL类似的
索引库(indices)->Databases 数据库
类型(type)->Table 数据表
文档(Document)->Row 行
字段(Field)->Columns 列
概念 | 说明 |
---|---|
索引库(indices) | indices是index的复数,代表许多的索引 |
类型(type) | 类型是模拟mysql中的table概念,一个索引库下可以有不同类型的索引,比如商品索引,订单索引,其数据格式不同。在Elasticsearch6.0中默认只能支持一个索引一个type |
文档(document) | 存入索引库原始的数据。比如每一条商品信息,就是一个文档 |
字段(field) | 文档中的属性 |
映射配置(mappings) | 字段的数据类型、属性、是否索引、是否存储等特性 |
在Elasticsearch中有一些集群相关的概念:
索引集(Indices,index的复数):逻辑上的完整索引
分片(shard):数据拆分后的各个部分
副本(replica):每个分片的复制
Elasticsearch本身就是分布式的,因此即便你只有一个节点,Elasticsearch默认也会对数据进行分片和副本操作,当向集群添加新数据时,数据也会在新加入的节点中进行平衡
地址:https://www.elastic.co/cn/downloads/past-releases
本次使用的Elasticsearch版本为6.2.4
下载后解压,进入elasticsearch-6.2.4\bin,双击elasticsearch.bat文件启动
访问http://127.0.0.1:9200/,看到如下效果证明启动成功
Elasticsearch-head将是一款专门针对于Elasticsearch的客户端工具
Elasticsearch-head配置包,下载地址:https://github.com/mobz/elasticsearch-head
安装node:https://nodejs.org/en/download/进行下载安装
安装grunt:
# 安装命令
npm install -g grunt-cli
# 查看安装版本号,检查是否安装成功
grunt -version
进入Elasticsearch安装目录下的config目录,修改elasticsearch.yml文件.在文件的末尾加入以下代码,并重启Elasticsearch
http.cors.enabled: true
http.cors.allow-origin: "*"
node.master: true
node.data: true
从GitHub中将Elasticsearch-head插件下载下来,修改目录下的Gruntfile.js文件,添加hostname: '*'
,修改后如下
connect: {
server: {
options: {
hostname: '*',
port: 9100,
base: '.',
keepalive: true
}
}
}
在elasticsearch-head-master目录下开启cmd窗口,依次执行如下命令:
# 安装
npm install
# 运行head插件
grunt server
或
npm run start
下载地址:https://github.com/medcl/elasticsearch-analysis-ik/releases
解压到到Elasticsearch的plugins目录下,改名为ik
目录结构如下:
重启Elasticsearch,启动日志中打印如下日志证明Ik分词器安装成功
<properties>
<comment>IK Analyzer 扩展配置comment>
<entry key="ext_dict">entry>
<entry key="ext_stopwords">entry>
properties>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-elasticsearchartifactId>
dependency>
版本对应关系:
Spring Data Elasticsearch | Elasticsearch |
---|---|
3.2.x | 6.7.2 |
3.1.x | 6.2.2 |
3.0.x | 5.5.0 |
2.1.x | 2.4.0 |
2.0.x | 2.2.0 |
1.3.x | 1.5.2 |
详细查看https://github.com/spring-projects/spring-data-elasticsearch
本次使用的是SpringBoot2.1.5.RELEASE版本,对应spring-data-elasticsearch的默认版本为3.1.8.RELEASE
#集群名和配置文件elasticsearch.yml中的cluster.name对应
spring.data.elasticsearch.cluster-name=my-application
#集群节点地址列表,用逗号分隔
spring.data.elasticsearch.cluster-nodes=localhost:9300
#开启Elasticsearch仓库
spring.data.elasticsearch.repositories.enabled=true
修改Elasticsearch的config目录下的配置文件elasticsearch.yml
cluster.name: my-application
业务:创建一个商品对象,有这些属性:
id,title,category,brand,price,图片地址
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Item {
private Long id;
private String title; //标题
private String category;// 分类
private String brand; // 品牌
private Double price; // 价格
private String images; // 图片地址
}
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分词器
@AllArgsConstructor
@NoArgsConstructor
@Data
@Document(indexName = "item", type = "docs")
public class Item {
@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; // 图片地址
}
ElasticsearchTemplate提供了API操作索引和映射关系的API
创建索引:
可以根据类的信息自动生成,也可以手动指定indexName和Settings
public <T> boolean createIndex(Class<T> clazz)
public boolean createIndex(String indexName)
public boolean createIndex(String indexName, Object settings)
public <T> boolean createIndex(Class<T> clazz, Object settings)
映射:
public <T> boolean putMapping(Class<T> clazz)
public <T> boolean putMapping(Class<T> clazz, Object mapping)
public boolean putMapping(String indexName, String type, Object mapping)
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
@Test
public void createIndexTest() {
elasticsearchTemplate.createIndex(Item.class);
elasticsearchTemplate.putMapping(Item.class);
}
public <T> boolean deleteIndex(Class<T> clazz)
public boolean deleteIndex(String indexName)
@Test
public void deleteIndexTest() {
elasticsearchTemplate.deleteIndex(Item.class);
}
@NoRepositoryBean
public interface ElasticsearchRepository<T, ID extends Serializable> extends ElasticsearchCrudRepository<T, ID> {
<S extends T> S index(S entity);
Iterable<T> search(QueryBuilder query);
Page<T> search(QueryBuilder query, Pageable pageable);
Page<T> search(SearchQuery searchQuery);
Page<T> searchSimilar(T entity, String[] fields, Pageable pageable);
void refresh();
Class<T> getEntityClass();
}
只要定义一个接口,继承ElasticsearchRepository就能实现基本的CRUD功能
public interface ItemRepository extends ElasticsearchRepository<Item, Long> {
}
@Autowired
private ItemRepository itemRepository;
@Test
public void insertTest() {
Item item = new Item(1L, "红米6A AI美颜 全网通4G手机 双卡双待 樱花粉", "手机",
"小米", 649.00, "https://img14.360buyimg.com/n0/jfs/t27382/325/1059766233/108388/312ba10a/5bc02ed1Nbb470630.jpg");
itemRepository.save(item);
}
@Test
public void insertListTest() {
List<Item> list = new ArrayList<>();
list.add(new Item(2L, "诺基亚 NOKIA X71", "手机",
"诺基亚", 1999.00, "https://img14.360buyimg.com/n0/jfs/t1/27974/40/14932/117930/5cab0a31Eabb2cce7/209b50731b5c3a24.jpg"));
list.add(new Item(3L, "华为(HUAWEI) 荣耀8X", "手机",
"华为", 1699.00, "https://img14.360buyimg.com/n0/jfs/t1/29898/28/9910/112181/5c81d469E3fef484f/0d84baad19fb22b8.jpg"));
itemRepository.saveAll(list);
}
4)、修改
Elasticsearch中本没有修改,它的修改原理是该是先删除在新增
修改和新增是同一个接口,区分的依据就是id
@Test
public void updateTest() {
Item item = new Item(1L, "红米6A", "手机",
"小米", 600.00, "https://img14.360buyimg.com/n0/jfs/t27382/325/1059766233/108388/312ba10a/5bc02ed1Nbb470630.jpg");
itemRepository.save(item);
}
ElasticsearchRepository提供了一些基本的查询方法:
@Test
public void findAllTest() {
Iterable<Item> list = itemRepository.findAll();
for (Item item : list) {
System.out.println(item);
}
}
Spring Data 的另一个强大功能,是根据方法名称自动实现功能
Keyword | Sample |
---|---|
And |
findByNameAndPrice |
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 |
NotIn |
findByNameNotIn(Collection |
Near |
findByStoreNear |
True |
findByAvailableTrue |
False |
findByAvailableFalse |
OrderBy |
findByAvailableTrueOrderByNameDesc |
通过Title查询商品然后按照Id进行排序
public interface ItemRepository extends ElasticsearchRepository<Item, Long> {
List<Item> findByTitleOrderById(String title);
}
添加测试数据:
@Test
public void insert() {
List<Item> list = new ArrayList<>();
list.add(new Item(4L, "红米6A 2", "手机",
"小米", 600.00, "https://img14.360buyimg.com/n0/jfs/t27382/325/1059766233/108388/312ba10a/5bc02ed1Nbb470630.jpg"));
list.add(new Item(5L, "红米6A 3", "手机",
"小米", 600.00, "https://img14.360buyimg.com/n0/jfs/t27382/325/1059766233/108388/312ba10a/5bc02ed1Nbb470630.jpg"));
list.add(new Item(6L, "红米6A 4", "手机",
"小米", 600.00, "https://img14.360buyimg.com/n0/jfs/t27382/325/1059766233/108388/312ba10a/5bc02ed1Nbb470630.jpg"));
list.add(new Item(7L, "红米6A 5", "手机",
"小米", 600.00, "https://img14.360buyimg.com/n0/jfs/t27382/325/1059766233/108388/312ba10a/5bc02ed1Nbb470630.jpg"));
itemRepository.saveAll(list);
}
@Test
public void test01() {
List<Item> list = itemRepository.findByTitleOrderById("红");
for (Item item : list) {
System.out.println(item);
}
}
//matchQuery
@Test
public void test01() {
//创建对象
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//在queryBuilder对象中自定义查询
//matchQuery底层就是使用的termQuery
queryBuilder.withQuery(QueryBuilders.matchQuery("title", "红米"));
//查询,search 默认就是分页查找
Page<Item> page = this.itemRepository.search(queryBuilder.build());
//获取数据
long totalElements = page.getTotalElements();
System.out.println("获取的总条数:" + totalElements);
for (Item item : page) {
System.out.println(item);
}
}
//termQuery:功能更强大,除了匹配字符串以外,还可以匹配int/long/double/float等
@Test
public void test02() {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
builder.withQuery(QueryBuilders.termQuery("price", 600.00));
//查找
Page<Item> page = this.itemRepository.search(builder.build());
for (Item item : page) {
System.out.println(item);
}
}
//boolQuery
@Test
public void test03() {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
builder.withQuery(
QueryBuilders.boolQuery().must(QueryBuilders.matchQuery("title", "红米"))
.must(QueryBuilders.matchQuery("brand", "小米")));
//查找
Page<Item> page = this.itemRepository.search(builder.build());
for (Item item : page) {
System.out.println(item);
}
}
//fuzzyQuery(模糊查询)
@Test
public void test04() {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
builder.withQuery(QueryBuilders.fuzzyQuery("title", "红"));
Page<Item> page = this.itemRepository.search(builder.build());
for (Item item : page) {
System.out.println(item);
}
}
matchQuery和termQuery的区别
matchQuery会先对搜索词进行分词,分词完毕后再逐个对分词结果进行匹配
termQuery是代表完全匹配,也就是精确查询,搜索前不会再对搜索词进行分词
@Test
public void searchByPage() {
//构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//添加基本分词查询
queryBuilder.withQuery(QueryBuilders.termQuery("category", "手机"));
// 分页:
int page = 0;
int size = 2;
queryBuilder.withPageable(PageRequest.of(page, size));
//搜索,获取结果
Page<Item> items = this.itemRepository.search(queryBuilder.build());
//总条数
long total = items.getTotalElements();
System.out.println("总条数 = " + total);
//总页数
System.out.println("总页数 = " + items.getTotalPages());
//当前页
System.out.println("当前页:" + items.getNumber());
//每页大小
System.out.println("每页大小:" + items.getSize());
for (Item item : items) {
System.out.println(item);
}
}
Elasticsearch中的分页是从第0页开始
@Test
public void searchAndSort(){
//构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//添加基本分词查询
queryBuilder.withQuery(QueryBuilders.termQuery("category", "手机"));
//排序
queryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.ASC));
//搜索,获取结果
Page<Item> items = this.itemRepository.search(queryBuilder.build());
//总条数
long total = items.getTotalElements();
System.out.println("总条数 = " + total);
for (Item item : items) {
System.out.println(item);
}
}
桶:按照某种方式对数据进行分组,每一组数据在Elasticsearch中称为一个桶
度量:分组完成以后,我们一般会对组中的数据进行聚合运算,例如求平均值、最大、最小、求和等,这些在Elasticsearch中称度量
按照品牌brand进行分组:
@Test
public void testAgg() {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//不查询任何结果
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null));
//1、添加一个新的聚合,聚合类型为terms,聚合名称为brands,聚合字段为brand
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());
}
}
运行结果:
小米
5
华为
1
诺基亚
1
关键API:
AggregationBuilders
:聚合的构建工厂类。所有聚合都由这个类来构建:
(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");
AggregatedPage
:聚合查询的结果类。它是Page
的子接口
//判断结果中是否有聚合
boolean hasAggregations();
//获取所有聚合形成的map,key是聚合名称
Aggregations getAggregations();
//根据聚合名称,获取指定聚合
Aggregation getAggregation(String name);
@Test
public void testSubAgg() {
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());
}
}
运行结果:
小米,共5台
平均售价:600.0
华为,共1台
平均售价:1699.0
诺基亚,共1台
平均售价:1999.0