xc-11 搜索服务

本文档详细介绍了课程搜索的需求分析、搜索流程、全文检索技术、课程索引技术方案以及如何维护索引信息。重点讲解了如何使用Logstash从Mysql同步数据到Elasticsearch,并创建索引库和映射。此外,还涵盖了搜索服务API的实现,包括按关键字、分类、难度搜索及分页与高亮展示。整个过程涉及数据库操作、Elasticsearch配置、Logstash安装和配置以及Java服务端的实现细节。
摘要由CSDN通过智能技术生成

课程搜索需求分析

需求分析

在这里插入图片描述
1.根据分类搜索课程信息
2.根据关键字搜索课程信息
3.根据难度等级搜索课程信息
4.搜索节点分页显示

搜索流程

在这里插入图片描述
1.课程管理服务将数据写到mysql
2.使用Logstash将Mysql数据库中的数据写到ES索引库中.
3.用户在前端搜索课程信息,请求到搜索服务器.
4.搜索服务请求ES搜索课程信息.

全文检索技术研究

课程索引

技术方案

如何维护索引信息?

1.在mysql中添加数据后,也要同时在ES中添加
(采用Logstach)实现,Logstash会从Mysql中将数据采集到es索引库中
2.在mysql中更新数据后同时在ES中更新该课程
采用Logstach实现
3.在mysql中删除数据后在ES中删除数据,
需要手动添加,在删除课程之后再索引库(ES)中删除

准备课程缩影信息

创建课程发布表

首先创建一个coursePubRepository

package com.xuecheng.manage_course.dao;

import com.xuecheng.framework.domain.course.CourseBase;
import com.xuecheng.framework.domain.course.CoursePub;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * Created by Administrator.
 */
public interface CoursePubRepository extends JpaRepository<CoursePub,String> {
}

Service:


    //    课程发布
    @Transactional
    public CoursePublishResult publish(String id) {

//        1.这个接口实现什么功能?
        CmsPage cmsPage = new CmsPage();
        cmsPage.setSiteId(publish_siteId);
        cmsPage.setDataUrl(publish_dataUrlPre + id);
        cmsPage.setPageName(id + ".html");
        cmsPage.setPageAliase(getCourseBaseById(id).getName());
        cmsPage.setPagePhysicalPath(publish_page_physicalpath);
        cmsPage.setPageWebPath(publish_page_webpath);
        cmsPage.setTemplateId(publish_templateId);

//        1.调用cms一键发布接口,将课程详情页面发布到服务器中
        CmsPostPageResult cmsPostPageResult = cmsPageClient.postPageQuick(cmsPage);
        if (!cmsPostPageResult.isSuccess()) {
//如果不成功,就说明发布失败
            return new CoursePublishResult(CommonCode.FAIL, null);

        }


//        2.保存课程的发布状态为已发布
        CourseBase courseBase = this.saveCoursePubState(id);

//        保存课程索引的一些信息
//        先创建一个CoursePub对象
//        这里调用下面的createCoursePub方法
        CoursePub coursePub = createCoursePub(id);


//        将CoursePub对象,保存到数据库(现在外面定义一个单独的)
//        在下面再创建一个方法,将coursePub保存到数据库
//        写好之后,在这里调用
        saveCoursePub(id,coursePub);

//        保存缓存的一些信息


//得到页面的url
        String pageUrl = cmsPostPageResult.getPageUrl();
        return new CoursePublishResult(CommonCode.SUCCESS, pageUrl);


    }

    //    将coursePub对象保存到数据库
    private CoursePub saveCoursePub(String id, CoursePub coursePub) {
        CoursePub coursePubNew = null;
//        根据课程id查询coursePub
        Optional<CoursePub> coursePubOptional = coursePubRepository.findById(id);
        if (coursePubOptional.isPresent()) {
            coursePubNew = coursePubOptional.get();
        } else {
//        如果查不到的话,那么就只能new一个了
//        如果数据库查到了,那么就用数据库里面的,如果没有查到,那么就new一个

            coursePubNew = new CoursePub();
//        将coursePub中的对象,保存到CoursePubNew中
            BeanUtils.copyProperties(coursePub, coursePubNew);
//            为了避免参数中的coursePub中的一些数据为空,这里还需要再设置一下
//           主键id
            coursePubNew.setId(id);
//            时间戳,主要是给logstach使用

            coursePubNew.setTimestamp(new Date());
//            发布时间
//            这里需要注意一下,因为pulish_time在存储中是一个字符串的形式,所以这里需要修改一下
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");
            String date = simpleDateFormat.format(new Date());
//            这样子的话,就将时间设置为varchar类型了
            coursePubNew.setPubTime(date);

            coursePubRepository.save(coursePubNew);
//             最后将这个对象返回给调用方
//            就完事了
        }
            return coursePubNew;
    }


    //    创建一个Pub对象,然后一会再去保存
    private CoursePub createCoursePub(String id) {

        CoursePub coursePub = new CoursePub();
//         根据id查询course_base
        Optional<CourseBase> baseOptional = courseBaseRepository.findById(id);
        if (baseOptional.isPresent()) {
            CourseBase courseBase = baseOptional.get();
//            将CourseBase的属性写到CoursePub中
            BeanUtils.copyProperties(courseBase, coursePub);

        }
//        课程图片
//        根据id查询Course_pic
//        将对象,全部查询出来后,放入对象即可

        Optional<CoursePic> picOptional = coursePicRepository.findById(id);
        if (picOptional.isPresent()) {
            CoursePic coursePic = picOptional.get();
            BeanUtils.copyProperties(coursePic, coursePub);
        }

//        课程营销信息
        Optional<CourseMarket> marketOptional = courseMarketRepository.findById(id);
        if (marketOptional.isPresent()) {
            CourseMarket courseMarket = marketOptional.get();
            BeanUtils.copyProperties(courseMarket, coursePub);
        }

//        课程计划信息和上面的三个不大一样
        TeachplanNode teachplanNode = teachplanMapper.selectList(id);
//        获得的是Json信息,将他转换成为一个字符串
        String jsonString = JSON.toJSONString(teachplanNode);
//        将课程计划信息的json串保存到course_pub中
        coursePub.setTeachplan(jsonString);
        return coursePub;


    }

