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

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

引言

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

一、Elasticsearch分页查询技术

1.1 传统分页方式:from/size

最简单的分页方式是使用from和size参数:

java 复制代码
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

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

java 复制代码
// 初始化滚动查询
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推荐的深度分页方案:

java 复制代码
// 初始查询
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的优点:

java 复制代码
// 创建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 基础高亮配置

java 复制代码
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 多字段高亮

java 复制代码
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的标准高亮器
java 复制代码
.fields("description", f -> f
    .type("plain")
    .preTags("<em>")
    .postTags("</em>")
)
  1. 快速向量高亮器:适用于大字段
java 复制代码
.fields("description", f -> f
    .type("fvh")
    .preTags("<em>")
    .postTags("</em>")
    .phraseLimit(50)  // 短语限制
)
  1. 统一高亮器:最强大,但可能较慢
java 复制代码
.fields("description", f -> f
    .type("unified")
    .preTags("<em>")
    .postTags("</em>")
)

2.4 高亮参数优化

java 复制代码
.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 高亮与分页结合

java 复制代码
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 合理设计映射
java 复制代码
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
java 复制代码
.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 只返回必要字段
java 复制代码
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 使用过滤器缓存
java 复制代码
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 避免深度分页
java 复制代码
// 使用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 使用异步搜索处理大查询
java 复制代码
// 提交异步搜索
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 选择合适的高亮器
java 复制代码
// 对于小文档,使用plain高亮器
.fields("title", f -> f
    .type("plain")
)

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

3.4 分页性能优化实战

3.4.1 浅分页优化
java 复制代码
// 对于浅分页,优化查询和返回字段
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 深度分页优化
java 复制代码
// 使用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 分页与高亮结合的优化
java 复制代码
// 高亮只应用于必要字段,限制片段大小和数量
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 实际案例:百万级数据的高效分页与高亮

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

java 复制代码
/**
 * 高效分页与高亮搜索服务
 */
@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创建测试脚本,模拟不同分页场景
java 复制代码
// 测试不同分页方法的性能
@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
java 复制代码
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分析查询性能
java 复制代码
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. 调整相关配置
java 复制代码
// 调整线程池大小
// 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的分页查询、关键词高亮和性能优化是构建高效搜索应用的关键要素。通过选择合适的分页策略、优化高亮配置和实施性能优化措施,可以显著提升搜索体验和系统性能。

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

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

相关推荐
SelectDB技术团队6 小时前
一条日志两套引擎的账:把 Elasticsearch 检索与分析合并到同一份数据的落地写法
大数据·数据库·elasticsearch·搜索引擎·全文检索·日志·apache doris
打工仔折腾 AI11 小时前
从零写一个CAD 02:实体容器、Esc取消与键盘失灵的排查
人工智能·后端·python·性能优化
Elastic 中国社区官方博客13 小时前
使用 Lucene 搜索你的 Bean —— Elasticsearch
大数据·开发语言·人工智能·elasticsearch·搜索引擎·全文检索·lucene
Sayai14 小时前
Neo4j 内嵌模式(Embedded)实战:Java 嵌入式 vs 服务端部署的写入性能对比与 GC 调优
java·开发语言·性能优化·neo4j·图数据库
闲蛋小超人笑嘻嘻14 小时前
Git Worktree 详解
大数据·elasticsearch·搜索引擎
MayBaymax15 小时前
Elasticsearch 原理与用法
java·elasticsearch
IT大白鼠15 小时前
搜索系列 · 第 01 篇——认知入门:Elasticsearch 是什么
elasticsearch·nosql
Elasticsearch16 小时前
14 个 alerts,1 个 incident:使用 Elasticsearch 中的 ES|QL 衡量 alerting rule 噪声
elasticsearch
Elasticsearch16 小时前
Kubernetes attributes processor v1:它对 EDOT Collector 意味着什么
elasticsearch
guo_wen_qiang16 小时前
jenkins流水线参数化配置
运维·docker·容器·jenkins·持续部署