elasticsearch项目整合全文检索功能

5 课程搜索

5.1 需求分析

5.1.1 模块介绍

搜索功能是一个系统的重要功能,是信息查询的方式。课程搜索是课程展示的渠道,用户通过课程搜索找到课程信息,进一步去查看课程的详细信息,进行选课、支付、学习。
本项目的课程搜索支持全文检索技术,什么是全文检索?
全文检索是指计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。全文搜索搜索引擎数据库中的数据。
全文检索可以简单理解为通过索引搜索文章。

全文检索的速度非常快,早期应用在搜索引擎技术中,比如:百度、google等,现在通常一些大型网站的搜索功能都是采用全文检索技术。

课程搜索也要将课程信息建立索引,在课程发布时建立课程索引,索引建立好用户可通过搜索网页去查询课程信息。

所以,课程搜索模块包括两部分:课程索引、课程搜索。
课程索引是将课程信息建立索引。
课程搜索是通过前端网页,通过关键字等条件去搜索课程。

5.1.2 业务流程

根据模块介绍的内容,课程搜索模块包括课程索引、课程搜索两部分。
1、课程索引
在课程发布操作执行后通过消息处理方式创建课程索引,如下图:

本项目使用elasticsearch作为索引及搜索服务。
2、课程搜索
课程索引创建完成,用户才可以通过前端搜索课程信息。
课程搜索可以从首页进入搜索页面。

下图是搜索界面,可以通过课程分类、课程难度等级等条件进行搜索。

5.2 准备环境

5.2.1 搭建elasticsearch

在课前下发的虚拟中已经在docker容器中安装了elasticsearch和kibana。
kibana 是 ELK(Elasticsearch , Logstash, Kibana )之一,kibana 一款开源的数据分析和可视化平台,通过可视化界面访问elasticsearch的索引库,并可以生成一个数据报表。
开发中主要使用kibana通过api对elasticsearch进行索引和搜索操作,通过浏览器访问 http://192.168.101.65:5601/app/dev_tools#/console进入kibana的开发工具界面。

可通过命令:GET /_cat/indices?v 查看所有的索引,通过此命令判断kibana是否正常连接elasticsearch。

5.2.2 创建搜索工程

下边创建搜索工程,此工程作为项目的搜索服务,提供索引和搜索两部分的功能。

pom.xml如下:

