编程

Typesense:通用数据类型搜索技巧

690 2024-08-27 12:15:00

本文中,我们将讨论在 Typesense 中如何为以下数据类型创建索引和搜索:

产品型号 / 部件号码 / SKU

假设你有一个包含产品标识符(型号、零件号或 SKU)的文档(document),其中混合了字母数字字符和特殊字符:

{
  "title": "Control Arm Bushing Kit",
  "part_number": "K83913.39F29.59444AT"
  //...
}

现在,假设你希望此产品出现在以下任何搜索词的搜索结果中

  • K83913
  • 83913
  • 39F29
  • 59444AT
  • 59444
  • 9444AT
  • K83913.39F29
  • 39F29.59444

默认行为

默认情况下,Typesense 在索引和搜索字段时会从字段中删除特殊字符。因此,K83913.39F29.59444AT 将被索引为K8391339F2959444AT

默认情况下,Typesense 执行前缀搜索,这意味着它只搜索搜索条件中位于字符串开头的记录。因此,搜索出现在 K83913.39F29.59444AT 中间的 39F29F29 不会调出该记录。但是搜索 K83913K83913.39K83913.39F29.59444K83913.39 将调出该记录。

改进

我们需要做的第一个更改是告诉 Typesense 将根据 . (点号)拆分产品标识符。这样,K83913.39F29.59444AT 将被索引为三个单独的 token(单词):K8391339F2959444AT。现在,当搜索 39F295944 时,将返回产品 K83913.39F29.59444AT

你可以在创建集合时通过在 schema 中设置 token_separators 来实现这一点:

{
  "name": "products",
  "fields": [
    {"name":  "title", "type":  "string"},
    {"name":  "part_number", "type":  "string"}
  ],
  "token_separators": ["."]
}

我们仍然会碰到搜索在字符串中间出现的 839139444AT 的情况。

要解决该问题,我们有两种方式:

使用 v0.23.0 起新增的 infix 搜索特性:

https://github.com/typesense/typesense/issues/393#issuecomment-1065367947(打开新窗口)

注意:对于长字符串,这可能是一个计算密集型的操作。如果你注意到特定用例的 CPU 使用率有所增加,则应使用下面的选项。

根据你期望用户搜索的方式预先拆分产品标识符:

{
  "title": "Control Arm Bushing Kit",
  "part_number": [
    "K83913.39F29.59444AT",
    "83913.39F29.59444AT",
    "3913.39F29.59444AT",
    "913.39F29.59444AT",
    "13.39F29.59444AT",
    "3.39F29.59444AT",
    "9F29.59444AT",
    "F29.59444AT",
    "29.59444AT",
    "9.59444AT",
    "9444AT",
    "444AT",
    "44AT",
    "4AT",
    "AT"
  ]
  //...
}

当将其与 token_separators 结合使用时,你将能够搜索我们上面讨论的所有模式。

电话号码

假设我们的电话号码格式是这样的:+1 (234) 567-8901,而我们希望用户通过以下任何一种格式可以拉取该记录:

  • 8901
  • 567-8901
  • 567 8901
  • 5678901
  • 234-567-8901
  • (234) 567-8901
  • (234)567-8901
  • 1-234-567-8901
  • +12345678901
  • 12345678901
  • 2345678901
  • +1(234)567-8901

默认行为

默认情况下,Typesense 将移除所有特殊字符,并按照空格拆分 token(单词),因此,+1 (234) 567-8901 会被索引为 1, 234, 5678901

因此,搜索 2345678901234 567-8901,将会返回结果,而其他格式不会返回预期的结果。

改进

首先告诉 Typesense 通过() 来拆分,并且在创建集合时在 schema 中使进行 token_separators 设置:

{
  "name": "users",
  "fields": [
    {"name":  "first_name", "type":  "string"},
    {"name":  "phone_number", "type":  "string"}
  ],
  "token_separators": ["(", ")", "-"]
}

这将导致 +1 (234) 567-8901 被索引为 12345678901,现在以下搜索将返回该文档:

  • 8901
  • 567-8901
  • 567 8901
  • 234-567-8901
  • (234) 567-8901
  • (234)567-8901
  • 1-234-567-8901
  • +1(234)567-8901