创建课程发布表模型

修改课程发布

搭建ES环境

ES安装库

在postman中删除掉之前的数据,然后新建一个course_andrew数据,

创建索引库

在这里插入图片描述

创建映射

{
"properties":{
"description":{
"analyzer":"ik_max_word","search_analyzer":"ik_smart",
"type":"text"},
"grade":{
"type":"keyword"},
"id":{
"type":"keyword"},
"mt":{
"type":"keyword"},
"name":{"analyzer":"ik_max_word",
"search_analyzer":"ik_smart","type":"text"
},"users":{
"index":false,"type":"text"
},
"charge":{
"type":"keyword"},
"valid":{
"type":"keyword"},
"pic":{
"index":false,"type":"keyword"
},
"qq":{
"index":false,"type":"keyword"
},
"price":{
"type":"float"},
"price_old":{"type":"float"
},
"st":{
"type":"keyword"},
"status":{
"type":"keyword"},
"studymodel":{"type":"keyword"
},"teachmode":{
"type":"keyword"},
"teachplan":{
"analyzer":"ik_max_word","search_analyzer":"ik_smart",
"type":"text"},
"expires":{
"type":"date","format":"yyyy‐MM‐ddHH:mm:ss"
},"pub_time":{
"type":"date",
"format":"yyyy‐MM‐ddHH:mm:ss"},
"start_time":{
"type":"date","format":"yyyy‐MM‐ddHH:mm:ss"
},"end_time":{
"type":"date","format":"yyyy‐MM‐ddHH:mm:ss"
}}}

在这里插入图片描述

LogStash创建索引

Logstash是ES下的一款开源软件,它能够同时从多个来源采集数据,转换数据,然后将数据发送到Elasticsearch中创建索引.
本项目使用Logstash将Mysql中的数据采用到ES索引中

下载Logstash

在这里插入图片描述

(这里主要要和es的版本保持一致)

安装Logstash-input-jdbc

logstash-input-jdbc是ruby开发的,先下载ruby并安装下载地址:https://rubyinstaller.org/downloads/
下载2.5版本即可。
安装完成查看是否安装成功
在这里插入图片描述
Logstash5.x以上版本本身自带有logstash-input-jdbc,6.x版本本身不带logstash-input-jdbc插件,需要手动安装
在这里插入图片描述
安装成功后我们可以在logstash根目录下的以下目录查看对应的插件版本
在这里插入图片描述

创建模板文件

Logstash的工作是从MySQL中读取数据,向ES中创建索引,这里需要提前创建mapping的模板文件以便logstash使用。
在logstach的config目录创建xc_course_template.json,内容如下:
本教程的xc_course_template.json目录是:D:/ElasticSearch/logstash-6.2.1/config/xc_course_template.json

