部分文档更新

一旦您在 Solr 索引中索引了所需的内容,您将需要开始考虑处理这些文档更改的策略。Solr 支持三种部分更改文档的更新方法。

第一种是原子更新。此方法允许仅更改文档的一个或多个字段,而无需重新索引整个文档。

第二种方法称为就地更新。此方法类似于原子更新(在某种意义上是原子更新的子集),但仅可用于更新基于 docValue 的单值非索引和非存储数字字段。

第三种方法称为乐观并发乐观锁定。它是许多 NoSQL 数据库的一个特性,允许根据文档的版本有条件地更新文档。此方法包含关于如何处理版本匹配或不匹配的语义和规则。

原子更新(和就地更新)和乐观并发可用作管理文档更改的独立策略,或者它们可以组合使用:您可以使用乐观并发有条件地应用原子更新。

原子更新

Solr 支持多个修改器,以原子方式更新文档的值。这允许仅更新特定字段,这有助于在索引添加速度对应用程序至关重要的环境中加快索引过程。

要使用原子更新,请将修改器添加到需要更新的字段。可以更新、添加到内容,或者,如果该字段具有数值类型,则可以递增或递减。

set

使用指定的值设置或替换字段值,或者,如果将 'null' 或空列表指定为新值,则删除这些值。

可以指定为单个值,或指定为多值字段的列表。

add

将指定的值添加到多值字段。可以指定为单个值,或指定为列表。

add-distinct

将指定的值添加到多值字段,前提是该值尚未存在。可以指定为单个值,或指定为列表。

remove

从多值字段中删除(所有出现的)指定的值。可以指定为单个值,或指定为列表。

removeregex

从多值字段中删除指定正则表达式的所有出现次数。可以指定为单个值,或指定为列表。

inc

将数字字段的值增加或减少特定量,该量指定为单个整数或浮点数。正数增加字段值,负数减少字段值。

字段存储

原子更新文档的核心功能要求您模式中的所有字段都必须配置为已存储 (stored="true") 或 docValues (docValues="true"),但 <copyField/> 目标字段除外,这些目标字段必须配置为 stored="false",并且 docValues="false"useDocValuesAsStored="false"。原子更新应用于由现有存储字段值表示的文档。copyField 目标字段中的所有数据都必须仅来自 copyField 源。

如果 <copyField/> 目标配置为已存储,则 Solr 将尝试索引字段的当前值以及来自任何源字段的额外副本。如果此类字段包含来自索引程序的一些信息和来自 copyField 的一些信息,则当进行原子更新时,最初来自索引程序的信息将丢失。

还有其他类型的派生字段也必须设置为不存储,就像上面提到的 <copyField/> 目标一样。某些空间字段类型(例如 BBoxField 和 LatLonSpatialFieldType)使用派生字段。CurrencyFieldType 也使用派生字段。这些类型创建通常由动态字段定义指定的附加字段。该动态字段定义必须不存储,否则索引将失败。

更新文档部分内容的示例

如果我们的集合中存在以下文档

{"id":"mydoc",
 "price":10,
 "popularity":42,
 "categories":["kids"],
 "sub_categories":["under_5","under_10"],
 "promo_ids":["a123x"],
 "tags":["free_to_try","buy_now","clearance","on_sale"]
}

并且我们应用以下更新命令

{"id":"mydoc",
 "price":{"set":99},
 "popularity":{"inc":-7},
 "categories":{"add":["toys","games"]},
 "sub_categories":{"add-distinct":"under_10"},
 "promo_ids":{"remove":"a123x"},
 "tags":{"remove":["free_to_try","on_sale"]}
}

我们集合中生成的文档将是

{"id":"mydoc",
 "price":99,
 "popularity":35,
 "categories":["kids","toys","games"],
 "sub_categories":["under_5","under_10"],
 "tags":["buy_now","clearance"]
}

更新子文档

Solr 支持在原子更新中修改、添加和删除子文档。从语法上讲,更改文档子项的更新与简单字段的常规原子更新非常相似,如下面的示例所示。

