6.2 Spring整合Elasticsearch

304 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第29天,点击查看活动详情

6.2 Spring整合Elasticsearch

image-20220726070407515

引入依赖

注意:我的SpringBoot父版本是2.1.5.RELEASE,对于高版本的elasticsearch配置可能会有所不同

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

配置Elasticsearch

application.properties 配置文件中进行配置

注:我这里使用的elasticsearch是低版本的,高版本的配置可能会略有不同。

127.0.0.0localhost 等价

es中9200是http访问的端口,9300是tcp端口也是默认启用的,我们应用服务通常会用9300端口tcp去访问它。

#ElasticsearchProperties
# 配置集群名字,以前我们在es配置文件里改过es集群的名字
spring.data.elasticsearch.cluster-name=nowcoder
# 配置集群中各个结点(当然,我们这里只有一个结点)
spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300

image-20220726075255192

elasticsearch底层是基于netty,我们之前安装的redis底层也是基于netty,这两者在启用netty的时候有冲突,主要体现在es底层代码上,我们需要稍微做一个变通。

在项目的启动入口类CommunityApplication里面

@PostConstruct
public void init(){
   // 解决Netty启动冲突问题(看NettyRuntime中setAvailableProcessors方法和Netty4Utils类)
   System.setProperty("es.set.netty.runtime.available.processors", "false");
}

image-20220726075457260

Spring Data Elasticsearch

我们现在要把数据库里存的帖子存到es服务器里,然后我们去es服务器搜索这个帖子,我们可以使用 ElasticsearchTemplateElasticsearchRepository 去做这个事情。

ElasticsearchRepository

ElasticsearchRepository简单,我们先用这种方案,当有些需求它不好解决的时候再用ElasticsearchTemplate

在使用 ElasticsearchRepository 之前我们需要做一个配置,需要告诉它帖子这个表和es里要存的那个索引之间是什么样的对应关系,这个表存到es里变成索引的时候每个字段对应是什么样的类型,用什么方式搜素,这些都要做配置,这个配置呢不需要我们写xml文件,我们通过注解就可以,实体类上要加上这个注解,因为我们是针对帖子的操作。

类上加上注解,映射到哪个索引上去,映射到什么类型上去,创建几个分片几个副本,日后调用api时,如果没有索引会自动创建索引,然后没有分片、副本,会自动根据配置创建,然后再往索引里插入数据。然后为了让实体中的属性索引中的字段对应,所以我们在属性上也需要加上注解配置。

我们搜帖子主要就是搜标题内容

analyzer存储的时候的解析器,会将搜索的词拆分成更多的词然后与这句话建立一个索引与这句话匹配,增大搜索范围

searchAnalyzer搜索的时候的解析器,聪明的分词器,拆分出少的但是符合预期的词

image-20220726083615035

下面我们定义ElasticsearchRepository接口

泛型里面写要处理的实体类和主键是什么类型

@Repository         // es可以被看成一个特殊的数据库
public interface DiscussPostRepository extends ElasticsearchRepository<DiscussPost, Integer> {

}

image-20220726084938878

接下来我们来测试一下 ElasticsearchRepositoryElasticsearchTemplate

注意:测试的时候一定要将 Elasticsearch 服务 和 Kafka(还有zookeeper,因为项目用到了这些服务)打开

测试类

