目录

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;
}