學成在線筆記八:課程搜索

課程發佈關聯改動

課程發佈時,同步索引庫數據

CoursePubRepository

package com.xuecheng.manage_course.dao;

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

public interface CoursePubRepository extends JpaRepository<CoursePub, String> {
}

CoursePubService

新增保存課程信息方法並在發佈課程成功時調用該方法。

    @Autowired
    private CoursePubRepository coursePubRepository;

	/**
     * 保存課程信息
     *
     * @param id        課程ID
     * @param coursePub 課程信息
     * @return CoursePub
     */
    public CoursePub saveCoursePub(String id, CoursePub coursePub) {
        if (StringUtils.isBlank(id)) {
            ExceptionCast.cast(CourseCode.COURSE_PUBLISH_COURSEIDISNULL);
        }
        CoursePub coursePubNew = null;
        Optional<CoursePub> coursePubOptional = coursePubRepository.findById(id);
        if (coursePubOptional.isPresent()) {
            coursePubNew = coursePubOptional.get();
        }
        if (coursePubNew == null) {
            coursePubNew = new CoursePub();
        }

        BeanUtils.copyProperties(coursePub, coursePubNew);
        //設置主鍵
        coursePubNew.setId(id);
        //更新時間戳爲最新時間
        coursePub.setTimestamp(new Date());
        //發佈時間
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("YYYY‐MM‐dd HH:mm:ss");
        String date = simpleDateFormat.format(new Date());
        coursePub.setPubTime(date);
        coursePubRepository.save(coursePub);
        return coursePub;

    }

    /**
     * 創建
     *
     * @param id
     * @return
     */
    private CoursePub createCoursePub(String id) {
        CoursePub coursePub = new CoursePub();
        coursePub.setId(id);

        //基礎信息
        Optional<CourseBase> courseBaseOptional = courseBaseRepository.findById(id);
        if (courseBaseOptional.isPresent()) {
            CourseBase courseBase = courseBaseOptional.get();
            BeanUtils.copyProperties(courseBase, coursePub);
        }
        //查詢課程圖片
        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 = coursePlanMapper.findList(id);
        //將課程計劃轉成json
        String teachPlanString = JSON.toJSONString(teachplanNode);
        coursePub.setTeachplan(teachPlanString);
        return coursePub;
    }

ES環境構建

ES安裝

參考下列文章:

Elasticsearch的介紹和安裝

Docker安裝Elasticsearch

Docker搭建Elasticsearch集羣

創建索引

使用HTTP工具發送請求

請求URL:http://192.168.136.110:9200/xc_course

請求方式:PUT

請求體:

{
	"settings": {
		"index": {
			"number_of_shards": "1",
			"number_of_replicas": "0"
		}
	}
}

創建Mapping

請求URL:http://192.168.136.110:9200/xc_course/doc/_mapping

請求方式:POST

請求體:

{
	"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-dd HH:mm:ss"
		},
		"pub_time": {
			"type": "date",
			"format": "yyyy-MM-dd HH:mm:ss"
		},
		"start_time": {
			"type": "date",
			"format": "yyyy-MM-dd HH:mm:ss"
		},
		"end_time": {
			"type": "date",
			"format": "yyyy-MM-dd HH:mm:ss"
		}
	}
}

數據導入(省略)

logstash腳本

input {
  stdin {
  }
  jdbc {
      jdbc_connection_string => "jdbc:mysql://192.168.136.110:3306/xc_course?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC"
      # 數據庫連接信息
      jdbc_user => "root"
      jdbc_password => "123456"
      # 驅動
      jdbc_driver_library => "/usr/share/logstash/config/mysql-connector-java-8.0.13.jar"
      # 驅動類
      jdbc_driver_class => "com.mysql.cj.jdbc.Driver"
      jdbc_paging_enabled => "true"
      jdbc_page_size => "50000"
      # 要執行的sql文件
      #statement_filepath => "/conf/course.sql"
      # 執行SQL
      statement => "select * from course_pub where timestamp > date_add(:sql_last_value,INTERVAL 8 HOUR)"
      #定時配置,依次爲:分、時、天、月和年,語法與cron表達式類似
      schedule => "0 0 * * *"
      record_last_run => true
      last_run_metadata_path => "/usr/share/logstash/config/logstash_metadata"
  }
}

