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++;
}
}