Layblog整合Elasticsearch实现站内文章搜索

564 阅读6分钟

本文主要讲解Layblog博客社区整合Elasticsearch的过程,以实现Layblog站内帖子文章信息在Elasticsearch初始化、查询。

本文主要涵盖以下内容:

1️⃣ Elasticsearch介绍
2️⃣ Elasticsearch安装和使用
3️⃣ Elasticsearch中文分词器使用
4️⃣ 集成spring-data-elasticsearch,分析核心注解、接口
5️⃣ ElasticSearch项目实战

一、ElasticSearch介绍

Elasticsearch是一个实时分布式搜索和分析引擎。它的内部使用 Lucene 做索引与搜索,但是它的目的是使全文检索变得简单, 通过隐藏 Lucene 的复杂性,取而代之的提供一套简单一致的 RESTful API。

然而ElasticSearch不仅仅是Lucene,并且不仅仅知识一个搜索引擎,它是这样被定义的:

  • 一个分布式的实时文档存储,每个字段都可以被索引和搜索。
  • 一个分布式实时分析搜索引擎。
  • 能胜任上百个服务节点的扩展,并支持PB级别的结构化或者非结构化数据搜索(天生支持集群)。

二、Elasticsearch安装和使用

Layblog采用的ES版本是ElasticSearch6.4.3版本,通过Docker命令拉取安装:

# 拉取es镜像并安装 
docker run -p 9200:9200 -p 9300:9300 -d --name layes -e ES_JAVA_OPTS="-Xms256m -Xmx256m" elasticsearch:6.4.3 

# 进入layes镜像 
docker exec -it layes /bin/bash 

# 进入es配置文件位置,修改es的集群名称,默认为docker-cluster,这里修改为kobe 
/usr/share/elasticsearch/config/elasticsearch.yml 

安装成功后,访问地址:http://112.124.38.197:9200/

image.png

三、安装中文分词器

执行以下命令拉取安装中文分词器:

./bin/elasticsearch-plugin install github.com/medcl/elast…

退出并重启layes镜像

  • exit
  • docker restart layes

四、集成spring-data-elasticsearch

Spring Data Elasticsearch 项目提供与 Elasticsearch 搜索引擎开发解决方案,Spring Data ElasticSearch为开发者提供:

  • 模板作为存储、搜索、排序文档和构建聚合的高级抽象。
  • 开发者可以通过自定义具体的方法和接口来实现查询。

Spring Data Elasticsearch - 参考文档

在pom.xml添加相关依赖

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

4.1核心接口CrudRepository

Spring Data 存储库抽象中的中央接口是Repository. 它需要域类来管理以及域类的 ID 类型作为类型参数。此接口主要用作标记接口,以捕获要使用的类型并帮助您发现扩展此接口的接口。该CrudRepository接口为被管理的实体类提供了复杂的CRUD功能。

public interface CrudRepository<T, ID> extends Repository<T, ID> {

   //保存传入的实体数据
   <S extends T> S save(S entity);
   
   //批量保存传入的实体数据
   <S extends T> Iterable<S> saveAll(Iterable<S> entities);

   //返回指定ID表示的实体
   Optional<T> findById(ID id);

   //判断是否存在对应实体
   boolean existsById(ID id);
   
   //返回所有的实体
   Iterable<T> findAll();

   //根据ids批量查询实体数据(返回多个)
   Iterable<T> findAllById(Iterable<ID> ids);

   //返回实体数量
   long count();

   //根据ID删除实体
   void deleteById(ID id);

   //删除实体
   void delete(T entity);

   //根据传入实体删除全部实体
   void deleteAll(Iterable<? extends T> entities);
   
   //删除所有
   void deleteAll();
}

4.2 核心接口PagingAndSortingRepository

PagingAndSortingRepositoryCrudRespository的扩展以提供额外的方式实现对实体的分页和排序功能。

public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {

   /**
    * 返回按给定选项排序的所有实体
    */
   Iterable<T> findAll(Sort sort);

   /**
    * 分页,返回满足Pageable对象中提供的Page限制的实体页面
    */
   Page<T> findAll(Pageable pageable);
}

比如要实现用户User查询分页,可以这样实现:

PagingAndSortingRepository<User, Long> repository = // … get access to a bean
Page<User> users = repository.findAll(PageRequest.of(1, 20));

4.3 常见注解

  • @Document

Document被称为文档,程序中大多的实体或对象能够被序列化为包含键值对的JSON对象,键(key)是字段(field)或属性(property)的名字,值(value)可以是字符串、数字、波尔类型、另一个对象、值数组或者其他特殊类型,比如表示日期的字符串或者表示地理位置的对象。

通常,我们可以认为对象(object)和文档(document)是等价相通的。不过,他们还是有所差别:对象(Object)是一个JSON结构体——类似于哈希、hashmap、字典或者关联数组;对象(Object)中还可能包含其他对象(Object)。Elasticsearch中,文档(document)这个术语有着特殊含义。它特指最顶层结构或者根对象(root object)序列化成的JSON数据(以唯一ID标识并存储于Elasticsearch中)

