索引嵌套文档
Solr 支持对嵌套文档进行索引,此处进行了描述,并且提供了非常高效地搜索和检索嵌套文档的方法。
通过示例:Solr 中的嵌套文档可用于将博客文章(父文档)与评论(子文档)绑定——或者作为将主要产品线建模为父文档的方式,其中多种类型的子文档表示单个 SKU(具有独特的尺寸/颜色)和支持文档(直接嵌套在产品下或在单个 SKU 下)。
具有所有子项的“最顶层”父项称为“根”文档或以前的“块文档”,它解释了一些相关功能的术语。
在查询时间,块连接查询解析器可以搜索这些关系,而[child
]文档转换器可以将子文档(或其他“后代”文档)附加到结果文档。在性能方面,编制文档之间关系的索引通常比等效的“查询时间连接”产生更快的查询,因为关系已经存储在索引中,无需计算。
但是,嵌套文档不如查询时间连接灵活,因为它施加了一些规则,有些应用程序可能无法接受。嵌套文档可以通过 XML 或 JSON 数据语法编制索引,并且还受带有 javabin 的SolrJ支持。
重新编制索引的注意事项
除了就地更新外,如果对嵌套文档树进行更新,Solr 必须在内部重新编制整个嵌套文档树的索引。对于某些应用程序,这可能会导致大量额外的索引开销,这可能不值得在查询时间获得的性能提升,而其他建模方法则值得。 |
在本页的示例中,始终提供子文档的 ID。但是,您不必生成此类 ID;您可以让 Solr 自动填充它们。它将以分隔符和应唯一的路径信息连接其父项的 ID。自己尝试一下!
示例索引语法:伪字段
此示例显示了索引两个根“产品”文档的情况,每个文档包含两种不同类型的子文档,这些子文档指定在“伪字段”中:“skus”和“manuals”。两种“sku”类型的文档都有自己的嵌套子“manuals”文档…
即使这些示例中的子文档在语法上作为字段值提供,但这仅仅是语法问题,因此 |
JSON
[{ "id": "P11!prod",
"name_s": "Swingline Stapler",
"description_t": "The Cadillac of office staplers ...",
"skus": [ { "id": "P11!S21",
"color_s": "RED",
"price_i": 42,
"manuals": [ { "id": "P11!D41",
"name_s": "Red Swingline Brochure",
"pages_i":1,
"content_t": "..."
} ]
},
{ "id": "P11!S31",
"color_s": "BLACK",
"price_i": 3
} ],
"manuals": [ { "id": "P11!D51",
"name_s": "Quick Reference Guide",
"pages_i":1,
"content_t": "How to use your stapler ..."
},
{ "id": "P11!D61",
"name_s": "Warranty Details",
"pages_i":42,
"content_t": "... lifetime guarantee ..."
} ]
},
{ "id": "P22!prod",
"name_s": "Mont Blanc Fountain Pen",
"description_t": "A Premium Writing Instrument ...",
"skus": [ { "id": "P22!S22",
"color_s": "RED",
"price_i": 89,
"manuals": [ { "id": "P22!D42",
"name_s": "Red Mont Blanc Brochure",
"pages_i":1,
"content_t": "..."
} ]
},
{ "id": "P22!S32",
"color_s": "BLACK",
"price_i": 67
} ],
"manuals": [ { "id": "P22!D52",
"name_s": "How To Use A Pen",
"pages_i":42,
"content_t": "Start by removing the cap ..."
} ]
} ]
|
XML
<add>
<doc>
<field name="id">P11!prod</field>
<field name="name_s">Swingline Stapler</field>
<field name="description_t">The Cadillac of office staplers ...</field>
<field name="skus">
<doc>
<field name="id">P11!S21</field>
<field name="color_s">RED</field>
<field name="price_i">42</field>
<field name="manuals">
<doc>
<field name="id">P11!D41</field>
<field name="name_s">Red Swingline Brochure</field>
<field name="pages_i">1</field>
<field name="content_t">...</field>
</doc>
</field>
</doc>
<doc>
<field name="id">P11!S31</field>
<field name="color_s">BLACK</field>
<field name="price_i">3</field>
</doc>
</field>
<field name="manuals">
<doc>
<field name="id">P11!D51</field>
<field name="name_s">Quick Reference Guide</field>
<field name="pages_i">1</field>
<field name="content_t">How to use your stapler ...</field>
</doc>
<doc>
<field name="id">P11!D61</field>
<field name="name_s">Warranty Details</field>
<field name="pages_i">42</field>
<field name="content_t">... lifetime guarantee ...</field>
</doc>
</field>
</doc>
<doc>
<field name="id">P22!prod</field>
<field name="name_s">Mont Blanc Fountain Pen</field>
<field name="description_t">A Premium Writing Instrument ...</field>
<field name="skus">
<doc>
<field name="id">P22!S22</field>
<field name="color_s">RED</field>
<field name="price_i">89</field>
<field name="manuals">
<doc>
<field name="id">P22!D42</field>
<field name="name_s">Red Mont Blanc Brochure</field>
<field name="pages_i">1</field>
<field name="content_t">...</field>
</doc>
</field>
</doc>
<doc>
<field name="id">P22!S32</field>
<field name="color_s">BLACK</field>
<field name="price_i">67</field>
</doc>
</field>
<field name="manuals">
<doc>
<field name="id">P22!D52</field>
<field name="name_s">How To Use A Pen</field>
<field name="pages_i">42</field>
<field name="content_t">Start by removing the cap ...</field>
</doc>
</field>
</doc>
</add>
SolrJ
try (SolrClient client = getSolrClient()) {
final SolrInputDocument p1 = new SolrInputDocument();
p1.setField("id", "P11!prod");
p1.setField("name_s", "Swingline Stapler");
p1.setField("description_t", "The Cadillac of office staplers ...");
{
final SolrInputDocument s1 = new SolrInputDocument();
s1.setField("id", "P11!S21");
s1.setField("color_s", "RED");
s1.setField("price_i", 42);
{
final SolrInputDocument m1 = new SolrInputDocument();
m1.setField("id", "P11!D41");
m1.setField("name_s", "Red Swingline Brochure");
m1.setField("pages_i", 1);
m1.setField("content_t", "...");
s1.setField("manuals", m1);
}
final SolrInputDocument s2 = new SolrInputDocument();
s2.setField("id", "P11!S31");
s2.setField("color_s", "BLACK");
s2.setField("price_i", 3);
p1.setField("skus", Arrays.asList(s1, s2));
}
{
final SolrInputDocument m1 = new SolrInputDocument();
m1.setField("id", "P11!D51");
m1.setField("name_s", "Quick Reference Guide");
m1.setField("pages_i", 1);
m1.setField("content_t", "How to use your stapler ...");
final SolrInputDocument m2 = new SolrInputDocument();
m2.setField("id", "P11!D61");
m2.setField("name_s", "Warranty Details");
m2.setField("pages_i", 42);
m2.setField("content_t", "... lifetime guarantee ...");
p1.setField("manuals", Arrays.asList(m1, m2));
}
final SolrInputDocument p2 = new SolrInputDocument();
p2.setField("id", "P22!prod");
p2.setField("name_s", "Mont Blanc Fountain Pen");
p2.setField("description_t", "A Premium Writing Instrument ...");
{
final SolrInputDocument s1 = new SolrInputDocument();
s1.setField("id", "P22!S22");
s1.setField("color_s", "RED");
s1.setField("price_i", 89);
{
final SolrInputDocument m1 = new SolrInputDocument();
m1.setField("id", "P22!D42");
m1.setField("name_s", "Red Mont Blanc Brochure");
m1.setField("pages_i", 1);
m1.setField("content_t", "...");
s1.setField("manuals", m1);
}
final SolrInputDocument s2 = new SolrInputDocument();
s2.setField("id", "P22!S32");
s2.setField("color_s", "BLACK");
s2.setField("price_i", 67);
p2.setField("skus", Arrays.asList(s1, s2));
}
{
final SolrInputDocument m1 = new SolrInputDocument();
m1.setField("id", "P22!D52");
m1.setField("name_s", "How To Use A Pen");
m1.setField("pages_i", 42);
m1.setField("content_t", "Start by removing the cap ...");
p2.setField("manuals", m1);
}
client.add(Arrays.asList(p1, p2));
模式配置
索引嵌套文档需要一个名为_root_
的索引字段
<field name="_root_" type="string" indexed="true" stored="false" docValues="false" />
不要将此字段添加到已包含数据的索引中!您必须重新索引。
-
Solr 会自动使用根文档的
id
值填充 所有 文档中的此字段,即其最高祖先(可能是它本身)。 -
此字段必须编入索引 (
indexed="true"
),但不需要存储 (stored="true"
) 或使用文档值 (docValues="true"
),但是如果您觉得有用,可以随意这样做。如果您想使用uniqueBlock(_root_)
字段类型限制,则应启用文档值。
最好,您还将定义 _nest_path_
,它添加了功能和易用性
<fieldType name="_nest_path_" class="solr.NestPathField" />
<field name="_nest_path_" type="_nest_path_" />`
-
Solr 会自动为任何子文档填充此字段,但不会为根文档填充。
-
此字段使 Solr 能够在使用
[child
] 文档转换器时正确记录和重建文档的命名和嵌套关系。-
如果此字段不存在,
[child]
转换器将返回所有后代子文档作为扁平列表,就像它们已被 编入匿名子文档索引 一样。
-
-
如果您不使用
_nest_path_
,强烈建议每个文档都有一些字段来区分根文档与其嵌套子文档,并区分不同“类型”的子文档。这不是严格必需的,只要可以编写一个“筛选”查询,该查询可用于隔离和仅选择父文档以用于 块连接查询解析器 和[child
] 文档转换器 -
可以在此字段上进行查询,尽管目前仅记录了如何在
[child] 的 `childFilter` 参数
的上下文中进行查询。
您可能希望选择性地定义 _nest_parent_
来存储父 ID
<field name="_nest_parent_" type="string" indexed="true" stored="true" />
-
Solr 会自动在子文档中填充此字段,但不会在根文档中填充。
最后,了解嵌套子文档本身就是文档,即使某些嵌套文档包含与父文档或其他子文档不同的信息,因此
-
架构中的所有字段名称只能在一种方式中配置,不同类型的子文档不能以不同的方式配置相同的字段名称。
-
对于所有类型的文档都不需要的任何字段名称,使用
required
可能是不可行的。 -
即使是子文档也需要一个全局唯一的
id
。
在使用 SolrCloud 时,最好使用 基于前缀的 compositeId,其中嵌套文档树中的所有文档都使用一个公共前缀。这使得对各个子文档应用 原子更新 变得更加容易。 |
通过更新和删除维护完整性
嵌套文档树可通过 xref:partial-document-updates.adoc#atomic-updates,原子更新>> 进行修改,以操作嵌套树中的任何文档,甚至添加新的子文档。此方面与更新任何普通文档没有什么不同——Solr 会在内部删除旧的嵌套文档树,并添加新修改的文档树。如果部分更新针对子文档,只需注意添加 root
字段,以便 Solr 知道它与哪个根文档相关。
Solr 要求集合中所有文档的 id
都是唯一的。Solr 会在分片内对根文档强制执行此要求,但不会对子文档强制执行,以避免检查开销。客户端应非常小心,切勿违反此要求。
要删除整个嵌套文档树,只需使用根文档的 id
执行按 ID 删除操作即可。按 ID 删除操作不适用于子文档的 id
,因为只有根文档 ID 才被考虑在内。相反,使用按查询删除(效率最高)或 原子更新 从其父级中删除子文档。
如果您使用 Solr 的按查询删除 API,则必须小心确保任何删除查询都经过构建,以确保不会保留任何要删除文档的后代子文档。否则,将违反 Solr 预期的完整性假设。
索引匿名子文档
虽然不推荐,但也可以“匿名”索引子文档
JSON
[{ "id": "P11!prod",
"name_s": "Swingline Stapler",
"type_s": "PRODUCT",
"description_t": "The Cadillac of office staplers ...",
"_childDocuments_": [
{ "id": "P11!S21",
"type_s": "SKU",
"color_s": "RED",
"price_i": 42,
"_childDocuments_": [
{ "id": "P11!D41",
"type_s": "MANUAL",
"name_s": "Red Swingline Brochure",
"pages_i":1,
"content_t": "..."
} ]
},
{ "id": "P11!S31",
"type_s": "SKU",
"color_s": "BLACK",
"price_i": 3
},
{ "id": "P11!D51",
"type_s": "MANUAL",
"name_s": "Quick Reference Guide",
"pages_i":1,
"content_t": "How to use your stapler ..."
},
{ "id": "P11!D61",
"type_s": "MANUAL",
"name_s": "Warranty Details",
"pages_i":42,
"content_t": "... lifetime guarantee ..."
}
]
} ]
XML
<add>
<doc>
<field name="id">P11!prod</field>
<field name="type_s">PRODUCT</field>
<field name="name_s">Swingline Stapler</field>
<field name="description_t">The Cadillac of office staplers ...</field>
<doc>
<field name="id">P11!S21</field>
<field name="type_s">SKU</field>
<field name="color_s">RED</field>
<field name="price_i">42</field>
<doc>
<field name="id">P11!D41</field>
<field name="type_s">MANUAL</field>
<field name="name_s">Red Swingline Brochure</field>
<field name="pages_i">1</field>
<field name="content_t">...</field>
</doc>
</doc>
<doc>
<field name="id">P11!S31</field>
<field name="type_s">SKU</field>
<field name="color_s">BLACK</field>
<field name="price_i">3</field>
</doc>
<doc>
<field name="id">P11!D51</field>
<field name="type_s">MANUAL</field>
<field name="name_s">Quick Reference Guide</field>
<field name="pages_i">1</field>
<field name="content_t">How to use your stapler ...</field>
</doc>
<doc>
<field name="id">P11!D61</field>
<field name="type_s">MANUAL</field>
<field name="name_s">Warranty Details</field>
<field name="pages_i">42</field>
<field name="content_t">... lifetime guarantee ...</field>
</doc>
</doc>
</add>
SolrJ
try (SolrClient client = getSolrClient()) {
final SolrInputDocument p1 = new SolrInputDocument();
p1.setField("id", "P11!prod");
p1.setField("type_s", "PRODUCT");
p1.setField("name_s", "Swingline Stapler");
p1.setField("description_t", "The Cadillac of office staplers ...");
{
final SolrInputDocument s1 = new SolrInputDocument();
s1.setField("id", "P11!S21");
s1.setField("type_s", "SKU");
s1.setField("color_s", "RED");
s1.setField("price_i", 42);
{
final SolrInputDocument m1 = new SolrInputDocument();
m1.setField("id", "P11!D41");
m1.setField("type_s", "MANUAL");
m1.setField("name_s", "Red Swingline Brochure");
m1.setField("pages_i", 1);
m1.setField("content_t", "...");
s1.addChildDocument(m1);
}
final SolrInputDocument s2 = new SolrInputDocument();
s2.setField("id", "P11!S31");
s2.setField("type_s", "SKU");
s2.setField("color_s", "BLACK");
s2.setField("price_i", 3);
final SolrInputDocument m1 = new SolrInputDocument();
m1.setField("id", "P11!D51");
m1.setField("type_s", "MANUAL");
m1.setField("name_s", "Quick Reference Guide");
m1.setField("pages_i", 1);
m1.setField("content_t", "How to use your stapler ...");
final SolrInputDocument m2 = new SolrInputDocument();
m2.setField("id", "P11!D61");
m2.setField("type_s", "MANUAL");
m2.setField("name_s", "Warranty Details");
m2.setField("pages_i", 42);
m2.setField("content_t", "... lifetime guarantee ...");
p1.addChildDocuments(Arrays.asList(s1, s2, m1, m2));
}
client.add(p1);
这种简化方法在 Solr 的较早版本中很常见,并且仍然可以与“仅根”模式一起使用,该模式不包含除 _root_
之外的任何其他嵌套相关字段。许多现有模式都是这样,仅仅是因为默认配置集是这样,即使应用程序不使用嵌套文档也是如此。
当模式包含 _nest_path_
字段时,不应使用此方法,因为该字段的存在会触发各种查询时间功能中的假设和行为更改,例如 [child
,当嵌套文档没有任何固有“嵌套路径”信息时,此功能将不起作用。
使用“仅根”架构对匿名嵌套子项进行索引的结果类似于使用“仅根”架构尝试对“伪字段”嵌套文档进行索引时发生的情况。值得注意的是:由于没有嵌套路径信息供 [child
] 转换器用于重建文档嵌套结构,因此它将所有匹配的子项作为平面列表返回,其结构类似于它们最初被编入索引的方式
JSON
$ curl --globoff 'http://localhost:8983/solr/gettingstarted/select?omitHeader=true&q=id:P11!prod&fl=*,[child%20parentFilter=%22type_s:PRODUCT%22]'
{
"response":{"numFound":1,"start":0,"maxScore":0.7002023,"numFoundExact":true,"docs":[
{
"id":"P11!prod",
"name_s":"Swingline Stapler",
"type_s":"PRODUCT",
"description_t":"The Cadillac of office staplers ...",
"_version_":1673055562829398016,
"_childDocuments_":[
{
"id":"P11!D41",
"type_s":"MANUAL",
"name_s":"Red Swingline Brochure",
"pages_i":1,
"content_t":"...",
"_version_":1673055562829398016},
{
"id":"P11!S21",
"type_s":"SKU",
"color_s":"RED",
"price_i":42,
"_version_":1673055562829398016},
{
"id":"P11!S31",
"type_s":"SKU",
"color_s":"BLACK",
"price_i":3,
"_version_":1673055562829398016},
{
"id":"P11!D51",
"type_s":"MANUAL",
"name_s":"Quick Reference Guide",
"pages_i":1,
"content_t":"How to use your stapler ...",
"_version_":1673055562829398016},
{
"id":"P11!D61",
"type_s":"MANUAL",
"name_s":"Warranty Details",
"pages_i":42,
"content_t":"... lifetime guarantee ...",
"_version_":1673055562829398016}]}]
}}
XML
$ curl --globoff 'http://localhost:8983/solr/gettingstarted/select?omitHeader=true&q=id:P11!prod&fl=*,[child%20parentFilter=%22type_s:PRODUCT%22]&wt=xml'
<?xml version="1.0" encoding="UTF-8"?>
<response>
<result name="response" numFound="1" start="0" maxScore="0.7002023" numFoundExact="true">
<doc>
<str name="id">P11!prod</str>
<str name="name_s">Swingline Stapler</str>
<str name="type_s">PRODUCT</str>
<str name="description_t">The Cadillac of office staplers ...</str>
<long name="_version_">1673055562829398016</long>
<doc>
<str name="id">P11!D41</str>
<str name="type_s">MANUAL</str>
<str name="name_s">Red Swingline Brochure</str>
<int name="pages_i">1</int>
<str name="content_t">...</str>
<long name="_version_">1673055562829398016</long></doc>
<doc>
<str name="id">P11!S21</str>
<str name="type_s">SKU</str>
<str name="color_s">RED</str>
<int name="price_i">42</int>
<long name="_version_">1673055562829398016</long></doc>
<doc>
<str name="id">P11!S31</str>
<str name="type_s">SKU</str>
<str name="color_s">BLACK</str>
<int name="price_i">3</int>
<long name="_version_">1673055562829398016</long></doc>
<doc>
<str name="id">P11!D51</str>
<str name="type_s">MANUAL</str>
<str name="name_s">Quick Reference Guide</str>
<int name="pages_i">1</int>
<str name="content_t">How to use your stapler ...</str>
<long name="_version_">1673055562829398016</long></doc>
<doc>
<str name="id">P11!D61</str>
<str name="type_s">MANUAL</str>
<str name="name_s">Warranty Details</str>
<int name="pages_i">42</int>
<str name="content_t">... lifetime guarantee ...</str>
<long name="_version_">1673055562829398016</long></doc></doc>
</result>
</response>