Solr索引:部分文档更新与原子操作完整指南

Solr索引:部分文档更新与原子操作完整指南

概述

一旦在Solr索引中索引了需要的内容,就需要开始考虑处理文档变更的策略。Solr支持三种处理部分变更文档的方法。

第一种是原子更新(Atomic Updates),允许在不重新索引整个文档的情况下更改文档的一个或多个字段。

第二种是就地更新(In-Place Updates),类似于原子更新,但只能用于更新基于docValue的单值非索引和非存储数值字段。

第三种是乐观并发控制(Optimistic Concurrency),是许多NoSQL数据库的特性,允许基于版本有条件地更新文档。

原子更新、就地更新和乐观并发可以作为独立策略使用,也可以结合使用:您可以使用乐观并发有条件地应用原子更新。

原子更新

Solr支持几种修饰符来原子更新文档的值。这允许仅更新特定字段,有助于在索引添加速度对应用程序至关重要的环境中加快索引过程。

要使用原子更新,请为需要更新的字段添加修饰符。内容可以被更新、添加到,或者如果字段是数值类型,可以递增或递减。

原子更新操作符

set
用指定值设置或替换字段值,如果指定’null’或空列表作为新值,则删除值。可以指定为单个值,或多值字段的列表。

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

add-distinct
将指定值添加到多值字段,仅在不存在时添加。可以指定为单个值或列表。

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

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

inc
将数值字段的值按特定数量递增或递减,指定为单个整数或浮点数。正值递增字段值,负值递减。

字段存储要求

原子更新文档的核心功能要求模式中的所有字段必须配置为存储(stored="true")或docValues(docValues="true"),除了<copyField/>目标字段,必须配置为stored="false"docValues="false"useDocValuesAsStored="false"

原子更新应用于由现有存储字段值表示的文档。copyField目标字段中的所有数据必须仅来自copyField源。

原子更新示例

如果集合中存在以下文档:

1
2
3
4
5
6
7
8
{"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"]
}

应用以下更新命令:

1
2
3
4
5
6
7
8
{"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"]}
}

集合中的结果文档将是:

1
2
3
4
5
6
7
{"id":"mydoc",
"price":99,
"popularity":35,
"categories":["kids","toys","games"],
"sub_categories":["under_5","under_10"],
"tags":["buy_now","clearance"]
}

更新子文档

Solr支持作为原子更新的一部分修改、添加和删除子文档。语法上,更改文档子文档的更新与常规简单字段的原子更新非常相似。

更新子文档的模式和配置要求与上述原子更新的字段存储要求相同。

重要提示
在SolrCloud中路由使用子文档ID的更新时,需要特别注意:

  • 客户端可以指定_route_参数,值为根文档的id
  • 使用默认compositeId路由器的”前缀路由”功能
  • 必须在部分更新的_root_字段中指定根文档的ID

修改子文档字段

所有原子更新操作都支持子文档的”真实”字段:

1
2
3
4
5
6
7
curl -X POST 'http://localhost: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关键字可用于替换伪字段中的所有子文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
curl -X POST 'http://localhost: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关键字可用于向伪字段添加额外的子文档:

1
2
3
4
5
6
7
8
9
curl -X POST 'http://localhost: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):

1
2
3
4
5
6
curl -X POST 'http://localhost:8983/solr/gettingstarted/update?commit=true' -H 'Content-Type: application/json' --data-binary '[
{
"id": "P11!S21",
"_root_": "P11!prod",
"manuals": { "remove": { "id": "P11!D41" } }
} ]'

就地更新

就地更新与原子更新非常相似;某种意义上,这是原子更新的子集。在常规原子更新中,整个文档在应用更新期间内部重新索引。然而,在这种方法中,只有要更新的字段受到影响,文档的其余部分不会内部重新索引。因此,就地更新的效率不受要更新文档大小的影响(即字段数量、字段大小等)。

就地更新条件

仅当要更新的字段满足这三个条件时,才使用就地方法执行原子更新操作:

  1. 是非索引(indexed="false")、非存储(stored="false")、单值(multiValued="false")数值docValues(docValues="true")字段
  2. _version_字段也是非索引、非存储单值docValues字段
  3. 更新字段的复制目标(如果有)也是非索引、非存储单值数值docValues字段

就地更新操作符

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

inc
将数值字段的值按特定数量递增或递减,指定为单个整数或浮点数。正值递增字段值,负值递减。

防止非就地原子更新

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

就地更新示例

如果价格和人气字段在模式中定义为:

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

如果集合中存在以下文档:

1
2
3
4
5
6
7
8
{
"id":"mydoc",
"price":10,
"popularity":42,
"categories":["kids"],
"promo_ids":["a123x"],
"tags":["free_to_try","buy_now","clearance","on_sale"]
}

应用以下更新命令:

1
2
3
4
5
{
"id":"mydoc",
"price":{"set":99},
"popularity":{"inc":20}
}

集合中的结果文档将是:

1
2
3
4
5
6
7
8
{
"id":"mydoc",
"price":99,
"popularity":62,
"categories":["kids"],
"promo_ids":["a123x"],
"tags":["free_to_try","buy_now","clearance","on_sale"]
}

乐观并发控制

