
Tantivy 中 JSON 数组查询为什么会匹配到不该命中的文档【免费下载链接】tantivyTantivy is a full-text search engine library inspired by Apache Lucene and written in Rust项目地址: https://gitcode.com/GitHub_Trending/ta/tantivy当使用 tantivy 的 json 字段自 tantivy 0.17 起支持见 doc/src/json.md建索引时你可能会遇到一种现象文档里一个 JSON 数组的每个元素单独看都不满足查询条件但用AND组合这些字段后查询仍然命中了该文档。本文解释这个现象的成因、给出一个可运行的复现方法并列出 JSON 字段查询相关的已知限制。为什么命中数组元素被展平成同一路径的 term 集合tantivy 在索引 json 对象时会对 JSON 做 flatten展平把内容转成一组(json_path, value_type, value)三元组 term。官方文档用这样一个文档演示了展平结果doc/src/json.md{ user: { name: Paul Masurel, address: { city: Tokyo, country: Japan }, created_at: 2018-11-12T23:20:50.52Z } }它会产出这些 term(name, Text, Paul)(name, Text, Masurel)(address.city, Text, Tokyo)(address.country, Text, Japan)(created_at, Date, 15420648505)对数组展平同样按 json path 进行数组下标不会成为 path 的一部分。关键在于tantivy 中文档是一袋 termbag of terms不保留数组元素的边界。因此AND查询的各子句只要求各自在文档中找到匹配 term这些 term 可以来自同一个数组里的不同元素。doc/src/json.md 的 Arrays do not work like nested object 一节给出的官方例子是{ cart_id: 3234234 , cart: [ {product_type: sneakers, attributes: {color: white} }, {product_type: t-shirt, attributes: {color: red}}, ] }查询cart.product_type:sneakers AND cart.attributes.color:red文档的结论是这个查询会命中上面的文档Actually match the document above。第一个子句命中数组第一个元素第二个子句命中第二个元素AND对整袋 term 成立于是文档被召回——这正是不该命中的文档被匹配的典型成因。复现现象用一个最小的 cart 数组例子以下复现代码的 API 骨架取自仓库中可直接运行的 examples/json_field.rs文档内容取自 doc/src/json.md 的 cart 示例。两点改编需要说明文档示例中的cart_id在 schema 里没有对应字段复现代码中省略了它不影响现象整个 JSON 放在名为attributes的 json 字段下tantivy 的 json 字段索引整个对象使查询路径与官方示例的cart.product_type:...形式一致。前置条件在自己的 Rust 项目中添加 tantivy 依赖。本仓库的版本为 0.27见根目录 Cargo.toml可写为[dependencies] tantivy 0.27复现代码例如放入examples/json_array_repro.rs用cargo run --example json_array_repro运行use tantivy::collector::Count; use tantivy::query::QueryParser; use tantivy::schema::{Schema, STORED, TEXT}; use tantivy::{Index, IndexWriter, TantivyDocument}; fn main() - tantivy::Result() { let mut schema_builder Schema::builder(); let attributes schema_builder.add_json_field(attributes, STORED | TEXT); let schema schema_builder.build(); let index Index::create_in_ram(schema.clone()); let mut index_writer: IndexWriter index.writer(50_000_000)?; let doc TantivyDocument::parse_json( schema, r#{ attributes: { cart: [ {product_type: sneakers, attributes: {color: white}}, {product_type: t-shirt, attributes: {color: red}} ] } }#, )?; index_writer.add_document(doc)?; index_writer.commit()?; let reader index.reader()?; let searcher reader.searcher(); let query_parser QueryParser::for_index(index, vec![attributes]); let query query_parser.parse_query(cart.product_type:sneakers AND cart.attributes.color:red)?; let count_docs searcher.search(*query, Count)?; assert_eq!(count_docs, 1); Ok(()) }索引中只有这一篇文档。断言通过说明查询确实命中了它命中数为 1即复现了逐元素看都不满足、整体却被命中的现象。仓库自带的 examples/json_field.rs 还演示了 json 字段作为默认查询字段、以及cart.product_id:103这类单值路径查询的完整流程可作为对照参考。另一个成因查询端会把一个字面量展开成多种类型即使没有数组JSON 查询也可能命中类型不符合预期的文档。doc/src/json.md 说明json 几乎不携带字面量的类型信息所有数字最终都映射为 Number日期也没有类型。索引端数字按u64、i64、f64的优先级依次尝试字符串先尝试按 RFC 3339 日期解释、再按普通字符串处理第一个解释成功的类型胜出且这种推断是按单篇文档进行的不会在 segment 层面推断一致的字段类型。查询端解析器无法知道类型一个查询字面量可能展开成多个类型。例如查询my_path.my_segment:233会被解释为(my_path.my_segment, String, 233) or (my_path.my_segment, u64, 233)如果查询里是 RFC 3339 日期同样可能发出两个 term因为该日期在入库时也可能只是文本里的一个 token。也就是说数字查询可能命中把该值存成字符串的文档反之亦然。这是第二个独立的匹配到不该命中文档的来源。限制与排查判断与本文场景直接相关的文档限制JSON 字段不支持范围查询doc/src/json.mdRange queries are not supported。数组没有 nested 语义官方文档的标题就是 Arrays do not work like nested object数组元素不会作为独立作用域参与查询当前文档也没有提供按数组元素限定查询范围的模式。类型推断是 per-document 的跨文档的字段类型不一致不会在索引时被发现。遇到不该命中的文档被匹配时可以按上面两个机制对照判断查询各子句的 term 分别来自同一 JSON 数组的不同元素——属于展平后的 bag of terms 行为命中 term 与查询字面量的类型不一致如数字查询命中字符串值——属于查询端类型展开行为。两者都是 doc/src/json.md 明确记录的设计行为而非 bug如果需要排除这类命中当前文档没有给出内置的替代方案只能通过调整 schema 设计例如把数组元素拆成明确定义的字段使类型和结构固定来规避这一行为的前提但这已超出 JSON 字段文档覆盖的范围。【免费下载链接】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),仅供参考