刚开始用 Typesense 做中文搜索的时候,我一度以为用个 locale: zh 就完事了, 直到线上数据一多,才发现分词效果、召回质量、内存占用这些细节,全都绕不过去。

ICU 自带的分词规则很稳,但在垂直领域又总觉得差那么一点点,Jieba 之类外部分词器看起来很香, 却会把索引体积和维护成本一起拉上来。

于是我花了一段时间把这几块细细拆开:从 ICU 的工作方式、和 Jieba 配合的几种实战玩法, 到 schema 怎么设计、更改哪些字段配置会直接反映在 RAM 账单上。

这篇文章,就是把这些试错和对比过程整理出来,既当一份给自己的备忘,也希望能帮后来者少踩一点坑。

Typesense 的信息提取:基于 ICU 的中文分词

locale + ICU:Typesense 的语言感知分词

Typesense 在做文本处理时,会根据字段的 locale 参数, 把文本交给 ICU(International Components for Unicode)库来分词和边界分析。

典型的中文字段 schema 大概是这样:

{
  "fields": [{ "name": "title", "type": "string", "locale": "zh" }]
}

locale: "zh" 时:

  • Typesense 会把文本交给 ICU 的中文分词规则;
  • ICU 会尝试用内置的 cjdict.txt(中日联合词典)做词典分割;
  • 如果字典匹配失败,就退回到 逐字切分 模式。

这套逻辑对所有 CJK 语言(Chinese / Japanese / Korean)都是类似的: 没有空格 → 用字典 + 字符边界做分词

你可以把它理解成:先查词典,查不到就按字符滑动

小结:不配置 locale 时,Typesense 默认按英文及欧语系规则分词, 对中文场景,一定要给需要搜索的字段加上 locale: "zh"

ICU vs Jieba:稳定性 vs 行业适配能力

从搜索效果和工程维护的角度,ICU 和 Jieba 对中文分词的特点可以这么看:

  • ICU(Typesense 内置)

    优点:

    1. 遵循 Unicode 规范,跨语言一致,同一套机制可以处理多语种;
    2. 分词结果相对稳定,升级成本低;
    3. 已经集成在 Typesense 内部,无需额外部署。

    不足:

    1. 词典体量相对克制,细分行业术语覆盖有限
    2. 对中文长词、新词、专有名词的识别能力一般;
    3. 某些场景命中失败时会退回到逐字切分,导致「召回多、相关性弱」。
  • Jieba / 其他中文专用分词器

    优点:

    1. 词典易扩展,适合引入业务领域词库;
    2. 配合 HMM / 统计模型,对新词、组合词识别更友好;
    3. 一般来说,语义准确度和业务相关性更高

    不足:

    1. 需要你自己部署和维护;
    2. 预分词或查询分词都要额外的 CPU 时间;
    3. 如果把「切得很碎」的结果直接塞到全文搜索里,也有可能增加索引体积。

因此:

如果你需要快上手、多语言支持、维护成本低,用 ICU(locale: "zh")即可。

如果你对中文召回和排序很敏感,且有行业词表,可以考虑 配合 Jieba 自行分词,Typesense 只负责存和搜。

如何在 Typesense 中接入自定义分词结果?

Typesense 在查询接口中提供了 pre_segmented_query 参数:当它为 true 时, Typesense 会认为你已经把查询分好词,只做基于空格的拆分,不再走内建 tokenization。

这给我们留出了两个常见用法:

  1. 仅对查询做自定义分词

    文档:仍然用 ICU + locale: "zh" 建索引;

    查询:用 Jieba 把用户输入按空格拼好,传给 Typesense,并设置 pre_segmented_query: true

  2. 对文档 + 查询同时做分词

    文档索引前,用 Jieba 把中文拆成用空格分隔的 token 字符串;

    Typesense 字段可以用默认 locale(甚至不设),因为你已经「模拟出有空格的语言」;

    查询同样用 Jieba 分词后传入 Typesense。

注意:如果你完全放弃 ICU,改用「自己分好词再喂给 Typesense」,就要自己保证不同字段、不同版本之间的分词策略和词典 完全一致,否则会出现「搜索不回文档」的坑。

典型中文 schema 设计示例

下面这个 schema,将搜索范围集中在标题、正文、分类等字段,同时利用 localeinfixfacet 等特性:

{
  "name": "articles",
  "fields": [
    { "name": "title", "type": "string", "locale": "zh" },
    { "name": "content", "type": "string", "locale": "zh", "infix": true },
    { "name": "category", "type": "string", "facet": true },
    { "name": "published_at", "type": "int64", "range_index": true },
    { "name": "image_url", "type": "string", "index": false, "optional": true }
  ]
}

关键点:

  1. title / content 是全文字段,启用 locale: "zh"content 还开了 infix,支持子串匹配但会增大内存占用。
  2. category 用作分面/过滤,设置 facet: true
  3. published_at 开启 range_index,更适合做时间范围过滤;
  4. image_url 只作为元信息展示,index: false 避免它占用宝贵的内存。