{
"mappings":{"doc":{
"properties":{"charge":{
"type":"keyword"},
"description":{
"analyzer":"ik_max_word","search_analyzer":"ik_smart","type":"text"
},
"end_time":{
"format":"yyyy‐MM‐ddHH:mm:ss","type":"date"
},
"expires":{
"format":"yyyy‐MM‐ddHH:mm:ss","type":"date"
},
"grade":{
"type":"keyword"},
"id":{
"type":"keyword"},
"mt":{
"type":"keyword"},
"name":{
"analyzer":"ik_max_word","search_analyzer":"ik_smart","type":"text"
},
"pic":{
"index":false,"type":"keyword"
},
"price":{
"type":"float"},
"price_old":{"type":"float"
},
"pub_time":{
"format":"yyyy‐MM‐ddHH:mm:ss","type":"date"
},
"qq":{
"index":false,"type":"keyword"
},
"st":{
"type":"keyword"},
"start_time":{
"format":"yyyy‐MM‐ddHH:mm:ss","type":"date"
},
"status":{
"type":"keyword"},
"studymodel":{"type":"keyword"
},
"teachmode":{"type":"keyword"
},
"teachplan":{
"analyzer":"ik_max_word","search_analyzer":"ik_smart","type":"text"
},
"users":{"index":false,"type":"text"
},
"valid":{
"type":"keyword"}
}}
},
"template":"xc_course"}

配置mysql.conf

在logstash的config目录下配置mysql.conf文件供logstash使用,logstash会根据mysql.conf文件的配置的地址从MySQL中读取数据向ES中写入索引。
参考https://www.elastic.co/guide/en/logstash/current/plugins-inputs-jdbc.html配置输入数据源和输出数据源。

input{
stdin{
}
jdbc{
jdbc_connection_string=>"jdbc:mysql://localhost:3306/xc_course?useUnicode=true&characterEncoding=utf‐8&useSSL=true&serverTimezone=UTC"
#theuserwewishtoexcuteourstatementas
jdbc_user=>"root"
jdbc_password=>mysql
#thepathtoourdownloadedjdbcdriver
jdbc_driver_library=>"F:/develop/maven/repository3/mysql/mysql‐connector‐java/5.1.41/mysql‐connector‐java‐5.1.41.jar"
#thenameofthedriverclassformysql
jdbc_driver_class=>"com.mysql.jdbc.Driver"
jdbc_paging_enabled=>"true"
jdbc_page_size=>"50000"
#要执行的sql文件
#statement_filepath=>"/conf/course.sql"
statement=>"select*fromcourse_pubwheretimestamp>date_add(:sql_last_value,INTERVAL8HOUR)"
#定时配置
schedule=>"*****"
record_last_run=>true
last_run_metadata_path=>"D:/ElasticSearch/logstash‐6.2.1/config/logstash_metadata"
}}
output{elasticsearch{#ES的ip地址和端口
hosts=>"localhost:9200"
#hosts=>["localhost:9200","localhost:9202","localhost:9203"]#ES索引库名称
index=>"xc_course"document_id=>"%{id}"
document_type=>"doc"
template=>"D:/ElasticSearch/logstash‐6.2.1/config/xc_course_template.json"template_name=>"xc_course"
template_overwrite=>"true"}
stdout{#日志输出
codec=>json_lines}
}

说明:
1、ES采用UTC时区问题
ES采用UTC时区,比北京时间早8小时,所以ES读取数据时让最后更新时间加8小时wheretimestamp>date_add(:sql_last_value,INTERVAL8HOUR)
2、logstash每个执行完成会在D:/ElasticSearch/logstash-6.2.1/config/logstash_metadata记录执行时间下次以此时间为基准进行增量同步数据到索引库。

测试

在logstash中的bin路径下执行命令

logstash.bat -f ../config/mysql.conf

我这里调用的是以前的项目,需要修改一下那个时间把>换成<,否则数据不会被选中
,不过在修改完成后需要再改回来,否则更新时间戳之后,就不会导入了

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
输入命令后采集到数据

课程搜索

需求分析

1.更具分类搜索课程信息

2.根据关键字搜索课程信息,全文检索,关键字匹配课程名称,课程内容

3.根据难度搜索课程信息

4.搜索节点分页显示.

技术分析:
1.根据关键字搜索,采用MultiMatchQuery,搜索name,description.teachplan
2.根据分类,课程等级搜索,采用过滤器实现.
3.分页查询.
4.高亮显示

创建搜索服务

API

再APi中添加search包,在其中建立ES接口

package com.xuecheng.api.search;

import com.xuecheng.framework.domain.course.CoursePub;
import com.xuecheng.framework.domain.search.CourseSearchParam;
import com.xuecheng.framework.model.response.QueryResponseResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;

/**
 * @author Andrewer
 * @version 1.0
 * @project xcEduService01
 * @description
 * @date 2022/12/28 18:39:34
 */
@Api(value = ("课程搜索功能"),description = "课程搜索",tags = {"课程搜索"})
public interface EsCourseControllerApi {
    @ApiOperation(value = "课程综合搜索")
    public QueryResponseResult<CoursePub> list(int page, int size, CourseSearchParam courseSearchParam);

} 

Service

这里方法比较多,复杂,分成三部

package com.xuecheng.search.service;

