springboot整合Elasticsearch实现搜索功能以及数据同步

Elasticsearch实战应用

1.业务背景:商品数据日益庞大,搜索需求应运而生。实现商品的高效搜索以及数据库与elasticsearch数据同步。

2.实现步骤

2.1.依赖引入

		<elasticsearch.version>7.12.1</elasticsearch.version>
		<dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-high-level-client</artifactId>
            <version>${elasticsearch.version}</version>
        </dependency>

2.2.连接es

aop方式连接,在需要使用的方法上加注解即可。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EsConnect {
}
@Aspect
@Component
@Slf4j
public class EsConnectAspect {
    public static RestHighLevelClient client;

    @Value("${es.url}")
    public static final String url = "";

    @Pointcut("@annotation(com.hmall.search.aspect.EsConnect)")
    public void connectPointCut() {
    }

    @Before("connectPointCut()")
    //初始化连接
    public void setUp() {
        client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create(url)
        ));
    }

    @After("connectPointCut()")
    //断开连接
    public void tearDown() throws IOException {
        client.close();
    }
}

2.3.实现搜索业务

@Slf4j
@Service
public class SearchServiceImpl implements SearchService {
    @Autowired
    private ItemClient itemClient;

    @Override
    @EsConnect
    public PageDTO<ItemDTO> search(ItemPageQuery itemPageQuery) throws IOException {
        // 1.创建Request
        SearchRequest request = new SearchRequest("items");
        // 2.组织请求参数
        // 2.1.准备bool查询
        BoolQueryBuilder bool = QueryBuilders.boolQuery();
        // 2.2.关键字搜索
        if (StringUtils.isNotBlank(itemPageQuery.getKey())) {
            bool.must(QueryBuilders.matchQuery("name", itemPageQuery.getKey()));
        }
        // 2.3.品牌过滤
        if (itemPageQuery.getBrand() != null) {
            bool.filter(QueryBuilders.termQuery("brand", itemPageQuery.getBrand()));
        }
        // 2.4.类目过滤
        if (itemPageQuery.getCategory() != null) {
            bool.filter(QueryBuilders.termQuery("category", itemPageQuery.getCategory()));
        }
        // 2.5.价格过滤
        if (itemPageQuery.getMinPrice() != null) {
            if (itemPageQuery.getMaxPrice() != null) {
                bool.filter(QueryBuilders.rangeQuery("price").gte(itemPageQuery.getMinPrice()).lte(itemPageQuery.getMaxPrice()));
            } else {
                bool.filter(QueryBuilders.rangeQuery("price").gte(itemPageQuery.getMinPrice()));
            }
        } else {
            if (itemPageQuery.getMaxPrice() != null) {
                bool.filter(QueryBuilders.rangeQuery("price").lte(itemPageQuery.getMaxPrice()));
            }
        }
        // 2.6.排序
        if (StringUtils.isNotBlank(itemPageQuery.getSortBy())) {
            if (itemPageQuery.getIsAsc()) {
                request.source().sort(itemPageQuery.getSortBy(), SortOrder.ASC);
            } else {
                request.source().sort(itemPageQuery.getSortBy(), SortOrder.DESC);
            }
        }
        // 2.7.分页
        request.source().from((itemPageQuery.getPageNo() - 1) * itemPageQuery.getPageSize()).size(itemPageQuery.getPageSize());
        request.source().query(bool);
        // 3.发送请求
        SearchResponse response = EsConnectAspect.client.search(request, RequestOptions.DEFAULT);
        // 4.解析响应
        ArrayList<ItemDocDTO> itemDocDTOS = handleResponse(response);
        ArrayList<ItemDTO> itemDTOS = new ArrayList<>();
        for (ItemDocDTO itemDocDTO : itemDocDTOS) {
            ItemDTO itemDTO = itemClient.queryItemById(Long.valueOf(itemDocDTO.getId()));
            itemDTOS.add(itemDTO);
        }
        PageDTO<ItemDTO> itemDTOPageDTO = new PageDTO<>();
        itemDTOPageDTO.setList(itemDTOS);
        long value = response.getHits().getTotalHits().value;
        itemDTOPageDTO.setTotal(value);
        itemDTOPageDTO.setPages(value / itemPageQuery.getPageSize() + 1);
        return itemDTOPageDTO;
    }

    private ArrayList<ItemDocDTO> handleResponse(SearchResponse response) {
        SearchHits searchHits = response.getHits();
        // 1.获取总条数
        long total = searchHits.getTotalHits().value;
        log.info("共搜索到" + total + "条数据");
        ArrayList<ItemDocDTO> itemDocDTOS = new ArrayList<>();
        // 2.遍历结果数组
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            // 3.得到_source,也就是原始json文档
            String source = hit.getSourceAsString();
            // 4.反序列化并打印
            ItemDocDTO item = JSONUtil.toBean(source, ItemDocDTO.class);
            itemDocDTOS.add(item);
        }
        return itemDocDTOS;
    }
}