filter{
    json{
        source => "message"
        remove_field => ["message"]
    }
}


output {
  elasticsearch {
      # ES的ip地址和端口
      hosts => "192.168.136.110:9200"
      # 集羣配置
      #hosts => ["localhost:9200","localhost:9202","localhost:9203"]
      # ES索引庫名稱
      index => "xc_course"
      document_id => "%{id}"
      document_type => "doc"
      template => "/usr/share/logstash/config/xc_course_template.json"
      template_name => "xc_course"
      template_overwrite => "true"
  }
  stdout {
      # 日誌輸出
      codec => json_lines
  }
}

課程搜索實現

需求分析

  1. 根據分類搜索課程信息。
  2. 根據關鍵字搜索課程信息,搜索方式爲全文檢索,關鍵字需要匹配課程的名稱、課程內容。
  3. 根據難度等級搜索課程。
  4. 搜索結點分頁顯示。

創建搜索微服務

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>xc-framework-parent</artifactId>
        <groupId>com.xuecheng</groupId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../xc-framework-parent/pom.xml</relativePath>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>xc-service-search</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.xuecheng</groupId>
            <artifactId>xc-framework-model</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.xuecheng</groupId>
            <artifactId>xc-framework-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.xuecheng</groupId>
            <artifactId>xc-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-io</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
    </dependencies>

</project>

注意:

我這裏使用的客戶端是Spring Data Elasticsearch,和教程上不一樣。

application.yml

server:
  port: 40100
spring:
  application:
    name: xc‐service‐search
  data:
    elasticsearch:
      cluster-nodes: 192.168.136.110:9300
      cluster-name: docker-cluster
elasticsearch:
  source_field: id,name,grade,mt,st,charge,valid,pic,qq,price,price_old,status,studymodel,teachmode,expires,pub_time,start_time,end_time      

啓動類

package com.xuecheng.search;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@EntityScan("com.xuecheng.framework.domain.search")//掃描實體類
@ComponentScan(basePackages={"com.xuecheng.api"})//掃描接口
@ComponentScan(basePackages={"com.xuecheng.search"})
@ComponentScan(basePackages={"com.xuecheng.framework"})//掃描common下的所有類
public class SearchApplication {
    public static void main(String[] args) {
        SpringApplication.run(SearchApplication.class, args);
    }
}

實體類

由於是使用的Spring Data Elasticsearch,所以需要定義實體類

package com.xuecheng.framework.domain.search;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;

import java.math.BigDecimal;
import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Document(indexName = "xc_course", type = "doc", shards = 1)
public class EsCoursePub {

    private static final long serialVersionUID = -916357110051689487L;

    @Id
    private String id;

    @Field
    private String name;
    @Field
    private String users;
    @Field
    private String mt;
    @Field
    private String st;
    @Field
    private String grade;
    @Field
    private String studymodel;
    @Field
    private String teachmode;
    @Field
    private String description;
    @Field
    private String pic;//圖片
    @Field
    private Date timestamp;//時間戳
    @Field
    private String charge;
    @Field
    private String valid;
    @Field
    private String qq;
    @Field
    private BigDecimal price;
    @Field
    private BigDecimal price_old;
    @Field
    private String expires;
    @Field
    private String teachplan;//課程計劃
    @Field
    private String pub_time;//課程發佈時間

}

注意:

字段必須和索引庫中一模一樣,比如:pub_time

基礎查詢實現

EsCourseControllerApi

package com.xuecheng.api.search;

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

