MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

排序技术:定制ElasticSearch搜索结果排序

2023-11-143.0k 阅读

ElasticSearch 排序基础

理解 ElasticSearch 中的默认排序

在 ElasticSearch 中,当执行搜索请求时,如果没有指定排序规则,ElasticSearch 会按照相关性得分 _score 进行排序。这个得分是基于 ElasticSearch 的评分算法(例如 TF/IDF,Term Frequency/Inverse Document Frequency)计算得出的,它反映了文档与查询条件的匹配程度。

例如,假设有一个简单的索引,存储了一些博客文章,我们执行如下基本查询:

GET /blog_posts/_search
{
    "query": {
        "match": {
            "content": "elasticsearch"
        }
    }
}

上述查询会在 blog_posts 索引的 content 字段中搜索包含 "elasticsearch" 的文档,并按照相关性得分 _score 对结果进行排序。得分越高的文档,在结果集中越靠前。

相关性得分计算的核心因素

  1. 词频(Term Frequency, TF):指的是一个词在文档中出现的频率。词频越高,说明该词在文档中越重要,对相关性得分的贡献也就越大。例如,在一篇关于 "ElasticSearch 性能优化" 的文章中,"ElasticSearch" 出现的次数越多,这篇文章与搜索 "ElasticSearch" 的相关性可能就越高。
  2. 逆文档频率(Inverse Document Frequency, IDF):与词在整个索引中的出现频率成反比。如果一个词在大多数文档中都出现,那么它的 IDF 值就会较低,因为它对区分文档的独特性贡献较小。例如,像 "the"、"and" 这样的常见词,在几乎所有文档中都会出现,它们的 IDF 值就非常低。而像 "ElasticSearch 分布式架构" 这样相对不常见的短语,其 IDF 值会较高,因为它能更有效地将包含该短语的文档与其他文档区分开来。
  3. 字段长度归一化(Field Length Norm):较短的字段通常被认为包含更重要的信息。因此,ElasticSearch 会对字段长度进行归一化处理,字段越短,相关性得分越高。例如,在一个博客文章索引中,标题字段通常比正文内容字段短,那么如果在标题字段中匹配到查询词,其对相关性得分的提升会比在正文字段中匹配到查询词更显著。

基本排序方式

按字段值排序

在 ElasticSearch 中,我们可以根据文档中某个字段的值对搜索结果进行排序。这在很多场景下非常有用,比如按照时间戳对日志进行排序,按照价格对商品进行排序等。

假设我们有一个存储商品信息的索引 products,其中包含 price 字段(表示商品价格),我们可以按照价格升序对商品进行排序:

GET /products/_search
{
    "query": {
        "match_all": {}
    },
    "sort": [
        {
            "price": {
                "order": "asc"
            }
        }
    ]
}

在上述示例中,sort 数组指定了排序规则。这里我们选择 price 字段,并将 order 设置为 asc 表示升序排列。如果要降序排列,只需将 order 设置为 desc

GET /products/_search
{
    "query": {
        "match_all": {}
    },
    "sort": [
        {
            "price": {
                "order": "desc"
            }
        }
    ]
}

多字段排序

在实际应用中,我们常常需要根据多个字段进行排序。例如,在电商场景下,我们可能希望先按照销量对商品进行排序,如果销量相同,再按照价格进行排序。

继续以 products 索引为例,假设该索引还包含 sales_count 字段(表示商品销量),我们可以这样进行多字段排序:

GET /products/_search
{
    "query": {
        "match_all": {}
    },
    "sort": [
        {
            "sales_count": {
                "order": "desc"
            }
        },
        {
            "price": {
                "order": "asc"
            }
        }
    ]
}

在这个例子中,首先会按照 sales_count 字段降序排列,对于销量相同的商品,再按照 price 字段升序排列。

按脚本计算值排序

有时候,我们需要根据更复杂的逻辑对文档进行排序,这时候就可以使用 ElasticSearch 的脚本排序功能。脚本可以基于文档的字段值进行计算,然后根据计算结果进行排序。

假设我们有一个 movies 索引,其中包含 rating 字段(表示电影评分,取值范围 0 - 10)和 views 字段(表示电影观看次数)。我们希望按照一个综合指标进行排序,该指标为 rating * log(views + 1)

首先,确保 ElasticSearch 启用了脚本功能(在较新版本中,默认启用了沙盒脚本执行)。然后,我们可以使用如下查询:

