Springboot-SpringDataElasticSearch基本使用

x33g5p2x  于12个月前 转载在 Spring  
字(18.3k)|赞(0)|评价(0)|浏览(159)

介绍

Spring Data Elasticsearch是Spring Data项目下的一个子模块。
查看 Spring Data的官网:http://projects.spring.io/spring-data/
Spring Data 的使命是给各种数据访问提供统一的编程接口,不管是关系型数据库(如MySQL),还是非关系数据库(如Redis),或者类似Elasticsearch这样的索引数据库。从而简化开发人员的代码,提高开发效率。

创建项目

项目结构

POM.xml

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.4.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.75</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

配置application.yaml文件

spring:
  data:
    elasticsearch:
      cluster-name: elasticsearch
      cluster-nodes: 192.168.0.22:9300

实体类及注解

public class Item {
    Long id;
    String title; //标题
    String category;// 分类
    String brand; // 品牌
    Double price; // 价格
    String images; // 图片地址
}

Spring Data通过注解来声明字段的映射属性,有下面的三个注解:

@Document

作用在类,标记实体类为文档对象,一般有四个属性

  • indexName:对应索引库名称 (好比数据库)
  • type:对应在索引库中的类型 (好比数据库表)
  • shards:分片数量,默认5
  • replicas:副本数量,默认1

@Id 作用在成员变量,标记一个字段作为id主键
*
@Field

作用在成员变量,标记为文档的字段,并指定字段映射属性:

  • type:字段类型,取值是枚举:FieldType
  • index:是否索引,布尔类型,默认是true
  • store:是否存储,布尔类型,默认是false
  • analyzer:分词器名称:ik_max_word

示例:

package cn.es.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
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;

@Document(indexName = "item",type = "docs")
@Data //必须有 get set
@AllArgsConstructor //必须有 全参
@NoArgsConstructor   //必须有 无惨
public class Item {
    @Id
    private Long id;

    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String title; //标题

    @Field(type = FieldType.Keyword) //Keywor 不分词
    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; // 图片地址
}

创建索引

@SpringBootTest
@RunWith(SpringRunner.class)
public class ElasticSearctTest {
    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;
    //创建库 (索引(数据库)和映射(表))
    @Test
    public void testCreate() {
        // 创建索引,会根据Item类的@Document注解信息来创建
        elasticsearchTemplate.createIndex(Item.class);
        // 配置映射,会根据Item类中的id、Field等字段来自动完成映射
        elasticsearchTemplate.putMapping(Item.class);
    }
}

增删改操作

Spring Data 的强大之处,就在于你不用写任何DAO处理,自动根据方法名或类的信息进行CRUD操作。只要你定义一个接口,然后继承Repository提供的一些子接口,就能具备各种基本的CRUD功能

编写 ItemRepository

package com.example.elasticsearch.repository;

import com.example.elasticsearch.pojo.Item;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

/** * @author john * @date 2019/12/8 - 14:39 */
public interface ItemRepository extends ElasticsearchRepository<Item,Long> {
}

增加

@Autowired
    private ItemRepository itemRepository;
    //插入数据
    @Test 
    public void testAdd() {
        Item item = new Item(1L, "小米手机7", " 手机",
                "小米", 3499.00, "http://image.leyou.com/13123.jpg");
        itemRepository.save(item);
    }

修改

//修改 (id如果存在本质上就是覆盖 ,否则就是插入)
    @Test
    public void testUpdate() {
        Item item = new Item(1L, "小米手机7777", " 手机",
                "小米", 9499.00, "http://image.leyou.com/13123.jpg");
        itemRepository.save(item);
    }

批量新增

@Autowired
    private ItemRepository itemRepository;

    @Test
    public void indexList() {
        List<Item> list = new ArrayList<>();
        list.add(new Item(2L, "坚果手机R1", " 手机", "锤子", 3699.00, "http://image.leyou.com/123.jpg"));
        list.add(new Item(3L, "华为META10", " 手机", "华为", 4499.00, "http://image.leyou.com/3.jpg"));
        // 接收对象集合,实现批量新增
        itemRepository.saveAll(list);
    }

删除操作

@Autowired
    private ItemRepository itemRepository;

    @Test
    public void testDelete() {
        itemRepository.deleteById(1L);
    }

根据id查询

