# 排序与相关性

Typesense 使用一种简单的分步排序算法对搜索结果进行排序,该算法可以依赖以下一个或多个因素:

  1. 文本匹配分数,通过特殊字段 _text_match 暴露
  2. 用户定义的索引数值字段(例如:popularity、rating、score 等)
  3. 用户定义的索引字符串字段(例如:name)v0.23.0
  4. 属性值应满足的条件(例如:提升所有 category 为 shoes 的记录)v26

# 文本匹配分数与匹配类型

Typesense 首先基于以下启发式规则计算每个字段的文本匹配分数:

  1. 词频:搜索查询与文本字段之间重叠的 token 数量。拥有更多重叠 token 的文档将排在重叠 token 较少的文档之前。
  2. 编辑距离:如果查询中的某个 token 未找到,我们会查找与查询 token 编辑距离在 num_typos 字符范围内的 token。完全匹配查询 token 的文档比包含较大编辑距离 token 的文档排名更高。
  3. 邻近度:查询 token 是精确出现还是与其他 token 交叉出现在字段中。查询 token 紧密相邻出现的文档将比查询 token 分散出现的文档排名更高。
  4. query_by 字段顺序:在 query_by 字段列表中较早匹配的文档被认为比在较晚字段匹配的文档更相关。这种隐式权重可以通过 query_by_weights 覆盖。
  5. query_by_weights 中指定的字段权重:匹配高分数字段的文档被认为比匹配低分数字段的文档更相关。这些权重会覆盖 query_by 字段顺序带来的隐式权重。

然后,这些字段级文本匹配分数会被用于计算文档级聚合文本匹配分数,当您将 sort_by 搜索参数设置为 _text_match:desc 时,该分数用于对文档排序。

由于不同的搜索用例需要不同的策略,Typesense 支持多种文本匹配模式来计算聚合文档级分数。这可以通过 text_match_type 搜索参数配置,可选值包括:

  • max_score(默认):选择所有字段中最大的文本匹配分数作为文档的代表性分数。字段权重仅在两文档文本匹配分数相同时作为平局决胜依据。在此模式下,我们优先考虑尽可能匹配查询词语的字段。只有当多个字段同等匹配查询时,才使用字段权重决定优先显示哪个文档。
  • max_weight:使用最高权重字段的文本匹配分数作为记录的代表性分数。在此模式下,我们优先考虑我们认为最重要的字段上的匹配。这样,在较高权重字段上部分匹配查询的文档被认为比在较低权重字段上完全匹配的文档更相关。
  • sum_score:我们将字段级加权文本匹配分数求和得到文档级分数。在此模式下,我们考虑所有字段对文档整体匹配查询的贡献。这种模式的缺点是,在多个低权重字段上部分匹配的文档可能会比在单个高权重字段上完全匹配的文档获得更高优先级。

# 基于平局决胜的排名机制

在某些情况下,多个文档可能包含搜索查询中的完全相同的词元。此时,这些文档的 _text_match 分数也会相同。这时就可以使用用户定义的索引数值字段和字符串字段来进行平局决胜。您最多可以指定两个这样的用户定义字段用于排名。

例如,假设我们正在用 short story 这样的查询搜索书籍。如果有多本书包含这些完全相同的词语,那么所有这些文档的文本匹配分数都会相同。

为了打破平局,我们可以指定最多两个额外的 sort_by 字段。例如,我们可以这样设置:

{
  "sort_by": "_text_match:desc,average_rating:desc,publication_year:desc"
}

这将按照以下方式对结果进行排序:

  1. 所有匹配记录首先按其文本匹配分数排序
  2. 如果有两个文档的文本匹配分数相同,则按平均评分排序
  3. 如果仍然存在平局,则按出版年份排序

# 默认排名顺序

当您没有为搜索请求提供 sort_by 参数时,文档将按以下顺序排名:

  1. 首先按 _text_match 分数排序
  2. 然后按集合模式中指定的默认排序字段值排序
  3. 最后,如果未指定则按文档插入顺序排序

# 严格排序或硬性排序

如果您希望严格按索引数值或字符串字段(如 pricename 等)对文档进行排序,可以将文本匹配分数标准移到末尾,如下所示:

{
  "sort_by": "price:desc,_text_match:desc"
}

# 基于相关性与流行度的排序

如果您为文档设置了流行度评分(可通过以下任一方式计算):

  1. 在您的应用程序中使用任意自定义公式计算得出
  2. 使用 Typesense 的计数器分析规则 计算得出

您可以让 Typesense 将自定义评分与其计算出的文本相关性评分相结合,从而使更受欢迎的结果(根据您的自定义评分定义)在排序中获得更高的提升。有两种实现方式:

# 使用固定数量的分桶

以下是将结果划分为特定数量相关性组的方法:

{
  "sort_by": "_text_match(buckets: 10):desc,weighted_score:desc"
}

其中 weighted_score 是文档中包含您自定义评分的字段。

该配置将执行以下操作:

  1. 获取所有匹配查询的结果
  2. 按文本相关性排序(文本匹配评分降序)
  3. 将结果划分为大小相等的10个桶(第一个桶包含最相关的结果)
  4. 强制每个桶内的所有结果在 _text_match 评分上相同。因此第一个桶内的所有结果将被强制赋予相同的文本匹配评分,第二个桶内的所有结果同理,以此类推。
  5. 这将导致每个桶内部出现评分并列,然后使用 weighted_score 来打破并列并重新排序每个桶内的结果。

分桶数量越大,基于加权评分的重新排序就越精细。 例如,如果有100个结果且设置 buckets: 50,则每个桶将包含2个结果,这两个结果将根据您的 weighted_score 在桶内重新排序。