import com.xuecheng.framework.domain.course.CoursePub;
import com.xuecheng.framework.domain.search.CourseSearchParam;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.QueryResponseResult;
import com.xuecheng.framework.model.response.QueryResult;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RestHighLevelClient;
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.builder.SearchSourceBuilder;
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 Andrewer
 * @version 1.0
 * @project xcEduService01
 * @description
 * @date 2022/12/29 20:39:45
 */
@Service
public class EsCourseService {
    @Value("${xuecheng.course.index}")
    private String index;
    @Value("${xuecheng.course.type}")
    private String type;
    @Value("${xuecheng.course.source.field}")
    private String source_field;
    @Autowired
    RestHighLevelClient restHighLevelClient;

    //    课程搜索
    public QueryResponseResult<CoursePub> list(int page, int size, CourseSearchParam courseSearchParam) {
        if (courseSearchParam == null) {
            courseSearchParam = new CourseSearchParam();
        }
//        创建请求对象
//        注意这里的索引库名称不要写死,后期变了怎么办?
        SearchRequest searchRequest = new SearchRequest(index);
//        设置搜索的类型
        searchRequest.types(type);
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//        指定过滤的原字段
        String[] source_field = this.source_field.split(",");
        searchSourceBuilder.fetchSource(source_field, new String[]{});
//            创建一个布尔查询对象
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();


//        搜索条件,根据关键字搜索
        if (StringUtils.isNotEmpty(courseSearchParam.getKeyword())) {
            MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(courseSearchParam.getKeyword(), "name", "description", "teachplan")
                    .minimumShouldMatch("70%")
                    .field("name", 10);
            boolQueryBuilder.must(multiMatchQueryBuilder);
        }
//        根据分类
//        .....

//

//        设置boolQueryBuilder到searchSourceBuilder
        searchSourceBuilder.query(boolQueryBuilder);
        searchRequest.source(searchSourceBuilder);

        QueryResult<CoursePub> queryResult = new QueryResult();
        List<CoursePub> list = new ArrayList<>();

//        执行搜索
//        这里需要用到客户端,这里要注入客户端
        try {
            SearchResponse search = restHighLevelClient.search(searchRequest);

//            获取响应结果
            SearchHits hits = search.getHits();
//            匹配的总的记录数
            long totalHits = hits.getTotalHits();
            queryResult.setTotal(totalHits);
            SearchHit[] hits1 = hits.getHits();
            for (SearchHit hit : hits1) {
                CoursePub coursePub = new CoursePub();
//                原文档
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();
//                取出name
                String name = (String) sourceAsMap.get("name");
                coursePub.setName(name);
//                图片
                String pic = (String) sourceAsMap.get("pic");
                coursePub.setPic(pic);
//                价格
                Double price = null;
                price = (Double) sourceAsMap.get("price");
                coursePub.setPrice(price);
                Double price_old = null;
                 price_old = (Double) sourceAsMap.get("price_old");
                coursePub.setPrice_old(price_old);
//                将对象放入list
                list.add(coursePub);
            }


        } catch (IOException e) {

            e.printStackTrace();
        }

        queryResult.setList(list);
        QueryResponseResult<CoursePub> responseResult = new QueryResponseResult<>(CommonCode.SUCCESS,queryResult);
        return responseResult;
    }

}

按关键字搜索

按分类难度和等级搜索

分页与高亮

//分页if(page<=0){
page=1;}if(size<=0){
size=20;}
intstart=(page‐1)*size;searchSourceBuilder.from(start);searchSourceBuilder.size(size);

//高亮设置
HighlightBuilderhighlightBuilder=newHighlightBuilder();highlightBuilder.preTags("<fontclass='eslight'>");highlightBuilder.postTags("</font>");
//设置高亮字段
highlightBuilder.fields().add(newHighlightBuilder.Field("name"));searchSourceBuilder.highlighter(highlightBuilder);
//请求搜索

//取出高亮字段内容
Map<String,HighlightField>highlightFields=hit.getHighlightFields();if(highlightFields!=null){
HighlightFieldnameField=highlightFields.get("name");if(nameField!=null){
Text[]fragments=nameField.getFragments();StringBufferstringBuffer=newStringBuffer();
for(Textstr:fragments){
stringBuffer.append(str.string());}
name=stringBuffer.toString();
}
}coursePub.setName(name);

Controller

@RestController@RequestMapping("/search/course")
publicclassEsCourseControllerimplementsEsCourseControllerApi{@Autowired
EsCourseServiceesCourseService;
@Override@GetMapping(value="/list/{page}/{size}")
publicQueryResponseResult<CoursePub>list(@PathVariable("page")intpage,
@PathVariable("size")intsize,CourseSearchParamcourseSearchParam)throwsIOException{returnesCourseService.list(page,size,courseSearchParam);
}}

测试

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值