这就顺滑地过渡到下一节:哪些字段的索引会常驻内存?对 RAM 有多大影响?

Typesense 的性能评估

内存模型

Typesense 的一大特点,是采用索引常驻内存 + 原始文档落盘的混合模型:

  • 可搜索字段的倒排 / 范围 / 向量索引会被加载到内存中;
  • 原始 JSON 文档(包括未索引字段)主要存放在磁盘,按需读取。

官方给出的经验规则是:

如果仅包括需要搜索的字段的数据集大小为 X MB, 那么通常需要 2X–3X MB 的 RAM 来存储索引。

例如:

  • 数据集总大小:5 GB;
  • 你只在 titlecontentcategorypublished_at 上做搜索 / 过滤,这部分累计大小约 1 GB;
  • 则推荐预留:2–3 GB RAM 给 Typesense 的索引。

社区实践也大致印证了这个量级:几百万到上千万文档的集合,索引常常会占用数 GB 甚至十几 GB 内存,尤其是在引入向量搜索之后。

字段配置如何影响 RAM 占用?

在 Typesense 里,某个字段是否进内存以及占多少内存,主要由 schema 配置决定。

几个关键开关:

  1. index(是否索引)

    默认 index: true,意味着 Typesense 会为这个字段建立适当的索引结构(倒排 / 范围等)。

    如果一个字段只用于展示,不需要参与搜索、过滤、排序、分面,应该显式设为:

      { "name": "image_url", "type": "string", "index": false }
    

    这样它主要占磁盘,不占内存索引空间。

  2. facet(分面)

    facet: true 会让 Typesense 为该字段建额外的数据结构,用于快速统计各取值的计数;

    在「过滤维度多、分面字段多」的场景(比如电商)会明显增加内存使用。

  3. range_index(范围索引)

    对时间戳、数值字段开启 range_index,可以加速区间过滤;

    内存开销相对可控,但仍然属于「要考虑」的项。

  4. infix(子串搜索)

    infix: true 让字段支持「包含」类搜索(如从中间匹配),文档中明确说明会带来显著的内存开销

    一般只对少数关键字段开启,比如标题或短标签,不宜大面积使用。

  5. 向量字段(向量搜索场景)

    引入 embedding 向量后,索引内存会与「向量维度 × 文档数」线性相关;

    社区里迁移到 0.25.x 并启用向量搜索的案例,2.3M 文档就已经消耗了 12GB RAM,可见向量对内存的影响非常显著。

小结:

schema 配置不是「纯逻辑层设计」,它直接等价为「内存账单」。

每多一个 index=truefacet=trueinfix=true,都在给 RAM 增压。

一套可落地的 RAM 估算流程

结合官方文档和社区经验,可以用下面这套简单流程来做 Typesense 的内存规划:

  1. 确定需要搜索的字段集合

    只统计:参与全文检索、过滤、排序、分面的字段;

    统计这些字段的压缩后大小,相加得到 X

  2. 按 2–3 倍估算索引 RAM

    初步估计:RAM_index ≈ 2X–3X

    若启用了 infix 或大规模向量字段,可倾向上限甚至更高。

  3. 考虑副本数和写入峰值

    如果部署多副本(比如 3 个节点),同一集合的索引会在每个节点各存一份;

    写入/重建索引期间,内存可能短时高于稳态。

  4. 预留系统与缓冲空间

    官方建议监控 RAM 使用,保证至少 15% 留给操作系统和其他进程;

    简单做法:总内存至少为 RAM_index / 0.85

  5. 实测校正

    启动 Typesense 后,通过 /metrics.json/stats.json 以及 Prometheus 导出器持续观测:

    • 初始化加载完成后的 RSS;
    • 索引构建过程中的峰值;
    • 查询压力下的波动。

信息提取策略对性能的影响

信息提取(分词 / tokenization)看起来只是「搜索相关」,其实也会反向影响索引体积和查询性能:

  1. 分词越细,token 越多,索引越大

    ICU 的词典模式相比逐字切分会更「省 token」;

    若你用外部分词器把文本切得非常细(甚至带很多短停用词),索引体积会明显增大。

  2. drop_tokens_threshold 与召回/性能的平衡

    Typesense 会在结果过少时尝试丢弃部分 token,以保证有足够结果;可以通过 drop_tokens_threshold 控制。

    设置为 0 可以禁止丢 token,保证「所有关键词必须同时命中」,但可能增加扫描成本和响应时间。

  3. 错别字容忍(typo tolerance)

    较高的 typo 容忍度会增加候选 token 匹配数,CPU 和内存压力上升;

    Typesense 默认将 typo 数限制为 2,原因之一就是避免组合爆炸。

  4. 预分词查询(pre_segmented_query

    当你自己分好词再交给 Typesense,可以更精准地控制 token 数量;

    对长查询(例如多句子组合搜索)尤其有利,能减少不必要的 token。