剩余需要处理的用例如下:

  • 5678901
  • +12345678901
  • 12345678901
  • 2345678901

要解决这个问题,你需要在文档中将这些格式作为 string[] 数组字段添加:

{
  "name": "users",
  "fields": [
    {"name":  "first_name", "type":  "string"},
    {"name":  "phone_number", "type":  "string[]"}
  ],
  "token_separators": ["(", ")", "-"]
}

 

{
  "name": "Tom",
  "phone_number": [
    "+1 (234) 567-8901",
    "12345678901", // Remove all spaces
    "2345678901", // Remove all spaces and country code
    "5678901" // Remove all space, country code and area code
  ]
}

现在,搜索上述任何模式都将调出此记录。

Email 地址

假设我们有一个类似于 contact+docs-example@typesense.org 的电子邮件地址,我们希望用户能够使用以下任何模式来提取此文档:

  • contact+docs-example
  • contact+docs-example@
  • contact+docs-example@typesense
  • contact+docs
  • contact docs
  • docs example
  • contact typesense
  • contact
  • docs
  • example
  • typesense
  • typesense.org

默认行为

默认情况下,Typesense 将在索引过程中删除所有特殊字符,并且只进行前缀搜索(搜索词应位于单词的开头),因此 contact+docs-example@typesense.org 将被索引为 contactdocsexampletypesense.org

因此,带有 ✅ 的搜索词将返回该记录,而带有 ❌ 的内容将 不会返回该记录:

  • contact+docs-example
  • contact+docs-example@
  • contact+docs-example@typesense
  • contact+docs
  • contact docs
  • docs example
  • contact typesense
  • contact
  • docs
  • example
  • typesense
  • typesense.org

改进

为了解决上述剩余的情况,我们可以在创建集合时使用模式中的 token_separators 设置:

{
  "name": "users",
  "fields": [
    {"name":  "first_name", "type":  "string"},
    {"name":  "email", "type":  "string"}
  ],
  "token_separators": ["+", "-", "@", "."]
}

这将使 contact+docs-example@typesense.org 被索引为 contactdocsexampletypesenseorg

现在所有这些搜索条件都能拉取该文档:

  • contact+docs-example
  • contact+docs-example@
  • contact+docs-example@typesense
  • contact+docs
  • contact docs
  • docs example
  • contact typesense
  • contact
  • docs
  • example
  • typesense
  • typesense.org

如果你也希望 ample 返回此记录,可以使用 v0.23.0 中提供的  infix 中缀搜索功能: https://github.com/typesense/typesense/issues/393#issuecomment-1065367947(打开新窗口)

日期/时间

Typesense 没有原生的日期/时间(date/time)数据类型。 

因此,你需要像此处描述的那样,将日期和时间转换成 Unix 时间戳。

嵌套对象

始于 Typesense v0.24.0

Typesense v0.24.0 原生支持嵌套对象及对象数组。

要启用嵌套字段,你需要在创建集合时使用 enable_nested_fields 属性,以及 objectobject[] 数据类型:

{
  "name": "docs", 
  "enable_nested_fields": true,
  "fields": [
    {"name": "person", "type": "object"},
    {"name": "details", "type": "object[]"}
  ]
}

此处了解更多详情。

Typesense v0.23.1 及其之前的版本

Typesense v0.23.1 及更早版本仅支持对整数浮点数字符串布尔值包含以上数据类型的数组的字段值进行索引。集合中的字段只能这些数据类型,这些字段将被索引。

重要附注:你仍然可以在 schema 里未提及的字段中将嵌套对象发送到 Typesense 中。它们将不会被索引或进行类型检查。只会存储在磁盘上,而如果文档是搜索查询的热门内容,则会返回。

