Elasticsearch分页查询、关键词高亮与性能优化全解析

发布于:2025-03-16 ⋅ 阅读:(44) ⋅ 点赞:(0)

Elasticsearch分页查询、关键词高亮与性能优化全解析

引言

在构建搜索功能时,分页查询和关键词高亮是两个基本需求,而随着数据量增长,性能优化变得尤为重要。本文将深入探讨Elasticsearch中的分页实现方式、高亮显示技术以及性能优化策略,帮助开发者构建高效、用户友好的搜索体验。

一、Elasticsearch分页查询技术

1.1 传统分页方式:from/size

最简单的分页方式是使用fromsize参数:

SearchRequest request = new SearchRequest.Builder()
    .index("products")
    .query(q -> q.matchAll(m -> m))
    .from(0)    // 起始位置
    .size(10)   // 每页大小
    .build();
    
SearchResponse<Product> response = client.search(request, Product.class);

工作原理

  • Elasticsearch需要计算并加载从0到from+size的所有文档
  • 然后丢弃前from个文档,只返回size个文档

优点

  • 实现简单,适用于大多数场景
  • 支持随机跳页

缺点

  • 深度分页问题:当from值很大时,性能急剧下降
  • 资源消耗大:需要在内存中排序from+size个文档
  • 默认限制:from + size不能超过10,000(可配置index.max_result_window调整)

1.2 滚动分页:Scroll API

适用于需要检索大量结果的场景,如数据导出:

// 初始化滚动查询
SearchRequest scrollRequest = new SearchRequest.Builder()
    .index("products")
    .query(q -> q.matchAll(m -> m))
    .scroll(Time.of(t -> t.time("1m")))  // 滚动上下文保持1分钟
    .size(100)  // 每批次大小
    .build();
    
SearchResponse<Product> scrollResponse = client.search(scrollRequest, Product.class);
String scrollId = scrollResponse.scrollId();

// 继续滚动获取下一批结果
while (true) {
    // 处理当前批次结果
    List<Hit<Product>> hits = scrollResponse.hits().hits();
    if (hits.isEmpty()) {
        break;  // 没有更多结果
    }
    
    // 获取下一批结果
    ScrollRequest nextScrollRequest = new ScrollRequest.Builder()
        .scrollId(scrollId)
        .scroll(Time.of(t -> t.time("1m")))
        .build();
        
    scrollResponse = client.scroll(nextScrollRequest, Product.class);
    scrollId = scrollResponse.scrollId();
}

// 清理滚动上下文
ClearScrollRequest clearScrollRequest = new ClearScrollRequest.Builder()
    .scrollId(scrollId)
    .build();
client.clearScroll(clearScrollRequest);

工作原理

  • 创建一个搜索上下文,保存当前状态
  • 每次请求返回下一批结果,而不是重新计算

优点

  • 高效处理大量数据
  • 性能稳定,不受已处理文档数量影响

缺点

  • 消耗服务器资源(维护搜索上下文)
  • 不支持实时更新(基于创建时的数据快照)
  • 不适合用户交互式分页

1.3 搜索后分页:Search After

适用于深度分页场景,是ES推荐的深度分页方案:

// 初始查询
SearchRequest initialRequest = new SearchRequest.Builder()
    .index("products")
    .query(q -> q.matchAll(m -> m))
    .sort(s -> s.field(f -> f.field("price").order(SortOrder.Asc)))
    .sort(s -> s.field(f -> f.field("_id").order(SortOrder.Asc)))  // 确保排序稳定
    .size(10)
    .build();
    
SearchResponse<Product> response = client.search(initialRequest, Product.class);