//表示映射到Elasticsearch文档上的领域对象
public @interface Document {
    //索引库名次,mysql中数据库的概念
    String indexName();
    
    //文档类型,mysql表的概念
    String type() default "";
    
    //默认分片数
    short shards() default 5;

    //默认副本数
    short replicas() default 1;
    
    ...
}
  • @Field
public @interface Field {
  //文档中字段的类型
	FieldType type() default FieldType.Auto;
  //是否建立倒排索引
	boolean index() default true;
  //是否进行存储
	boolean store() default false;
  //分词器名次
	String analyzer() default "";
}
//为文档自动指定元数据类型
public enum FieldType {
	Text,//会进行分词并建了索引的字符类型
	Integer,
	Long,
	Date,
	Float,
	Double,
	Boolean,
	Object,
	Auto,//自动判断字段类型
	Nested,//嵌套对象类型
	Ip,
	Attachment,
	Keyword//不会进行分词建立索引的类型
}

五、Layblog之ElasticSearch实战

5.1 定义PostDocument文档


/**
 * model的内容根据文章列表的展示提取需要字段,需要标题,作者,作者id,VIP,分类,阅读数量,评论数量,创建时间等等...
 *
 * @Author 林必昭
 * @Date 2022/2/7 10:53
 */

@Data
@Document(indexName = "post", type = "post") //indexName为索引库名:建议使用项目名称命名,type:类型,个人建议以实体类名称命名
public class PostDocument implements Serializable {

    @Id
    private Long id;

    /**
     * 索引时,为了提供索引覆盖范围,通常analyzer采用ik_max_word分析器,以最细颗粒度分词索引,这样可以搜索的词语更多,
     * searchAnalyzer表示搜索时候的分词,搜索时为了提高搜索准确度,会采用ik_smart分析器,会以粗粒度分词
     */
    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
    private String title;

    private Long authorId;

    @Field(type = FieldType.Keyword)
    private String authorName;

    private String authorVip;

    private String authorAvatar;

    private Long categoryId;

    /**
     * FieldType.Keyword需要完全匹配,不进行分词,直接索引
     */
    @Field(type = FieldType.Keyword)
    private String categoryName;

    private Boolean recommend;

    private Integer level;

    /**
     * FieldType.Text表示文本,需要经过分词,然后进行索引
     */
    @Field(type = FieldType.Text)
    private String tags;

    private Integer commentCount;

    private Integer viewCount;

    private Date created;
}

5.2 定义PostRepository继承ElasticsearchRepository获取常用的数据操作方法

/**
 * 只需要继承ElasticsearchRepository,调用其search()方法,接口之间的继承关系:
 *
 * @Index——>Repository——>CrudRepository——>PageAndSortRepository——>ElasticSearchCrudRepository——>ElasticSearchRepository
 * @Author 林必昭
 * @Date 2022/2/7 10:52
 */

@Repository
public interface PostRepository extends ElasticsearchRepository<PostDocument, Long> {
}

在接口内可以衍生对应的查询:

image.png

比如定义根据作者名称检索文章信息方法:

/**
 * 根据作者查询文章信息
 * 
 * @author: jacklin
 * @since 2022/8/10 23:42
 **/
Page<PostDocument> findPostDocumentByAuthorName(String authorName);

5.3 定义search接口

/**
 * 搜索方法接口
 *
 * @param current
 * @param size
 * @param q
 * @return
 */
@RequestMapping("/search")
public String search(@RequestParam(defaultValue = "1") Integer current,
                     @RequestParam(defaultValue = "10") Integer size,
                     String q) {
    com.baomidou.mybatisplus.extension.plugins.pagination.Page page = getPage();
    IPage<PostDocument> pageResult = searchService.query(page, q);
    req.setAttribute("pageData", pageResult);
    req.setAttribute("q", q);

    return "search";
}

5.4 搜索逻辑实现

/**
 * query搜索查询方法逻辑实现
 *
 * @param page
 * @param q
 * @return
 */
@Override
public IPage<PostDocument> query(Page page, String q) {
    // 分页信息 mybatis plus的page 转成 jpa的page
    Long current = page.getCurrent() - 1;
    Long size = page.getSize();
    Pageable pageable = PageRequest.of(current.intValue(), size.intValue());

    //多个字段匹配只要满足一个即可返回结果
    MultiMatchQueryBuilder queryBuilder = QueryBuilders.multiMatchQuery(q,
            IndexKey.POST_TITLE,
            IndexKey.POST_AUTHOR,
            IndexKey.POST_CATEGORY,
            IndexKey.POST_DESCRIPTION,
            IndexKey.POST_TAGS);
    SearchQuery searchQuery = new NativeSearchQueryBuilder()
            .withQuery(queryBuilder)
            .withPageable(pageable).build();

    org.springframework.data.domain.Page<PostDocument> documents = postRepository.search(searchQuery);
    IPage pageData = new Page(page.getCurrent(), page.getSize(), documents.getTotalElements());
    pageData.setRecords(documents.getContent());
    return pageData;
}

5.5 实现效果

image.png

输入Layblog,回车搜索:

image.png

搜索到1记录返回文章数据:

image.png