22-07-27 西安 Elasticsearch(02)Spring Data Elasticsearch

Spring Data Elasticsearch

Spring Data ElasticSearch 基于 spring data API 简化 elasticSearch操作,将原始操elasticSearch的客户端API 进行封装 。

官方网站:Spring Data Elasticsearch

spring-data-Elasticsearch 使用之前,必须先确定版本,elasticsearch 对版本的要求比较高。

springboot和elasticsearch的版本对照:

1、环境搭建—依赖、配置

创建工程 elasticsearch-springdata-es

1.1引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    <version>2.3.7.RELEASE</version>
</dependency>

1.2 配置文件application.properties

# es服务地址
elasticsearch.host=127.0.0.1
# es服务端口
elasticsearch.port=9200
# 配置日志级别,开启debug日志
logging.level.com.atguigu=debug

或者如下:(谷粒商城项目)

#单机情况下
spring.elasticsearch.rest.uris=http://127.0.0.1:9200

1.3主启动

@SpringBootApplication
public class SpringDataESApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringDataESApplication.class,args);
    }
}

1.4配置类

ElasticsearchRestTemplate基于RestHighLevelClient客户端的。需要自定义配置类,继承AbstractElasticsearchConfiguration,并实现elasticsearchClient()抽象方法,创建RestHighLevelClient对象

@ConfigurationProperties(prefix = "elasticsearch")
@Configuration
@Getter
@Setter
public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {
    private String host ;
    private Integer port ;

    //重写父类方法
    @Override
    public RestHighLevelClient elasticsearchClient() {
        //高级客户端
        RestClientBuilder builder = RestClient.builder(new HttpHost(host, port));
        RestHighLevelClient restHighLevelClient = new RestHighLevelClient(builder);
        return restHighLevelClient;
    }
}

2、ES中的实体类

Spring Data ES中实体类可是顶大用啊。Spring Data通过注解来声明字段的映射属性,有下面的三个注解:

@Document 作用在类,标记实体类为文档对象,属性解释如下:

indexName:对应索引库名称

shards:分片数量,默认1

replicas:副本数量,默认1

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

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

type:字段类型,取值是枚举:如 FieldType.Text

index:是否索引,布尔类型,默认是true

store:是否存储,布尔类型,默认是false

analyzer:分词器名称:如 ik_max_word
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "item",shards = 1, replicas = 1)
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; // 图片地址
}

运行如下测试类,控制台打印

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringDataESTest {
    @Autowired
    private ElasticsearchRestTemplate elasticsearchTemplate;

    @Test
    public void test0(){
        System.out.println(elasticsearchTemplate);
    }
}


3、创建索引库和映射关系

1.创建索引库

    // ElasticsearchTemplate是TransportClient客户端 已过期
    // ElasticsearchRestTemplate是RestHighLevel客户端
    @Autowired
    ElasticsearchRestTemplate restTemplate;

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

但是上述代码会这样

所以就还有一种创建索引库方式 

写法2(谷粒商城)
    @Autowired
    ElasticsearchRestTemplate elasticsearchTemplate;

    @Test
    public void test2(){
        // 索引库操作对象
        IndexOperations ops = elasticsearchTemplate.indexOps(User.class);
        ops.create(); // 初始化索引库
        ops.putMapping(ops.createMapping(User.class)); // 更新索引的映射
    }

4、文档操作

1.添加文档

    //2、存入文档
    @Test
    void save(){
        User user = new User();
        user.setAge(20);
        user.setId("2");
        user.setName("qiqi");
        user.setBirthday(new Date());
        user.setSalary(10000.0);
        elasticsearchTemplate.save(user);
    }

2.根据id查询文档

    //查询指定id的文档
    @Test
    void findById(){
        User user = elasticsearchTemplate.get("2", User.class);
        System.out.println(user);
    }

3.查询全部

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

    //4、查询所有的goods文档
    @Test
    void queryAll(){
        //构建查询的DSL需要通过
        NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
//        builder.withQuery()  添加查询方式
//        builder.addAggregation() 聚合查询
//        builder.withFields() 结果过滤
//        builder.withPageable()  分页
//        builder.withFilter()  过滤查询
//        builder.withHighlightBuilder()  高亮查询
//        builder.withSort()  排序查询
        builder.withQuery(QueryBuilders.matchAllQuery());
        Query query = builder.build();
        //执行查询
        SearchHits<Goods> search = elasticsearchTemplate.search(query, Goods.class);

        search.getSearchHits().forEach(goodsSearchHit -> {
            System.out.println(goodsSearchHit.getContent());
        });
    }

