• 作者:老汪软件技巧
  • 发表时间:2024-09-29 07:00
  • 浏览量:

在使用Elasticsearch(简称ES)进行大规模数据查询时,面对大量数据可能会遇到查询效率低的问题。我们可以通过多种手段优化查询效率,包括索引设计优化、查询语句优化、缓存机制、分页查询等。

下面的场景是一个电商平台的订单查询系统,用户可以通过前端对订单进行各种维度的查询,比如根据订单状态、订单创建时间区间、用户ID、商品ID等进行组合查询。假设订单数据量非常大,因此我们需要通过各种手段优化查询性能。

关键优化点:ES索引优化:设置适当的分片和副本数,使用合适的mapping,避免不必要的字段进行全文检索。查询优化:使用match查询代替term查询,对于大数据量的情况,使用scroll分页查询或search_after进行深度分页优化。缓存和异步查询:结合Redis进行缓存,避免频繁访问ES,并通过异步查询进一步提升响应性能。实例场景:

我们创建一个Spring Boot项目,用户通过API进行订单查询,后端通过Elasticsearch查询并返回结果,同时应用缓存和分页查询进行优化。

步骤1:Spring Boot 项目依赖

<dependencies>
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-elasticsearchartifactId>
    dependency>
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-redisartifactId>
    dependency>
    
    <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
        <scope>providedscope>
    dependency>
dependencies>

步骤2:ES索引Mapping设计

{
  "mappings": {
    "properties": {
      "order_id": {
        "type": "keyword"
      },
      "user_id": {
        "type": "keyword"
      },
      "product_id": {
        "type": "keyword"
      },
      "status": {
        "type": "integer"
      },
      "created_at": {
        "type": "date",
        "format": "yyyy-MM-dd'T'HH:mm:ss.SSSZ||epoch_millis"
      },
      "total_price": {
        "type": "double"
      }
    }
  }
}

该索引设计中,将order_id、user_id、product_id等设置为keyword类型,不会进行全文索引,节省性能消耗。created_at为date类型,以便可以进行时间范围查询。

步骤3:Spring Data Elasticsearch 配置

@Configuration
public class ElasticSearchConfig extends AbstractElasticsearchConfiguration {
    @Override
    @Bean
    public RestHighLevelClient elasticsearchClient() {
        ClientConfiguration clientConfiguration = ClientConfiguration.builder()
            .connectedTo("localhost:9200")
            .build();
        return RestClients.create(clientConfiguration).rest();
    }
}

案例场景描述是什么_场景数据分析_

步骤4:订单实体类

@Data
@Document(indexName = "orders")
public class Order {
    @Id
    private String orderId;
    private String userId;
    private String productId;
    private Integer status;
    private Date createdAt;
    private Double totalPrice;
}

步骤5:Repository接口

public interface OrderRepository extends ElasticsearchRepository {
}

步骤6:查询服务类

这里实现了分页查询,并使用了search_after进行深度分页优化,防止在大量数据时性能急剧下降。同时使用Redis缓存最近查询的结果。

@Service
@RequiredArgsConstructor
public class OrderService {
    private final RestHighLevelClient client;
    private final RedisTemplate> redisTemplate;
    private final OrderRepository orderRepository;
    public List searchOrders(String userId, String productId, Integer status, Date startTime, Date endTime, String searchAfter, int pageSize) throws IOException {
        // 首先检查缓存
        String cacheKey = generateCacheKey(userId, productId, status, startTime, endTime, searchAfter);
        List cachedOrders = redisTemplate.opsForValue().get(cacheKey);
        if (cachedOrders != null) {
            return cachedOrders;
        }
        // 构建查询条件
        BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
        if (userId != null) {
            queryBuilder.must(QueryBuilders.termQuery("user_id", userId));
        }
        if (productId != null) {
            queryBuilder.must(QueryBuilders.termQuery("product_id", productId));
        }
        if (status != null) {
            queryBuilder.must(QueryBuilders.termQuery("status", status));
        }
        if (startTime != null && endTime != null) {
            queryBuilder.must(QueryBuilders.rangeQuery("created_at").gte(startTime.getTime()).lte(endTime.getTime()));
        }
        // 分页,使用 search_after 进行深度分页
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder()
            .query(queryBuilder)
            .size(pageSize)
            .sort("created_at", SortOrder.DESC);
        if (searchAfter != null) {
            searchSourceBuilder.searchAfter(new Object[]{searchAfter});
        }
        SearchRequest searchRequest = new SearchRequest("orders");
        searchRequest.source(searchSourceBuilder);
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
        List orders = Arrays.stream(searchResponse.getHits().getHits())
            .map(hit -> {
                // 这里可以使用 ObjectMapper 进行 JSON 到 Order 的转换
                return mapToOrder(hit.getSourceAsMap());
            }).collect(Collectors.toList());
        // 将查询结果缓存
        redisTemplate.opsForValue().set(cacheKey, orders, 10, TimeUnit.MINUTES);
        return orders;
    }
    private String generateCacheKey(String userId, String productId, Integer status, Date startTime, Date endTime, String searchAfter) {
        return String.format("order_search:%s:%s:%d:%s:%s:%s", userId, productId, status,
            startTime != null ? startTime.getTime() : "",
            endTime != null ? endTime.getTime() : "",
            searchAfter);
    }
    private Order mapToOrder(Map sourceMap) {
        // 这里进行数据的转换,从Map到Order实体对象
        Order order = new Order();
        order.setOrderId((String) sourceMap.get("order_id"));
        order.setUserId((String) sourceMap.get("user_id"));
        order.setProductId((String) sourceMap.get("product_id"));
        order.setStatus((Integer) sourceMap.get("status"));
        order.setCreatedAt(new Date((Long) sourceMap.get("created_at")));
        order.setTotalPrice((Double) sourceMap.get("total_price"));
        return order;
    }
}

步骤7:控制器层

@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
    private final OrderService orderService;
    @GetMapping("/search")
    public List searchOrders(
        @RequestParam(required = false) String userId,
        @RequestParam(required = false) String productId,
        @RequestParam(required = false) Integer status,
        @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date startTime,
        @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date endTime,
        @RequestParam(required = false) String searchAfter,
        @RequestParam(defaultValue = "10") int pageSize
    ) throws IOException {
        return orderService.searchOrders(userId, productId, status, startTime, endTime, searchAfter, pageSize);
    }
}

关键点总结:search_after深度分页:避免在大量数据情况下使用普通分页的性能问题。Redis缓存:对于高频查询的结果进行缓存,减少Elasticsearch查询压力。查询优化:通过条件组合查询,避免不必要的全局检索。

通过以上的设计,可以极大提升大数据量场景下的查询性能。在实际项目中,还可以根据需要扩展为异步查询、批量查询等。