更新子文档的架构和配置要求与上面提到的原子更新的字段存储要求相同。

在底层,Solr 对嵌套文档的行为在概念上与对非嵌套文档的行为类似,只不过它应用于整个嵌套文档树(从根开始),而不是独立的文档。因此,您可以预期会产生更多开销。就地更新可以避免这种情况。

在 SolrCloud 中使用子文档 ID 路由更新

当 SolrCloud 收到文档更新时,将使用集合的文档路由规则来基于文档的 id 确定哪个分片应处理更新。

当发送指定子文档id 的更新时,默认情况下这将不起作用:发送文档的正确分片是基于子文档所在的块的“根”文档的 id而不是正在更新的子文档的 id

Solr 提供了两种解决方案来解决此问题

  • 客户端可以在每次更新时指定一个 _route_ 参数,并将根文档的 id 作为参数值,以告知 Solr 哪个分片应处理更新。

  • 客户端可以在索引所有文档时使用(默认的)compositeId 路由器的“前缀路由”功能,以确保块中的所有子/后代文档都使用与根级别文档相同的 id 前缀。这将导致 Solr 的默认路由逻辑自动将子文档更新发送到正确的分片。

此外,您必须在此部分更新的 _root_ 字段中指定根文档的 ID。这是 Solr 了解您正在更新子文档而不是根文档的方式。

以下所有示例都使用 id 前缀,因此这些示例不需要 _route_ 参数。

对于接下来的示例,我们将假设索引包含 索引嵌套文档中涵盖的相同文档

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

修改子文档字段

以上提到的所有原子更新操作都支持子文档的“真实”字段

curl -X POST 'https://127.0.0.1:8983/solr/gettingstarted/update?commit=true' -H 'Content-Type: application/json' --data-binary '[
{
  "id": "P11!S31",
  "_root_": "P11!prod",
  "price_i": { "inc": 73 },
  "color_s": { "set": "GREY" }
} ]'

替换所有子文档

与普通的(多值)字段一样,可以使用 set 关键字来替换伪字段中的所有子文档

curl -X POST 'https://127.0.0.1:8983/solr/gettingstarted/update?commit=true' -H 'Content-Type: application/json' --data-binary '[
{
  "id": "P22!S22",
  "_root_": "P22!prod",
  "manuals": { "set": [ { "id": "P22!D77",
                          "name_s": "Why Red Pens Are the Best",
                          "content_t": "... correcting papers ...",
                        },
                        { "id": "P22!D88",
                          "name_s": "How to get Red ink stains out of fabric",
                          "content_t": "... vinegar ...",
                        } ] }

} ]'

添加子文档

与普通的(多值)字段一样,可以使用 add 关键字向伪字段添加其他子文档

curl -X POST 'https://127.0.0.1:8983/solr/gettingstarted/update?commit=true' -H 'Content-Type: application/json' --data-binary '[
{
  "id": "P11!S21",
  "_root_": "P11!prod",
  "manuals": { "add": { "id": "P11!D99",
                        "name_s": "Why Red Staplers Are the Best",
                        "content_t": "Once upon a time, Mike Judge ...",
                      } }
} ]'

请注意,这是添加或替换(按 ID)。这意味着,如果文档 P11!S21 恰好已经有一个 ID 为 P11!D99 的子文档(我们要添加的文档),那么它将被替换。

删除子文档

与普通的(多值)字段一样,可以使用 remove 关键字从其伪字段中删除子文档(按 id

curl -X POST 'https://127.0.0.1:8983/solr/gettingstarted/update?commit=true' -H 'Content-Type: application/json' --data-binary '[
{
  "id": "P11!S21",
  "_root_": "P11!prod",
  "manuals": { "remove": { "id": "P11!D41" } }
} ]'

就地更新

就地更新与原子更新非常相似;在某种意义上,这是原子更新的一个子集。在常规原子更新中,在应用更新期间,整个文档会在内部重新索引。但是,在这种方法中,仅影响要更新的字段,并且文档的其余部分不会在内部重新索引。因此,就地更新的效率不受更新的文档大小(即字段数、字段大小等)的影响。除了这些内部效率差异之外,原子更新和就地更新之间没有功能上的差异。

