4.3 entity包
4.3.1 Result.java
package com.fox.es.entity; import lombok.Data; /** * 统一返回对象 * * @author 狐狸半面添 * @create 2023-03-22 18:34 */ @Data public class Result { private Integer code; private String msg; private Object data; private Result(Integer code, String msg, Object data) { this.code = code; this.msg = msg; this.data = data; } private Result(Integer code, String msg) { this.code = code; this.msg = msg; } public static Result ok() { return new Result(200, "success"); } public static Result ok(Object data) { return new Result(200, "success", data); } public static Result error(String msg) { return new Result(500, msg); } public Result error(Integer code, String msg) { return new Result(code, msg); } }
4.3.2 JacksonObjectMapper.java
package com.fox.es.entity; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; import java.math.BigInteger; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; /** * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象] * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON] * * @author 狐狸半面添 * @create 2023-01-18 20:34 */ public class JacksonObjectMapper extends ObjectMapper { public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss"; public JacksonObjectMapper() { super(); //收到未知属性时不报异常 this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false); //反序列化时,属性不存在的兼容处理 this.getDeserializationConfig().withoutFeatures(FAIL_ON_UNKNOWN_PROPERTIES); SimpleModule simpleModule = new SimpleModule() .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))) .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))) .addSerializer(BigInteger.class, ToStringSerializer.instance) .addSerializer(Long.class, ToStringSerializer.instance) .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))) .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))); //注册功能模块 例如,可以添加自定义序列化器和反序列化器 this.registerModule(simpleModule); } }
4.4 config包
4.4.1 ElasticsearchConfig.java
package com.fox.es.config; import org.apache.http.HttpHost; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestClientBuilder; import org.elasticsearch.client.RestHighLevelClient; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author 狐狸半面添 * @create 2023-03-22 17:51 */ @Configuration public class ElasticsearchConfig { @Value("${elasticsearch.hostname}") private String hostname; @Value("${elasticsearch.port}") private Integer port; @Bean public RestHighLevelClient restHighLevelClient() { RestClientBuilder builder = RestClient.builder( new HttpHost(hostname, port, "http") ); return new RestHighLevelClient(builder); } }
4.4.2 WebMvcConfig.java
package com.fox.es.config; import com.fox.es.entity.JacksonObjectMapper; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; import java.util.List; /** * @author 狐狸半面添 * @create 2023-02-11 23:25 */ @Configuration public class WebMvcConfig extends WebMvcConfigurationSupport { @Override protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) { MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter(); messageConverter.setObjectMapper(new JacksonObjectMapper()); converters.add(0, messageConverter); } }
4.5 BlogSimpleInfoDTO.java
package com.fox.es.dto; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.time.LocalDateTime; /** * @author 狐狸半面添 * @create 2023-03-22 21:33 */ @Data @NoArgsConstructor @AllArgsConstructor public class BlogSimpleInfoDTO { /** * 主键id */ private Integer id; /** * 用户id */ private Long userId; /** * 标题 */ private String title; /** * 介绍 */ private String introduce; /** * 创建时间 */ private LocalDateTime createTime; }
4.5 主启动类
package com.fox.es; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @author 狐狸半面添 * @create 2023-03-22 18:07 */ @SpringBootApplication public class ElasticsearchDemoApplication { public static void main(String[] args) { SpringApplication.run(ElasticsearchDemoApplication.class, args); } }
4.6 ⭐测试是否连接 es 成功
package com.fox.es.controller; import com.fox.es.entity.Result; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.core.MainResponse; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.io.IOException; /** * @author 狐狸半面添 * @create 2023-03-22 18:33 */ @RestController public class TestController { @Resource private RestHighLevelClient restHighLevelClient; /** * 用于测试是否连接 es 成功 * * @return 返回 es 的基本信息,等价于访问:http://127.0.0.1:9200 * @throws IOException 异常信息 */ @GetMapping("/getEsInfo") public Result getEsInfo() throws IOException { MainResponse info = restHighLevelClient.info(RequestOptions.DEFAULT); return Result.ok(info); } }
浏览器访问:http://localhost:9999/getEsInfo
4.7 ⭐搜索服务
4.7.1 controller层
package com.fox.es.controller; import com.fox.es.entity.Result; import com.fox.es.service.ShareResourceSearchService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; /** * @author 狐狸半面添 * @create 2023-03-22 20:16 */ @RestController @RequestMapping("/search") public class ShareResourceSearchController { @Resource private ShareResourceSearchService shareResourceSearchService; /** * 通过关键词获取数据列表 * * @param keyWords 关键词 * @param pageNo 页码 * @param pageSize 每页大小 * @return 数据列表,按照相关性从高到低进行排序 */ @GetMapping("/list") public Result list(@RequestParam("keyWords") String keyWords, @RequestParam("pageNo") Integer pageNo, @RequestParam("pageSize") Integer pageSize) { return shareResourceSearchService.list(keyWords, pageNo, pageSize); } }
4.7.2 service接口层
package com.fox.es.service; import com.fox.es.entity.Result; /** * @author 狐狸半面添 * @create 2023-03-22 20:18 */ public interface ShareResourceSearchService { /** * 通过关键词获取数据列表 * * @param keyWords 关键词 * @param pageNo 页码 * @param pageSize 每页大小 * @return 数据列表,按照相关性从高到低进行排序 */ Result list(String keyWords, int pageNo, int pageSize); }
4.7.3 service实现层
package com.fox.es.service.impl; import com.alibaba.fastjson.JSON; import com.fox.es.dto.BlogSimpleInfoDTO; import com.fox.es.entity.Result; import com.fox.es.service.ShareResourceSearchService; import lombok.extern.slf4j.Slf4j; 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.builder.SearchSourceBuilder; import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder; import org.elasticsearch.search.fetch.subphase.highlight.HighlightField; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import javax.annotation.Resource; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @author 狐狸半面添 * @create 2023-03-22 20:18 */ @Slf4j @Service public class ShareResourceSearchServiceImpl implements ShareResourceSearchService { @Resource private RestHighLevelClient restHighLevelClient; @Value("${elasticsearch.blog.index}") private String blogIndexStore; @Value("${elasticsearch.blog.source_fields}") private String blogFields; public Result list(String keyWords, int pageNo, int pageSize) { // 1.设置索引 - blog SearchRequest searchRequest = new SearchRequest(blogIndexStore); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); // 2.source源字段过虑 String[] sourceFieldsArray = blogFields.split(","); searchSourceBuilder.fetchSource(sourceFieldsArray, new String[]{}); // 3.关键字 if (StringUtils.hasText(keyWords)) { // 哪些字段匹配关键字 MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(keyWords, "title", "tags", "introduce", "content"); // 设置匹配占比(表示最少匹配的子句个数,例如有五个可选子句,最少的匹配个数为5*70%=3.5.向下取整为3,这就表示五个子句最少要匹配其中三个才能查到) multiMatchQueryBuilder.minimumShouldMatch("70%"); // 提升字段的Boost值 multiMatchQueryBuilder.field("title", 10); multiMatchQueryBuilder.field("tags", 7); multiMatchQueryBuilder.field("introduce", 5); boolQueryBuilder.must(multiMatchQueryBuilder); } // 4.分页 int start = (pageNo - 1) * pageSize; searchSourceBuilder.from(start); searchSourceBuilder.size(pageSize); // 布尔查询 searchSourceBuilder.query(boolQueryBuilder); // 6.高亮设置 HighlightBuilder highlightBuilder = new HighlightBuilder(); highlightBuilder.preTags("<font color='red'>"); highlightBuilder.postTags("</font>"); // 设置高亮字段 ArrayList<HighlightBuilder.Field> fields = new ArrayList<>(); fields.add(new HighlightBuilder.Field("title")); fields.add(new HighlightBuilder.Field("introduce")); highlightBuilder.fields().addAll(fields); searchSourceBuilder.highlighter(highlightBuilder); // 请求搜索 searchRequest.source(searchSourceBuilder); SearchResponse searchResponse; try { searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); } catch (IOException e) { log.error("博客搜索异常:{}", e.getMessage()); return Result.error(e.getMessage()); } // 结果集处理 SearchHits hits = searchResponse.getHits(); SearchHit[] searchHits = hits.getHits(); // 记录总数 long totalHitsCount = hits.getTotalHits().value; // 数据列表 List<BlogSimpleInfoDTO> list = new ArrayList<>(); for (SearchHit hit : searchHits) { String sourceAsString = hit.getSourceAsString(); // json 转 对象 BlogSimpleInfoDTO blog = JSON.parseObject(sourceAsString, BlogSimpleInfoDTO.class); // 取出高亮字段内容 Map<String, HighlightField> highlightFields = hit.getHighlightFields(); if (highlightFields != null) { blog.setTitle(parseHighlightStr(blog.getTitle(), highlightFields.get("title"))); blog.setIntroduce(parseHighlightStr(blog.getIntroduce(), highlightFields.get("introduce"))); } list.add(blog); } // 封装信息返回前端 HashMap<String, Object> resultMap = new HashMap<>(4); // 页码 resultMap.put("pageNo", pageNo); // 每页记录数量 resultMap.put("pageSize", pageSize); // 总记录数 resultMap.put("total", totalHitsCount); // 该页信息 resultMap.put("items", list); return Result.ok(resultMap); } public String parseHighlightStr(String text, HighlightField field) { if (field != null) { Text[] fragments = field.getFragments(); StringBuilder stringBuilder = new StringBuilder(); for (Text str : fragments) { stringBuilder.append(str.string()); } return stringBuilder.toString(); } else { return text; } } }
这里我们使用 apipost7
进行测试:
5.源码获取
这个demo如果你按照我的前三步进行了执行,那么直接运行源码应该是没有问题的。
源码地址:Mr-Write/SpringbootDemo: 各种demo案例 (github.com)
对应的是 elasticsearch-demo
包模块。
6.存在的问题与进阶说明
我们还存在另外一个需要解决的重要问题:与MySQL的数据一致性问题
❓ 为什么不直接使用ES存储所有的项目数据呢?
- 非关系型表达,大量的反范式存储,导致维护基础数据异常(新增异常,修改异常,删除异常),需要MySQL做托底。
- 不支持事务,一致性需要自己维护。
- 从存储的角度来说,Elasticsearch是基于“字段”的,大行超多字段性能并不好。
数据同步的方式有很多种,比如通过ES的API进行增删改查,或者通过中间件像canal
、MQ
进行数据全量、增量的同步。
具体解决方案,可参考:docker环境安装mysql、canal、elasticsearch,基于binlog利用canal实现mysql的数据同步到elasticsearch中