Tantivy 如何用 Exists 查询只检索包含指定字段或 JSON 动态列的文档?
【免费下载链接】tantivyTantivy is a full-text search engine library inspired by Apache Lucene and written in Rust项目地址: https://gitcode.com/GitHub_Trending/ta/tantivy
当索引里存在大量可选字段时(例如只有部分用户有email,或者 schema-less 的 JSON 字段里各文档的动态列不一样),常见的诉求是:只检索出"该字段有值"的那批文档,而不是匹配所有文档。Tantivy 提供ExistsQuery来完成这件事:它匹配所有在指定字段上至少有一个非 null 值的文档,命中文档的 score 统一为 1.0。在 query parser 中还有对应的语法field:*。
Tantivy 运行在 stable Rust 上,支持 Linux、macOS 与 Windows(见 README)。本文基于仓库中 src/query/exist_query.rs 的实现与测试、query parser 文档 及其测试来演示完整用法。
前提:字段必须配置为 FAST
ExistsQuery依赖 fast field 的列索引来判断"值是否存在",因此有两个硬性约束(来自 src/query/exist_query.rs 中Weight::weight的实现):
- 字段必须在 schema 中存在,否则搜索时报错
The field does not exist: '{field}'; - 字段必须配置了
FAST,否则报Schema error: 'Field xxx is not a fast field.'。
构造器ExistsQuery::new(field, json_subpaths)本身不会失败,错误在真正执行搜索时才返回。
schema 定义参考 examples/basic_search.rs 的风格,只保留 Exists 查询需要的部分:
use tantivy::schema::{Schema, FAST, TEXT}; let mut schema_builder = Schema::builder(); // 普通 fast field:u64 只需 FAST let fast = schema_builder.add_u64_field("fast", FAST); // JSON 字段:TEXT 提供倒排检索,FAST 让 Exists 查询可用 let json = schema_builder.add_json_field("json", TEXT | FAST); let schema = schema_builder.build();FAST标记支持的字段类型见test_exists_query_misc_supported_types测试:bool、bytes、date、f64、ip_addr、facet、u64、text、json均可使用 Exists 查询。
方式一:直接用 ExistsQuery API
下面这段代码与 src/query/exist_query.rs 中test_exists_query_simple测试的结构一致:100 个文档,偶数i写入even,奇数i写入odd,每 10 个写入multi(多值),never从未写入。
use tantivy::collector::Count; use tantivy::query::ExistsQuery; use tantivy::{doc, Index}; // index 为已创建并写入、commit 后的 Index let reader = index.reader()?; let searcher = reader.searcher(); let query = ExistsQuery::new("odd".to_string(), false); let count = searcher.search(&query, &Count)?; // 源测试中的预期结果:50 assert_eq!(count, 50);ExistsQuery::new的第二个参数json_subpaths只对 JSON 字段有意义(见下一节),普通字段传false即可。旧的new_exists_query构造器已被标记#[deprecated],新代码请使用new。
用Countcollector 得到的命中数就是验证方式:源测试中断言的命中数依次为all→ 100、odd→ 50、even→ 50、multi→ 10、never→ 0。
也可以把 Exists 查询作为过滤条件与其他查询组合。源测试用BooleanQuery求交:RangeQuery(all >= 50) AND ExistsQuery("even")命中 25 个文档;RangeQuery(all 在 [0,50]) AND ExistsQuery("odd")同样命中 25 个:
use std::ops::Bound; use tantivy::query::{BooleanQuery, ExistsQuery, RangeQuery}; use tantivy::Term; let query = BooleanQuery::intersection(vec![ Box::new(RangeQuery::new( Bound::Included(Term::from_field_u64(all_field, 50)), Bound::Unbounded, )), Box::new(ExistsQuery::new("even".to_string(), false)), ]);由于所有命中文档 score 都是 1.0,用TopDocs排序时命中顺序不受 Exists 查询本身影响,实际排序由组合查询中的其他部分决定。
方式二:query parser 的field:*语法
不想手写ExistsQuery时,可以直接在解析式查询里用field:*。query parser 文档 对它的说明是:
exists query:
field:*will match documents that contain a non-null value in the specified field. The field must be configured as a fast field. For JSON fields, values in subpaths also count as existing.
注意 parser 生成的等价查询是ExistsQuery::new(field, true)——即field:*对 JSON 字段固定启用json_subpaths,而 API 方式可以自己控制这个开关。
test_exists_query_with_documents测试给出了一个可直接对照的验证场景。写入 4 个文档:
index_writer.add_document(doc!( fast => 1u64, json => json!({"other": 1u64}), not_fast => "present", ))?; index_writer.add_document(doc!(json => json!({"other": 2u64})))?; index_writer.add_document(doc!(json => json!({"nested": null})))?; index_writer.add_document(doc!())?; index_writer.commit()?;随后用QueryParser::for_index(&index, Vec::new())解析查询,各查询的命中数(均为源测试断言值):
| 查询 | 命中数 | 说明 |
|---|---|---|
fast:* | 1 | 只有文档 1 的fast有值 |
json:* | 2 | 子路径上的非 null 值算作存在;仅含{"nested": null}的文档和空文档不命中 |
json.nested:* | 1 | 命中显式写有nested键(值为 null)的文档 |
* NOT fast:* | 3 | Exists 查询可直接参与 NOT 组合,筛出"没有该字段"的文档 |
* NOT fast:*这一条值得注意:要检索"不包含某字段的文档"时,无需反向构造 Exists 查询,直接对全量*做 NOT 即可。
JSON 动态列的 json_subpaths 语义
ExistsQuery对 JSON 字段的行为由json_subpaths控制(见 src/query/exist_query.rs 顶部的文档注释):
json_subpaths = true:查询myfield或myfield.mysubfield都要求"任一子路径上有非 null 值"即命中,多个动态列的存在性取并集;json_subpaths = false:只查字段名本身的列,不看子路径。
test_exists_query_json测试(100 个文档,偶数含{"all": i, "even": true},奇数含{"all": i, "odd": true})给出的对照结果:
| 查询 | json_subpaths | 命中数 |
|---|---|---|
json.all | 任意 | 100 |
json.even | 任意 | 50 |
json | false | 0 |
json | true | 100 |
json.absent(文档中不存在的子路径) | 任意 | 0 |
两点边界:
- JSON 根字段名本身通常没有列,
ExistsQuery::new("json", false)命中 0 是预期行为,不是 bug——要看"任意子路径有值"必须传true。 - 不存在于 schema 的字段(如
does_not_exists.absent)无论json_subpaths取什么值都会返回FieldNotFound错误,而"存在但文档中没写过"的子路径(json.absent)返回 0 命中,两者结果不同:前者是错误,后者是空结果。
当没有任何一个子路径单独覆盖全部文档、但并集覆盖全部时(test_exists_query_json_union_no_single_full_subpath:偶数文档只有json.a,奇数文档只有json.b),ExistsQuery::new("json", true)命中 100 个,false命中 0。仓库里还有一个对应的性能基准 benches/exists_json.rs,它构造了"每个文档只写一个轮换子路径、并集覆盖全部文档"的 50 万文档索引,专门测量ExistsQuery::new("json".to_string(), true)的联合存在性检查;CHANGELOG.md记录了后续版本对大量动态列场景下ExistsQuery的优化(预计算位图)。
结果验证与错误判断
验证 Exists 查询是否按预期工作,直接用Countcollector 对比命中数即可(如前文各节)。当结果不符合预期时,按文档中实际会出现的错误信息判断原因:
The field does not exist: 'does_not_exists' Schema error: 'Field not_fast is not a fast field.'- 第一条:schema 里找不到该字段名,检查拼写;JSON 子路径的字段名格式是
json字段名.子路径; - 第二条:字段存在但没有
FAST标记,需要回到 schema 定义加上FAST后重建索引。
限制小结
- 只对 FAST 字段有效,纯
TEXT(无FAST)字段会报 Schema 错误; - 命中文档 score 固定 1.0,Exists 查询不能单独用于排序;
field:*语法在 parser 层固定json_subpaths = true,需要精确控制时请使用ExistsQuery::new直接构造;- 字段名不存在会报错,而 JSON 子路径"未写入"只是 0 命中,排查时要区分这两种情况。
想进一步了解 JSON 字段如何被 flatten 成(json_path, value_type, value)三元组的内部格式,可阅读 doc/src/json.md;JSON 字段不支持 range query,文档中也有说明。
【免费下载链接】tantivyTantivy is a full-text search engine library inspired by Apache Lucene and written in Rust项目地址: https://gitcode.com/GitHub_Trending/ta/tantivy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考