@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class ElasticsearchTests {

    @Autowired
    private DiscussPostMapper discussMapper;    // 先从mysql取出数据

    @Autowired
    private DiscussPostRepository discussRepository;   // 注入刚才的那个接口以便于将数据存到es查询

    @Autowired
    private ElasticsearchTemplate elasticTemplate;    // 有些情况DiscussPostRepository解决不了就用这个

    @Test
    public void testInsert() {
        // 插入数据
        discussRepository.save(discussMapper.selectDiscussPostById(241));
        discussRepository.save(discussMapper.selectDiscussPostById(242));
        discussRepository.save(discussMapper.selectDiscussPostById(243));
    }

    @Test
    public void testInsertList() {
        discussRepository.saveAll(discussMapper.selectDiscussPosts(101, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(102, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(103, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(111, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(112, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(131, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(132, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(133, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(134, 0, 100));
    }

    @Test
    public void testUpdate() {
        DiscussPost post = discussMapper.selectDiscussPostById(231);
        post.setContent("我是新人,使劲灌水.");
        discussRepository.save(post);
    }

    @Test
    public void testDelete() {
         //discussRepository.deleteById(231);
        discussRepository.deleteAll();
    }
    // 搜索功能
    @Test
    public void testSearchByRepository() {
        // 构造搜索条件:要不要排序、分页并且搜索结果要不要高亮显示等
        SearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.multiMatchQuery("互联网寒冬", "title", "content"))//搜索的关键词并且在哪搜
                .withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC))        // 排序方式:倒序
                .withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC))       // 排序方式:倒序
                .withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC))  // 排序方式:倒序
                .withPageable(PageRequest.of(0, 10))                        // 分页方式
                .withHighlightFields(       // 指定哪些字段要高亮显示,怎么高亮显示
                        new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),  // 高亮显示
                        new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>") // 高亮显示
                ).build();

        // elasticTemplate.queryForPage(searchQuery, class, SearchResultMapper)
        // 底层获取得到了高亮显示的值, 但是没有返回.

        // 这个Page不是我们自己写的那个实体类,而是java提供的
        Page<DiscussPost> page = discussRepository.search(searchQuery);
        System.out.println(page.getTotalElements());        // 一共有多少条数据匹配
        System.out.println(page.getTotalPages());           // 一共有多少页
        System.out.println(page.getNumber());               // 当前处在第几页
        System.out.println(page.getSize());                 // 每一页最多显示几条数据
        for (DiscussPost post : page) {                     // 查看查询到的数据
            System.out.println(post);
        }
    }

    @Test
    public void testSearchByTemplate() {
        // 构造搜索条件:要不要排序、分页并且搜索结果要不要高亮显示等
        SearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.multiMatchQuery("互联网寒冬", "title", "content"))//搜索的关键词并且在哪搜
                .withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC))        // 排序方式:倒序
                .withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC))       // 排序方式:倒序
                .withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC))  // 排序方式:倒序
                .withPageable(PageRequest.of(0, 10))                        // 分页方式
                .withHighlightFields(       // 指定哪些字段要高亮显示,怎么高亮显示
                        new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),  // 高亮显示
                        new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>") // 高亮显示
                ).build();
        // 参数1:搜索条件      参数2:实体类型    参数3:SearchResultMapper接口(实现一个匿名内部类或者传一个实现类)
        Page<DiscussPost> page = elasticTemplate.queryForPage(searchQuery, DiscussPost.class, new SearchResultMapper() {
            @Override            // queryForPage得到结果然后交给mapResults处理,然后通过SearchResponse参数处理
            public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> aClass, Pageable pageable) {
                SearchHits hits = response.getHits();   // 先取到这次搜索命令的数据(里面可以是多条数据)
                if (hits.getTotalHits() <= 0) {         // 判断有没有数据
                    return null;
                }
                // 执行到这里说明有数据
                List<DiscussPost> list = new ArrayList<>();
                for (SearchHit hit : hits) {        // 遍历命中的数据将其放在集合里
                    DiscussPost post = new DiscussPost();   // 将命中的数据包装到实体类中
                    // hit里面是将数据封装成了map并且里面key和value都是String类型,我们可以从中取值
                    String id = hit.getSourceAsMap().get("id").toString();
                    post.setId(Integer.valueOf(id)); // 将字符类型的数转成整数存入实体类的id属性

                    String userId = hit.getSourceAsMap().get("userId").toString();
                    post.setUserId(Integer.valueOf(userId));

                    String title = hit.getSourceAsMap().get("title").toString();
                    post.setTitle(title);

                    String content = hit.getSourceAsMap().get("content").toString();
                    post.setContent(content);

                    String status = hit.getSourceAsMap().get("status").toString();
                    post.setStatus(Integer.valueOf(status));

                    String createTime = hit.getSourceAsMap().get("createTime").toString();
                    post.setCreateTime(new Date(Long.valueOf(createTime)));

                    String commentCount = hit.getSourceAsMap().get("commentCount").toString();
                    post.setCommentCount(Integer.valueOf(commentCount));

                    // 处理高亮显示的结果
                    HighlightField titleField = hit.getHighlightFields().get("title");
                    if (titleField != null) {
                        // getFragments()返回的是一个数组,因为匹配的词条有可能是多个,我们只将第一个设置成高亮即可
                        post.setTitle(titleField.getFragments()[0].toString());
                    }

                    HighlightField contentField = hit.getHighlightFields().get("content");
                    if (contentField != null) {
                        post.setContent(contentField.getFragments()[0].toString());
                    }

                    list.add(post);
                }
                // AggregatedPageImpl   参数1:集合 参数2:方法参数pageable 参数3:一共多少条数据
                //                      参数4:     参数5:                 参数6:
                return new AggregatedPageImpl(list, pageable,
                        hits.getTotalHits(), response.getAggregations(), response.getScrollId(), hits.getMaxScore());
            }
        });

        System.out.println(page.getTotalElements());
        System.out.println(page.getTotalPages());
        System.out.println(page.getNumber());
        System.out.println(page.getSize());
        for (DiscussPost post : page) {
            System.out.println(post);
        }
    }
}