使用背景

在项目的一些关键字模糊查询、全局查询的背景下,使用直接操作数据库的模糊查询 ‘%xx%’ 这样的语句,不会使用索引查,而是效率低的全表扫描。如果再加一个条件,一个关键字查询多张表的全局查询,在数据量大的情况下,造成的时间复杂度会更高。
这个情况在常用的软件中会经常遇到,look一下CSDN的一个搜索功能,输入几个关键字,它会将标题、内容、广告…中带有关键字的全部找到,还有关键词高亮的功能。速度也是那么快。让我们来一探究竟。
在这里插入图片描述

ElasticSearch简介

  • 一个分布式的、Restful风格的搜索引擎。
  • 支持对各种类型的数据的检索。
  • 搜索速度快,可以提供实时的搜索服务。
  • 便于水平扩展,每秒可以处理PB级海量数据。
    优点如上这么明显,拙劣的解释一下它的实现:它不同于数据库的索引查询原理,采用的是倒叙索引的算法(通俗来说正序索引是通过key来找到value,而倒叙索引是通过value找key)。可以看下面的一张表:
idnameage
1张三18
2李四19
3李三四18

如果一个查询条件是name,那么就是以name为倒叙索引,可以形成下面的格式:

termdocument
张三[1]
李四[2,3]
李三[3]
三四[3]

我们是将内容进行了分词(这里是最细粒划分)。然后指向了我们document的一个唯一的标识,能够找到位置的地址。
这样,当我们在程序发出一个查询请求后,比如“李四张三”。首先会把这个查询内容分词:“李四”、“张三”。然后就找到对应的数据[1,2,3]。这三条数据了,比我们在mysql中模糊查询快的多。这是其中的一个原因。
我们将“张三”、“李四”、“三四…这样的叫做term。如果有很多个term,那么我们如何找到对应的term呢。

我们以term是英文为例:具体的做法是
term index。term index有点像一本字典的大的章节表。如果所有的term都是英文字符的话,可能这个term index就真的是26个英文字符表构成的了。但是实际的情况是,term未必都是英文字符,term可以是任意的byte数组。而且26个英文字符也未必是每一个字符都有均等的term,比如x字符开头的term可能一个都没有,而s开头的term又特别多。实际的term index是一棵trie 树:
在这里插入图片描述
可以总结处理就是一个字典树了,后来在加入一些压缩技术,(Luncene?),这种方式就很快就能够查找到对应的分词,然后在对应的分词就找到了对应的主键,就可以直接找到对应的数据了。关于字典树后期准备发一篇(记一下)。

Elasticsearch术语

  • 索引、类型、文档、字段。
  • 集群、节点、分片、副本。

解释一下:

  1. 索引:相当于数据库中的database 改版后作为table

  2. 类型:相当于数据库中的table 不再使用

  3. 文档:相当于数据库中的一行数据,数据结构为JSON

  4. 字段:相当于数据库中的一列
    Elasticsearch6.0以后开始逐步废除类型的概念,索引的含义中也包括了类型。

  5. 集群:分布式部署,提高性能

  6. 节点:集群中的每一台服务器

  7. 分片:对一个索引的进一步划分存储,提高并发处理能力

  8. 副本:对分片的备份,提高可用性

相关连接:Elasticsearch官网

安装配置Elasticsearch

配置一下:在安装后的config/elasticsearch.yml下:

在这里插入图片描述
环境变量也需要配置:
在这里插入图片描述
因为经常搜索是中文,还需要搭配一下中文的分词插件ik,在github下:
在这里插入图片描述
解压到指定目录下:
在这里插入图片描述

IKAnalyzer.cfg 可以自己配置新词,例如一些网络用语或者所属项目常用词:
在这里插入图片描述
在这里插入图片描述

对Elasticsearch的操作

因为Elasticsearch服务是基于Restful风格的,可以用postman直接操作url进行增删改查。

  • 建索引:
    在这里插入图片描述
  • 提交数据:

test:索引
_doc:固定格式
1:id号 然后在请求body中写数据
PUT localhost:9200/test/_doc/1

在这里插入图片描述

  • 查数据

GET localhost:9200/test/_doc/1

在这里插入图片描述

  • 搜索数据
    在这里插入图片描述

Spring整合Elasticsearch

  • pom引入依赖
		<dependency>
			<groupId>org.springframework.data</groupId>
			<artifactId>spring-data-elasticsearch</artifactId>
		</dependency>
  • 配置properties
#elasticsearchProperties
spring.data.elasticsearch.cluster-name=nowcoder
spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300
  • 解决Netty冲突问题

问题原因:Redis底层使用了Netty,Elasticsearch也用了Netty,当被注册两次就会报错

在启动类中解决
在这里插入图片描述
整合完毕,就是对其的使用了。

使用Elasticsearch

项目中使用的是将一张论坛的内容表加入到es中,在实体类中,使用@Document注解(映射索引、类型、分片、副本)和@field注解(映射字段)。如下:

package com.nowcoder.community.entity;

import java.util.Date;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data;
import lombok.experimental.Accessors;
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;

import java.io.Serializable;

/**
 * (DiscussPost)表实体类
 *
 * @author makejava
 * @since 2021-07-21 15:29:51
 */
