索引嵌套文档

Solr 支持索引嵌套文档,此处描述了这些文档,以及非常高效的搜索和检索方法。

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

包含所有子项的“最顶层”父项称为“根”文档(或以前的“块文档”),它解释了相关功能的一些命名法。

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

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

重新索引注意事项

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

在此页面上的示例中,始终会提供子文档的 ID。但是,您无需生成此类 ID;您可以让 Solr 自动填充它们。它将父级的 ID 与一个分隔符和应唯一的路径信息连接起来。亲自尝试一下!

示例索引语法:伪字段

本示例展示了如何索引两个根“产品”文档,每个文档包含两种不同类型的子文档,这些子文档在“伪字段”中指定:“skus”和“manuals”。其中两个 “sku” 类型文档拥有自己的嵌套子 “manuals” 文档……

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

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

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

<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>