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"
},
"lastChapterUpdateTime" : {
"type": "keyword"
"type": "long"
},
"picUrl" : {
"type" : "keyword",

View File

@ -109,10 +109,6 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<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.resp.*;
import io.github.xxyopen.novel.service.BookService;
import io.github.xxyopen.novel.service.SearchService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@ -25,6 +26,8 @@ public class BookController {
private final BookService bookService;
private final SearchService searchService;
/**
* 小说分类列表查询接口
*/
@ -38,7 +41,7 @@ public class BookController {
*/
@GetMapping("search_list")
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;
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.BulkResponse;
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.stereotype.Component;
import java.time.ZoneOffset;
import java.util.List;
/**
@ -65,7 +67,7 @@ public class BookToEsTask {
.id(book.getId().toString())
.document(esBook)
)
);
).timeout(Time.of(t -> t.time("10s")));
maxId = book.getId();
}
@ -103,7 +105,8 @@ public class BookToEsTask {
.workDirection(book.getWorkDirection())
.lastChapterId(book.getLastChapterId())
.lastChapterName(book.getLastChapterName())
.lastChapterUpdateTime(book.getLastChapterUpdateTime())
.lastChapterUpdateTime(book.getLastChapterUpdateTime()
.toInstant(ZoneOffset.ofHours(8)).toEpochMilli())
.build();
}
}

View File

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

View File

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

View File

@ -1,9 +1,7 @@
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.BookAddReqDto;
import io.github.xxyopen.novel.dto.req.BookSearchReqDto;
import io.github.xxyopen.novel.dto.req.ChapterAddReqDto;
import io.github.xxyopen.novel.dto.req.UserCommentReqDto;
import io.github.xxyopen.novel.dto.resp.*;
@ -19,14 +17,6 @@ import java.util.List;
*/
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;
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.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.constant.DatabaseConsts;
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.dto.AuthorInfoDto;
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.UserCommentReqDto;
import io.github.xxyopen.novel.dto.resp.*;
@ -67,25 +64,6 @@ public class BookServiceImpl implements BookService {
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
public RestResp<List<BookRankRespDto>> 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());
}
}
}