@Autowired
    private ItemRepository itemRepository;
    @Test
    public void testQuery(){
        Optional<Item> optional = itemRepository.findById(2L);
        System.out.println(optional.get());
    }

查询全部,并按照价格降序排序

@Autowired
    private ItemRepository itemRepository;

	@Test
    public void testFind(){
        // 查询全部,并按照价格降序排序
        Iterable<Item> items = this.itemRepository.findAll(Sort.by(Sort.Direction.DESC, "price"));
        items.forEach(item-> System.out.println(item));
    }

自定义方法

Spring Data 的另一个强大功能,是根据方法名称自动实现功能。
比如:你的方法名叫做:findByTitle,那么它就知道你是根据title查询,然后自动帮你完成,无需写实现类。
当然,方法名称要符合一定的约定:

KeywordSampleElasticsearch Query String
AndfindByNameAndPrice{"bool" : {"must" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}}
OrfindByNameOrPrice{"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}}
IsfindByName{"bool" : {"must" : {"field" : {"name" : "?"}}}}
NotfindByNameNot{"bool" : {"must_not" : {"field" : {"name" : "?"}}}}
BetweenfindByPriceBetween{"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : ?,"include_lower" : true,"include_upper" : true}}}}}
LessThanEqualfindByPriceLessThan{"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}}
GreaterThanEqualfindByPriceGreaterThan{"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}}
BeforefindByPriceBefore{"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}}
AfterfindByPriceAfter{"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}}
LikefindByNameLike{"bool" : {"must" : {"field" : {"name" : {"query" : "?/*","analyze_wildcard" : true}}}}}
StartingWithfindByNameStartingWith{"bool" : {"must" : {"field" : {"name" : {"query" : "?/*","analyze_wildcard" : true}}}}}
EndingWithfindByNameEndingWith{"bool" : {"must" : {"field" : {"name" : {"query" : "/*?","analyze_wildcard" : true}}}}}
Contains/ContainingfindByNameContaining{"bool" : {"must" : {"field" : {"name" : {"query" : "/*/*?/*/*","analyze_wildcard" : true}}}}}
InfindByNameIn(Collection<String>names){"bool" : {"must" : {"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"name" : "?"}} ]}}}}
NotInfindByNameNotIn(Collection<String>names){"bool" : {"must_not" : {"bool" : {"should" : {"field" : {"name" : "?"}}}}}}
NearfindByStoreNearNot Supported Yet !
TruefindByAvailableTrue{"bool" : {"must" : {"field" : {"available" : true}}}}
FalsefindByAvailableFalse{"bool" : {"must" : {"field" : {"available" : false}}}}
OrderByfindByAvailableTrueOrderByNameDesc{"sort" : [{ "name" : {"order" : "desc"} }],"bool" : {"must" : {"field" : {"available" : true}}}}

例如,我们来按照价格区间查询,定义这样的一个方法:

public interface ItemRepository extends ElasticsearchRepository<Item,Long> {

    /** * 根据价格区间查询 * @param price1 * @param price2 * @return */
    List<Item> findByPriceBetween(double price1, double price2);
}

然后添加一些测试数据:

@Autowired
    private ItemRepository itemRepository; 

	@Test
    public void indexList_custom () { //添加一些测试数据
        List<Item> list = new ArrayList<>();
        list.add(new Item(1L, "小米手机7", "手机", "小米", 3299.00, "http://image.leyou.com/13123.jpg"));
        list.add(new Item(2L, "坚果手机R1", "手机", "锤子", 3699.00, "http://image.leyou.com/13123.jpg"));
        list.add(new Item(3L, "华为META10", "手机", "华为", 4499.00, "http://image.leyou.com/13123.jpg"));
        list.add(new Item(4L, "小米Mix2S", "手机", "小米", 4299.00, "http://image.leyou.com/13123.jpg"));
        list.add(new Item(5L, "荣耀V10", "手机", "华为", 2799.00, "http://image.leyou.com/13123.jpg"));
        // 接收对象集合,实现批量新增
        itemRepository.saveAll(list);
    }

不需要写实现类,调用接口,然后我们直接去运行:

@Autowired
 private ItemRepository itemRepository;

@Test
public void queryByPriceBetween(){
    List<Item> list = this.itemRepository.findByPriceBetween(2000.00, 3500.00);
    for (Item item : list) {
        System.out.println("item = " + item);
    }
}

虽然基本查询和自定义方法已经很强大了,但是如果是复杂查询(模糊、通配符、词条查询等)就显得力不从心了。此时,我们只能使用原生查询。

高级查询

先看看基本玩法

@Test
public void testBaseQuery(){
    // 词条查询
    MatchQueryBuilder queryBuilder = QueryBuilders.matchQuery("title", "小米");
    // 执行查询
    Iterable<Item> items = this.itemRepository.search(queryBuilder);
    items.forEach(System.out::println);
}

QueryBuilders提供了大量的静态方法,用于生成各种不同类型的查询对象,例如:词条、模糊、通配符等QueryBuilder对象。

elasticsearch提供很多可用的查询方式,但是不够灵活。如果想玩过滤或者聚合查询等就很难了。

分页查询

// 分页查询
    @Test
    public void testNativeQuery2(){
        // 构建查询条件
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        // 添加基本的分词查询
        queryBuilder.withQuery(QueryBuilders.termQuery("category", "手机"));

        // 初始化分页参数
        int page = 0;
        int size = 3;
        // 设置分页参数
        queryBuilder.withPageable(PageRequest.of(page, size));

        // 执行搜索,获取结果
        Page<Item> items = this.itemRepository.search(queryBuilder.build());
        // 打印总条数
        System.out.println("总条数"+items.getTotalElements());
        // 打印总页数
        System.out.println("总页数"+items.getTotalPages());
        // 每页大小
        System.out.println("每页大小"+items.getSize());
        // 当前页
        System.out.println("当前页"+items.getNumber());
        items.forEach(System.out::println);
    }

NativeSearchQueryBuilder:Spring提供的一个查询条件构建器,帮助构建json格式的请求体

Page<item>:默认是分页查询,因此返回的是一个分页的结果对象,包含属性:

  • totalElements:总条数
  • totalPages:总页数
  • Iterator:迭代器,本身实现了Iterator接口,因此可直接迭代得到当前页的数据
  • 其它属性:

可以发现,Elasticsearch中的分页是从第0页开始

排序

排序也通用通过NativeSearchQueryBuilder完成:

//排序
    @Test
    public void testSort(){
        // 构建查询条件
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        // 添加基本的分词查询
        queryBuilder.withQuery(QueryBuilders.termQuery("category", "手机"));

        // 排序
        queryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC));

        // 执行搜索,获取结果
        Page<Item> items = this.itemRepository.search(queryBuilder.build());
        // 打印总条数
        System.out.println(items.getTotalElements());
        items.forEach(System.out::println);
    }

聚合

聚合为桶

桶就是分组,比如这里我们按照品牌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());
    }

}

嵌套聚合,求平均值

@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());
    }

}

高亮查询

//高亮查询(多字段查询 分页 排序)
    public List<Item> findByNameAndHighlightAdnPageable(String name, int page,int size) {

        if(page <= 0){
            page = 1;
        }
        if( size <= 0){
            size = 5; //最低一页显示5条
        }
                HighlightBuilder.Field nameField = new HighlightBuilder .Field("*")
                .preTags("<span style='color:red'>")
                .postTags("</span>").requireFieldMatch(false);

  //多字段查询,可同时在title 和category查询 对应实体类中的属性名 注意:使用高亮只能查询文本类型的字段不能查询数字类型的字段
        NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.multiMatchQuery(name, "title","category"))
                .withPageable(PageRequest.of(page - 1, size))   //分页
                .withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC))   //排序
                .withHighlightFields(nameField)
                .build();