GET /movies/_search
{
    "query": {
        "match_all": {}
    },
    "sort": [
        {
            "_script": {
                "type": "number",
                "script": {
                    "source": "doc['rating'].value * Math.log(doc['views'].value + 1)",
                    "lang": "painless"
                },
                "order": "desc"
            }
        }
    ]
}

在上述示例中,_script 部分定义了排序规则。type 指定了脚本返回值的类型为 numberscript 中的 source 字段包含了具体的计算逻辑,这里使用了 Painless 脚本语言。doc['rating'].valuedoc['views'].value 分别获取文档中的 ratingviews 字段值,然后进行计算。最后,order 设置为 desc 表示按照计算结果降序排列。

与相关性得分结合排序

保留相关性得分并结合其他字段排序

在某些情况下,我们既希望考虑文档与查询的相关性,又希望结合其他字段进行排序。例如,在搜索酒店时,我们希望优先展示相关性高的酒店,但对于相关性相近的酒店,按照价格进行排序。

假设我们有一个 hotels 索引,包含 price 字段,并且我们在 description 字段中搜索 "luxury" 相关的酒店。我们可以这样编写查询:

GET /hotels/_search
{
    "query": {
        "match": {
            "description": "luxury"
        }
    },
    "sort": [
        {
            "_score": {
                "order": "desc"
            }
        },
        {
            "price": {
                "order": "asc"
            }
        }
    ]
}

在这个查询中,首先按照相关性得分 _score 降序排列,对于相关性得分相同的酒店,再按照 price 字段升序排列。

自定义相关性得分与排序的权重

有时候,我们可能需要对相关性得分和其他排序字段的权重进行调整。例如,在电商搜索中,我们可能希望相关性得分占 70% 的权重,价格因素占 30% 的权重。

为了实现这种自定义权重的排序,我们可以借助 ElasticSearch 的函数得分查询(Function Score Query)。假设我们有一个 products 索引,包含 price 字段,并且在 title 字段中搜索 "smartphone"。我们可以这样编写查询:

GET /products/_search
{
    "query": {
        "function_score": {
            "query": {
                "match": {
                    "title": "smartphone"
                }
            },
            "functions": [
                {
                    "field_value_factor": {
                        "field": "price",
                        "modifier": "reciprocal",
                        "factor": 0.3
                    }
                }
            ],
            "score_mode": "multiply",
            "boost_mode": "multiply"
        }
    },
    "sort": [
        {
            "_score": {
                "order": "desc"
            }
        }
    ]
}

在上述示例中,function_score 部分用于调整得分。functions 数组中的 field_value_factor 定义了基于 price 字段的得分调整。modifier 设置为 reciprocal 表示取倒数,这样价格越低,得分越高。factor 设置为 0.3 表示价格因素占 30% 的权重。score_mode 设置为 multiply 表示将原始相关性得分与基于价格调整后的得分相乘。最后,按照调整后的 _score 进行降序排序。

地理位置相关排序

按距离地理位置排序

在处理与地理位置相关的数据时,ElasticSearch 提供了强大的功能来按照距离某个地理位置点的远近对文档进行排序。例如,在一个餐厅搜索应用中,我们可能希望按照餐厅与用户当前位置的距离对搜索结果进行排序。

假设我们有一个 restaurants 索引,其中每个文档包含一个 location 字段,该字段存储了餐厅的地理位置信息(使用 Geo - Point 类型)。我们可以使用如下查询来按照距离用户当前位置(假设为纬度 37.7749,经度 -122.4194)的远近对餐厅进行排序:

GET /restaurants/_search
{
    "query": {
        "match_all": {}
    },
    "sort": [
        {
            "_geo_distance": {
                "location": {
                    "lat": 37.7749,
                    "lon": -122.4194
                },
                "order": "asc",
                "unit": "km",
                "distance_type": "arc"
            }
        }
    ]
}

在这个查询中,_geo_distance 用于指定按地理位置距离排序。location 字段指定了参考的地理位置点。order 设置为 asc 表示按照距离升序排列,即距离用户位置最近的餐厅排在前面。unit 设置为 km 表示距离单位为千米。distance_type 设置为 arc 表示使用球面距离计算方法。

结合地理位置与其他因素排序

在实际应用中,我们通常不会仅仅按照距离排序,还会结合其他因素,比如餐厅的评分、价格等。例如,我们希望优先展示距离用户近且评分高的餐厅。

假设 restaurants 索引还包含 rating 字段(表示餐厅评分,取值范围 0 - 5),我们可以这样编写查询:

GET /restaurants/_search
{
    "query": {
        "function_score": {
            "query": {
                "match_all": {}
            },
            "functions": [
                {
                    "field_value_factor": {
                        "field": "rating",
                        "modifier": "multiply",
                        "factor": 10
                    }
                },
                {
                    "gauss": {
                        "location": {
                            "origin": {
                                "lat": 37.7749,
                                "lon": -122.4194
                            },
                            "scale": "10km",
                            "offset": "0km",
                            "decay": 0.5
                        }
                    }
                }
            ],
            "score_mode": "sum",
            "boost_mode": "multiply"
        }
    },
    "sort": [
        {
            "_score": {
                "order": "desc"
            }
        }
    ]
}

在上述查询中,function_score 用于综合考虑距离和评分因素。field_value_factor 基于 rating 字段调整得分,factor 设置为 10 表示评分对得分的影响较大。gauss 函数用于基于距离调整得分,scale 设置为 10km 表示距离参考点 10 千米处得分开始衰减,decay 设置为 0.5 表示衰减因子。score_mode 设置为 sum 表示将基于评分和距离调整后的得分相加。最后,按照调整后的 _score 进行降序排序。

处理特殊情况的排序

处理缺失值排序

在数据中,可能会存在某些文档的特定字段值缺失的情况。在排序时,我们需要考虑如何处理这些缺失值。例如,在按照价格排序的商品索引中,如果某些商品没有设置价格(即价格字段缺失),我们可能希望将这些商品排在最前面或者最后面。

假设我们有一个 products 索引,在按照 price 字段排序时,希望将价格缺失的商品排在最后面,可以这样编写查询:

GET /products/_search
{
    "query": {
        "match_all": {}
    },
    "sort": [
        {
            "price": {
                "order": "asc",
                "missing": "_last"
            }
        }
    ]
}

在上述示例中,missing 设置为 _last 表示将缺失 price 字段值的文档排在结果集的最后。如果希望将缺失值文档排在最前面,可以将 missing 设置为 _first

处理多值字段排序

当文档中的某个字段包含多个值时,ElasticSearch 需要确定如何根据这个多值字段进行排序。例如,在一个音乐专辑索引中,genres 字段可能包含多个音乐流派(如 ["rock", "pop", "jazz"])。

假设我们有一个 albums 索引,并且希望按照 genres 字段中包含的流派数量进行排序(流派数量越多越靠前),可以使用如下脚本排序:

GET /albums/_search
{
    "query": {
        "match_all": {}
    },
    "sort": [
        {
            "_script": {
                "type": "number",
                "script": {
                    "source": "doc['genres'].size()",
                    "lang": "painless"
                },
                "order": "desc"
            }
        }
    ]
}

在这个查询中,通过 Painless 脚本 doc['genres'].size() 获取 genres 字段的元素数量,并按照这个数量进行降序排序。

性能优化与注意事项

排序性能优化

  1. 字段数据类型选择:选择合适的数据类型对于排序性能至关重要。例如,对于数值类型的排序,使用 longdouble 比使用 keyword 类型性能更好,因为数值类型在内部存储和比较时更加高效。
  2. 索引设计:为排序字段创建适当的索引可以显著提高排序性能。对于经常用于排序的字段,确保其索引设置合理,避免不必要的字段折叠或分析。例如,对于日期字段,确保使用 date 类型并设置合适的格式。
  3. 分页与排序:当进行分页并结合排序时,要注意性能问题。随着分页深度的增加,ElasticSearch 需要获取和排序的数据量也会增加,这可能导致性能下降。可以考虑使用滚动(Scroll)API 来处理大量数据的分页,或者采用其他优化策略,如基于主键的分页。

注意事项

  1. 脚本安全:在使用脚本排序时,要注意脚本的安全性。确保启用了脚本沙盒,防止恶意脚本执行。同时,尽量避免在脚本中进行复杂的计算,因为这可能会影响性能。
  2. 数据一致性:在分布式环境中,由于数据的复制和分片,排序结果可能会受到数据一致性的影响。在某些情况下,可能需要等待数据同步完成后再进行排序查询,以确保得到一致的结果。
  3. 内存使用:排序操作可能会消耗大量的内存,尤其是在处理大量数据和复杂排序规则时。要密切关注 ElasticSearch 节点的内存使用情况,避免因内存不足导致节点故障。可以通过调整 ElasticSearch 的内存设置参数,如 heap.size 来优化内存使用。

通过深入理解和灵活运用上述 ElasticSearch 排序技术,开发人员能够根据具体的业务需求定制出高效、准确的搜索结果排序,提升应用的用户体验和性能。在实际应用中,需要根据数据特点、业务场景和性能要求不断优化排序策略,以达到最佳效果。