乐观并发是Solr的一个特性,可被更新/替换文档的客户端应用程序使用,以确保他们正在替换/更新的文档没有被另一个客户端应用程序同时修改。

工作流程

使用乐观并发的一般工作流程包括以下步骤:

  1. 客户端读取文档
  2. 客户端本地更改文档
  3. 客户端将更改后的文档重新提交到Solr
  4. 如果存在版本冲突(HTTP错误代码409),客户端重新开始该过程

版本控制语义

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

  • 如果_version_字段的内容大于’1’(即’12345’),则文档中的_version_必须匹配索引中的_version_
  • 如果_version_字段的内容等于’1’,则文档必须简单存在。在这种情况下,不进行版本匹配,但如果文档不存在,更新将被拒绝
  • 如果_version_字段的内容小于’0’(即’-1’),则文档必须不存在。在这种情况下,不进行版本匹配,但如果文档存在,更新将被拒绝
  • 如果_version_字段的内容等于’0’,则版本是否匹配或文档是否存在都无关紧要。如果存在,将被覆盖;如果不存在,将被添加

批处理和版本冲突

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

获取新版本

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

乐观并发示例

添加文档并获取版本

1
2
3
curl -X POST -H 'Content-Type: application/json' 'http://localhost:8983/solr/techproducts/update?versions=true&omitHeader=true' --data-binary '
[ { "id" : "aaa" },
{ "id" : "bbb" } ]'

响应:

1
2
3
4
{
"adds":[
"aaa",1632740120218042368,
"bbb",1632740120250548224]}

版本冲突示例

1
2
3
curl -X POST -H 'Content-Type: application/json' 'http://localhost:8983/solr/techproducts/update?_version_=999999&versions=true&omitHeader=true' --data-binary '
[{ "id" : "aaa",
"foo_s" : "update attempt with wrong existing version" }]'

错误响应:

1
2
3
4
5
6
7
{
"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}}

正确版本更新

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

成功响应:

1
2
3
{
"adds":[
"aaa",1632740462042284032]}

文档中嵌入版本

1
2
3
curl -X POST -H 'Content-Type: application/json' 'http://localhost: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" }]'

批处理中的版本控制

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

响应:

1
2
3
{
"adds":[
"ccc",1632740949182382080]}

在这个例子中,由于指定了_version_=-1,不应添加已存在的文档”aaa”。请求成功且不抛出错误,因为指定了failOnVersionConflicts=false参数。响应显示只添加了文档”ccc”,”aaa”被静默忽略。

基于文档的版本约束

在某些情况下,用户可能希望配置自己的文档特定版本字段,其中版本值由外部系统按每个文档分配,让Solr拒绝尝试用”较旧”版本替换文档的更新。在这种情况下,DocBasedVersionConstraintsProcessorFactory可能很有用。

基本配置

solrconfig.xml中配置DocBasedVersionConstraintsProcessorFactory作为UpdateRequestProcessorChain的一部分:

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

配置参数

versionField(必需):
要检查版本号的字段的逗号分隔列表。配置后,此更新处理器将拒绝(HTTP错误代码409)任何尝试更新现有文档的操作,其中”新”文档中my_version_l字段的值不大于现有文档中该字段的值。

ignoreOldUpdates(可选,默认:false):
如果设置为true,更新将被静默忽略(并向客户端返回状态200),而不是拒绝versionField太低的更新。

deleteVersionParam(可选):
可以指定的字符串参数,指示此处理器也应检查Delete By Id命令。此选项的值应该是处理器认为对所有Delete By Id尝试都是强制性的请求参数的名称,必须由客户端使用以指定versionField的值,该值大于要删除文档的现有值。

supportMissingVersionOnOldDocs(可选,默认:false):
如果设置为true,允许在启用此功能之前编写并且缺少versionField的任何文档被覆盖。

重要说明

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

实践建议

选择合适的更新策略

  1. 大量字段更新:使用原子更新
  2. 数值字段频繁更新:配置就地更新
  3. 并发环境:使用乐观并发控制
  4. 批处理场景:结合版本控制和failOnVersionConflicts参数

性能优化

  1. 字段配置:为就地更新正确配置docValues字段
  2. 批处理大小:平衡批处理大小和内存使用
  3. 版本检查:仅在必要时使用版本控制
  4. 存储策略:合理配置字段存储属性

错误处理

  1. 版本冲突:实现重试机制
  2. 网络故障:使用适当的超时和重试策略
  3. 批处理失败:根据业务需求选择失败策略

监控和调试

  1. 更新性能:监控更新延迟和吞吐量
  2. 版本冲突率:跟踪并发冲突频率
  3. 内存使用:监控就地更新的内存效率

总结

Solr的部分文档更新功能为不同场景提供了灵活的解决方案。原子更新适合一般的字段更新需求,就地更新为特定类型的字段提供高效的更新机制,而乐观并发控制确保在并发环境中的数据一致性。

正确选择和配置这些更新策略,可以显著提高应用程序的性能和可靠性。结合适当的错误处理和监控机制,可以构建健壮的文档更新系统。

© 2025 Solr Community of China All Rights Reserved. 本站访客数人次 本站总访问量
Theme by hiero