// 还有就是除了 QueryBuilders.multiMatchQuery 查询的方式可以使用其他的方式 
//比如: QueryBuilders.termQuery 看自己实际需求
        
        
        

        AggregatedPage<Item> Items = elasticsearchTemplate.queryForPage(nativeSearchQuery, Item.class, new SearchResultMapper() {
                    @Override
                    public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
                        SearchHits searchHits = response.getHits();
                        SearchHit[] hits = searchHits.getHits();
                        ArrayList<Item> Items = new ArrayList<Item>();
                        for (SearchHit hit : hits) {
                            Item Item = new Item();
                            //原始map 需要显示的数据字段(需要跟随实体类进行修改)
                            Map<String, Object> sourceAsMap = hit.getSourceAsMap();
                            Item.setId(Long.parseLong(sourceAsMap.get("id").toString()));
                            Item.setTitle(sourceAsMap.get("title").toString());
                            Item.setCategory(sourceAsMap.get("category").toString());
                            Item.setBrand(sourceAsMap.get("brand").toString());
                            Item.setPrice(Double.parseDouble(sourceAsMap.get("price").toString()));
                            Item.setImages(sourceAsMap.get("images").toString());


                            //添加需要高亮的字段 要和查询的字段 一 一对应
                            Map<String, HighlightField> highlightFields = hit.getHighlightFields();
                            if (highlightFields.get("title") != null) {
                                String nameHighlight = highlightFields.get("title").getFragments()[0].toString();
                                Item.setTitle(nameHighlight);
                            }
                            if (highlightFields.get("category") != null) {
                                String contentHighlight = highlightFields.get("category").getFragments()[0].toString();
                                Item.setCategory(contentHighlight);
                            }
                            Items.add(Item);
                        }
                        return new AggregatedPageImpl<T>((List<T>) Items);
                    }
                });
        return Items.getContent();
    }

    @Test
    public void getHighlight(){
        List<Item> sj = findByNameAndHighlightAdnPageable("手机", 1, 10);
        sj.forEach(System.out::println);

    }