只有当要更新的字段满足以下三个条件时,才使用此就地方法执行原子更新操作

  • 是非索引 (indexed="false")、非存储 (stored="false")、单值 (multiValued="false") 的数值 docValues (docValues="true") 字段;

  • _version_ 字段也是一个非索引、非存储的单值 docValues 字段;并且,

  • 更新字段的副本目标(如果有)也是非索引、非存储的单值数值 docValues 字段。

要使用就地更新,请将修饰符添加到需要更新的字段。可以更新内容或增加/减少内容。

set

使用指定的值设置或替换字段值。可以指定为单个值。

inc

将数字字段的值增加或减少特定量,该量指定为单个整数或浮点数。正数增加字段值,负数减少字段值。

防止无法就地完成的原子更新

由于要确保满足所有必要条件以确保可以就地完成更新可能很棘手,因此 Solr 支持一个名为 update.partial.requireInPlace 的请求参数选项。设置为 true 时,无法就地完成的原子更新将失败。用户可以在他们希望更新请求如果无法就地完成则“快速失败”时指定此选项。

就地更新示例

如果价格和受欢迎程度字段在架构中定义为

<field name="price" type="float" indexed="false" stored="false" docValues="true"/>

<field name="popularity" type="float" indexed="false" stored="false" docValues="true"/>

启用 Doc Values

对于版本 >= 1.7 的架构,docValues="true" 是默认值,因此可以省略。

如果我们的集合中存在以下文档

{
 "id":"mydoc",
 "price":10,
 "popularity":42,
 "categories":["kids"],
 "promo_ids":["a123x"],
 "tags":["free_to_try","buy_now","clearance","on_sale"]
}

并且我们应用以下更新命令

{
 "id":"mydoc",
 "price":{"set":99},
 "popularity":{"inc":20}
}

我们集合中生成的文档将是

{
 "id":"mydoc",
 "price":99,
 "popularity":62,
 "categories":["kids"],
 "promo_ids":["a123x"],
 "tags":["free_to_try","buy_now","clearance","on_sale"]
}

乐观并发

乐观并发是 Solr 的一项功能,客户端应用程序可以使用该功能来更新/替换文档,以确保它们正在替换/更新的文档没有被另一个客户端应用程序同时修改。此功能的工作原理是在索引中的所有文档上都需要一个 _version_ 字段,并将该字段与作为更新命令一部分指定的 _version_ 进行比较。默认情况下,Solr 的架构包含 _version_ 字段,并且此字段会自动添加到每个新文档中。

通常,使用乐观并发涉及以下工作流程

  1. 客户端读取文档。在 Solr 中,可以使用 /get 处理程序检索文档,以确保具有最新版本。

  2. 客户端在本地更改文档。

  3. 客户端将更改后的文档重新提交给 Solr,例如,可能使用 /update 处理程序。

  4. 如果存在版本冲突(HTTP 错误代码 409),则客户端会重新开始此过程。

当客户端将更改后的文档重新提交给 Solr 时,可以在更新中包含 _version_ 以调用乐观并发控制。使用特定的语义来定义何时应更新文档或何时报告冲突。

  • 如果 _version_ 字段中的内容大于“1”(即“12345”),则文档中的 _version_ 必须与索引中的 _version_ 匹配。

  • 如果 _version_ 字段中的内容等于“1”,则文档必须存在。在这种情况下,不会发生版本匹配,但如果文档不存在,则更新将被拒绝。

  • 如果 _version_ 字段中的内容小于“0”(即“-1”),则文档必须存在。在这种情况下,不会发生版本匹配,但如果文档存在,则更新将被拒绝。

  • 如果 _version_ 字段中的内容等于“0”,则版本是否匹配或文档是否存在都无关紧要。如果它存在,它将被覆盖;如果它不存在,则将被添加。

