Lance 全文搜索(FTS)索引完全指南:倒排索引存储格式、分词器体系与两阶段训练管线
【免费下载链接】lanceOpen Lakehouse Format for Multimodal AI. Convert from Parquet in 2 lines of code for 100x faster random access, vector index, and data versioning. Compatible with Pandas, DuckDB, Polars, Pyarrow, and PyTorch with more integrations coming..项目地址: https://gitcode.com/GitHub_Trending/la/lance
全文搜索索引(Full Text Search Index,简称 FTS,又称倒排索引 inverted index)是 Lance 标量索引家族中专门服务于文本检索的一类索引:它把"词项(term)"映射到"包含该词项的文档",从而在亿级文本行上实现毫秒级的关键词检索。本文以 docs/src/format/index/scalar/fts.md 为主线,结合 rust/lance-index/src/scalar/inverted 下的实际实现,完整讲解 FTS 索引的磁盘文件布局与 Schema、InvertedIndexParams全部配置参数、simple/whitespace/raw/ngram/ICU/Jieba/Lindera 分词器、text 与 json 两类文档的 token 化规则、两阶段训练管线及内存调优、分布式训练,以及match/phrase/boolean/multi_match/boost等加速查询语法。读完本文,你可以独立完成 FTS 索引的创建、参数调优、查询与故障排查。
FTS 索引是什么
FTS 索引的核心思想是倒排:为每个出现在文档中的词项建立一条"倒排列表(posting list)",记录哪些文档包含该词项以及出现频次。查询时只需在词项字典中定位查询词,再扫描其倒排列表即可,而无需逐行扫描原始列。
在 Lance 中,FTS 索引专为高性能文本检索设计,支持多种打分算法(如 BM25)与短语查询(phrase query)。索引的详细参数通过 protobuf 消息InvertedIndexDetails持久化(对应代码位于 rust/lance-index/src/scalar/inverted/index/inverted_index.rs,其中通过pbold::InvertedIndexDetails::try_from(¶ms)完成与 proto 消息的互转),训练与查询阶段的参数则统一收敛到 rust/lance-index/src/scalar/inverted/tokenizer.rs 中定义的InvertedIndexParams结构体。
存储布局:四类文件与分区
一个 FTS 索引由多组文件构成,分别存放词项字典、文档信息和倒排列表:
| 文件 | 作用 |
|---|---|
tokens.lance | 词项字典,把 token 字符串映射为 token ID |
docs.lance | 文档元数据,包括每个文档的 token 计数 |
invert.lance | 每个 token 的压缩倒排列表 |
metadata.lance | 索引元数据与配置(JSON 序列化) |
索引可能包含多个分区(partition)。每个分区拥有自己独立的一套 token、文档与倒排列表文件,文件名以分区 ID 为前缀,例如part_0_tokens.lance、part_0_docs.lance、part_0_invert.lance。metadata.lance中列出了索引包含的全部分区 ID。
查询时每个分区都必须被搜索,结果合并后产生最终排序输出。因此分区数越少,查询性能通常越好——每个分区都需要一次独立的词项字典查找和倒排列表扫描。分区数量由训练配置控制,核心是环境变量LANCE_FTS_TARGET_SIZE:它决定每个合并后的分区可以长到多大(详见训练过程)。
Token 字典文件 Schema(tokens.lance)
| 列 | 类型 | 可空 | 说明 |
|---|---|---|---|
_token | Utf8 | 否 | token 字符串 |
_token_id | UInt32 | 否 | token 的唯一标识 |
文档文件 Schema(docs.lance)
| 列 | 类型 | 可空 | 说明 |
|---|---|---|---|
_rowid | UInt64 | 否 | 文档行 ID |
_num_tokens | UInt32 | 否 | 文档中的 token 数量 |
分区的docs.lance文件还支持可选的 schema metadata 键total_tokens,其值为该文件内_num_tokens之和(十进制 UInt64)。读取端利用该元数据构造精确的语料统计,而无需扫描整个列;当键缺失时,则回退为对_num_tokens求和。写入端在同一文件提交中依据同一张文档表生成该键。若该键存在但无法解析,或与随后加载的_num_tokens求和结果不一致,则视为文件损坏。该键的常量定义与一致性校验可在 rust/lance-index/src/scalar/inverted/documents.rs(TOTAL_TOKENS_KEY: &str = "total_tokens")中看到。
倒排列表文件 Schema(invert.lance)
| 列 | 类型 | 可空 | 说明 |
|---|---|---|---|
_posting | List<LargeBinary> | 否 | 压缩倒排列表(delta 编码的行 ID 与频次) |
_max_score | Float32 | 否 | 该 token 的最大得分(用于查询优化) |
_length | UInt32 | 否 | 包含该 token 的文档数量 |
_compressed_position | List<List<LargeBinary>> | 是 | 可选的压缩位置列表,用于短语查询 |
倒排列表文件的 schema metadata 中包含posting_block_size:每个压缩倒排块编码的文档数量。缺少该元数据的旧索引按传统块大小128读取。block_size参数(见下节)最终会写入该元数据并被构建器读取,例如 rust/lance-index/src/scalar/inverted/builder.rs 中self.params.block_size被用于初始化写入器。
元数据文件 Schema(metadata.lance)
元数据文件是 JSON 序列化的配置与分区信息,包含两个键:
| 键 | 类型 | 说明 |
|---|---|---|
partitions | Array<UInt64> | 分区 ID 列表,用于分布式索引组织 |
params | JSON Object | 序列化的 InvertedIndexParams(含 tokenizer 配置) |
文件名常量metadata.lance定义于 rust/lance-index/src/scalar/inverted/index/format.rs(pub const METADATA_FILE: &str = "metadata.lance")。
InvertedIndexParams:全部配置参数
params中的InvertedIndexParams是 FTS 索引行为的"总开关",字段定义见 rust/lance-index/src/scalar/inverted/tokenizer.rs 第 105 行起的结构体:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
base_tokenizer | String | "simple" | 基础分词器类型(见 Tokenizers 一节) |
language | String | "English" | 词干提取与停用词所用语言 |
with_position | Boolean | false | 是否存储词项位置以支持短语查询(显著增大索引体积) |
max_token_length | UInt32? | None | 最大 token 长度,超过则被移除 |
lower_case | Boolean | true | 是否将 token 转为小写 |
stem | Boolean | false | 是否应用语言相关的词干提取(stemming) |
remove_stop_words | Boolean | false | 是否移除指定语言的常见停用词 |
ascii_folding | Boolean | true | 是否将带重音字符转换为 ASCII 等价形式 |
min_gram | UInt32 | 2 | 最小 n-gram 长度(仅 ngram 分词器) |
max_gram | UInt32 | 15 | 最大 n-gram 长度(仅 ngram 分词器) |
prefix_only | Boolean | false | 是否只生成前缀 n-gram(仅 ngram 分词器) |
block_size | UInt32 | 128 | 每个压缩倒排块编码的文档数,只能为 128 或 256;旧索引缺失时按 128 读取;256仍属实验性,可能引入破坏性变更 |
这些参数在构建索引时被逐一应用到 tokenizer 流水线上:base_tokenizer决定词项如何切分,language只在stem或remove_stop_words为真时生效,with_position决定是否写入_compressed_position列(对应源码中InvertedIndexBuilder的with_position字段与写入器初始化逻辑,见 builder.rs)。
分词器体系
FTS 索引针对不同文本处理需求提供了多套分词器。基础分词器负责把文本切分为词项,随后可按顺序叠加 token 过滤器。
基础分词器一览
| 分词器 | 说明 | 适用场景 |
|---|---|---|
| simple | 按空白与标点切分,移除非字母数字字符 | 通用文本(默认) |
| whitespace | 仅按空白字符切分 | 需要保留标点 |
| raw | 不切分,整个文本作为一个 token | 精确匹配 |
| ngram | 切分为重叠的字符序列 | 子串/模糊搜索 |
| icu | ICU 基于字典的 Unicode 词切分 | 混合语言文本 |
| icu/split | ICU 切分后再次按 simple 风格分隔符切分 | 混合语言标识符 |
| jieba/* | 中文分词(词级切分) | 中文文本 |
| lindera/* | 日语形态分析分词 | 日文文本 |
以上取值及code(代码感知分词,需配合analyzer='code')在 tokenizer.rs 的build_base_tokenizer()中有完整的match分支实现。
ICU 分词器(混合语言文本)
ICU 分词器使用 Unicode 词边界规则,并对复杂文字采用基于字典的切分。对于默认simple分词器会把手写 CJK 连续串当成一个巨大 token 的混合语言文本,ICU 尤为适用。
默认情况下,Lance 原样保留 ICU 返回的词段;使用base_tokenizer: "icu/split"可对 ICU 词段再次按非字母数字分隔符(如下划线与标点)切分。例如hello_world こんにちは世界会被切分为hello、world、こんにちは、世界。
- 模型:使用随 Lance 打包的编译版 ICU4X segmenter 数据
- 用法:指定为
icu;或icu/split以切分标点分隔的标识符 - 特性:Unicode 感知的词边界检测;对中文、日文、高棉文、老挝文、缅甸文、泰文进行基于字典的切分;无需下载外部语言模型
Jieba 分词器(中文)
Jieba 是流行的中文分词库,采用基于字典加统计方法的词切分。
- 配置:模型目录中的
config.json文件 - 模型:需下载后放入 Lance 主目录下的
jieba/目录 - 用法:指定为
jieba/<model_name>,或直接用jieba使用默认模型 - 配置结构:
{ "main": "path/to/main/dictionary", "users": ["path/to/user/dict1", "path/to/user/dict2"] }- 特性:对简体与繁体中文的准确词切分;支持自定义用户词典;支持多种切分模式(精确、全模式、搜索引擎模式)
Lindera 分词器(日文)
Lindera 是专为日语设计的形态分析分词器,解决日语没有词间空格、需要正确分词的问题。
- 配置:模型目录中的
config.yml文件 - 模型:需下载后放入 Lance 主目录下的
lindera/目录 - 用法:指定为
lindera/<model_name>,其中<model_name>是包含模型文件的子目录名 - 特性:带词性标注的形态分析;基于字典的分词;支持自定义用户词典
Token 过滤器
过滤器在基础分词器之后按顺序应用:
| 过滤器 | 说明 | 配置项 |
|---|---|---|
| RemoveLong | 移除超过max_token_length的 token | max_token_length |
| LowerCase | 转为小写 | lower_case(默认 true) |
| Stemmer | 还原词根 | stem、language |
| StopWords | 移除 "the"、"is"、"at" 等常见词 | remove_stop_words、language |
| AsciiFolding | 重音字符转 ASCII | ascii_folding(默认 true) |
在源码中,这些过滤器由 tantivy 的TextAnalyzer流水线组装(见 tokenizer.rs 中build_base_tokenizer()对LowerCaser、AsciiFoldingFilter、stemmer、stop words 的组合)。
支持的语言
用于词干提取与停用词移除的语言包括:Arabic、Danish、Dutch、English、Finnish、French、German、Greek、Hungarian、Italian、Norwegian、Portuguese、Romanian、Russian、Spanish、Swedish、Tamil、Turkish。
文档类型:text 与 json
Lance 支持两类文档:text与json。不同文档类型采用不同的 token 化规则,解析出的 token 格式也不同。
Text 类型
Text 类型包括纯文本与文本列表,token 由base_tokenizer生成。例如句子:
Tom lives in San Francisco.被解析为以下 token:
Tom lives in San FranciscoJson 类型
Json 是嵌套结构,Lance 会把 json 文档拆解为path,type,value三元组(triplet)形式的 token。合法类型为:str、number、bool、null。
当三元组的 value 是字符串时,该文本值会进一步用base_tokenizer切分,产生多个三元组 token。查询时,Json Tokenizer 使用三元组格式而非原始 json 格式,从而简化查询语法。
例如给定如下 json 文档:
{ "name": "Lance", "legal.age": 30, "address": { "city": "San Francisco", "zip:us": 94102 } }解析后得到以下 token:
name,str,Lance legal.age,number,30 address.city,str,San address.city,str,Francisco address.zip:us,number,94102随后以三元组格式进行全文检索。要搜索 "San Francisco",可用以下任一三元组查询:
address.city:San Francisco address.city:San address.city:Francisco训练过程:两阶段管线与调优
构建 FTS 索引是一条多阶段流水线:扫描源列 → 并行 token 化文档 → 中间结果溢出(spill)到磁盘 part 文件 → 将 part 文件合并为最终输出分区。对应实现位于 rust/lance-index/src/scalar/inverted/builder.rs 的InvertedIndexBuilder。
Phase 1:Tokenization(token 化)
输入列以 record batch 流读取,并分发给一池 tokenizer 工作任务。每个 worker 独立完成文档 token 化,在内存中累积 token、倒排列表与文档元数据。
当某个 worker 累积的数据达到分区大小上限,或文档数触及u32::MAX时,它会将数据以一组 part 文件刷写到磁盘:part_<id>_tokens.lance、part_<id>_invert.lance、part_<id>_docs.lance。若单个 worker 处理的数据量足够大,它可能产出多个 part 文件。
Phase 2:Merge(合并)
所有 worker 结束后,part 文件被合并为输出分区。part 文件以有界缓冲(bounded buffering)流式加载,避免一次性载入全部数据。对每个 part 文件:统一 token 字典、拼接文档集合、以调整后的 ID 重写倒排列表。
当一个合并分区达到目标大小后,即写入目标存储并开启新分区。所有 part 文件消费完毕后,冲刷最后一个分区,并写出metadata.lance,其中列出分区 ID 与索引参数。
配置环境变量
| 环境变量 | 默认值 | 说明 |
|---|---|---|
LANCE_FTS_NUM_SHARDS | 计算密集型 CPU 数量 | 并行 tokenizer worker 任务数。越大索引吞吐越高,但内存占用越多 |
LANCE_FTS_PARTITION_SIZE | 256(MiB) | worker 内存缓冲在溢出为 part 文件前的最大未压缩大小 |
LANCE_FTS_TARGET_SIZE | 4096(MiB) | 合并输出分区的目标未压缩大小。更少更大的分区利于查询性能 |
这三个变量在 builder.rs 中均有对应的LazyLock静态定义(如LANCE_FTS_NUM_SHARDS、LANCE_FTS_PARTITION_SIZE),构建器通过resolve_num_workers()与resolve_worker_memory_limit_bytes()将其解析为实际 worker 数与每 worker 内存上限。此外,仓库中还提供了更多进阶环境变量可供排查与调优,例如LANCE_FTS_WRITE_QUEUE_SIZE、LANCE_FTS_POSTING_BATCH_ROWS、查询侧LANCE_FTS_SEARCH_CHUNK(见 format.rs)、LANCE_FTS_POSTING_GROUP_MAX_TOKENS(见 prewarm.rs)以及LANCE_FTS_REUSE_PREPARED_SCORER(见 search.rs)。
内存与性能考量
内存占用主要由两个因素决定:
LANCE_FTS_NUM_SHARDS——每个 worker 持有独立的进程内缓冲。峰值内存约为NUM_SHARDS * PARTITION_SIZE,再加上 token 字典与倒排列表结构的开销。LANCE_FTS_PARTITION_SIZE——取值越大,part 文件越少,合并阶段代价越低;取值越小,单 worker 内存越低,但会产出更多 part 文件。
合并阶段的内存被流式方案所约束:part 文件逐个加载,仅带少量并发缓冲;合并分区的进程内大小受LANCE_FTS_TARGET_SIZE限制。
构建 FTS 索引还需要临时磁盘空间来存放 token 化阶段生成的 part 文件。临时空间大小高度依赖是否启用位置信息:
with_position: true时,每个 token 在每篇文档中的每次出现都要记录位置,临时磁盘空间很容易达到原列体积的 10 倍以上;- 不带位置的索引通常比原列更小,总磁盘空间一般不超过原列体积的 2 倍。
性能建议:
LANCE_FTS_TARGET_SIZE越大,输出分区越少,对查询越有利(因为查询必须扫描每个分区的 token 字典)。内存允许时,优先选择更少、更大的分区。with_position: true会为每一次出现存储词项位置,显著增大索引体积,仅在需要短语查询时开启。- ngram 分词器相比词级分词器为每篇文档生成多得多的 token,索引体积与内存占用都会明显增大。
分布式训练
FTS 索引支持分布式训练:不同 worker 节点各自索引数据的一个子集,之后由协调节点汇总结果。这一流程与 builder.rs 中InvertedIndexBuilder::new_with_fragment_mask的实现一一对应:
- 每个分布式 worker 被分配一个fragment mask(
(fragment_id as u64) << 32),并 OR 进它生成的分区 ID 中,从而保证跨 worker 的分区 ID 全局唯一。 - worker 设置
skip_merge: true,直接写出各自的 part 文件,不执行合并阶段。 - 不再写出单一的
metadata.lance,每个 worker 改而写出按分区命名的元数据文件part_<id>_metadata.lance。 - 所有 worker 完成后,协调节点合并元数据文件:收集全部分区 ID,将它们重映射为从 0 开始的连续序列(同时重命名对应的数据文件),并写出最终统一的
metadata.lance。
这种设计让每个 worker 在 token 化阶段完全独立工作;只有最后的元数据合并需要单节点步骤,而它只是重命名文件与写一个小元数据文件,非常轻量。相关函数包括write_part_metadata、part_metadata_file_path与元数据合并逻辑(均在 builder.rs 中),对应的测试覆盖可见 rust/lance-index/src/scalar/inverted/index/tests/format_and_builder.rs(其中构造了fragment_mask = 7_u64 << 32及skip_merge: true的用例)。
加速查询 API 与查询类型
Lance SDK 提供了专用的全文搜索 API 来发挥 FTS 索引能力,支持远超简单 token 匹配的复杂查询类型。Python 端入口为 python/python/lance/dataset.py 中ScannerBuilder.full_text_search(query, columns=None):传入字符串时执行 match 查询;传入FullTextQuery对象时可表达所有下述复杂查询(且此时忽略columns参数)。查询 JSON 的解析逻辑(match/phrase/boost/multi_match/boolean分派)位于 rust/lance-index/src/scalar/inverted/parser.rs。
| 查询类型 | 说明 | 示例用法 | 结果类型 |
|---|---|---|---|
| contains_tokens | 基于 token 的基础搜索(UDF),使用 BM25 打分并自动排序结果 | SQL:contains_tokens(column, 'search terms') | AtMost |
| match | 可配置 AND/OR 运算符与相关性打分的匹配查询 | {"match": {"query": "text", "operator": "and/or"}} | AtMost |
| phrase | 基于位置信息的精确短语匹配(要求with_position: true) | {"phrase": {"query": "exact phrase"}} | AtMost |
| boolean | 含 must/should/must_not 子句的复杂布尔查询 | {"boolean": {"must": [...], "should": [...]}} | AtMost |
| multi_match | 跨多字段统一打分的搜索 | {"multi_match": [{"field1": "query"}, ...]} | AtMost |
| boost | 按可配置因子提升特定词项或查询的相关性得分 | {"boost": {"query": {...}, "factor": 2.0}} | AtMost |
索引创建与运维提示
- 在执行搜索前,必须先在目标列上创建倒排索引。Python 侧通过
Dataset.create_index(column, index_type="INVERTED", with_position=..., replace=True)完成(python/python/lance/dataset.py 第 4247 行起),with_position参数对应上文InvertedIndexParams.with_position,开启后支持短语查询。 - 若开启短语查询并希望避免冷启动时的位置数据延迟加载,可使用
prewarm_index(name, with_position=True)在预热阶段一并把位置数据载入索引缓存(见 python/python/lance/dataset.py 第 4681 行起的实现)。 - 查询前可先通过索引元数据查看分区数:分区数越少(受
LANCE_FTS_TARGET_SIZE控制),每次查询需要做的 token 字典查找与倒排列表扫描越少。
小结
Lance 的 FTS 索引通过tokens.lance、docs.lance、invert.lance、metadata.lance四类文件实现了完整的倒排存储:词项字典、文档统计(含total_tokens元数据校验)、压缩倒排列表(含posting_block_size兼容策略)与 JSON 参数(InvertedIndexParams)分层清晰。分词层面覆盖通用(simple/whitespace/raw)、子串(ngram)、多语言(ICU)、中文(Jieba)与日文(Lindera),并支持 18 种语言的词干与停用词处理。训练侧的两阶段管线以LANCE_FTS_NUM_SHARDS、LANCE_FTS_PARTITION_SIZE、LANCE_FTS_TARGET_SIZE三个旋钮调节吞吐、内存与查询性能,fragment mask 与 per-partition metadata 机制则让大规模分布式建索引成为可能。最后,match、phrase、boolean、multi_match、boost等查询类型为上层应用提供了从简单关键词到复杂布尔检索的完整能力矩阵。更多标量索引(如 ngram、btree、zonemap、bloom_filter 等)可继续阅读 docs/src/format/index/scalar 目录下的对应文档。
【免费下载链接】lanceOpen Lakehouse Format for Multimodal AI. Convert from Parquet in 2 lines of code for 100x faster random access, vector index, and data versioning. Compatible with Pandas, DuckDB, Polars, Pyarrow, and PyTorch with more integrations coming..项目地址: https://gitcode.com/GitHub_Trending/la/lance
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考