elasticsearch项目整合全文检索功能

news/2023/6/7 0:16:26

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: spring.profiles.activegroup:xuecheng−plus−projectfile−extension:yamlrefresh−enabled:trueshared−configs:−data−id:swagger−{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(“elasticsearch.course.index")privateStringcourseIndexStore;@Value("{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);//取出sourceMap<String, Object> sourceAsMap = hit.getSourceAsMap();//课程idLong 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添加课程信息。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.exyb.cn/news/show-4553870.html

如若内容造成侵权/违法违规/事实不符,请联系郑州代理记账网进行投诉反馈,一经查实,立即删除!

相关文章

2021年美国对中国货物出口情况:美对中的出口额达到1492亿美元,同比增长21.30% [图]

根据统计&#xff0c;2020年中美贸易额为5867.21亿美元&#xff0c;同比增长4.98%&#xff1b;2021年中美贸易额为7556.45亿美元&#xff0c;同比增长28.79%。 2015-2021年美国与中国双边货物进出口额及增速 数据来源&#xff1a;中国海关、智研咨询整理 2021年&#xff0c;美…

数据库被勒索删除,解决方法

突然数据库被黑了&#xff0c;有一条勒索信息: To recover your lost Database send 0.018 Bitcoin (BTC) to our Bitcoin address: bc1qe4yefrptv2k8shawu3h84j0n8kyvxfk4wwate5 After this, contact us by email with your Server IP or Domain Name and a Proof of Payment …

SIMD性能优化

文章目录前言MMXSSEAVX使用内置函数使用SSE/AVX命名规则SSE/AVX操作类别实战汇编使用优化前代码详解优化后代码详解引用文章#mermaid-svg-sNu7iEVk2jpyjjtX {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#fff;}#mermaid-svg-sNu7iEVk2…

如何判断是否是ssd硬盘?win10查看固态硬盘的方法

转自&#xff1a;http://www.w10zj.com/Win10xy/Win10yh_7732.html 如何判断是否是ssd硬盘&#xff1f;在win10操作系统中我们该如何查看当前主机中安装的是固态硬盘还是机械硬盘呢&#xff1f;除了开箱验机之外在本文中小编要教大家几个查看win10系统中是否是固态硬盘的方法。…

springBoot工程入门

文章目录基本知识快速搭建springBoot工程起步依赖原理分析spring-boot-starter-parentspring-boot-starter-web配置yaml数据格式读取配置profileprofile配置方式多profile文件方式yml多文档方式profile激活方式内部配置加载顺序外部配置加载顺序整合其他框架Junitredismybatiss…

指数平滑指标怎么看?

一、应用 指数平滑可继续拆分为一次平滑&#xff0c;二次平滑和三次平滑&#xff08;即Holt-Winters法&#xff09;&#xff0c;一次平滑法为历史数据的加权预测&#xff0c;二次平滑法适用于具有一定线性趋势的数据&#xff0c;三次平滑法在二次平滑法基础上再平滑一次&#…

【易语言组合框和列表框联动】

添加组合框和列表框联动 右击组合框选择设置项目 列表框添加组合框得选中项 列表框1.加入项目 (组合框1.内容, )列表框双击删除内容 列表框1.删除项目 (列表框1.现行选中项)组合框和组合框联动 没啥写的 就是if判断和组合框清空与加入项目的方法 组合框3.清空 () .判断开…

易语言读取计算机配置,易语言取所有配置节名和配置项名

本视频教程演示了取配置节名称、取配置项名称、取所有配置节和所有配置项名称&#xff0c;通过了精易模块和多种方法来实现。视频链接&#xff1a;511遇见易语言教程教程源码:.版本 2.支持库 spec.程序集 窗口程序集_启动窗口.子程序 _按钮1_被单击.局部变量 序号, 整数型.如果…