5、ElasticsearchRestTemplate综合

    @Autowired
    ElasticsearchRestTemplate elasticsearchTemplate;

    @Test
    void queryByCondition(){
        NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        //模糊查询:查询标题中带apple
        //.must(QueryBuilders.termQuery("attr.brand.keyword" , "小米")
//        boolQueryBuilder.must(QueryBuilders.matchQuery("title","小米手机"));
        boolQueryBuilder.must(QueryBuilders.fuzzyQuery("title","appl")
                .fuzziness(Fuzziness.TWO));
        boolQueryBuilder.filter(QueryBuilders.rangeQuery("price")
                .gte(999).lte(3999));
        builder.withQuery(boolQueryBuilder);
        //高亮
        builder.withHighlightBuilder(new HighlightBuilder()
                .field("title")
                .preTags("<em>").postTags("</em>"));
        //分页查询:   PageRequest.of(0,5) 参数1代表查询的起始索引
        builder.withPageable(PageRequest.of(0,5));
        //排序
        builder.withSort(SortBuilders.fieldSort("price")
                .order(SortOrder.DESC));
        //结果过滤:查询指定的字段
        builder.withFields("title","id","attr.brand","price");
        //使用品牌聚合,统计每个品牌的平均价格
        builder.addAggregation(AggregationBuilders.terms("brands")
                .field("attr.brand.keyword")
                .subAggregation(AggregationBuilders.avg("avgPrice")
                        .field("price"))
                .subAggregation(AggregationBuilders.terms("cates")
                        .field("attr.category.keyword")));

        Query query = builder.build();

        SearchHits<Goods> searchHits = elasticsearchTemplate.search(query, Goods.class);
        //解析hits结果集
        for (SearchHit<Goods> searchHit : searchHits.getSearchHits()) {
            Goods goods = searchHit.getContent();
            //高亮的标题设置给goods
            List<String> field = searchHit.getHighlightField("title");
            String join = StringUtils.join(field, "");
            goods.setTitle(join);
            //输出查询完毕的goods内容
            System.out.println(goods);
        }
        //解析聚合结果
        Aggregations aggregations = searchHits.getAggregations();
        Map<String, Aggregation> map = aggregations.asMap();
        ParsedStringTerms brands = (ParsedStringTerms) map.get("brands");
//        System.out.println(brands.getClass().getName());
        List<? extends Terms.Bucket> buckets = brands.getBuckets();//获取所有的brand品牌桶

        for (Terms.Bucket bucket : buckets) {
            System.out.println("品牌: "+bucket.getKey()+" 数量:"+bucket.getDocCount());
            ParsedAvg avgPrice = (ParsedAvg) bucket.getAggregations().asMap().get("avgPrice");
//            System.out.println(avgPrice.getClass().getName());
            System.out.println("平均价格:"+avgPrice.getValue());
            ParsedStringTerms cates = (ParsedStringTerms) bucket.getAggregations().asMap().get("cates");
            for (Terms.Bucket catesBucket : cates.getBuckets()) {
                System.out.println("分类:"+ catesBucket.getKey()+" , 数量:"+catesBucket.getDocCount());
            }
            System.out.println("==================================================");
        }

    }

6、RestHighLevelClient 综合

如果是6.8.x推荐使用原生RestHighLevelClient客户端

RestHighLevelClient还有一个好处,就是可以输出它生成的DSL语句

    //比较偏原生一些
    @Autowired
    RestHighLevelClient restHighLevelClient;

    /**
     * 指定条件的查询
     * @throws IOException
     */
    @Test
    void queryByCondition2() throws IOException {
        SearchRequest searchRequest = new SearchRequest("goods");//一定要指定索引库
        //DSL语句的构建器
        SearchSourceBuilder builder = new SearchSourceBuilder();
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();

        //标题匹配查询:小米手机
        boolQuery.should(QueryBuilders.matchQuery("title","小米手机").operator(Operator.OR));
        boolQuery.should(QueryBuilders.fuzzyQuery("title","华为").fuzziness(Fuzziness.ONE));

        //价格区间查询:1999~3999(不能影响文档评分)
        boolQuery.filter(QueryBuilders.rangeQuery("price")
                .gte(1999).lte(3999));
        builder.query(boolQuery);

        //标题高亮显示
        builder.highlighter(new HighlightBuilder()
                .field("title")
                .preTags("<font style='color:red'>")
                .postTags("</font>"));

        //分页查询第一页 前5条
        builder.from(0);
        builder.size(5);

        //结果过滤: 只查询title  price  id   attr.category分类
        builder.fetchSource(new String[]{
                "title","id","price","attr.brand","attr.category"
        } , null);

        //按照价格降序排列
        builder.sort("price",SortOrder.DESC);

        //使用品牌聚合,统计每个品牌的平均价格
        builder.aggregation(AggregationBuilders.terms("品牌桶")
                .field("attr.brand.keyword")
                .subAggregation(AggregationBuilders.avg("平均价格")
                        .field("price")));

        searchRequest.source(builder);
        //发起请求获取结果
        SearchResponse search = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        //解析结果
        org.elasticsearch.search.SearchHit[] hits = search.getHits().getHits();
        Gson gson = new Gson();
        for (org.elasticsearch.search.SearchHit hit : hits) {
//            System.out.println(hit.getSourceAsString());
            Goods goods = gson.fromJson(hit.getSourceAsString(), Goods.class);

            HighlightField field = hit.getHighlightFields().get("title");
            Text[] texts = field.getFragments();
            goods.setTitle(StringUtils.join(texts, ""));

            System.out.println(goods);
        }

        ParsedStringTerms brands = (ParsedStringTerms) search.getAggregations().asMap().get("品牌桶");

        for (Terms.Bucket bucket : brands.getBuckets()) {
            System.out.println(bucket.getKey()+" , "+ bucket.getDocCount());
            ParsedAvg avgPrice = (ParsedAvg) bucket.getAggregations().asMap().get("平均价格");
            System.out.println(avgPrice.getType()+" , "+ avgPrice.getValue());
        }
    }