// 获取最后一个文档的排序值
List<Hit<Product>> hits = response.hits().hits();
if (!hits.isEmpty()) {
    Hit<Product> lastHit = hits.get(hits.size() - 1);
    List<FieldValue> searchAfterValues = lastHit.sort();
    
    // 使用search_after获取下一页
    SearchRequest nextPageRequest = new SearchRequest.Builder()
        .index("products")
        .query(q -> q.matchAll(m -> m))
        .sort(s -> s.field(f -> f.field("price").order(SortOrder.Asc)))
        .sort(s -> s.field(f -> f.field("_id").order(SortOrder.Asc)))
        .searchAfter(searchAfterValues)
        .size(10)
        .build();
        
    SearchResponse<Product> nextPageResponse = client.search(nextPageRequest, Product.class);
}

工作原理

  • 使用上一页最后一个文档的排序值作为下一页的起点
  • 必须指定稳定的排序(通常添加_id字段确保唯一性)

优点

  • 高效处理深度分页
  • 实时数据,反映索引的最新状态
  • 资源消耗低

缺点

  • 只能顺序遍历,不支持随机跳页
  • 需要跟踪上一页的最后一个文档
  • 排序字段必须唯一或添加次要排序字段

1.4 点击式分页:Point in Time (PIT)

ES 7.10引入的新特性,结合了Scroll和Search After的优点:

// 创建PIT
OpenPointInTimeRequest pitRequest = new OpenPointInTimeRequest.Builder()
    .index("products")
    .keepAlive(Time.of(t -> t.time("1m")))
    .build();
    
OpenPointInTimeResponse pitResponse = client.openPointInTime(pitRequest);
String pitId = pitResponse.id();

// 初始查询
SearchRequest initialRequest = new SearchRequest.Builder()
    .pit(p -> p.id(pitId).keepAlive(Time.of(t -> t.time("1m"))))
    .sort(s -> s.field(f -> f.field("price").order(SortOrder.Asc)))
    .sort(s -> s.field(f -> f.field("_id").order(SortOrder.Asc)))
    .size(10)
    .build();
    
SearchResponse<Product> response = client.search(initialRequest, Product.class);

// 获取更新后的PIT ID和最后一个文档的排序值
pitId = response.pitId();
List<Hit<Product>> hits = response.hits().hits();
if (!hits.isEmpty()) {
    Hit<Product> lastHit = hits.get(hits.size() - 1);
    List<FieldValue> searchAfterValues = lastHit.sort();
    
    // 获取下一页
    SearchRequest nextPageRequest = new SearchRequest.Builder()
        .pit(p -> p.id(pitId).keepAlive(Time.of(t -> t.time("1m"))))
        .sort(s -> s.field(f -> f.field("price").order(SortOrder.Asc)))
        .sort(s -> s.field(f -> f.field("_id").order(SortOrder.Asc)))
        .searchAfter(searchAfterValues)
        .size(10)
        .build();
        
    SearchResponse<Product> nextPageResponse = client.search(nextPageRequest, Product.class);
}

// 关闭PIT
ClosePointInTimeRequest closePitRequest = new ClosePointInTimeRequest.Builder()
    .id(pitId)
    .build();
client.closePointInTime(closePitRequest);

工作原理

  • 创建数据快照,但使用search_after进行分页
  • 不绑定到特定索引,而是使用PIT ID

优点

  • 高效处理深度分页
  • 提供一致的结果视图
  • 不受索引变化影响

缺点

  • 需要管理PIT生命周期
  • 不支持随机跳页

1.5 分页方案选择指南

分页方式 适用场景 优势 劣势
from/size 浅分页(<1000条) 简单,支持随机跳页 深度分页性能差
Scroll 数据导出,批处理 处理大量数据稳定 资源消耗大,不适合交互式分页
Search After 深度分页,无限滚动 高效,实时数据 只能顺序遍历
PIT 需要一致性视图的深度分页 高效,数据一致性 管理复杂,只能顺序遍历

二、关键词高亮实现

2.1 基础高亮配置