| XML_<?_xml version="1.0" encoding="UTF-8"_?>_
](https://maven.apache.org/xsd/maven-4.0.0.xsd%22%3E)
4.0.0

xuecheng-plus-parent
com.xuecheng
0.0.1-SNAPSHOT
…/xuecheng-plus-parent

xuecheng-plus-search

<dependencies>
    <dependency>
        <groupId>com.xuecheng</groupId>
        <artifactId>xuecheng-plus-base</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-high-level-client</artifactId>
    </dependency>

    <dependency>
        <groupId>org.elasticsearch</groupId>
        <artifactId>elasticsearch</artifactId>
    </dependency>

    <!-- Spring Boot 的 Spring Web MVC 集成 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <!-- 排除 Spring Boot 依赖的日志包冲突 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!-- Spring Boot 集成 Junit -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <!-- Spring Boot 集成 log4j2 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-log4j2</artifactId>
    </dependency>

    <!-- Spring Boot 集成 swagger -->
    <dependency>
        <groupId>com.spring4all</groupId>
        <artifactId>swagger-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

bootstrap.yml配置如下:

| YAMLspring:
application:
name: search
cloud:
nacos:
server-addr: 192.168.101.65:8848
discovery:
namespace: ${spring.profiles.active}
group: xuecheng-plus-project
config:
namespace: s p r i n g . p r o f i l e s . a c t i v e g r o u p : x u e c h e n g − p l u s − p r o j e c t f i l e − e x t e n s i o n : y a m l r e f r e s h − e n a b l e d : t r u e s h a r e d − c o n f i g s : − d a t a − i d : s w a g g e r − {spring.profiles.active} group: xuecheng-plus-project file-extension: yaml refresh-enabled: true shared-configs: - data-id: swagger- spring.profiles.activegroup:xuechengplusprojectfileextension:yamlrefreshenabled:truesharedconfigs:dataid:swagger{spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
- data-id: logging-${spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true

profiles:
active: dev |
| — |

在nacos添加search-dev.yaml

| YAMLserver:
servlet:
context-path: /search
port: 63080

elasticsearch:
hostlist: 192.168.101.65:9200 #多个结点中间用逗号分隔
course:
index: course-publish
source_fields: id,name,grade,mt,st,charge,pic,price,originalPrice,teachmode,validDays,createDate |
| — |

编写elasticsearch配置类ElasticsearchConfig

| Javapackage com.xuecheng.search.config;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ElasticsearchConfig {

@Value("${elasticsearch.hostlist}")
private String hostlist;

@Bean
public RestHighLevelClient restHighLevelClient(){
    //解析hostlist配置信息
    String[] split = hostlist.split(",");
    //创建HttpHost数组,其中存放es主机和端口的配置信息
    HttpHost[] httpHostArray = new HttpHost[split.length];
    for(int i=0;i<split.length;i++){
        String item = split[i];
        httpHostArray[i] = new HttpHost(item.split(":")[0], Integer._parseInt_(item.split(":")[1]), "http");
    }
    //创建RestHighLevelClient客户端
    return new RestHighLevelClient(RestClient._builder_(httpHostArray));
}
}

5.2.3 创建索引

要使用elasticsearch需要建立索引,索引相当于MySQL中的表,Elasticsearch与MySQL之间概念的对应关系见下表:

1、创建索引,并指定Mapping。
PUT /course-publish

| JSON{
“settings”: {
“number_of_shards”: 1,
“number_of_replicas”: 0
},
“mappings”: {
“properties”: {
“id”: {
“type”: “keyword”
},
“companyId”: {
“type”: “keyword”
},
“companyName”: {
“analyzer”: “ik_max_word”,
“search_analyzer”: “ik_smart”,
“type”: “text”
},
“name”: {
“analyzer”: “ik_max_word”,
“search_analyzer”: “ik_smart”,
“type”: “text”
},
“users”: {
“index”: false,
“type”: “text”
},
“tags”: {
“analyzer”: “ik_max_word”,
“search_analyzer”: “ik_smart”,
“type”: “text”
},
“mt”: {
“type”: “keyword”
},
“mtName”: {
“type”: “keyword”
},
“st”: {
“type”: “keyword”
},
“stName”: {
“type”: “keyword”
},
“grade”: {
“type”: “keyword”
},
“teachmode”: {
“type”: “keyword”
},
“pic”: {
“index”: false,
“type”: “text”
},
“description”: {
“analyzer”: “ik_max_word”,
“search_analyzer”: “ik_smart”,
“type”: “text”
},
“createDate”: {
“format”: “yyyy-MM-dd HH:mm:ss”,
“type”: “date”
},
“status”: {
“type”: “keyword”
},
“remark”: {
“index”: false,
“type”: “text”
},
“charge”: {
“type”: “keyword”
},
“price”: {
“type”: “scaled_float”,
“scaling_factor”: 100
},
“originalPrice”: {
“type”: “scaled_float”,
“scaling_factor”: 100
},
“validDays”: {
“type”: “integer”
}
}
}

}

2、查询索引
通过 GET /_cat/indices?v 查询所有的索引,查找course-publish是否创建成功。
通过GET /course-publish/_mapping 查询course-publish的索引结构。

| JSON{
“course-publish” : {
“mappings” : {
“properties” : {
“charge” : {
“type” : “keyword”
},
“companyId” : {
“type” : “keyword”
},
“companyName” : {
“type” : “text”,
“analyzer” : “ik_max_word”,
“search_analyzer” : “ik_smart”
},
“createDate” : {
“type” : “date”,
“format” : “yyyy-MM-dd HH:mm:ss”
},
“description” : {
“type” : “text”,
“analyzer” : “ik_max_word”,
“search_analyzer” : “ik_smart”
},
“grade” : {
“type” : “keyword”
},
“id” : {
“type” : “keyword”
},
“mt” : {
“type” : “keyword”
},
“mtName” : {
“type” : “keyword”
},
“name” : {
“type” : “text”,
“analyzer” : “ik_max_word”,
“search_analyzer” : “ik_smart”
},
“originalPrice” : {
“type” : “scaled_float”,
“scaling_factor” : 100.0
},
“pic” : {
“type” : “text”,
“index” : false
},
“price” : {
“type” : “scaled_float”,
“scaling_factor” : 100.0
},
“remark” : {
“type” : “text”,
“index” : false
},
“st” : {
“type” : “keyword”
},
“stName” : {
“type” : “keyword”
},
“status” : {
“type” : “keyword”
},
“tags” : {
“type” : “text”,
“analyzer” : “ik_max_word”,
“search_analyzer” : “ik_smart”
},
“teachmode” : {
“type” : “keyword”
},
“users” : {
“type” : “text”,
“index” : false
},
“validDays” : {
“type” : “integer”
}
}
}
}

}

3、删除索引
如果发现创建的course-publish不正确可以删除重新创建。
删除索引后当中的文档数据也同时删除,一定要谨慎操作!
删除索引命令:DELETE /course-publish

5.3 索引管理

5.3.1 REST API

5.3.1.1 添加文档

索引创建好就可以向其它添加文档,此时elasticsearch会根据索引的mapping配置对有些字段进行分词。
这里我们要向course_publish中添加课程信息。
使用rest api进行测试,如下:
使用post请求,/course-publish/_doc/103 第一部分为索引名称,_doc固定,103为文档的主键id,这里为课程id。
课程内容使用json表示。

| JSONPOST /course-publish/_doc/103
{
“charge” : “201001”,
“companyId” : 100000,
“companyName” : “北京黑马程序”,
“createDate” : “2022-09-25 09:36:11”,
“description” : “HTML/CSS”,
“grade” : “204001”,
“id” : 102,
“mt” : “1-1”,
“mtName” : “前端开发”,
“name” : “Html参考大全”,
“originalPrice” : 200.0,
“pic” : “/mediafiles/2022/09/20/e726b71ba99c70e8c9d2850c2a7019d7.jpg”,
“price” : 100.0,
“remark” : “没有备注”,
“st” : “1-1-1”,
“stName” : “HTML/CSS”,
“status” : “203002”,
“tags” : “没有标签”,
“teachmode” : “200002”,
“validDays” : 222

}

如果要修改文档的内容可以使用上边相同的方法,如果没有则添加,如果存在则更新。

5.3.1.2 查询文档

添加文档成功后可以通过主键id查询该文档的信息。
语法如下:

JSONGET /{索引库名称}/_doc/{id}
5.3.1.3 更新文档

更新文档分为全量更新和局部更新。
全量更新是指先删除再更新,语法如下:

| JSONPUT /{索引库名}/_doc/文档id
{
    “字段1”: “值1”,
    “字段2”: “值2”,
// … 略

}

局部更新语法如下:

| JSONPOST /{索引库名}/_update/文档id
{
    “doc”: {
“字段名”: “新的值”,
}

}
5.3.1.4 删除文档

删除文档将从索引中删除文档的记录。
语法如下:

JSONDELETE /{索引库名}/_doc/id值

5.3.2 接口定义

当课程发布时请求添加课程接口添加课程信息到索引,当课程下架时请求删除课程接口从索引中删除课程信息,这里先实现添加课程接口。
根据索引的mapping结构创建po类:

| Javapackage com.xuecheng.search.po;

import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;

import java.io.Serializable;
import java.time.LocalDateTime;

/**

  • 课程索引信息

  • @author itcast
    */
    @Data
    public class CourseIndex implements Serializable {

    private static final long _serialVersionUID _= 1L;

    /**

    • 主键
      */
      private Long id;

    /**

    • 机构ID
      */
      private Long companyId;

    /**

    • 公司名称
      */
      private String companyName;

    /**

    • 课程名称
      */
      private String name;

    /**

    • 适用人群
      */
      private String users;

    /**

    • 标签
      */
      private String tags;

    /**

    • 大分类
      */
      private String mt;

    /**

    • 大分类名称
      */
      private String mtName;

    /**

    • 小分类
      */
      private String st;

    /**

    • 小分类名称
      */
      private String stName;

    /**

    • 课程等级
      */
      private String grade;

    /**

    • 教育模式
      /
      private String teachmode;
      /
      *
    • 课程图片
      */
      private String pic;

    /**

    • 课程介绍
      */
      private String description;

    /**

    • 发布时间
      */
      @JSONField(format=“yyyy-MM-dd HH:mm:ss”)
      @JsonFormat(pattern = “yyyy-MM-dd HH:mm:ss”)
      private LocalDateTime createDate;

    /**

    • 状态
      */
      private String status;

    /**

    • 备注
      */
      private String remark;

    /**

    • 收费规则,对应数据字典–203
      */
      private String charge;

    /**

    • 现价
      /
      private Float price;
      /
      *
    • 原价
      */
      private Float originalPrice;

    /**

    • 课程有效期天数
      */
      private Integer validDays;
}

创建索引接口如下:

| Javapackage com.xuecheng.search.controller;

import com.xuecheng.base.execption.XueChengPlusException;
import com.xuecheng.search.po.CourseIndex;
import com.xuecheng.search.service.IndexService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**

  • @author Mr.M

  • @version 1.0

  • @description 课程索引接口

  • @date 2022/9/24 22:31
    */
    @Api(value = “课程信息索引接口”, tags = “课程信息索引接口”)
    @RestController
    @RequestMapping(“/index”)
    public class CourseIndexController {

    @ApiOperation(“添加课程索引”)
    @PostMapping(“course”)
    public Boolean add(@RequestBody CourseIndex courseIndex) {

    }
    } |
    | — |

5.3.3 接口开发

定义service接口,请求elasticsearch添加课程信息。
注意:为了适应其它文档信息,将添加文档定义为通用的添加文档接口,此接口不仅适应添加课程还适应添加其它信息。

| Javapackage com.xuecheng.search.service;

import com.xuecheng.search.po.CourseIndex;

/**

  • @author Mr.M

  • @version 1.0

  • @description 课程索引service

  • @date 2022/9/24 22:40
    */
    public interface IndexService {

    /**

    • @param _indexName _索引名称
    • @param _id _主键
    • @param _object _索引对象
    • @return Boolean true表示成功,false失败
    • @description 添加索引
    • @author Mr.M
    • @date 2022/9/24 22:57
      */
      public Boolean addCourseIndex(String indexName,String id,Object object);
}

接口实现如下:

| Javapackage com.xuecheng.search.service.impl;

import com.alibaba.fastjson.JSON;
import com.xuecheng.base.execption.XueChengPlusException;
import com.xuecheng.search.po.CourseIndex;
import com.xuecheng.search.service.IndexService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.DocWriteResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;

/**

  • @description 课程索引管理接口实现
  • @author Mr.M
  • @date 2022/9/25 7:23
  • @version 1.0
    */
    @Slf4j
    @Service
    public class IndexServiceImpl implements IndexService {

@Autowired
RestHighLevelClient client;

@Override
public Boolean addCourseIndex(String indexName,String id,Object object) {
String jsonString = JSON.toJSONString(object);
IndexRequest indexRequest = new IndexRequest(indexName).id(id);
//指定索引文档内容
indexRequest.source(jsonString,XContentType.JSON);
//索引响应对象
IndexResponse indexResponse = null;
try {
indexResponse = client.index(indexRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
log.error(“添加索引出错:{}”,e.getMessage());
e.printStackTrace();
XueChengPlusException.cast(“添加索引出错”);
}
String name = indexResponse.getResult().name();
System.out.println(name);
return name.equalsIgnoreCase(“created”) || name.equalsIgnoreCase(“updated”);

}

}

5.3.4 接口完善

完善接口:

| Javapackage com.xuecheng.search.controller;

import com.xuecheng.base.execption.XueChengPlusException;
import com.xuecheng.search.po.CourseIndex;
import com.xuecheng.search.service.IndexService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**

  • @author Mr.M

  • @version 1.0

  • @description 课程索引接口

  • @date 2022/9/24 22:31
    */
    @Api(value = “课程信息索引接口”, tags = “课程信息索引接口”)
    @RestController
    @RequestMapping(“/index”)
    public class CourseIndexController {

    @Value(“${elasticsearch.course.index}”)
    private String courseIndexStore;

    @Autowired
    IndexService indexService;

    @ApiOperation(“添加课程索引”)
    @PostMapping(“course”)
    public Boolean add(@RequestBody CourseIndex courseIndex) {

     Long id = courseIndex.getId();
     if(id==null){
         XueChengPlusException._cast_("课程id为空");
     }
     Boolean result = indexService.addCourseIndex(courseIndexStore, String._valueOf_(id), courseIndex);
     if(!result){
         XueChengPlusException._cast_("添加课程索引失败");
     }
     return result;
    

    }
    } |
    | — |

5.3.5 接口测试

使用httpclient进行测试

| JSON### 添加课程索引
POST {{search_host}}/search/index/course
Content-Type: application/json

{
“charge” : “201000”,
“companyId” : 100000,
“companyName” : “北京黑马程序员”,
“createDate” : “2022-09-25 09:36:11”,
“description” : “《Java编程思想》是2007年6月1日机械工业出版社出版的图书,作者是埃克尔,译者是陈昊鹏。主要内容本书赢得了全球程序员的广泛赞誉,即使是最晦涩的概念,在Bruce Eckel的文字亲和力和小而直接的编程示例面前也会化解于无形。从Java的基础语法到最高级特性(深入的面向对象概念、多线程、自动项目构建、单元测试和调试等),本书都能逐步指导你轻松掌握。从本书获得的各项大奖以及来自世界各地的读者评论中,不难看出这是一本经典之作”,
“grade” : “204001”,
“id” : 102,
“mt” : “1-3”,
“mtName” : “编程开发”,
“name” : “Java编程思想”,
“originalPrice” : 200.0,
“pic” : “/mediafiles/2022/09/20/1d0f0e6ed8a0c4a89bfd304b84599d9c.png”,
“price” : 100.0,
“remark” : “没有备注”,
“st” : “1-3-2”,
“stName” : “Java语言”,
“status” : “203002”,
“tags” : “没有标签”,
“teachmode” : “200002”,
“validDays” : 222

}

5.4 搜索

5.4.1 需求分析

索引信息维护完成下一步定义搜索接口搜索课程信息,首先需要搞清楚搜索功能的需求。
进入搜索界面,如下图:

根据搜索界面可知需求如下:
1、根据一级分类、二级分类搜索课程信息。
2、根据关键字搜索课程信息,搜索方式为全文检索,关键字需要匹配课程的名称、 课程内容。
3、根据难度等级搜索课程。
4、搜索结点分页显示。
技术点:
1、整体采用布尔查询。
2、根据关键字搜索,采用MultiMatchQuery,搜索name、description字段。
3、根据分类、课程等级搜索采用过虑器实现。
4、分页查询。
5、高亮显示。
为什么课程分类、课程等待等查询使用过虑器方式?

5.4.2 接口定义

1、定义搜索条件DTO类

| Javapackage com.xuecheng.search.dto;

import lombok.Data;
import lombok.ToString;

/**

  • @description 搜索课程参数dtl
  • @author Mr.M
  • @date 2022/9/24 22:36
  • @version 1.0
    */
    @Data
    @ToString
    public class SearchCourseParamDto {

//关键字
private String keywords;

//大分类
private String mt;

//小分类
private String st;
//难度等级
private String grade;

}

2、为了适应后期的扩展,定义搜索结果类,让它继承PageResult

| Javapackage com.xuecheng.search.dto;

import com.xuecheng.base.model.PageResult;
import lombok.Data;
import lombok.ToString;

import java.util.List;

/**

  • @author Mr.M

  • @version 1.0

  • @description TODO
    _ _* @date 2022/9/25 17:51
    */
    @Data
    @ToString
    public class SearchPageResultDto extends PageResult {

    public SearchPageResultDto(List items, long counts, long page, long pageSize) {
    super(items, counts, page, pageSize);
    }

}

接口定义如下:

| Javapackage com.xuecheng.search.controller;

import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.search.dto.SearchCourseParamDto;
import com.xuecheng.search.po.CourseIndex;
import com.xuecheng.search.service.CourseSearchService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**

  • @description 课程搜索接口
  • @author Mr.M
  • @date 2022/9/24 22:31
  • @version 1.0
    */
    @Api(value = “课程搜索接口”,tags = “课程搜索接口”)
    @RestController
    @RequestMapping(“/course”)
    public class CourseSearchController {

@ApiOperation(“课程搜索列表”)
@GetMapping(“/list”)
public PageResult list(PageParams pageParams, SearchCourseParamDto searchCourseParamDto){

}

}

5.4.3 基本功能实现

定义service接口,如下:

| Javapackage com.xuecheng.search.service;

import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.search.dto.SearchCourseParamDto;
import com.xuecheng.search.dto.SearchPageResultDto;
import com.xuecheng.search.po.CourseIndex;

/**

  • @description 课程搜索service

  • @author Mr.M

  • @date 2022/9/24 22:40

  • @version 1.0
    */
    public interface CourseSearchService {

    /**

    • @description 搜索课程列表
    • @param _pageParams _分页参数
    • @param _searchCourseParamDto _搜索条件
    • @return com.xuecheng.base.model.PageResult<com.xuecheng.search.po.CourseIndex> 课程列表
    • @author Mr.M
    • @date 2022/9/24 22:45
      */
      SearchPageResultDto queryCoursePubIndex(PageParams pageParams, SearchCourseParamDto searchCourseParamDto);
}

搜索接口的内容较多,我们分几步实现,首先实现根据分页搜索,接口实现如下:

| Javapackage com.xuecheng.search.service.impl;

import com.alibaba.fastjson.JSON;
import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.search.dto.SearchCourseParamDto;
import com.xuecheng.search.dto.SearchPageResultDto;
import com.xuecheng.search.po.CourseIndex;
import com.xuecheng.search.service.CourseSearchService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.lucene.search.TotalHits;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**

  • @author Mr.M

  • @version 1.0

  • @description 课程搜索service实现类

  • @date 2022/9/24 22:48
    */
    @Slf4j
    @Service
    public class CourseSearchServiceImpl implements CourseSearchService {

    @Value(“ e l a s t i c s e a r c h . c o u r s e . i n d e x " ) p r i v a t e S t r i n g c o u r s e I n d e x S t o r e ; @ V a l u e ( " {elasticsearch.course.index}") private String courseIndexStore; @Value(" elasticsearch.course.index")privateStringcourseIndexStore;@Value("{elasticsearch.course.source_fields}”)
    private String sourceFields;

    @Autowired
    RestHighLevelClient client;

    @Override
    public SearchPageResultDto queryCoursePubIndex(PageParams pageParams, SearchCourseParamDto courseSearchParam) {

     //设置索引
     SearchRequest searchRequest = new SearchRequest(courseIndexStore);
    
     SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
     BoolQueryBuilder boolQueryBuilder = QueryBuilders._boolQuery_();
     //source源字段过虑
     String[] sourceFieldsArray = sourceFields.split(",");
     searchSourceBuilder.fetchSource(sourceFieldsArray, new String[]{});
     
     //分页
     Long pageNo = pageParams.getPageNo();
     Long pageSize = pageParams.getPageSize();
     int start = (int) ((pageNo-1)*pageSize);
     searchSourceBuilder.from(start);
     searchSourceBuilder.size(Math._toIntExact_(pageSize));
    //布尔查询
     searchSourceBuilder.query(boolQueryBuilder);
     
     //请求搜索
     searchRequest.source(searchSourceBuilder);
    
     
     SearchResponse searchResponse = null;
     try {
         searchResponse = client.search(searchRequest, RequestOptions._DEFAULT_);
     } catch (IOException e) {
         e.printStackTrace();
         _log_.error("课程搜索异常:{}",e.getMessage());
         return new SearchPageResultDto<CourseIndex>(new ArrayList(),0,0,0);
     }
    
     //结果集处理
     SearchHits hits = searchResponse.getHits();
     SearchHit[] searchHits = hits.getHits();
     //记录总数
     TotalHits totalHits = hits.getTotalHits();
     //数据列表
     List<CourseIndex> list = new ArrayList<>();
    
     for (SearchHit hit : searchHits) {
    
         String sourceAsString = hit.getSourceAsString();
         CourseIndex courseIndex = JSON._parseObject_(sourceAsString, CourseIndex.class);
         list.add(courseIndex);
    
     }
     SearchPageResultDto<CourseIndex> pageResult = new SearchPageResultDto<>(list, totalHits.value,pageNo,pageSize);
    
     
    
     return pageResult;
    

    }

}

5.4.4 基本功能测试

当输入查询条件时会查询全部课程信息并支持分页查询。
1、准备测试
启动nginx、网关、搜索服务。
使用kibana通过rest api向索引库添加课程信息,或通过httpclient添加课程信息,至少添加两条信息。
2、进入搜索界面
默认查询出刚才添加的课程信息。
3、修改分页参数测试分页
打开course/ search.html页面 ,找到如下图所示位置:

修改pageSize为1,即一页显示一条记录。
刷新搜索界面,每页显示一条记录,如下图:

5.4.5 根据条件搜索

下边实现根据关键、一级分类、二级分类、难度等级搜索。

| Java@Override
public SearchPageResultDto queryCoursePubIndex(PageParams pageParams, SearchCourseParamDto courseSearchParam) {

//设置索引
SearchRequest searchRequest = new SearchRequest(courseIndexStore);

SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders._boolQuery_();
//source源字段过虑
String[] sourceFieldsArray = sourceFields.split(",");
searchSourceBuilder.fetchSource(sourceFieldsArray, new String[]{});
if(courseSearchParam==null){
    courseSearchParam = new SearchCourseParamDto();
}
//关键字
if(StringUtils._isNotEmpty_(courseSearchParam.getKeywords())){
    //匹配关键字
    MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders._multiMatchQuery_(courseSearchParam.getKeywords(), "name", "description");
    //设置匹配占比
    multiMatchQueryBuilder.minimumShouldMatch("70%");
    //提升另个字段的Boost值
    multiMatchQueryBuilder.field("name",10);
    boolQueryBuilder.must(multiMatchQueryBuilder);
}
//过虑
if(StringUtils._isNotEmpty_(courseSearchParam.getMt())){
    boolQueryBuilder.filter(QueryBuilders._termQuery_("mtName",courseSearchParam.getMt()));
}
if(StringUtils._isNotEmpty_(courseSearchParam.getSt())){
    boolQueryBuilder.filter(QueryBuilders._termQuery_("stName",courseSearchParam.getSt()));
}
if(StringUtils._isNotEmpty_(courseSearchParam.getGrade())){
    boolQueryBuilder.filter(QueryBuilders._termQuery_("grade",courseSearchParam.getGrade()));
}
//分页
Long pageNo = pageParams.getPageNo();
Long pageSize = pageParams.getPageSize();
int start = (int) ((pageNo-1)*pageSize);
searchSourceBuilder.from(start);
searchSourceBuilder.size(Math._toIntExact_(pageSize));
//布尔查询
searchSourceBuilder.query(boolQueryBuilder);

//请求搜索
searchRequest.source(searchSourceBuilder);

SearchResponse searchResponse = null;
try {
    searchResponse = client.search(searchRequest, RequestOptions._DEFAULT_);
} catch (IOException e) {
    e.printStackTrace();
    _log_.error("课程搜索异常:{}",e.getMessage());
    return new SearchPageResultDto<CourseIndex>(new ArrayList(),0,0,0);
}

//结果集处理
SearchHits hits = searchResponse.getHits();
SearchHit[] searchHits = hits.getHits();
//记录总数
TotalHits totalHits = hits.getTotalHits();
//数据列表
List<CourseIndex> list = new ArrayList<>();

for (SearchHit hit : searchHits) {

    String sourceAsString = hit.getSourceAsString();
    CourseIndex courseIndex = JSON._parseObject_(sourceAsString, CourseIndex.class);

    
    list.add(courseIndex);

}
SearchPageResultDto<CourseIndex> pageResult = new SearchPageResultDto<>(list, totalHits.value,pageNo,pageSize);



return pageResult;
}

5.4.6 条件搜索测试

进入搜索界面,输入关键字进行测试。
一级分类、二级分类在下边的聚合搜索中测试。

5.4.7 聚合搜索

搜索界面上显示的一级分类、二级分类来源于搜索结果,使用聚合搜索实现找到搜索结果中的一级分类、二级分类。
1、首先在搜索结构DTO中添加一级分类、二级分类列表

| Javapackage com.xuecheng.search.dto;

import com.xuecheng.base.model.PageResult;
import lombok.Data;
import lombok.ToString;

import java.util.List;

/**

  • @author Mr.M

  • @version 1.0

  • @description TODO
    _ _* @date 2022/9/25 17:51
    */
    @Data
    @ToString
    public class SearchPageResultDto extends PageResult {

    //大分类列表
    List mtList;
    //小分类列表
    List stList;

    public SearchPageResultDto(List items, long counts, long page, long pageSize) {
    super(items, counts, page, pageSize);
    }

}

2、搜索方法如下:

| Java@Override
public SearchPageResultDto queryCoursePubIndex(PageParams pageParams, SearchCourseParamDto courseSearchParam) {

//设置索引
SearchRequest searchRequest = new SearchRequest(courseIndexStore);

SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders._boolQuery_();
//source源字段过虑
String[] sourceFieldsArray = sourceFields.split(",");
searchSourceBuilder.fetchSource(sourceFieldsArray, new String[]{});
if(courseSearchParam==null){
    courseSearchParam = new SearchCourseParamDto();
}
//关键字
if(StringUtils._isNotEmpty_(courseSearchParam.getKeywords())){
    //匹配关键字
    MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders._multiMatchQuery_(courseSearchParam.getKeywords(), "name", "description");
    //设置匹配占比
    multiMatchQueryBuilder.minimumShouldMatch("70%");
    //提升另个字段的Boost值
    multiMatchQueryBuilder.field("name",10);
    boolQueryBuilder.must(multiMatchQueryBuilder);
}
//过虑
if(StringUtils._isNotEmpty_(courseSearchParam.getMt())){
    boolQueryBuilder.filter(QueryBuilders._termQuery_("mtName",courseSearchParam.getMt()));
}
if(StringUtils._isNotEmpty_(courseSearchParam.getSt())){
    boolQueryBuilder.filter(QueryBuilders._termQuery_("stName",courseSearchParam.getSt()));
}
if(StringUtils._isNotEmpty_(courseSearchParam.getGrade())){
    boolQueryBuilder.filter(QueryBuilders._termQuery_("grade",courseSearchParam.getGrade()));
}
//分页
Long pageNo = pageParams.getPageNo();
Long pageSize = pageParams.getPageSize();
int start = (int) ((pageNo-1)*pageSize);
searchSourceBuilder.from(start);
searchSourceBuilder.size(Math._toIntExact_(pageSize));
//布尔查询
searchSourceBuilder.query(boolQueryBuilder);


//请求搜索
searchRequest.source(searchSourceBuilder);
//聚合设置
buildAggregation(searchRequest);
SearchResponse searchResponse = null;
try {
    searchResponse = client.search(searchRequest, RequestOptions._DEFAULT_);
} catch (IOException e) {
    e.printStackTrace();
    _log_.error("课程搜索异常:{}",e.getMessage());
    return new SearchPageResultDto<CourseIndex>(new ArrayList(),0,0,0);
}

//结果集处理
SearchHits hits = searchResponse.getHits();
SearchHit[] searchHits = hits.getHits();
//记录总数
TotalHits totalHits = hits.getTotalHits();
//数据列表
List<CourseIndex> list = new ArrayList<>();

for (SearchHit hit : searchHits) {

    String sourceAsString = hit.getSourceAsString();
    CourseIndex courseIndex = JSON._parseObject_(sourceAsString, CourseIndex.class);

    

    list.add(courseIndex);

}
SearchPageResultDto<CourseIndex> pageResult = new SearchPageResultDto<>(list, totalHits.value,pageNo,pageSize);

//获取聚合结果
List<String> mtList= getAggregation(searchResponse.getAggregations(), "mtAgg");
List<String> stList = getAggregation(searchResponse.getAggregations(), "stAgg");

pageResult.setMtList(mtList);
pageResult.setStList(stList);

return pageResult;
}

5.4.8 聚合搜索测试

进入搜索界面,观察搜索请求的响应内容中是否存在mtList和stList.
观察页面一级分类、二级分类是否有分类信息。
注意:当选中一个一级分类时才会显示二级分类。

5.4.9 高亮设置

最后实现关键词在课程名称中高亮显示。

| Java@Override
public SearchPageResultDto queryCoursePubIndex(PageParams pageParams, SearchCourseParamDto courseSearchParam) {

//设置索引
SearchRequest searchRequest = new SearchRequest(courseIndexStore);

SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders._boolQuery_();
//source源字段过虑
String[] sourceFieldsArray = sourceFields.split(",");
searchSourceBuilder.fetchSource(sourceFieldsArray, new String[]{});
if(courseSearchParam==null){
    courseSearchParam = new SearchCourseParamDto();
}
//关键字
if(StringUtils._isNotEmpty_(courseSearchParam.getKeywords())){
    //匹配关键字
    MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders._multiMatchQuery_(courseSearchParam.getKeywords(), "name", "description");
    //设置匹配占比
    multiMatchQueryBuilder.minimumShouldMatch("70%");
    //提升另个字段的Boost值
    multiMatchQueryBuilder.field("name",10);
    boolQueryBuilder.must(multiMatchQueryBuilder);
}
//过虑
if(StringUtils._isNotEmpty_(courseSearchParam.getMt())){
    boolQueryBuilder.filter(QueryBuilders._termQuery_("mtName",courseSearchParam.getMt()));
}
if(StringUtils._isNotEmpty_(courseSearchParam.getSt())){
    boolQueryBuilder.filter(QueryBuilders._termQuery_("stName",courseSearchParam.getSt()));
}
if(StringUtils._isNotEmpty_(courseSearchParam.getGrade())){
    boolQueryBuilder.filter(QueryBuilders._termQuery_("grade",courseSearchParam.getGrade()));
}
//分页
Long pageNo = pageParams.getPageNo();
Long pageSize = pageParams.getPageSize();
int start = (int) ((pageNo-1)*pageSize);
searchSourceBuilder.from(start);
searchSourceBuilder.size(Math._toIntExact_(pageSize));
//布尔查询
searchSourceBuilder.query(boolQueryBuilder);
//高亮设置
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.preTags("<font class='eslight'>");
highlightBuilder.postTags("</font>");
//设置高亮字段
highlightBuilder.fields().add(new HighlightBuilder.Field("name"));
searchSourceBuilder.highlighter(highlightBuilder);
//请求搜索
searchRequest.source(searchSourceBuilder);
//聚合设置
buildAggregation(searchRequest);
SearchResponse searchResponse = null;
try {
    searchResponse = client.search(searchRequest, RequestOptions._DEFAULT_);
} catch (IOException e) {
    e.printStackTrace();
    _log_.error("课程搜索异常:{}",e.getMessage());
    return new SearchPageResultDto<CourseIndex>(new ArrayList(),0,0,0);
}

//结果集处理
SearchHits hits = searchResponse.getHits();
SearchHit[] searchHits = hits.getHits();
//记录总数
TotalHits totalHits = hits.getTotalHits();
//数据列表
List<CourseIndex> list = new ArrayList<>();

for (SearchHit hit : searchHits) {

    String sourceAsString = hit.getSourceAsString();
    CourseIndex courseIndex = JSON._parseObject_(sourceAsString, CourseIndex.class);

    //取出source
    Map<String, Object> sourceAsMap = hit.getSourceAsMap();

    //课程id
    Long id = courseIndex.getId();
    //取出名称
    String name = courseIndex.getName();
    //取出高亮字段内容
    Map<String, HighlightField> highlightFields = hit.getHighlightFields();
    if(highlightFields!=null){
        HighlightField nameField = highlightFields.get("name");
        if(nameField!=null){
            Text[] fragments = nameField.getFragments();
            StringBuffer stringBuffer = new StringBuffer();
            for (Text str : fragments) {
                stringBuffer.append(str.string());
            }
            name = stringBuffer.toString();

        }
    }
    courseIndex.setId(id);
    courseIndex.setName(name);

    list.add(courseIndex);

}
SearchPageResultDto<CourseIndex> pageResult = new SearchPageResultDto<>(list, totalHits.value,pageNo,pageSize);

//获取聚合结果
List<String> mtList= getAggregation(searchResponse.getAggregations(), "mtAgg");
List<String> stList = getAggregation(searchResponse.getAggregations(), "stAgg");

pageResult.setMtList(mtList);
pageResult.setStList(stList);

return pageResult;
}

5.4.10 高亮设置测试

输入关键字,观察搜索结果,标题中是否对关键字信息进行高亮显示。
5.5 课程发布任务完善
5.5.1 需求分析
执行课程发布操作后,由消息处理机制向elasticsearch索引保存课程信息,本节实现该操作。
执行流程如下:

由内容管理服务远程调用搜索服务,添加课程信息索引。
搜索服务请求elasticsearch添加课程信息。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林寻星辰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值