Elasticsearch 搜索高级
建议阅读顺序:
- Elasticsearch 入门
- Elasticsearch 搜索
- Elasticsearch 搜索高级(本文)
1. 修改文档得分
1.1 function_score
当我们利用 match 查询时,文档结果会根据与搜索词条的关联度打分 (_score
),返回结果时按照分值降序排列。
在实际业务需求中,常常会有竞价排名的功能。不是相关度越高排名越靠前,而是掏的钱多的排名靠前。
例子:给小米这个品牌的手机算分提高十倍
json
GET /items/_search
{
"query": {
"function_score": {
"query": { "match": { "name":"手机" } },
"functions": [
{
"filter": { "term": { "brand": "小米" } },
"weight": 10
}
],
"boost_mode": "multiply"
}
},
"from": 0, "size": 10
}
function score 查询中包含四部分内容:
- 原始查询 条件:query 部分,基于这个条件搜索文档,并且基于 BM25 算法给文档打分,原始算分;
- 过滤条件:filter 部分,符合该条件的文档才会重新算分
- 算分函数 :符合 filter 条件的文档要根据这个函数做运算,得到的函数算分 ,有四种函数
- weight:函数结果是常量
- field_value_factor:以文档中的某个字段值作为函数结果,适用于那些需要根据某个数值字段来影响文档排序的情况。
- random_score:以随机数作为函数结果,用于测试或某些特定的用例,比如创建一个随机排序的效果
- script_score:自定义算分函数算法,允许你在查询时动态地编写脚本来计算每个文档的分数
- boost_mode 运算模式 :决定了如何将评分函数的结果与基础查询得分相结合,包括:
multiply
:评分函数的结果与基础查询的得分相乘。这是默认行为,适用于希望评分函数增强或减弱基础查询得分的情况。replace
:评分函数的结果将完全替换基础查询的得分。这意味着最终得分将完全基于评分函数的结果,而不考虑基础查询的原始得分。sum
:评分函数的结果与基础查询的得分相加。这使得评分函数的结果直接增加到基础查询得分上,适合于希望累加评分因素的情况。avg
:评分函数的结果与基础查询的得分取平均值。这种方式适用于希望平衡基础查询得分与评分函数得分的情况。
1.2 Java Client
java
@Test
void testFunctionScoreQuery() throws Exception {
//构建请求
SearchRequest.Builder builder = new SearchRequest.Builder();
//设置索引
builder.index("items");
//设置查询条件
SearchRequest.Builder searchRequestBuilder = builder.query(
q -> q.functionScore(
f -> f.query(
q1 -> q1.match(
m -> m.field("name").query("手机")
)
)
.functions(
fn -> fn.filter(
f1 -> f1.term(
t -> t.field("brand").value("小米")
)
)
.weight(10d)
)
.boostMode(FunctionBoostMode.Multiply)
)
).from(0).size(10);
SearchRequest build = searchRequestBuilder.build();
//执行请求
SearchResponse<ItemDoc> searchResponse = esClient.search(build, ItemDoc.class);
//解析结果
handleResponse(searchResponse);
}
2. 深度分页
2.1 深度分页问题
elasticsearch 的数据一般会采用分片存储,也就是把一个索引中的数据分成 N 份,存储到不同节点上。这种存储方式比较有利于数据扩展,但给分页带来了一些麻烦。
例如:此时需要取出所有数据的前 1000 名,就需要先在每个分片中取出前 1000,再汇总后,再取出前 1000。
当查询分页深度较大时,汇总数据过多,对内存和 CPU 会产生非常大的压力,特别是在 from
值非常大的情况下。这是因为 Elasticsearch 需要先跳过前面的所有文档才能获取到所需的文档,这可能导致大量的磁盘 I/O 操作和 CPU 使用率。
因此 elasticsearch 会限制 from + size
请求:
-
size
参数的最大值:默认情况下,
size
参数的最大值被限制为 10000。这意味着每次查询最多只能返回 10000 条记录。 -
from
参数的最大值:默认情况下,
from
参数的最大值也被限制为 10000。这意味着请求不能超过第 10000 条记录之后的数据。
这意味着,理论上,您可以查询的最大页数是 10000 / size
。例如,如果 size
设置为 100,则最多可以查询 100 页。
2.2 search after
针对深度分页,elasticsearch 提供了两种解决方案:
search after
:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。scroll
滚动查询:原理将排序后的文档id形成快照,保存下来,基于快照做分页。官方已经不推荐使用。
search after 举例:
查询第一页:
查询第二页:
依次类推。。。。
如何使用 search_after 实现降序排序呢,排序字段是 ID?
第一页的 search_after 值就需要设置一个最大值
json
GET /items/_search
{
"query": { "bool": {} },
"sort": [
{ "id": { "order": "desc" } }
],
"size": 10,
"search_after":[999999999999]
}
使用 Java client 实现深度分页
java
@Test
void testSearchAfter() throws IOException {
// 1.创建Request
SearchRequest.Builder builder = new SearchRequest.Builder();
builder.index("items");
builder.query(
q -> q.bool(b -> b)
).sort(
s1 -> s1.field(f -> f.field("id").order(SortOrder.Asc))
)
.searchAfter("0")
.size(1);
SearchRequest request = builder.build();
SearchResponse<ItemDoc> response = esClient.search(request, ItemDoc.class);
// 解析响应
handleResponse(response);
}
2.3 总结
大多数情况下,我们采用普通分页就可以了。查看百度、京东等网站,会发现其分页都有限制。例如百度最多支持 77 页,每页不足 20 条。京东最多 100 页,每页最多 60 条。
因此,一般我们采用限制分页深度的方式即可。
3. 地理坐标查询
3.1 介绍
所谓的地理坐标查询,其实就是根据经纬度查询
常见的使用场景包括:
- 携程:搜索我附近的酒店
- 滴滴:搜索我附近的出租车
- 微信:搜索我附近的人
3.2 矩形范围查询
矩形范围查询,也就是 geo_bounding_box
查询,查询坐标落在某个矩形范围的所有文档:
查询时,需要指定矩形的左上、右下两个点的坐标,然后画出一个矩形,落在该矩形内的都是符合条件的点。
语法如下:
json
// geo_bounding_box查询
GET /indexName/_search
{
"query": {
"geo_bounding_box": {
"FIELD": {
// 左上点
"top_left": {
// 纬度
"lat": 31.1,
// 经度
"lon": 121.5
},
// 右下点
"bottom_right": { "lat": 30.9, "lon": 121.7 }
}
}
}
}
-
操作
添加映射:
jsonPUT /items/_mapping { "properties": { "location": { "type": "geo_point" } } }
通过 高德地图API 提供的 API 获取经纬度
更新数据坐标
jsonPOST /items/_update/584391 { "doc": { "location": { "lat": 40.06, "lon": 116.34 } } }
-
测试
找到左上和右下的坐标
jsonGET /items/_search { "query": { "geo_bounding_box": { "location": { "top_left": { "lat": 40.08, "lon": 116.32 }, "bottom_right": { "lat": 40.04, "lon": 116.36 } } } } }
3.3 附近搜索
附近查询,也叫做距离查询(geo_distance
):查询到指定中心点小于某个距离值的所有文档。
语法:
json
// geo_distance 查询
GET /indexName/_search
{
"query": {
"geo_distance": {
"distance": "15km", // 半径
"FIELD": "31.21,121.5" // 圆心
}
}
}
测试:
GET /items/_search
{
"query": {
"geo_distance": {
"distance": "15km",
"location": "40.061034,116.345999"
}
}
}
4. Java Client
GeoPoint 类型是 Spring data elasticsearch 中的一个类,需要将它的依赖添加到 pom.xml 中
xml
<!-- 加入spring data elasticsearch -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
</dependency>
在实体类中添加属性:
java
@ApiModelProperty("地理坐标")
private GeoPoint location;
DSL 语句:
json
GET /items/_search
{
"query": {
"geo_bounding_box": {
"location": {
"top_left": { "lat": 40.08, "lon": 116.32 },
"bottom_right": { "lat": 40.04, "lon": 116.36 }
}
}
}
}
代码:
java
@Test
void testGeo() throws Exception {
//构建请求
SearchRequest.Builder builder = new SearchRequest.Builder();
builder.index("items");
builder.query(
q -> q.geoBoundingBox(
g -> g.field("location").boundingBox(
b -> b.tlbr(
tlbr->tlbr.topLeft(t -> t.latlon(l -> l.lat(40.08).lon(116.32)))
.bottomRight(b1 -> b1.latlon(l -> l.lat(40.04).lon(116.36)))
)
)
)
);
SearchRequest build = builder.build();
//执行请求
SearchResponse<ItemDoc> searchResponse = esClient.search(build, ItemDoc.class);
//解析结果
handleResponse(searchResponse);
}