SearchRequest highlightRequest = new SearchRequest.Builder()
    .index("products")
    .query(q -> q
        .match(m -> m
            .field("description")
            .query("iPhone")
        )
    )
    .highlight(h -> h
        .fields("description", f -> f
            .preTags("<em>")
            .postTags("</em>")
            .numberOfFragments(3)
            .fragmentSize(150)
        )
    )
    .build();
    
SearchResponse<Product> response = client.search(highlightRequest, Product.class);

// 处理高亮结果
for (Hit<Product> hit : response.hits().hits()) {
    Product product = hit.source();
    Map<String, List<String>> highlights = hit.highlight();
    if (highlights != null && highlights.containsKey("description")) {
        List<String> fragments = highlights.get("description");
        // 使用高亮片段
    }
}

2.2 多字段高亮

SearchRequest multiFieldHighlightRequest = new SearchRequest.Builder()
    .index("products")
    .query(q -> q
        .multiMatch(m -> m
            .fields("name", "description")
            .query("iPhone Pro")
        )
    )
    .highlight(h -> h
        // 全局高亮设置
        .preTags("<em>")
        .postTags("</em>")
        // 字段特定设置
        .fields("name", f -> f
            .numberOfFragments(0)  // 返回完整字段
        )
        .fields("description", f -> f
            .numberOfFragments(3)
            .fragmentSize(150)
        )
    )
    .build();

2.3 高亮器类型

Elasticsearch提供三种高亮器:

  1. 普通高亮器(默认):基于Lucene的标准高亮器
.fields("description", f -> f
    .type("plain")
    .preTags("<em>")
    .postTags("</em>")
)
  1. 快速向量高亮器:适用于大字段
.fields("description", f -> f
    .type("fvh")
    .preTags("<em>")
    .postTags("</em>")
    .phraseLimit(50)  // 短语限制
)
  1. 统一高亮器:最强大,但可能较慢
.fields("description", f -> f
    .type("unified")
    .preTags("<em>")
    .postTags("</em>")
)

2.4 高亮参数优化

.highlight(h -> h
    .fields("description", f -> f
        .preTags("<em>")
        .postTags("</em>")
        .numberOfFragments(3)      // 返回片段数量
        .fragmentSize(150)         // 每个片段的大小
        .fragmentOffset(0)         // 片段偏移量
        .encoder("html")           // HTML编码特殊字符
        .requireFieldMatch(true)   // 只高亮匹配查询的字段
        .boundaryMaxScan(50)       // 边界扫描最大字符数
        .boundaryChars(new char[]{'.', ',', '!', '?', ' '})  // 边界字符
        .highlighterType("unified")  // 高亮器类型
        .order("score")            // 片段排序方式
    )
)

2.5 高亮与分页结合

SearchRequest request = new SearchRequest.Builder()
    .index("products")
    .query(q -> q
        .match(m -> m
            .field("description")
            .query("iPhone")
        )
    )
    .highlight(h -> h
        .fields("description", f -> f
            .preTags("<em>")
            .postTags("</em>")
            .numberOfFragments(3)
        )
    )
    .from(0)
    .size(10)
    .build();

三、性能优化策略

3.1 索引优化

3.1.1 合理设计映射
CreateIndexRequest request = new CreateIndexRequest.Builder()
    .index("products")
    .mappings(m -> m
        // 禁用动态映射
        .dynamic(DynamicMapping.Strict)
        // 只索引需要搜索的字段
        .properties("id", p -> p.keyword(k -> k))
        .properties("name", p -> p.text(t -> t
            .analyzer("ik_max_word")
            .fields("keyword", f -> f.keyword(k -> k))  // 添加keyword子字段用于排序和聚合
        ))
        .properties("price", p -> p.float_())
        .properties("description", p -> p.text(t -> t
            .analyzer("ik_max_word")
            .index(true)
            .store(false)  // 不存储,减少索引大小
        ))
        .properties("createTime", p -> p.date())
        // 不需要搜索的字段设为keyword或禁用索引
        .properties("otherInfo", p -> p.keyword(k -> k.index(false)))
    )
    .build();