@Data
@Accessors(chain = true)
@TableName("discuss_post")
@Document(indexName = "discusspost",type = "doc",shards =6,replicas =3)   //映射索引、类型、分片、副本
public class DiscussPost extends Model<DiscussPost> {

    @Id
    @TableId(value = "id",type = IdType.AUTO)
    private Integer id;

    @Field(type = FieldType.Integer)
    private Integer userId;
    @Field(type = FieldType.Text,analyzer = "ik_max_word",searchAnalyzer = "ik_smart")
    private String title;
    @Field(type = FieldType.Text,analyzer = "ik_max_word",searchAnalyzer = "ik_smart")
    private String content;
    //0-普通; 1-置顶;
    @Field(type = FieldType.Integer)
    private Integer type;
    //0-正常; 1-精华; 2-拉黑;
    @Field(type = FieldType.Integer)
    private Integer status;
    @Field(type = FieldType.Date)
    private Date createTime;
    @Field(type = FieldType.Integer)
    private Integer commentCount;
    @Field(type = FieldType.Double)
    private double score;
}

在dao下建立子包elasticsearch,并创建DiscussPostRepository接口
在这里插入图片描述
此接口继承了ElasticsearchRepository,很方便的调用了es中的一些crud方法,下面会使用到。测试一波:

1、插入数据使用save和saveAll方法;
2、更新可以先查询出一个对应的对象,使用save做出更新,因为对象有唯一的主键区分
3、删除使用delete和deleteById方法
4、主要是查询,所谓搜索引擎,查询的逻辑和方法也很有魅力,首先是构建SearchQuery对象,如下,使用new NativeSearchQueryBuilder()中的方法,可以加入各种搜索条件,结果是得到了一个Page对象,一个简单的查询完毕。

@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class ElasticsearchTest {
    @Autowired
    private DiscussPostMapper discussMapper;
    @Autowired
    private DiscussPostRepository discussRepository;
    //有些功能上边的解决不了,所以引入下边的
    @Autowired
    private ElasticsearchTemplate elasticTemplate;
    @Test
    public void testInsert() {
        discussRepository.save(discussMapper.selectDiscussPostById(271));
        discussRepository.save(discussMapper.selectDiscussPostById(272));
        discussRepository.save(discussMapper.selectDiscussPostById(273));
    }
    @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));
    }
    //localhost:9200/discusspost/_doc/231
    @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)
        // 底层获取得到了高亮显示的值, 但是没有返回.所以为了得到高亮显示直接用elasticTemplate.queryForPage见下面
        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();
        Page<DiscussPost> page = elasticTemplate.queryForPage(searchQuery, DiscussPost.class, new SearchResultMapper() {
            @Override
            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();
                    String id = hit.getSourceAsMap().get("id").toString();
                    post.setId(Integer.valueOf(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(); //long类型的字符串
                    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) {
                        post.setTitle(titleField.getFragments()[0].toString());
                    }
                    HighlightField contentField = hit.getHighlightFields().get("content");
                    if (contentField != null) {
                        post.setContent(contentField.getFragments()[0].toString());
                    }
                    list.add(post);
                }
                return new AggregatedPageImpl(list, pageable,
                        hits.getTotalHits(), response.getAggregations(), response.getScrollId(), hits.getMaxScore());
            }
            @Override
            public <T> T mapSearchHit(SearchHit searchHit, Class<T> aClass) {
                return null;
            }
        });
        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);
        }
    }
}

接下来是对业务的处理,编写对应的service。使用了对ElasticsearchTemplate的操作。

package com.nowcoder.community.service;

import com.nowcoder.community.dao.elasticsearch.DiscussPostRepository;
import com.nowcoder.community.entity.DiscussPost;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.SearchResultMapper;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * @author cjy
 * @Package com.nowcoder.community.service
 * @date 2021/8/31 8:52
 */
@Service
public class ElasticsearchService {
    @Autowired
    private DiscussPostRepository discussPostRepository;
    @Autowired
    private ElasticsearchTemplate elasticTemplate;
    //保存帖子到el服务器
    public void saveDiscussPost(DiscussPost discussPost){
        discussPostRepository.save(discussPost);
    }
    //删除
    public void deleteDiscussPost(int id){
        discussPostRepository.deleteById(id);
    }
    //分页查询展示
    public Page<DiscussPost> searchDiscussPost(String keyword,int current,int limit){
        SearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.multiMatchQuery(keyword, "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(current, limit))
                .withHighlightFields(
                        new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),
                        new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>")
                ).build();
        return elasticTemplate.queryForPage(searchQuery, DiscussPost.class, new SearchResultMapper() {
            @Override
            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();
                    String id = hit.getSourceAsMap().get("id").toString();
                    post.setId(Integer.valueOf(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(); //long类型的字符串
                    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) {
                        post.setTitle(titleField.getFragments()[0].toString());
                    }
                    HighlightField contentField = hit.getHighlightFields().get("content");
                    if (contentField != null) {
                        post.setContent(contentField.getFragments()[0].toString());
                    }
                    list.add(post);
                }
                return new AggregatedPageImpl(list, pageable,
                        hits.getTotalHits(), response.getAggregations(), response.getScrollId(), hits.getMaxScore());
            }
        });
    }
}
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