@Api(value = "課程搜索", description = "課程搜索", tags = {"課程搜索"})
public interface EsCourseControllerApi {

    @ApiOperation("課程搜索")
    QueryResponseResult list(int page, int size, CourseSearchParam courseSearchParam);


}

EsCourseController

package com.xuecheng.search.controller;

import com.xuecheng.api.search.EsCourseControllerApi;
import com.xuecheng.framework.domain.search.CourseSearchParam;
import com.xuecheng.framework.model.response.QueryResponseResult;
import com.xuecheng.framework.web.BaseController;
import com.xuecheng.search.service.EsCourseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("search/course")
public class EsCourseController extends BaseController implements EsCourseControllerApi {

    @Autowired
    private EsCourseService esCourseService;

    @Override
    @GetMapping("list/{page}/{size}")
    public QueryResponseResult list(@PathVariable int page,
                                    @PathVariable int size,
                                    CourseSearchParam courseSearchParam) {
        return esCourseService.findList(page, size, courseSearchParam);
    }
}

EsCourseService

package com.xuecheng.search.service;

import com.xuecheng.framework.domain.search.CourseSearchParam;
import com.xuecheng.framework.domain.search.EsCoursePub;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.QueryResponseResult;
import com.xuecheng.framework.model.response.QueryResult;
import com.xuecheng.framework.service.BaseService;
import com.xuecheng.search.config.ElasticsearchConfig;
import com.xuecheng.search.dao.CourseRepository;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class EsCourseService extends BaseService {

    @Autowired
    private CourseRepository courseRepository;

    @Autowired
    private ElasticsearchConfig elasticsearchConfig;

    /**
     * 查詢課程索引
     *
     * @param page              當前頁碼
     * @param size              每頁記錄數
     * @param courseSearchParam 查詢條件
     * @return QueryResponseResult
     */
    public QueryResponseResult findList(int page, int size, CourseSearchParam courseSearchParam) {
        if (page < 0) {
            page = 1;
        }
        // Spring Data頁碼都是從0開始
        page = page - 1;

        // 分頁查詢
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        nativeSearchQueryBuilder.withPageable(PageRequest.of(page, size));

        // 結果過濾
        nativeSearchQueryBuilder.withSourceFilter(new FetchSourceFilter(elasticsearchConfig.getSourceField().split(","), null));

        // 查詢方式
        QueryBuilder queryBuilder = buildBasicQuery(courseSearchParam);
        nativeSearchQueryBuilder.withQuery(queryBuilder);

        Page<EsCoursePub> esCoursePubPage = courseRepository.search(nativeSearchQueryBuilder.build());


        // 返回結果
        QueryResult<EsCoursePub> esCoursePubQueryResult = new QueryResult<>(esCoursePubPage.getContent(), esCoursePubPage.getTotalElements());

        return new QueryResponseResult(CommonCode.SUCCESS, esCoursePubQueryResult);
    }

    /**
     * 構建查詢
     *
     * @param courseSearchParam 查詢條件
     * @return QueryBuilder
     */
    private QueryBuilder buildBasicQuery(CourseSearchParam courseSearchParam) {
        BoolQueryBuilder queryBuilder = new BoolQueryBuilder();

        // 基礎查詢
        if (StringUtils.isNotBlank(courseSearchParam.getKeyword())) {
            MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders
                    .multiMatchQuery(courseSearchParam.getKeyword(), "name", "teachplan", "description")
                    .minimumShouldMatch("70%") // 相似度
                    .field("name", 10);// name字段權重佔比提高10倍
            queryBuilder.must(multiMatchQueryBuilder);
        }

        return queryBuilder;
    }

}

CourseRepository

package com.xuecheng.search.dao;

import com.xuecheng.framework.domain.search.EsCoursePub;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

public interface CourseRepository extends ElasticsearchRepository<EsCoursePub, String> {
}

測試

查詢關鍵字JAVA,總記錄數爲:4

按分類和難度等級過濾