3.1.2 使用doc_values和fielddata
.properties("price", p -> p.float_(f -> f
    .docValues(true)  // 启用doc_values,用于排序和聚合
))

.properties("description", p -> p.text(t -> t
    .fielddata(false)  // 禁用fielddata,避免内存压力
))

3.2 查询优化

3.2.1 只返回必要字段
SearchRequest request = new SearchRequest.Builder()
    .index("products")
    .query(q -> q.matchAll(m -> m))
    .source(SourceConfig.of(s -> s
        .filter(f -> f
            .includes("id", "name", "price")  // 只返回这些字段
        )
    ))
    .build();
3.2.2 使用过滤器缓存
SearchRequest request = new SearchRequest.Builder()
    .index("products")
    .query(q -> q
        .bool(b -> b
            .must(m -> m
                .match(t -> t
                    .field("name")
                    .query("iPhone")
                )
            )
            .filter(f -> f  // 使用filter而非must,可以缓存
                .range(r -> r
                    .field("price")
                    .gte(JsonData.of(5000))
                )
            )
        )
    )
    .build();
3.2.3 避免深度分页
// 使用search_after代替深度分页
SearchRequest request = new SearchRequest.Builder()
    .index("products")
    .query(q -> q.matchAll(m -> m))
    .sort(s -> s.field(f -> f.field("price").order(SortOrder.Asc)))
    .sort(s -> s.field(f -> f.field("_id").order(SortOrder.Asc)))
    .searchAfter(lastSortValues)
    .size(10)
    .build();
3.2.4 使用异步搜索处理大查询
// 提交异步搜索
SubmitAsyncSearchRequest asyncRequest = new SubmitAsyncSearchRequest.Builder()
    .index("products")
    .query(q -> q.matchAll(m -> m))
    .waitForCompletionTimeout(Time.of(t -> t.time("1s")))  // 等待1秒
    .keepAlive(Time.of(t -> t.time("10m")))  // 保持结果10分钟
    .build();
    
SubmitAsyncSearchResponse<Product> asyncResponse = 
    client.asyncSearch().submit(asyncRequest, Product.class);

String asyncId = asyncResponse.id();

// 检查结果
GetAsyncSearchRequest getAsyncRequest = new GetAsyncSearchRequest.Builder()
    .id(asyncId)
    .build();
    
GetAsyncSearchResponse<Product> getAsyncResponse = 
    client.asyncSearch().get(getAsyncRequest, Product.class);

// 处理结果
if (getAsyncResponse.isRunning()) {
    // 查询仍在运行
} else {
    // 查询完成,处理结果
    SearchResponse<Product> searchResponse = getAsyncResponse.response();
}

3.3 高亮性能优化

3.3.1 选择合适的高亮器
// 对于小文档,使用plain高亮器
.fields("title", f -> f
    .type("plain")
)

// 对于大文档,使用fvh高亮器
.fields("content", f -> f
    .type("fvh")
    .matchedFields("content", "content.synonym")
)
3.3.2 限制高亮范围
.highlight(h -> h
    .fields("description", f -> f
        .fragmentSize(100)         // 减小片段大小
        .numberOfFragments(2)      // 减少片段数量
        .requireFieldMatch(true)   // 只高亮匹配查询的字段
    )
)
3.3.3 使用term_vector加速高亮
// 在映射中预先存储term vectors
.properties("description", p -> p.text(t -> t
    .analyzer("ik_max_word")
    .termVector(TermVectorOption.WithPositionsOffsets)  // 存储位置和偏移信息
))

3.4 分页性能优化实战

