索引嵌套文档

Solr 支持对嵌套文档进行索引,此处进行了描述,并且提供了非常高效地搜索和检索嵌套文档的方法。

通过示例:Solr 中的嵌套文档可用于将博客文章(父文档)与评论(子文档)绑定——或者作为将主要产品线建模为父文档的方式,其中多种类型的子文档表示单个 SKU(具有独特的尺寸/颜色)和支持文档(直接嵌套在产品下或在单个 SKU 下)。

具有所有子项的“最顶层”父项称为“根”文档或以前的“块文档”,它解释了一些相关功能的术语。

在查询时间,块连接查询解析器可以搜索这些关系,而[child]文档转换器可以将子文档(或其他“后代”文档)附加到结果文档。在性能方面,编制文档之间关系的索引通常比等效的“查询时间连接”产生更快的查询,因为关系已经存储在索引中,无需计算。

但是,嵌套文档不如查询时间连接灵活,因为它施加了一些规则,有些应用程序可能无法接受。嵌套文档可以通过 XML 或 JSON 数据语法编制索引,并且还受带有 javabin 的SolrJ支持。

重新编制索引的注意事项

除了就地更新外,如果对嵌套文档树进行更新,Solr 必须在内部重新编制整个嵌套文档树的索引。对于某些应用程序,这可能会导致大量额外的索引开销,这可能不值得在查询时间获得的性能提升,而其他建模方法则值得。

在本页的示例中,始终提供子文档的 ID。但是,您不必生成此类 ID;您可以让 Solr 自动填充它们。它将以分隔符和应唯一的路径信息连接其父项的 ID。自己尝试一下!

示例索引语法:伪字段

此示例显示了索引两个根“产品”文档的情况,每个文档包含两种不同类型的子文档,这些子文档指定在“伪字段”中:“skus”和“manuals”。两种“sku”类型的文档都有自己的嵌套子“manuals”文档…​

即使这些示例中的子文档在语法上作为字段值提供,但这仅仅是语法问题,因此skusmanuals不是文档中的实际字段。因此,这些字段名称不必在模式中定义,而且可能不应该定义,因为这会令人困惑。没有“子文档”字段类型。

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 ..."
                } ]
 } ]

/update/json/docs便捷路径默认情况下会自动展平复杂的 JSON 文档——因此,要索引嵌套 JSON 文档,请务必使用/update

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>