2.4.使用消息队列进行数据同步(缺点:需要保证消息队列的稳定性,也可以使用canal伪装成mysql的从节点监听binlog进行数据同步)

生产者采用confirm callback保证消息完整性(代码就不贴了,百度工具类一大把)

@Component
public class RabbitMqListener {

    public static final String ITEM_QUEUE_NAME = "item_queue";

    @RabbitListener(queues = {ITEM_QUEUE_NAME})
    public void receiveOrder(Message message, String msg, Channel channel) throws IOException {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            //全是数字即是id,直接走删除逻辑
            if (isNumeric(msg)) {
                delete(msg);
            } else {
                ItemDTO itemDTO = JSONUtil.toBean(msg, ItemDTO.class);
                ItemDocDTO itemDocDTO = BeanUtil.copyProperties(itemDTO, ItemDocDTO.class);
                if (searchIfExists(Long.valueOf(itemDocDTO.getId()))) {
                    //存在即修改
                    update(itemDocDTO);
                } else {
                    //不存在即新增
                    add(itemDocDTO);
                }
            }
            if (channel != null) {
                channel.basicAck(deliveryTag, true);
            }
        } catch (Exception e) {
            channel.basicNack(deliveryTag, true, false);
        }
    }

    @EsConnect
    public boolean searchIfExists(Long id) throws IOException {
        // 1.准备Request对象
        GetRequest request = new GetRequest("items").id(String.valueOf(id));
        // 2.发送请求
        GetResponse response = client.get(request, RequestOptions.DEFAULT);
        // 3.获取响应结果中的source
        String json = response.getSourceAsString();

        ItemDocDTO itemDTO = JSONUtil.toBean(json, ItemDocDTO.class);

        return itemDTO.getId() != null;
    }

    @EsConnect
    public void add(ItemDocDTO itemDocDTO) throws IOException {
        String doc = JSONUtil.toJsonStr(itemDocDTO);

        // 1.准备Request对象
        IndexRequest request = new IndexRequest("items").id(itemDocDTO.getId());
        // 2.准备Json文档
        request.source(doc, XContentType.JSON);
        // 3.发送请求
        client.index(request, RequestOptions.DEFAULT);
    }

    @EsConnect
    public void update(ItemDocDTO itemDocDTO) throws IOException {
        //先删除,再新增
        // 1.准备Request,两个参数,第一个是索引库名,第二个是文档id
        DeleteRequest request = new DeleteRequest("items", itemDocDTO.getId());
        // 2.发送请求
        client.delete(request, RequestOptions.DEFAULT);

        add(itemDocDTO);
    }

    public boolean isNumeric(String str) {
        //Pattern pattern = Pattern.compile("^-?[0-9]+"); //这个也行
        Pattern pattern = Pattern.compile("^-?\\d+(\\.\\d+)?$");//这个也行
        Matcher isNum = pattern.matcher(str);
        return isNum.matches();
    }

    @EsConnect
    public void delete(String id) throws IOException {
        // 1.准备Request,两个参数,第一个是索引库名,第二个是文档id
        DeleteRequest request = new DeleteRequest("items", id);
        // 2.发送请求
        client.delete(request, RequestOptions.DEFAULT);
    }

}

3.初始化es数据

一般采用canal方式伪装为mysql的从节点,监听binlog将数据同步到es。想要更方便也可以自己写方法同步。

@Test
    void testLoadItemDocs() throws IOException {
        // 分页查询商品数据
        int pageNo = 1;
        int size = 500;
        while (true) {
            Page<Item> page = itemService.lambdaQuery().eq(Item::getStatus, 1).page(new Page<Item>(pageNo, size));
            // 非空校验
            List<Item> items = page.getRecords();
            if (CollUtils.isEmpty(items)) {
                return;
            }
            log.info("加载第{}页数据,共{}条", pageNo, items.size());
            // 1.创建Request
            BulkRequest request = new BulkRequest("items");
            // 2.准备参数,添加多个新增的Request
            for (Item item : items) {
                // 2.1.转换为文档类型ItemDTO
                ItemDocDTO itemDTO = BeanUtil.copyProperties(item, ItemDocDTO.class);
                // 2.2.创建新增文档的Request对象
                request.add(new IndexRequest()
                        .id(itemDTO.getId())
                        .source(JSONUtil.toJsonStr(itemDTO), XContentType.JSON));
            }
            // 3.发送请求
            client.bulk(request, RequestOptions.DEFAULT);

            // 翻页
            pageNo++;
        }
    }
  • 15
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

紫为你棋菲

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值