3.4.1 浅分页优化
// 对于浅分页,优化查询和返回字段
SearchRequest request = new SearchRequest.Builder()
    .index("products")
    .query(q -> q
        .bool(b -> b
            .must(m -> m
                .match(t -> t
                    .field("name")
                    .query("iPhone")
                )
            )
        )
    )
    .source(SourceConfig.of(s -> s
        .filter(f -> f
            .includes("id", "name", "price")  // 只返回必要字段
        )
    ))
    .from(0)
    .size(10)
    .build();
3.4.2 深度分页优化
// 使用PIT + Search After实现高效深度分页
// 1. 创建PIT
OpenPointInTimeRequest pitRequest = new OpenPointInTimeRequest.Builder()
    .index("products")
    .keepAlive(Time.of(t -> t.time("1m")))
    .build();
    
OpenPointInTimeResponse pitResponse = client.openPointInTime(pitRequest);
String pitId = pitResponse.id();

// 2. 初始查询
SearchRequest initialRequest = new SearchRequest.Builder()
    .pit(p -> p.id(pitId).keepAlive(Time.of(t -> t.time("1m"))))
    .query(q -> q
        .bool(b -> b
            .must(m -> m
                .match(t -> t
                    .field("name")
                    .query("iPhone")
                )
            )
        )
    )
    .source(SourceConfig.of(s -> s
        .filter(f -> f
            .includes("id", "name", "price")  // 只返回必要字段
        )
    ))
    .sort(s -> s.field(f -> f.field("_score").order(SortOrder.Desc)))
    .sort(s -> s.field(f -> f.field("_id").order(SortOrder.Asc)))
    .size(10)
    .build();
    
SearchResponse<Product> response = client.search(initialRequest, Product.class);

// 3. 获取下一页
pitId = response.pitId();  // 更新PIT ID
List<Hit<Product>> hits = response.hits().hits();
if (!hits.isEmpty()) {
    Hit<Product> lastHit = hits.get(hits.size() - 1);
    List<FieldValue> searchAfterValues = lastHit.sort();
    
    SearchRequest nextPageRequest = new SearchRequest.Builder()
        .pit(p -> p.id(pitId).keepAlive(Time.of(t -> t.time("1m"))))
        .query(q -> q
            .bool(b -> b
                .must(m -> m
                    .match(t -> t
                        .field("name")
                        .query("iPhone")
                    )
                )
            )
        )
        .source(SourceConfig.of(s -> s
            .filter(f -> f
                .includes("id", "name", "price")
            )
        ))
        .sort(s -> s.field(f -> f.field("_score").order(SortOrder.Desc)))
        .sort(s -> s.field(f -> f.field("_id").order(SortOrder.Asc)))
        .searchAfter(searchAfterValues)
        .size(10)
        .build();
        
    SearchResponse<Product> nextPageResponse = client.search(nextPageRequest, Product.class);
}
3.4.3 分页与高亮结合的优化
// 高亮只应用于必要字段,限制片段大小和数量
SearchRequest request = new SearchRequest.Builder()
    .index("products")
    .query(q -> q
        .match(m -> m
            .field("description")
            .query("iPhone")
        )
    )
    .highlight(h -> h
        .fields("description", f -> f
            .type("unified")  // 使用统一高亮器
            .preTags("<em>")
            .postTags("</em>")
            .numberOfFragments(2)  // 限制片段数量
            .fragmentSize(100)     // 限制片段大小
            .requireFieldMatch(true)
        )
    )
    .source(SourceConfig.of(s -> s
        .filter(f -> f
            .includes("id", "name", "price", "description")
        )
    ))
    .from(0)
    .size(10)
    .build();

3.5 实际案例:百万级数据的高效分页与高亮

以下是一个完整的实现,结合了所有优化技术:

/**
 * 高效分页与高亮搜索服务
 */
@Service
public class OptimizedSearchService {
    
    private final ElasticsearchClient client;
    
    @Autowired
    public OptimizedSearchService(ElasticsearchClient client) {
        this.client = client;
    }
    