Typesense 特别不支持索引、搜索或过滤嵌套对象或对象数组。作为(#227)的一部分,Typesense 计划很快添加对此的支持。与此同时,在将数据发送到 Typesense 之前,你必须将对象和对象数组展开(flatten)为顶级键。
比如,这样一个包含嵌套对象的文档:

{
  "nested_field": {
    "field1": "value1",
    "field2": ["value2", "value3", "value4"],
    "field3": {
      "fieldA": "valueA",
      "fieldB": ["valueB", "valueC", "valueD"]
    }
  }
}  

需要在将其索引到 Typesense 之前,将其展开为:

{
  "nested_field.field1": "value1",
  "nested_field.field2":  ["value2", "value3", "value4"],
  "nested_field.field3.fieldA": "valueA",
  "nested_field.field3.fieldB": ["valueB", "valueC", "valueD"]
}

为了简化对结果中数据的遍历,你可能希望将展开后的嵌套字段的和未处理过的两个版本都发送到 Typesense 中,并且只将展开的键(flattened key)设置为集合架构中的索引,并将其用于搜索/过滤/分面。在解析结果的显示时,你可以使用嵌套版本。

地理坐标

Typesense 支持在文档中使用纬度/经度数据进行 GeoSearch 查询。你可以在 lat/lng 周围的给定半径内过滤文档,或按与给定 lat/lng 的接近程度对结果进行排序,或在边界框内返回结果。

此处有 GeoSearch 地理查询的更多信息:GeoSearch API 相关

长篇文本

如果你有长篇文本,比如一篇长篇期刊文章、网站页面、成绩单等,我们建议你将长篇文本分解为更小的“段落”,并将每个段落存储在 Typesense 中的单独文档中。

这有助于提高搜索结果的粒度并提高相关性,因为如果文本足够长,文档之间的关键字可能会有足够的重叠,搜索常见关键字最终会匹配大多数文章。

HTML 内容

如果搜索的是 HTML 内容,你需要在文档中创建一个仅包含内容的纯文本版本而不包含 HTML 标签的字段,并在 query_by 搜索参数中使用该字段。

你仍然可以将原始 HTML 字段作为未索引字段存储在文档中(只需将其从 schema 中保留即可),因此当原始 HTML 命中时,它将在文档中返回。

此处有更多关于这方面的内容。

搜索 null 或空值

Typesense 原生无法过滤属性值为 null 或空值的文档。

但你仍然可以通过以下方法实现这一点。
假设文档中有一个名为 tags 的可选字段,该字段可以为 null

{
  "tags": null
}

如果你想获取所有 tags 设置为 null 的文档,你需要首先在索引时在每个文档中创建一个名为 is_tags_null: true | false 的额外字段:

[
  {
    "tags": null,
    "is_tags_null": true
  },
  {
    "tags": ["tag1", "tag3"],
    "is_tags_null": false
  }
]

当你在索引时将所有文档中的该字段进行设置后,你可以使用如下方式查询该文档:

{
  "filter_by": "is_tags_null:true"
}

URL 或文件路径

假设你有一组像如下这样的 URL 或文件路径的文档,你希望像在其上面进行搜索:

{"url": "https://url1.com/path1"}
{"url": "https://url2.com/path2"}
{"url": "https://url3.com/path3"}

并且你希望当用户搜索 url1path1 时,Typesense 返回结果。

默认行为

默认情况下,Typesense 将删除所有特殊字符,并将第一个文档索引为 httpsurl1compath1。此外,Typesense 进行前缀搜索(匹配应该在单词的开头),因此 url1path1 不会返回任何结果,因为它们出现在索引字符串的中间。

改进

为解决此问题,使之仍然可以获取 url1path1 的结果,您需要添加:./ 到集合 schema 中的 token_separators 设置中:

{
  "name": "pages",
  "fields": [
    {"name":  "title", "type":  "string"},
    {"name":  "url", "type":  "string"}
  ],
  "token_separators": [":", "/", "."]
}

这将导致 URL 被索引为单独的单词:httpsurl1compath1

现在,当搜索 url1path 时,它将匹配这些单独的单词并返回该文档。

其他数据类型

如果有其他特定类型的数据需要帮助在  Typesense 中进行索引,请到 GitHub issue 或在 Slack 社区中询问。