其他查询总结

等值查询

查询name=小李的

QueryBuilders.termQuery("name", "小李")

范围查询

查询年龄大于等于18,并且小于等于50的记录

QueryBuilders.rangeQuery("age").gte(18).lte(50);

模糊查询

查询姓名中包含有小李的的文档记录:

QueryBuilders.boolQuery().must(QueryBuilders.wildcardQuery("name", "*小李*"));

多条件查询

查询姓名为:小李,并且年龄在10-50之间的文档

QueryBuilders.boolQuery()
    .must(QueryBuilders.termQuery("name", "小李"))
    .must(QueryBuilders.rangeQuery("age").gte(10).lte(50));

集合查询

查询地址在北京、上海、杭州,并且年龄在10至50,名字叫做李明的文档

List<String> list = Arrays.asList("北京", "上海", "杭州");
QueryBuilders.boolQuery()
   .must(QueryBuilders.termQuery("name", "李明"))
   .must(QueryBuilders.termsQuery("address", list))
   .must(QueryBuilders.rangeQuery("age").gte(10).lte(50));

使用should查询

查询姓名包含小李或者是地址是北京的记录,should相当于或者or

QueryBuilders.boolQuery()
    .should(QueryBuilders.wildcardQuery("name", "*小李*"))
    .should(QueryBuilders.termQuery("address", "北京"));

should和must配合查询

查询性别为男,姓名包含小李或地址为北京的记录,/*/minimumShouldMatch(1)//*表示最少要匹配到一个should条件

QueryBuilders.boolQuery()
   .must(QueryBuilders.termQuery("sex", "男"))
   .should(QueryBuilders.wildcardQuery("name", "*小李*"))
   .should(QueryBuilders.termQuery("address", "北京"))
   .minimumShouldMatch(1);

must:必须满足的条件

should:非必须满足的条件

minimumShouldMatch(1):至少要满足一个 should 条件

以上queryBuilder可以理解为需要满足一个must条件,并且至少要满足一个should条件。

有值查询

查询name有值,tag不存在值的文档

QueryBuilders.boolQuery()
     .must(QueryBuilders.existsQuery("name"))
     .mustNot(QueryBuilders.existsQuery("tag"));
should(QueryBuilders.termQuery("address", "北京"));

should和must配合查询

查询性别为男,姓名包含小李或地址为北京的记录,/*/minimumShouldMatch(1)//*表示最少要匹配到一个should条件

QueryBuilders.boolQuery()
   .must(QueryBuilders.termQuery("sex", "男"))
   .should(QueryBuilders.wildcardQuery("name", "*小李*"))
   .should(QueryBuilders.termQuery("address", "北京"))
   .minimumShouldMatch(1);

must:必须满足的条件

should:非必须满足的条件

minimumShouldMatch(1):至少要满足一个 should 条件

以上queryBuilder可以理解为需要满足一个must条件,并且至少要满足一个should条件。

有值查询

查询name有值,tag不存在值的文档

QueryBuilders.boolQuery()
     .must(QueryBuilders.existsQuery("name"))
     .mustNot(QueryBuilders.existsQuery("tag"));

相关文章