    /**
     * 执行高效分页搜索
     * @param keyword 搜索关键词
     * @param pageToken 分页令牌(首页传null)
     * @param size 每页大小
     * @return 搜索结果与下一页令牌
     */
    public SearchResultDTO search(String keyword, PageToken pageToken, int size) throws IOException {
        // 创建或使用现有PIT
        String pitId;
        List<FieldValue> searchAfterValues = null;
        
        if (pageToken == null) {
            // 首页,创建新PIT
            OpenPointInTimeRequest pitRequest = new OpenPointInTimeRequest.Builder()
                .index("products")
                .keepAlive(Time.of(t -> t.time("1m")))
                .build();
                
            OpenPointInTimeResponse pitResponse = client.openPointInTime(pitRequest);
            pitId = pitResponse.id();
        } else {
            // 非首页,使用传入的PIT和searchAfter值
            pitId = pageToken.getPitId();
            searchAfterValues = pageToken.getSearchAfterValues();
        }
        
        // 构建搜索请求
        SearchRequest.Builder requestBuilder = new SearchRequest.Builder()
            .pit(p -> p.id(pitId).keepAlive(Time.of(t -> t.time("1m"))))
            .query(q -> q
                .bool(b -> b
                    .must(m -> m
                        .multiMatch(mm -> mm
                            .fields("name^2", "description")  // name字段权重更高
                            .query(keyword)
                            .type(TextQueryType.BestFields)
                        )
                    )
                    // 使用filter可以利用缓存
                    .filter(f -> f
                        .range(r -> r
                            .field("createTime")
                            .gte(JsonData.of("now-1y"))  // 过去一年的数据
                        )
                    )
                )
            )
            // 只返回必要字段
            .source(SourceConfig.of(s -> s
                .filter(f -> f
                    .includes("id", "name", "price", "description")
                )
            ))
            // 高亮配置
            .highlight(h -> h
                .fields("name", f -> f
                    .type("unified")
                    .preTags("<em>")
                    .postTags("</em>")
                    .numberOfFragments(0)  // 返回完整字段
                )
                .fields("description", f -> f
                    .type("unified")
                    .preTags("<em>")
                    .postTags("</em>")
                    .numberOfFragments(2)
                    .fragmentSize(150)
                )
            )
            // 排序(确保稳定排序)
            .sort(s -> s.field(f -> f.field("_score").order(SortOrder.Desc)))
            .sort(s -> s.field(f -> f.field("_id").order(SortOrder.Asc)))
            .size(size);
            
        // 非首页,添加search_after参数
        if (searchAfterValues != null) {
            requestBuilder.searchAfter(searchAfterValues);
        }
        
        // 执行搜索
        SearchRequest request = requestBuilder.build();
        SearchResponse<Product> response = client.search(request, Product.class);
        
        // 处理结果
        List<Hit<Product>> hits = response.hits().hits();
        List<ProductDTO> products = new ArrayList<>();
        
        for (Hit<Product> hit : hits) {
            Product product = hit.source();
            Map<String, List<String>> highlights = hit.highlight();
            
            // 构建产品DTO,包含高亮信息
            ProductDTO dto = new ProductDTO();
            dto.setId(product.getId());
            dto.setPrice(product.getPrice());
            
            // 应用高亮(如果有)
            if (highlights != null && highlights.containsKey("name")) {
                dto.setName(highlights.get("name").get(0));
            } else {
                dto.setName(product.getName());
            }
            
            if (highlights != null && highlights.containsKey("description")) {
                dto.setDescription(String.join("...", highlights.get("description")));
            } else {
                dto.setDescription(product.getDescription());
            }
            
            products.add(dto);
        }
        
        // 创建下一页令牌
        PageToken nextPageToken = null;
        if (!hits.isEmpty()) {
            Hit<Product> lastHit = hits.get(hits.size() - 1);
            nextPageToken = new PageToken(response.pitId(), lastHit.sort());
        } else {
            // 关闭PIT(没有更多结果)
            ClosePointInTimeRequest closePitRequest = new ClosePointInTimeRequest.Builder()
                .id(pitId)
                .build();
            client.closePointInTime(closePitRequest);
        }
        
        // 构建并返回结果
        SearchResultDTO result = new SearchResultDTO();
        result.setProducts(products);
        result.setTotal(response.hits().total().value());
        result.setNextPageToken(nextPageToken);
        
        return result;
    }
    
