Elasticsearch分页查询、关键词高亮与性能优化全解析
引言
在构建搜索功能时,分页查询和关键词高亮是两个基本需求,而随着数据量增长,性能优化变得尤为重要。本文将深入探讨Elasticsearch中的分页实现方式、高亮显示技术以及性能优化策略,帮助开发者构建高效、用户友好的搜索体验。
一、Elasticsearch分页查询技术
1.1 传统分页方式:from/size
最简单的分页方式是使用from
和size
参数:
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提供三种高亮器:
- 普通高亮器(默认):基于Lucene的标准高亮器
.fields("description", f -> f
.type("plain")
.preTags("<em>")
.postTags("</em>")
)
- 快速向量高亮器:适用于大字段
.fields("description", f -> f
.type("fvh")
.preTags("<em>")
.postTags("</em>")
.phraseLimit(50) // 短语限制
)
- 统一高亮器:最强大,但可能较慢
.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 性能测试方法
- 基准测试:使用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");
}
性能指标:
响应时间
吞吐量
CPU和内存使用率
磁盘I/O
4.2 监控与调优
- 使用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());
}
- 使用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") + "纳秒");
}
}
}
- 调整相关配置
// 调整线程池大小
// 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 分页策略选择
用户界面分页:
浅分页(<1000条):使用from/size
无限滚动:使用search_after或PIT
数据导出:
小数据量:使用from/size
大数据量:使用scroll或异步搜索
实时数据要求:
需要实时数据:使用search_after
需要一致性视图:使用PIT
5.2 高亮最佳实践
高亮器选择:
小文档:plain高亮器
大文档:fvh高亮器
复杂查询:unified高亮器
性能优化:
限制高亮字段数量
减少片段数量和大小
使用term_vector加速高亮
用户体验:
使用HTML标签突出显示
返回上下文片段
对重要字段使用不同样式
5.3 性能优化清单
索引优化:
合理设计映射
只索引必要字段
使用适当的分析器
优化分片数量
查询优化:
只返回必要字段
使用过滤器缓存
避免深度分页
使用复合索引
系统优化:
分配足够内存
调整JVM设置
使用SSD存储
监控和调优
结论
Elasticsearch的分页查询、关键词高亮和性能优化是构建高效搜索应用的关键要素。通过选择合适的分页策略、优化高亮配置和实施性能优化措施,可以显着提升搜索体验和系统性能。
本文详细介绍了各种分页技术的工作原理、高亮实现方法以及性能优化策略,并提供了实际案例和代码示例。希望这些内容能帮助开发者构建出高效、用户友好的搜索功能。
记住,没有一种万能的解决方案适用于所有场景。根据具体需求和数据特性,选择最适合的技术组合,并通过持续监控和调优来保持系统的高性能。