当批量添加/更新文档时,即使一个版本冲突也可能导致拒绝整个批次。当一个或多个文档的版本约束失败时,请使用参数 failOnVersionConflicts=false 来避免整个批次的失败。

如果要更新的文档不包含 _version_ 字段,并且未使用原子更新,则 Solr 将按普通规则处理该文档,通常是放弃以前的版本。

使用乐观并发时,客户端可以包含一个可选的 versions=true 请求参数,以指示应将添加的文档的版本包含在响应中。这使客户端能够立即知道每个添加的文档的 _version_ 是什么,而无需进行冗余的 /get 请求

以下是一些在查询中使用 versions=true 的示例

$ curl -X POST -H 'Content-Type: application/json' 'https://127.0.0.1:8983/solr/techproducts/update?versions=true&omitHeader=true' --data-binary '
[ { "id" : "aaa" },
  { "id" : "bbb" } ]'
{
  "adds":[
    "aaa",1632740120218042368,
    "bbb",1632740120250548224]}

在此示例中,我们添加了 2 个文档“aaa”和“bbb”。因为我们将 versions=true 添加到请求中,所以响应显示了每个文档的文档版本。

$ curl -X POST -H 'Content-Type: application/json' 'https://127.0.0.1:8983/solr/techproducts/update?_version_=999999&versions=true&omitHeader=true' --data-binary '
  [{ "id" : "aaa",
     "foo_s" : "update attempt with wrong existing version" }]'
{
  "error":{
    "metadata":[
      "error-class","org.apache.solr.common.SolrException",
      "root-error-class","org.apache.solr.common.SolrException"],
    "msg":"version conflict for aaa expected=999999 actual=1632740120218042368",
    "code":409}}

在此示例中,我们尝试更新文档“aaa”,但在请求中指定了错误的版本:version=999999 与我们添加文档时刚获得的文档版本不匹配。我们在响应中收到错误。

$ curl -X POST -H 'Content-Type: application/json' 'https://127.0.0.1:8983/solr/techproducts/update?_version_=1632740120218042368&versions=true&commit=true&omitHeader=true' --data-binary '
[{ "id" : "aaa",
   "foo_s" : "update attempt with correct existing version" }]'
{
  "adds":[
    "aaa",1632740462042284032]}

现在,我们发送了一个更新,其中 _version_ 的值与索引中的值匹配,并且更新成功。因为我们在更新请求中包含了 versions=true,所以响应中包含的 _version_ 字段的值不同。

$ curl -X POST -H 'Content-Type: application/json' 'https://127.0.0.1:8983/solr/techproducts/update?&versions=true&commit=true&omitHeader=true' --data-binary '
[{ "id" : "aaa", _version_ : 100,
   "foo_s" : "update attempt with wrong existing version embedded in document" }]'
{
  "error":{
    "metadata":[
      "error-class","org.apache.solr.common.SolrException",
      "root-error-class","org.apache.solr.common.SolrException"],
    "msg":"version conflict for aaa expected=100 actual=1632740462042284032",
    "code":409}}

现在,我们发送了一个更新,其中 _version_ 的值嵌入在文档本身中。此请求失败,因为我们指定了错误的版本。当在批次中发送文档并且需要为每个文档指定不同的 _version_ 值时,此方法很有用。

$ curl -X POST -H 'Content-Type: application/json' 'https://127.0.0.1:8983/solr/techproducts/update?&versions=true&commit=true&omitHeader=true' --data-binary '
[{ "id" : "aaa", _version_ : 1632740462042284032,
   "foo_s" : "update attempt with correct version embedded in document" }]'
{
  "adds":[
    "aaa",1632741942747987968]}

现在,我们发送了一个更新,其中 _version_ 的值嵌入在文档本身中。此请求失败,因为我们指定了错误的版本。当在批次中发送文档并且需要为每个文档指定不同的 _version_ 值时,此方法很有用。