7、Repository文档操作(推荐)

Spring Data 的另一个强大功能,是根据方法名称自动实现功能。

只要你定义一个接口,然后继承Repository提供的一些子接口,就能具备各种基本的CRUD功能。

public interface UserRepository extends ElasticsearchRepository<User, Long> {

}

使用repository新增文档、删除文档、查询文档

@Autowired
UserRepository userRepository;
@Test
void testAdd(){
    User user = new User();
    user.setAge(20);
    user.setId("1");
    user.setName("zhang3");
    user.setBirthday(new Date());
    user.setSalary(10000.0);
    this.userRepository.save(user);
}

@Test
void testDelete(){
    this.userRepository.deleteById(1l);
}

@Test
void testFind(){
    System.out.println(this.userRepository.findById(1l).get());
}

结果:

基本查询扩展

比如:你的方法名叫做:findByTitle,那么它就知道你是根据title查询,然后自动帮你完成,无需写实现类。不用在类上加任何的注解,但是泛型要写对

其中ElasticsearchRepository接口功能最强大。该接口的方法包括:

测试方法如下

    @Test
    public void testFindByTitile(){
        // 查询全部,并按照价格降序排序
        Iterable<Item> items = itemDao.findByTitle("手机");
        items.forEach(item-> System.out.println(item));
    }

首先,目前索引库item中所有的文档如下:

 运行后控制台打印如下:效果他不就有了吗

但是方法名称要符合一定的约定

Keyword

Sample方法命名

Elasticsearch Query String执行方式

And

findByNameAndPrice

{"bool" : {"must" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}}

Or

findByNameOrPrice

{"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}}

Is

findByName

{"bool" : {"must" : {"field" : {"name" : "?"}}}}

Not

findByNameNot

{"bool" : {"must_not" : {"field" : {"name" : "?"}}}}

Between

findByPriceBetween

{"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : ?,"include_lower" : true,"include_upper" : true}}}}}

LessThanEqual

findByPriceLessThan

{"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}}

GreaterThanEqual

findByPriceGreaterThan

{"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}}

Before

findByPriceBefore

{"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}}

After

findByPriceAfter

{"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}}

Like

findByNameLike

{"bool" : {"must" : {"field" : {"name" : {"query" : "?*","analyze_wildcard" : true}}}}}

StartingWith

findByNameStartingWith

{"bool" : {"must" : {"field" : {"name" : {"query" : "?*","analyze_wildcard" : true}}}}}

EndingWith

findByNameEndingWith

{"bool" : {"must" : {"field" : {"name" : {"query" : "*?","analyze_wildcard" : true}}}}}

Contains/Containing

findByNameContaining

{"bool" : {"must" : {"field" : {"name" : {"query" : "**?**","analyze_wildcard" : true}}}}}

In

findByNameIn(Collection<String>names)

{"bool" : {"must" : {"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"name" : "?"}} ]}}}}

NotIn

findByNameNotIn(Collection<String>names)

{"bool" : {"must_not" : {"bool" : {"should" : {"field" : {"name" : "?"}}}}}}

Near

findByStoreNear

Not Supported Yet !

True

findByAvailableTrue

{"bool" : {"must" : {"field" : {"available" : true}}}}

False

findByAvailableFalse

{"bool" : {"must" : {"field" : {"available" : false}}}}

OrderBy

findByAvailableTrueOrderByNameDesc

{"sort" : [{ "name" : {"order" : "desc"} }],"bool" : {"must" : {"field" : {"available" : true}}}}

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


8、重建索引

ElasticSearch的索引一旦创建,只允许添加字段,不允许改变字段。因为改变字段,需要重建倒排索引,影响内部缓存结构,性能太低。

解决办法:重建一个新的索引,并将原有索引的数据导入到新索引中。
原索引库 :student_index_v1
新索引库 :student_index_v2

# 2:将student_index_v1 数据拷贝到 student_index_v2
POST _reindex
{
  "source": {
    "index": "student_index_v1"
  },
  "dest": {
    "index": "student_index_v2"
  }
}

9、一些问题

User实体类中没有_class字段,反序列化时就会出错

 解决办法,加上注解@JsonIgnoreProperties("_class")

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值