feat: 实现 Elasticsearch 8.2.0 高级搜索功能

This commit is contained in:
xiongxiaoyang 2022-05-24 00:44:57 +08:00
parent 4c331224a4
commit ab2bead9b3
11 changed files with 235 additions and 51 deletions

View File

@ -38,7 +38,7 @@ PUT /book
"analyzer": "ik_smart" "analyzer": "ik_smart"
}, },
"lastChapterUpdateTime" : { "lastChapterUpdateTime" : {
"type": "keyword" "type": "long"
}, },
"picUrl" : { "picUrl" : {
"type" : "keyword", "type" : "keyword",

View File

@ -109,10 +109,6 @@
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency> <dependency>
<groupId>mysql</groupId> <groupId>mysql</groupId>

View File

@ -6,6 +6,7 @@ import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.dto.req.BookSearchReqDto; import io.github.xxyopen.novel.dto.req.BookSearchReqDto;
import io.github.xxyopen.novel.dto.resp.*; import io.github.xxyopen.novel.dto.resp.*;
import io.github.xxyopen.novel.service.BookService; import io.github.xxyopen.novel.service.BookService;
import io.github.xxyopen.novel.service.SearchService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -25,6 +26,8 @@ public class BookController {
private final BookService bookService; private final BookService bookService;
private final SearchService searchService;
/** /**
* 小说分类列表查询接口 * 小说分类列表查询接口
*/ */
@ -38,7 +41,7 @@ public class BookController {
*/ */
@GetMapping("search_list") @GetMapping("search_list")
public RestResp<PageRespDto<BookInfoRespDto>> searchBooks(BookSearchReqDto condition) { public RestResp<PageRespDto<BookInfoRespDto>> searchBooks(BookSearchReqDto condition) {
return bookService.searchBooks(condition); return searchService.searchBooks(condition);
} }
/** /**

View File

@ -1,6 +1,7 @@
package io.github.xxyopen.novel.core.task; package io.github.xxyopen.novel.core.task;
import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.Time;
import co.elastic.clients.elasticsearch.core.BulkRequest; import co.elastic.clients.elasticsearch.core.BulkRequest;
import co.elastic.clients.elasticsearch.core.BulkResponse; import co.elastic.clients.elasticsearch.core.BulkResponse;
import co.elastic.clients.elasticsearch.core.bulk.BulkResponseItem; import co.elastic.clients.elasticsearch.core.bulk.BulkResponseItem;
@ -17,6 +18,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.time.ZoneOffset;
import java.util.List; import java.util.List;
/** /**
@ -65,7 +67,7 @@ public class BookToEsTask {
.id(book.getId().toString()) .id(book.getId().toString())
.document(esBook) .document(esBook)
) )
); ).timeout(Time.of(t -> t.time("10s")));
maxId = book.getId(); maxId = book.getId();
} }
@ -103,7 +105,8 @@ public class BookToEsTask {
.workDirection(book.getWorkDirection()) .workDirection(book.getWorkDirection())
.lastChapterId(book.getLastChapterId()) .lastChapterId(book.getLastChapterId())
.lastChapterName(book.getLastChapterName()) .lastChapterName(book.getLastChapterName())
.lastChapterUpdateTime(book.getLastChapterUpdateTime()) .lastChapterUpdateTime(book.getLastChapterUpdateTime()
.toInstant(ZoneOffset.ofHours(8)).toEpochMilli())
.build(); .build();
} }
} }

View File

@ -1,13 +1,9 @@
package io.github.xxyopen.novel.dto.es; package io.github.xxyopen.novel.dto.es;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import lombok.AllArgsConstructor;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/** /**
* Elasticsearch 存储小说 DTO * Elasticsearch 存储小说 DTO
@ -15,6 +11,8 @@ import java.time.LocalDateTime;
* @date 2022/5/23 * @date 2022/5/23
*/ */
@Data @Data
@NoArgsConstructor
@AllArgsConstructor
@Builder @Builder
public class EsBookDto { public class EsBookDto {
@ -96,9 +94,7 @@ public class EsBookDto {
/** /**
* 最新章节更新时间 * 最新章节更新时间
*/ */
@JsonDeserialize(using = LocalDateTimeDeserializer.class) private Long lastChapterUpdateTime;
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime lastChapterUpdateTime;
/** /**
* 是否收费;1-收费 0-免费 * 是否收费;1-收费 0-免费

View File

@ -1,7 +1,6 @@
package io.github.xxyopen.novel.dto.resp; package io.github.xxyopen.novel.dto.resp;
import lombok.Builder; import lombok.*;
import lombok.Data;
/** /**
* 小说信息 响应DTO * 小说信息 响应DTO
@ -10,6 +9,8 @@ import lombok.Data;
* @date 2022/5/15 * @date 2022/5/15
*/ */
@Data @Data
@NoArgsConstructor
@AllArgsConstructor
@Builder @Builder
public class BookInfoRespDto { public class BookInfoRespDto {

View File

@ -1,9 +1,7 @@
package io.github.xxyopen.novel.service; package io.github.xxyopen.novel.service;
import io.github.xxyopen.novel.core.common.resp.PageRespDto;
import io.github.xxyopen.novel.core.common.resp.RestResp; import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.dto.req.BookAddReqDto; import io.github.xxyopen.novel.dto.req.BookAddReqDto;
import io.github.xxyopen.novel.dto.req.BookSearchReqDto;
import io.github.xxyopen.novel.dto.req.ChapterAddReqDto; import io.github.xxyopen.novel.dto.req.ChapterAddReqDto;
import io.github.xxyopen.novel.dto.req.UserCommentReqDto; import io.github.xxyopen.novel.dto.req.UserCommentReqDto;
import io.github.xxyopen.novel.dto.resp.*; import io.github.xxyopen.novel.dto.resp.*;
@ -19,14 +17,6 @@ import java.util.List;
*/ */
public interface BookService { public interface BookService {
/**
* 小说搜索
*
* @param condition 搜索条件
* @return 搜索结果
*/
RestResp<PageRespDto<BookInfoRespDto>> searchBooks(BookSearchReqDto condition);
/** /**
* 小说点击榜查询 * 小说点击榜查询
* *

View File

@ -0,0 +1,25 @@
package io.github.xxyopen.novel.service;
import io.github.xxyopen.novel.core.common.resp.PageRespDto;
import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.dto.req.BookSearchReqDto;
import io.github.xxyopen.novel.dto.resp.BookInfoRespDto;
/**
* 搜索 服务类
*
* @author xiongxiaoyang
* @date 2022/5/23
*/
public interface SearchService {
/**
* 小说搜索
*
* @param condition 搜索条件
* @return 搜索结果
*/
RestResp<PageRespDto<BookInfoRespDto>> searchBooks(BookSearchReqDto condition);
}

View File

@ -1,10 +1,8 @@
package io.github.xxyopen.novel.service.impl; package io.github.xxyopen.novel.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.github.xxyopen.novel.core.auth.UserHolder; import io.github.xxyopen.novel.core.auth.UserHolder;
import io.github.xxyopen.novel.core.common.constant.ErrorCodeEnum; import io.github.xxyopen.novel.core.common.constant.ErrorCodeEnum;
import io.github.xxyopen.novel.core.common.resp.PageRespDto;
import io.github.xxyopen.novel.core.common.resp.RestResp; import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.core.constant.DatabaseConsts; import io.github.xxyopen.novel.core.constant.DatabaseConsts;
import io.github.xxyopen.novel.dao.entity.*; import io.github.xxyopen.novel.dao.entity.*;
@ -14,7 +12,6 @@ import io.github.xxyopen.novel.dao.mapper.BookContentMapper;
import io.github.xxyopen.novel.dao.mapper.BookInfoMapper; import io.github.xxyopen.novel.dao.mapper.BookInfoMapper;
import io.github.xxyopen.novel.dto.AuthorInfoDto; import io.github.xxyopen.novel.dto.AuthorInfoDto;
import io.github.xxyopen.novel.dto.req.BookAddReqDto; import io.github.xxyopen.novel.dto.req.BookAddReqDto;
import io.github.xxyopen.novel.dto.req.BookSearchReqDto;
import io.github.xxyopen.novel.dto.req.ChapterAddReqDto; import io.github.xxyopen.novel.dto.req.ChapterAddReqDto;
import io.github.xxyopen.novel.dto.req.UserCommentReqDto; import io.github.xxyopen.novel.dto.req.UserCommentReqDto;
import io.github.xxyopen.novel.dto.resp.*; import io.github.xxyopen.novel.dto.resp.*;
@ -67,25 +64,6 @@ public class BookServiceImpl implements BookService {
private static final Integer REC_BOOK_COUNT = 4; private static final Integer REC_BOOK_COUNT = 4;
@Override
public RestResp<PageRespDto<BookInfoRespDto>> searchBooks(BookSearchReqDto condition) {
Page<BookInfoRespDto> page = new Page<>();
page.setCurrent(condition.getPageNum());
page.setSize(condition.getPageSize());
List<BookInfo> bookInfos = bookInfoMapper.searchBooks(page, condition);
return RestResp.ok(PageRespDto.of(condition.getPageNum(), condition.getPageSize(), page.getTotal()
, bookInfos.stream().map(v -> BookInfoRespDto.builder()
.id(v.getId())
.bookName(v.getBookName())
.categoryId(v.getCategoryId())
.categoryName(v.getCategoryName())
.authorId(v.getAuthorId())
.authorName(v.getAuthorName())
.wordCount(v.getWordCount())
.lastChapterName(v.getLastChapterName())
.build()).toList()));
}
@Override @Override
public RestResp<List<BookRankRespDto>> listVisitRankBooks() { public RestResp<List<BookRankRespDto>> listVisitRankBooks() {
return RestResp.ok(bookRankCacheManager.listVisitRankBooks()); return RestResp.ok(bookRankCacheManager.listVisitRankBooks());

View File

@ -0,0 +1,51 @@
package io.github.xxyopen.novel.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.github.xxyopen.novel.core.common.resp.PageRespDto;
import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.dao.entity.BookInfo;
import io.github.xxyopen.novel.dao.mapper.BookInfoMapper;
import io.github.xxyopen.novel.dto.req.BookSearchReqDto;
import io.github.xxyopen.novel.dto.resp.BookInfoRespDto;
import io.github.xxyopen.novel.service.SearchService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 数据库搜索 服务实现类
*
* @author xiongxiaoyang
* @date 2022/5/23
*/
@ConditionalOnProperty(prefix = "spring.elasticsearch", name = "enable", havingValue = "false")
@Service
@RequiredArgsConstructor
@Slf4j
public class DbSearchServiceImpl implements SearchService {
private final BookInfoMapper bookInfoMapper;
@Override
public RestResp<PageRespDto<BookInfoRespDto>> searchBooks(BookSearchReqDto condition) {
Page<BookInfoRespDto> page = new Page<>();
page.setCurrent(condition.getPageNum());
page.setSize(condition.getPageSize());
List<BookInfo> bookInfos = bookInfoMapper.searchBooks(page, condition);
return RestResp.ok(PageRespDto.of(condition.getPageNum(), condition.getPageSize(), page.getTotal()
, bookInfos.stream().map(v -> BookInfoRespDto.builder()
.id(v.getId())
.bookName(v.getBookName())
.categoryId(v.getCategoryId())
.categoryName(v.getCategoryName())
.authorId(v.getAuthorId())
.authorName(v.getAuthorName())
.wordCount(v.getWordCount())
.lastChapterName(v.getLastChapterName())
.build()).toList()));
}
}

View File

@ -0,0 +1,141 @@
package io.github.xxyopen.novel.service.impl;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.query_dsl.MatchQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.RangeQuery;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.TotalHits;
import co.elastic.clients.json.JsonData;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import io.github.xxyopen.novel.core.common.resp.PageRespDto;
import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.core.constant.EsConsts;
import io.github.xxyopen.novel.dto.es.EsBookDto;
import io.github.xxyopen.novel.dto.req.BookSearchReqDto;
import io.github.xxyopen.novel.dto.resp.BookInfoRespDto;
import io.github.xxyopen.novel.service.SearchService;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Elasticsearch 搜索 服务实现类
*
* @author xiongxiaoyang
* @date 2022/5/23
*/
@ConditionalOnProperty(prefix = "spring.elasticsearch", name = "enable", havingValue = "true")
@Service
@RequiredArgsConstructor
@Slf4j
public class EsSearchServiceImpl implements SearchService {
private final ElasticsearchClient esClient;
@SneakyThrows
@Override
public RestResp<PageRespDto<BookInfoRespDto>> searchBooks(BookSearchReqDto condition) {
SearchResponse<EsBookDto> response = esClient.search(s -> {
SearchRequest.Builder searchBuilder = s.index(EsConsts.IndexEnum.BOOK.getName());
buildSearchCondition(condition, searchBuilder);
searchBuilder.sort(o ->
o.field(f -> f.field(StringUtils
.underlineToCamel(condition.getSort().split(" ")[0]))
.order(SortOrder.Desc))
)
.from((condition.getPageNum() - 1) * condition.getPageSize())
.size(condition.getPageSize());
return searchBuilder;
},
EsBookDto.class
);
TotalHits total = response.hits().total();
List<BookInfoRespDto> list = new ArrayList<>();
List<Hit<EsBookDto>> hits = response.hits().hits();
for (Hit<EsBookDto> hit : hits) {
EsBookDto book = hit.source();
assert book != null;
list.add(BookInfoRespDto.builder()
.id(book.getId())
.bookName(book.getBookName())
.categoryId(book.getCategoryId())
.categoryName(book.getCategoryName())
.authorId(book.getAuthorId())
.authorName(book.getAuthorName())
.wordCount(book.getWordCount())
.lastChapterName(book.getLastChapterName())
.build());
}
assert total != null;
return RestResp.ok(PageRespDto.of(condition.getPageNum(), condition.getPageSize(), total.value(), list));
}
private void buildSearchCondition(BookSearchReqDto condition, SearchRequest.Builder searchBuilder) {
if (!StringUtils.isBlank(condition.getKeyword())) {
searchBuilder.query(q -> q.match(t -> t
.field("bookName")
.query(condition.getKeyword())
.boost(2.0f)
.field("authorName")
.query(condition.getKeyword())
.boost(1.8f)
//.field("categoryName")
//.query(condition.getKeyword())
//.boost(1.0f)
//.field("bookDesc")
//.query(condition.getKeyword())
//.boost(0.1f)
)
);
}
if (Objects.nonNull(condition.getWorkDirection())) {
searchBuilder.query(MatchQuery.of(m -> m
.field("workDirection")
.query(condition.getWorkDirection())
)._toQuery());
}
if (Objects.nonNull(condition.getCategoryId())) {
searchBuilder.query(MatchQuery.of(m -> m
.field("categoryId")
.query(condition.getCategoryId())
)._toQuery());
}
if (Objects.nonNull(condition.getWordCountMin())) {
searchBuilder.query(RangeQuery.of(m -> m
.field("wordCount")
.gte(JsonData.of(condition.getWordCountMin()))
)._toQuery());
}
if (Objects.nonNull(condition.getWordCountMax())) {
searchBuilder.query(RangeQuery.of(m -> m
.field("wordCount")
.lt(JsonData.of(condition.getWordCountMax()))
)._toQuery());
}
if (Objects.nonNull(condition.getUpdateTimeMin())) {
searchBuilder.query(RangeQuery.of(m -> m
.field("lastChapterUpdateTime")
.gte(JsonData.of(condition.getUpdateTimeMin().getTime()))
)._toQuery());
}
}
}