    /**
     * 分页令牌类
     */
    public static class PageToken {
        private final String pitId;
        private final List<FieldValue> searchAfterValues;
        
        public PageToken(String pitId, List<FieldValue> searchAfterValues) {
            this.pitId = pitId;
            this.searchAfterValues = searchAfterValues;
        }
        
        public String getPitId() {
            return pitId;
        }
        
        public List<FieldValue> getSearchAfterValues() {
            return searchAfterValues;
        }
    }
    
    /**
     * 产品DTO类
     */
    public static class ProductDTO {
        private String id;
        private String name;
        private float price;
        private String description;
        
        // getters and setters
        public String getId() {
            return id;
        }
        
        public void setId(String id) {
            this.id = id;
        }
        
        public String getName() {
            return name;
        }
        
        public void setName(String name) {
            this.name = name;
        }
        
        public float getPrice() {
            return price;
        }
        
        public void setPrice(float price) {
            this.price = price;
        }
        
        public String getDescription() {
            return description;
        }
        
        public void setDescription(String description) {
            this.description = description;
        }
    }
    
    /**
     * 搜索结果DTO类
     */
    public static class SearchResultDTO {
        private List<ProductDTO> products;
        private long total;
        private PageToken nextPageToken;
        
        // getters and setters
        public List<ProductDTO> getProducts() {
            return products;
        }
        
        public void setProducts(List<ProductDTO> products) {
            this.products = products;
        }
        
        public long getTotal() {
            return total;
        }
        
        public void setTotal(long total) {
            this.total = total;
        }
        
        public PageToken getNextPageToken() {
            return nextPageToken;
        }
        
        public void setNextPageToken(PageToken nextPageToken) {
            this.nextPageToken = nextPageToken;
        }
    }
}

四、性能测试与监控

4.1 性能测试方法

  1. 基准测试:使用JMeter或Gatling创建测试脚本,模拟不同分页场景
// 测试不同分页方法的性能
@Test
public void testPaginationPerformance() {
    // 测试from/size
    long fromSizeStart = System.currentTimeMillis();
    for (int page = 0; page < 10; page++) {
        searchWithFromSize(page * 100, 100);
    }
    long fromSizeTime = System.currentTimeMillis() - fromSizeStart;
    
    // 测试search_after
    long searchAfterStart = System.currentTimeMillis();
    List<FieldValue> lastSort = null;
    for (int page = 0; page < 10; page++) {
        SearchResponse<Product> response = searchWithSearchAfter(lastSort, 100);
        if (!response.hits().hits().isEmpty()) {
            lastSort = response.hits().hits().get(response.hits().hits().size() - 1).sort();
        }
    }
    long searchAfterTime = System.currentTimeMillis() - searchAfterStart;
    
    System.out.println("From/Size时间: " + fromSizeTime + "ms");
    System.out.println("Search After时间: " + searchAfterTime + "ms");
}
  1. 性能指标

  2. 响应时间

  3. 吞吐量

  4. CPU和内存使用率

  5. 磁盘I/O

4.2 监控与调优

  1. 使用Elasticsearch监控API
NodesStatsRequest nodesStatsRequest = new NodesStatsRequest.Builder().build();
NodesStatsResponse nodesStatsResponse = client.nodes().stats(nodesStatsRequest);