$ curl 'https://127.0.0.1:8983/solr/techproducts/query?q=*:*&fl=id,_version_&omitHeader=true'
{
  "response":{"numFound":3,"start":0,"docs":[
      { "_version_":1632740120250548224,
        "id":"bbb"},
      { "_version_":1632741942747987968,
        "id":"aaa"}]
  }}

最后,我们可以发出一个查询,要求在响应中包含 _version_ 字段,并且我们可以看到示例索引中的两个文档都有该字段。

$ curl -X POST -H 'Content-Type: application/json' 'https://127.0.0.1:8983/solr/techproducts/update?versions=true&_version_=-1&failOnVersionConflicts=false&omitHeader=true' --data-binary '
[ { "id" : "aaa" },
  { "id" : "ccc" } ]'
{
  "adds":[
    "ccc",1632740949182382080]}

在这个例子中,我们添加了两个文档 "aaa" 和 "ccc"。由于我们指定了参数 _version_=-1,此请求不应该添加 id 为 aaa 的文档,因为它已经存在。由于指定了 failOnVersionConflicts=false 参数,所以请求成功并且不会抛出任何错误。响应显示只添加了文档 ccc,而 aaa 被静默忽略。

更多信息,请参阅 Yonik Seeley 在 Apache Lucene EuroCon 2012 上关于 Solr 4 中的 NoSQL 功能的演示。

文档中心版本控制约束

乐观并发非常强大,并且工作效率很高,因为它使用内部分配的、全局唯一的 _version_ 字段值。然而,在某些情况下,用户可能希望配置他们自己的文档特定版本字段,其中版本值由外部系统按文档分配,并让 Solr 拒绝尝试用“旧”版本替换文档的更新。在这种情况下,DocBasedVersionConstraintsProcessorFactory 可能会很有用。

DocBasedVersionConstraintsProcessorFactory 的基本用法是在 solrconfig.xml 中将其配置为 UpdateRequestProcessorChain 的一部分,并在您的 schema 中指定您自定义的 versionField 的名称,该字段应该在验证更新时进行检查

<processor class="solr.DocBasedVersionConstraintsProcessorFactory">
  <str name="versionField">my_version_l</str>
</processor>

请注意,versionField 是一个逗号分隔的字段列表,用于检查版本号。配置完成后,此更新处理器将拒绝(HTTP 错误代码 409)任何尝试更新现有文档的操作,如果“新”文档中 my_version_l 字段的值不大于现有文档中该字段的值。

versionField vs _version_

Solr 用于其正常乐观并发的 _version_ 字段在更新如何分发到 SolrCloud 中的副本方面也具有重要的语义,并且必须由 Solr 内部赋值。用户不能重新利用该字段并将其指定为 versionField 以用于 DocBasedVersionConstraintsProcessorFactory 配置。

DocBasedVersionConstraintsProcessorFactory 支持以下额外的配置参数,这些参数都是可选的

ignoreOldUpdates

可选

默认值:false

如果设置为 true,则会静默忽略更新(并向客户端返回状态 200),而不是拒绝 versionField 过低的更新。

deleteVersionParam

可选

默认值:无

可以指定一个 String 参数来表示此处理器也应该检查按 ID 删除命令。

此选项的值应该是一个请求参数的名称,该参数对于所有尝试按 ID 删除的操作都是强制性的,并且必须由客户端使用来为 versionField 指定一个大于要删除的文档的现有值的值。

当使用此请求参数时,任何具有足够高的文档版本号以成功的按 ID 删除命令都将在内部转换为添加文档命令,该命令将现有文档替换为一个新的空文档,除了唯一键和 versionField 之外,以记录已删除的版本,以便将来如果“新”版本不够高,则添加文档命令将失败。

如果 versionField 被指定为一个列表,那么此参数也必须被指定为一个相同大小的逗号分隔的列表,以便参数与字段对应。

supportMissingVersionOnOldDocs

可选

默认值:false

如果设置为 true,则允许任何在启用此功能之前写入且缺少 versionField 的文档被覆盖。

请参阅 DocBasedVersionConstraintsProcessorFactory javadocs测试 solrconfig.xml 文件 以获取更多信息和示例用法。