# 使用固定桶大小

另一种方式是将结果分组到固定大小的相关性组中:

{
  "sort_by": "_text_match(bucket_size: 3):desc,weighted_score:desc"
}

这种方法会:

  1. 获取所有匹配查询的结果
  2. 按文本相关性排序
  3. 将结果分组到固定大小的桶中(例如每桶3个结果)
  4. 在每个固定大小的桶内应用 weighted_score 排序

例如,如果你有100个结果且设置 bucket_size: 3,Typesense 会创建约33个桶,每个桶包含3个结果。每组3个文本相关性相近的结果会按其 weighted_score 排序。

# 方案选择建议

  • 当需要特定数量的相关性分组时,使用 buckets 参数
  • 当需要确保每次比较的热门结果数量一致时,使用 bucket_size 参数
  • 这两种方法都能帮助你在搜索结果中平衡文本相关性和热门程度

# 基于相关性和时效性的排序

一个常见需求是让近期发布的结果比旧结果排名更高。

要在 Typesense 中实现这一点,您需要将文档发布时的 Unix 时间戳 存储为一个 int64 字段(例如:published_at_timestamp)。

现在要基于文本相关性和时效性进行排序,可以使用以下 sort_by 参数:

{
  "sort_by": "_text_match(buckets: 10):desc,published_at_timestamp:desc"
}

这将执行以下操作:

  1. 获取所有匹配查询的结果
  2. 按文本相关性排序(文本匹配分数降序)
  3. 将结果分成大小相等的 10 个桶(第一个桶包含最相关的结果)
  4. 强制每个桶内的所有结果在 _text_match 分数上相同。因此第一个桶中的所有结果将被强制具有相同的文本匹配分数,第二个桶中的所有结果将被强制具有相同的文本匹配分数,以此类推。
  5. 这将导致每个桶内出现平局,然后使用 published_at_timestamp 来打破平局并重新排序每个桶内的结果。

桶的数量越多,基于加权分数的重新排序就越精细。 例如,如果您有 100 个结果,并且 buckets: 50,那么每个桶将有 2 个结果,这两个结果将在每个桶内根据 published_at_timestamp 重新排序。

# 精确匹配的排序

默认情况下,如果搜索查询完全匹配某个字段值,Typesense 会认为该文档具有最高相关性并优先显示。

但在某些情况下,这可能不是期望的行为。您可以在 搜索 时设置 prioritize_exact_match=false 来关闭此功能。

# 基于条件的排序

您可以使用特殊的 _eval(<表达式>) 操作作为 sort_by 参数,基于任何评估结果为 truefalse 的表达式对文档进行排序。

_eval() 内部的表达式语法与 filter_by 搜索参数 相同,因此我们也称此功能为"可选过滤"。

例如:

{
  "sort_by": "_eval(in_stock:true):desc,popularity:desc"
}

这将使 in_stock 设为 true 的文档排在前面,然后是 in_stock 设为 false 的文档。

# 提升/降级记录集

除了像上面那样仅基于 true/false 值排序外,我们还可以为匹配一组过滤条件的记录提供自定义分数。

例如,如果我们有一个 shoes 集合,并且希望将所有 Nike 鞋排在 Adidas 鞋之前,可以这样做:

{
  "sort_by": "_eval([ (brand:Nike):3, (brand:Adidas):2 ]):desc"
}

_eval 中可以包含任意数量的表达式,每个表达式都可以像标准的 filter_by 表达式一样复杂。

# 提升或隐藏搜索结果(商品推广)

您可以通过指定记录ID将其固定在特定排名位置(或隐藏):

  1. 基于搜索查询设置覆盖规则(即人工干预),或
  2. 动态使用pinned_hitshidden_hits搜索参数

例如:当用户搜索phone时,您可以设置覆盖规则将某个优惠力度大的商品固定在第一位。

另一个常见的固定搜索结果用例是电商网站的商品推广,商品运营人员(或您自定义的机器学习模型)可能希望针对特定商品类别精确控制哪些产品应该相邻展示。根据用户正在浏览的类别页面,您可以使用pinned_hits参数定义该页面中哪些记录应该出现在哪些位置。

如果您在CMS系统中维护分类页面 -> pinned_hits的映射关系,可以让内部用户修改这些映射,并在渲染特定分类页面时由应用程序获取这些映射配置。

# 调整拼写容错

Typesense 默认自动处理拼写错误,但在某些用例中(如零件编号、电话号码),您可能需要关闭拼写容错或调整其敏感度。

要完全关闭拼写容错,在搜索时设置 num_typos=0typo_tokens_threshold=0

您也可以根据需要提高这些值来调整拼写容错的敏感度。要基于单词长度控制拼写容错,可以使用 搜索参数 min_len_1typomin_len_2typo

您还可以通过为 num_typos 指定多个逗号分隔的值来调整单个字段的拼写容错设置。例如:如果有 query_by=name,phone_number,zip_code 且不想对 phone_numberzip_code 启用拼写容错,可以设置 num_typos=2,0,0

# 处理无结果情况

在某些用例中,如果用户的所有搜索词都不匹配任何文档,您可能不希望向用户显示"未找到结果"的消息。

在这种情况下,可以让 Typesense 自动从用户搜索查询中逐个删除词/标记(token),并重复搜索以显示接近用户原始查询的结果。

此行为由搜索参数 drop_tokens_threshold 控制,默认值为 1。这意味着如果搜索查询只返回 1 或 0 个结果,Typesense 将开始删除搜索关键词并重复搜索,直到找到至少 1 个结果。

要关闭此行为,请设置 drop_tokens_threshold=0