// 查看搜索线程池状态
for (NodeStats nodeStats : nodesStatsResponse.nodes()) {
    Map<String, ThreadPoolStats.ThreadPoolStat> threadPools = nodeStats.threadPool();
    ThreadPoolStats.ThreadPoolStat searchPool = threadPools.get("search");
    System.out.println("搜索线程池活跃线程: " + searchPool.active());
    System.out.println("搜索线程池队列大小: " + searchPool.queue());
    System.out.println("搜索线程池已完成任务: " + searchPool.completed());
    System.out.println("搜索线程池被拒绝任务: " + searchPool.rejected());
}
  1. 使用Profile API分析查询性能
SearchRequest profileRequest = new SearchRequest.Builder()
    .index("products")
    .query(q -> q
        .match(m -> m
            .field("description")
            .query("iPhone")
        )
    )
    .profile(true)  // 启用查询分析
    .build();
    
SearchResponse<Product> profileResponse = client.search(profileRequest, Product.class);
Map<String, List<Map<String, Object>>> profileResults = profileResponse.profile();

// 分析查询执行时间
List<Map<String, Object>> shardResults = profileResults.get("shards");
for (Map<String, Object> shard : shardResults) {
    List<Map<String, Object>> searches = (List<Map<String, Object>>) shard.get("searches");
    for (Map<String, Object> search : searches) {
        System.out.println("查询耗时: " + search.get("time") + "纳秒");
        List<Map<String, Object>> collectors = (List<Map<String, Object>>) search.get("collectors");
        for (Map<String, Object> collector : collectors) {
            System.out.println("收集器: " + collector.get("name") + ", 耗时: " + collector.get("time") + "纳秒");
        }
    }
}
  1. 调整相关配置
// 调整线程池大小
// elasticsearch.yml
thread_pool.search.size: 20
thread_pool.search.queue_size: 1000

// 调整分片大小和数量
// 创建索引时设置
CreateIndexRequest request = new CreateIndexRequest.Builder()
    .index("products")
    .settings(s -> s
        .numberOfShards("5")
        .numberOfReplicas("1")
        .refreshInterval(new Time.Builder().time("5s").build())
    )
    .build();

五、最佳实践总结

5.1 分页策略选择

  1. 用户界面分页

  2. 浅分页(<1000条):使用from/size

  3. 无限滚动:使用search_after或PIT

  4. 数据导出

  5. 小数据量:使用from/size

  6. 大数据量:使用scroll或异步搜索

  7. 实时数据要求

  8. 需要实时数据:使用search_after

  9. 需要一致性视图:使用PIT

5.2 高亮最佳实践

  1. 高亮器选择

  2. 小文档:plain高亮器

  3. 大文档:fvh高亮器

  4. 复杂查询:unified高亮器

  5. 性能优化

  6. 限制高亮字段数量

  7. 减少片段数量和大小

  8. 使用term_vector加速高亮

  9. 用户体验

  10. 使用HTML标签突出显示

  11. 返回上下文片段

  12. 对重要字段使用不同样式

5.3 性能优化清单

  1. 索引优化

  2. 合理设计映射

  3. 只索引必要字段

  4. 使用适当的分析器

  5. 优化分片数量

  6. 查询优化

  7. 只返回必要字段

  8. 使用过滤器缓存

  9. 避免深度分页

  10. 使用复合索引

  11. 系统优化

  12. 分配足够内存

  13. 调整JVM设置

  14. 使用SSD存储

  15. 监控和调优

结论

Elasticsearch的分页查询、关键词高亮和性能优化是构建高效搜索应用的关键要素。通过选择合适的分页策略、优化高亮配置和实施性能优化措施,可以显着提升搜索体验和系统性能。

本文详细介绍了各种分页技术的工作原理、高亮实现方法以及性能优化策略,并提供了实际案例和代码示例。希望这些内容能帮助开发者构建出高效、用户友好的搜索功能。

记住,没有一种万能的解决方案适用于所有场景。根据具体需求和数据特性,选择最适合的技术组合,并通过持续监控和调优来保持系统的高性能。