索引嵌套文档
Solr 支持索引嵌套文档,此处描述了这些文档,以及非常高效的搜索和检索方法。
通过示例说明:Solr 中的嵌套文档可用于将博客文章(父文档)与评论(子文档)绑定在一起,或者作为一种将主要产品线建模为父文档的方式,其中多种类型的子文档表示单独的 SKU(具有唯一的尺寸/颜色)和支持文档(直接嵌套在产品下,或在单个 SKU 下)。
包含所有子项的“最顶层”父项称为“根”文档(或以前的“块文档”),它解释了相关功能的一些命名法。
在查询时,块连接查询解析器可以搜索这些关系,而[child
] 文档转换器可以将子文档(或其他“后代”)附加到结果文档。就性能而言,索引文档之间的关系通常比等效的“查询时连接”产生更快的查询速度,因为这些关系已经存储在索引中,而无需计算。
但是,嵌套文档不如查询时连接灵活,因为它强加了一些应用程序可能无法接受的规则。嵌套文档可以通过 XML 或 JSON 数据语法进行索引,并且SolrJ也支持使用 javabin。
重新索引注意事项
除了原地更新之外,如果嵌套文档树有更新,Solr 必须在内部重新索引整个嵌套文档树。对于某些应用程序来说,这可能会导致大量的额外索引开销,相对于其他建模方法而言,可能不值得在查询时获得的性能提升。 |
在此页面上的示例中,始终会提供子文档的 ID。但是,您无需生成此类 ID;您可以让 Solr 自动填充它们。它将父级的 ID 与一个分隔符和应唯一的路径信息连接起来。亲自尝试一下!
示例索引语法:伪字段
本示例展示了如何索引两个根“产品”文档,每个文档包含两种不同类型的子文档,这些子文档在“伪字段”中指定:“skus”和“manuals”。其中两个 “sku” 类型文档拥有自己的嵌套子 “manuals” 文档……
即使这些示例中的子文档在语法上作为字段值提供,但这仅仅是语法上的问题。因此, |
-
JSON
-
XML
-
SolrJ
[{ "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 ..."
} ]
} ]
|
<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>
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"
) 或使用 doc values (docValues="true"
),当然,如果您觉得这样做有用,也可以这样做。如果要使用uniqueBlock(_root_)
字段类型限制,则应启用 docValues。
最好,您也定义 _nest_path_
,这会增加功能和易用性
<fieldType name="_nest_path_" class="solr.NestPathField" />
<field name="_nest_path_" type="_nest_path_" />`
-
Solr 会自动为任何子文档填充此字段,但不会为根文档填充此字段。
-
当使用
[child
] 文档转换器时,此字段使 Solr 能够正确记录并重建文档的命名和嵌套关系。-
如果此字段不存在,
[child]
转换器将返回所有后代子文档作为扁平列表——就像它们被索引为匿名子文档一样。
-
-
如果您不使用
_nest_path_
,强烈建议每个文档都应包含一些字段,以区分根文档和它们的嵌套子文档,并区分不同“类型”的子文档。这不是绝对必要的,只要可以编写一个“过滤器”查询,用于隔离和选择父文档,以在 Block Join Query Parser 和[child
] 文档转换器中使用即可。 -
可以在此字段上进行查询,尽管目前只有在
[child]
的childFilter
参数的上下文中记录了如何进行查询。
您可以选择定义 _nest_parent_
来存储父 ID
<field name="_nest_parent_" type="string" indexed="true" stored="true" />
-
Solr 会自动在子文档中填充此字段,但不会在根文档中填充此字段。
最后,请理解嵌套子文档本身也是文档,即使某些嵌套文档与父文档或其他子文档持有不同的信息,因此
-
模式中的所有字段名称只能配置一种方式——不同类型的子文档不能以不同的方式配置相同的字段名称。
-
对于并非所有文档类型都必需的任何字段名称,可能不宜使用
required
。 -
即使是子文档也需要一个全局唯一的
id
。
在使用 SolrCloud 时,强烈建议使用基于前缀的复合 ID,其中嵌套文档树中的所有文档都具有一个公共前缀。这使得更容易对单个子文档应用原子更新。 |
通过更新和删除维护完整性
可以使用原子更新来修改嵌套的文档树,从而操作嵌套树中的任何文档,甚至可以添加新的子文档。这方面与更新任何普通文档没有区别——Solr 内部会删除旧的嵌套文档树并添加新修改的文档树。请注意,如果部分更新是针对子文档的,请务必添加一个 root
字段,以便 Solr 知道它与哪个根文档相关。
Solr 要求集合中所有文档的 id
都是唯一的。Solr 对分片中的根文档强制执行此要求,但不对子文档强制执行,以避免检查的开销。客户端应非常小心,永远不要违反此要求。
要删除整个嵌套的文档树,您可以使用根文档的 id
进行简单的 ID 删除。ID 删除不适用于子文档的 id
,因为只考虑根文档 ID。相反,请使用查询删除(最有效)或原子更新来从其父文档中删除子文档。
如果您使用 Solr 的查询删除 API,则必须小心确保任何删除查询都经过结构化,以确保要删除的任何文档都没有剩余的后代子文档。否则将会违反 Solr 所期望的完整性假设。
索引匿名子文档
尽管不建议这样做,但也可以“匿名”地索引子文档
-
JSON
-
XML
-
SolrJ
[{ "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 ..."
}
]
} ]
<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>
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
-
XML
$ curl --globoff 'https://127.0.0.1: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}]}]
}}
$ curl --globoff 'https://127.0.0.1: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>