ネストされたドキュメントのインデックス作成

Solr は、ここに説明するようにネストされたドキュメントのインデックス作成と、それらを非常に効率的に検索および取得する方法をサポートしています。

例として、Solr のネストされたドキュメントは、ブログ投稿(親ドキュメント)をコメント(子ドキュメント)と紐付けるために使用できます。または、主要な製品ラインを親ドキュメントとしてモデル化し、個々の SKU(固有のサイズ/色を持つ)とサポートドキュメント(製品の下、または個々の SKU の下)を表す複数のタイプの子ドキュメントを使用することもできます。

すべての子を持つ「最上位」の親は、「ルート」ドキュメントまたは以前の「ブロックドキュメント」と呼ばれ、関連機能の命名法のいくつかを説明しています。

クエリ時には、Block Join クエリパーサーがこれらの関係を検索し、[child] ドキュメントトランスフォーマーが子(またはその他の「子孫」)ドキュメントを結果ドキュメントにアタッチできます。パフォーマンスの観点から、ドキュメント間の関係をインデックス作成すると、関係がすでにインデックスに格納されており、計算する必要がないため、同等の「クエリ時結合」よりも通常はるかに高速なクエリが得られます。

ただし、ネストされたドキュメントは、一部のアプリケーションでは受け入れられない可能性があるルールを課すため、クエリ時結合よりも柔軟性が低くなります。ネストされたドキュメントは、XML または JSON データ構文のいずれかを介してインデックスを作成でき、javabin を使用したSolrJでもサポートされています。

再インデックス作成の考慮事項

インプレース更新を除き、Solr は更新がある場合、内部的にネストされたドキュメントツリー全体を再インデックス作成する必要があります。一部のアプリケーションでは、これにより、他のモデリングアプローチと比較して、クエリ時のパフォーマンス向上に見合う価値がない、多くの余分なインデックス作成オーバーヘッドが発生する可能性があります。

このページの例では、子ドキュメントの ID が常に提供されています。ただし、そのような ID を生成する必要はありません。Solr に自動的に ID を入力させることができます。Solr は、親の ID を一意である必要がある区切り文字とパス情報で連結します。ぜひ試してみてください。

インデックス作成構文の例:疑似フィールド

この例では、2つのルート「product」ドキュメントをインデックス化する際の見た目を示しています。各ドキュメントには、「擬似フィールド」で指定された2つの異なるタイプのドキュメント(「skus」と「manuals」)が含まれています。「sku」タイプのドキュメントの2つには、独自のネストされた子「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_) フィールドタイプの制限を使用する場合は、docValuesを有効にする必要があります。

できれば、機能と使いやすさを向上させる_nest_path_も定義します。

<fieldType name="_nest_path_" class="solr.NestPathField" />
<field name="_nest_path_" type="_nest_path_" />`
  • Solrは、ルートドキュメントではなく、すべての子ドキュメントに対してこのフィールドを自動的に入力します。

  • このフィールドにより、[child]ドキュメントトランスフォーマーを使用する際に、ドキュメントの名前付きおよびネストされた関係をSolrが適切に記録および再構築できるようになります。

    • このフィールドが存在しない場合、[child]トランスフォーマーは、すべての下位の子ドキュメントをフラット化されたリストとして返します。これは、匿名の子としてインデックス化した場合と同じです。

  • _nest_path_を使用しない場合は、すべてのドキュメントに、ルートドキュメントとネストされた子を区別するフィールドと、異なるタイプのサブドキュメントを区別するフィールドを付けることを強くお勧めします。これは厳密には必須ではありませんが、ブロック結合クエリパーサー[child]ドキュメントトランスフォーマーで使用するために、親ドキュメントのみを分離して選択するために使用できる「フィルター」クエリを作成できる限りは問題ありません。

  • このフィールドに対してクエリを実行することは可能ですが、現時点では[child]の`childFilter`パラメーターのコンテキストでの使用方法のみがドキュメント化されています。

必要に応じて、親IDを保存するために_nest_parent_を定義することもできます。

<field name="_nest_parent_" type="string" indexed="true" stored="true" />
  • Solrは、ルートドキュメントではなく、子ドキュメントにこのフィールドを自動的に入力します。

最後に、ネストされた子ドキュメントは、特定のネストされたドキュメントが親ドキュメントや他の子ドキュメントとは異なる情報を持っている場合でも、それ自体がドキュメントであることを理解してください。

  • スキーマ内のすべてのフィールド名は1つの方法でのみ構成できます。異なるタイプの子ドキュメントで同じフィールド名を異なる方法で構成することはできません。

  • すべてのタイプのドキュメントで必須ではないフィールド名にrequiredを使用することは現実的ではない可能性があります。

  • 子ドキュメントでさえ、グローバルに一意のidが必要です。

SolrCloudを使用する場合、ネストされたドキュメントツリー内のすべてのドキュメントに共通のプレフィックスを使用して、プレフィックスベースの複合IDを使用することを強くお勧めします。これにより、個々の子ドキュメントにアトミック更新を適用することがはるかに簡単になります。

更新と削除による整合性の維持

ネストされたドキュメントツリーは、xref:partial-document-updates.adoc#atomic-updates,アトミック更新>>を使用して変更して、ネストされたツリー内の任意のドキュメントを操作したり、新しい子ドキュメントを追加したりできます。この点は、通常のドキュメントの更新と違いはありません。Solrは内部的に古いネストされたドキュメントツリーを削除し、新しく変更されたドキュメントツリーを追加します。部分更新が子ドキュメントに対するものである場合は、Solrがどのルートドキュメントに関連付けられているかを認識できるように、_root_フィールドを追加するように注意してください。

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 '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}]}]
  }}

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