EsCourseService

修改EsCourseService中的buildBasicQuery方法,修改後的內容如下:

/**
     * 構建查詢
     *
     * @param courseSearchParam 查詢條件
     * @return QueryBuilder
     */
    private QueryBuilder buildBasicQuery(CourseSearchParam courseSearchParam) {
        BoolQueryBuilder queryBuilder = new BoolQueryBuilder();

        // 基礎查詢
        if (StringUtils.isNotBlank(courseSearchParam.getKeyword())) {
            MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders
                    .multiMatchQuery(courseSearchParam.getKeyword(), "name", "teachplan", "description")
                    .minimumShouldMatch("70%") // 相似度
                    .field("name", 10);// name字段權重佔比提高10倍
            queryBuilder.must(multiMatchQueryBuilder);
        }

        // 分類過濾
        if (StringUtils.isNotBlank(courseSearchParam.getMt())) {
            queryBuilder.filter(QueryBuilders.termQuery("mt", courseSearchParam.getMt()));
        }
        if (StringUtils.isNotBlank(courseSearchParam.getSt())) {
            queryBuilder.filter(QueryBuilders.termQuery("st", courseSearchParam.getSt()));
        }

        // 難度過濾
        if (StringUtils.isNotBlank(courseSearchParam.getGrade())) {
            queryBuilder.filter(QueryBuilders.termQuery("grade", courseSearchParam.getGrade()));
        }

        return queryBuilder;
    }

設置高亮

EsCourseService

Spring Data Elasticsearch的高亮就比較麻煩了

修改EsCourseService中的findList方法,加入高亮設置,修改後的代碼內容如下:

    /**
     * 查詢課程索引
     *
     * @param page              當前頁碼
     * @param size              每頁記錄數
     * @param courseSearchParam 查詢條件
     * @return QueryResponseResult
     */
    public QueryResponseResult findList(int page, int size, CourseSearchParam courseSearchParam) {
        if (page < 0) {
            page = 1;
        }
        // Spring Data頁碼都是從0開始
        page = page - 1;

        // 分頁查詢
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        nativeSearchQueryBuilder.withPageable(PageRequest.of(page, size));

        // 結果過濾
        nativeSearchQueryBuilder.withSourceFilter(new FetchSourceFilter(elasticsearchConfig.getSourceField().split(","), null));

        // 查詢方式
        QueryBuilder queryBuilder = buildBasicQuery(courseSearchParam);
        nativeSearchQueryBuilder.withQuery(queryBuilder);

        // 高亮設置
        nativeSearchQueryBuilder.withHighlightFields(new HighlightBuilder.Field("name").preTags("<font class='eslight'>").postTags("</font>"));

        AggregatedPage<EsCoursePub> esCoursePubPage = elasticsearchTemplate.queryForPage(nativeSearchQueryBuilder.build(), EsCoursePub.class, new SearchResultMapper() {
            @Override
            public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
                List<EsCoursePub> chunk = new ArrayList<>();
                for (SearchHit searchHit : response.getHits()) {
                    if (response.getHits().getHits().length <= 0) {
                        return null;
                    }
                    EsCoursePub coursePub =
                            JSON.parseObject(JSON.toJSONString(searchHit.getSource()), EsCoursePub.class);
                    HighlightField nameField = searchHit.getHighlightFields().get("name");
                    if (nameField != null) {
                        coursePub.setName(nameField.fragments()[0].toString());
                    }

                    chunk.add(coursePub);
                }
                if (chunk.size() > 0) {
                    return new AggregatedPageImpl<>((List<T>) chunk);
                }
                return null;
            }
        });


        // 返回結果
        QueryResult<EsCoursePub> esCoursePubQueryResult = new QueryResult<>(esCoursePubPage.getContent(), esCoursePubPage.getTotalElements());

        return new QueryResponseResult(CommonCode.SUCCESS, esCoursePubQueryResult);
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章