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 | {"id":"mydoc", |
应用以下更新命令:
1 | {"id":"mydoc", |
集合中的结果文档将是:
1 | {"id":"mydoc", |
更新子文档
Solr支持作为原子更新的一部分修改、添加和删除子文档。语法上,更改文档子文档的更新与常规简单字段的原子更新非常相似。
更新子文档的模式和配置要求与上述原子更新的字段存储要求相同。
重要提示:
在SolrCloud中路由使用子文档ID的更新时,需要特别注意:
- 客户端可以指定
_route_
参数,值为根文档的id
- 使用默认
compositeId
路由器的”前缀路由”功能 - 必须在部分更新的
_root_
字段中指定根文档的ID
修改子文档字段
所有原子更新操作都支持子文档的”真实”字段:
1 | curl -X POST 'http://localhost:8983/solr/gettingstarted/update?commit=true' -H 'Content-Type: application/json' --data-binary '[ |
替换所有子文档
与普通(多值)字段一样,set
关键字可用于替换伪字段中的所有子文档:
1 | curl -X POST 'http://localhost:8983/solr/gettingstarted/update?commit=true' -H 'Content-Type: application/json' --data-binary '[ |
添加子文档
与普通(多值)字段一样,add
关键字可用于向伪字段添加额外的子文档:
1 | curl -X POST 'http://localhost:8983/solr/gettingstarted/update?commit=true' -H 'Content-Type: application/json' --data-binary '[ |
注意这是添加或替换(按ID)。如果文档P11!S21
已经有ID为P11!D99
的子文档,那么它将被替换。
删除子文档
与普通(多值)字段一样,remove
关键字可用于从其伪字段中删除子文档(按id
):
1 | curl -X POST 'http://localhost:8983/solr/gettingstarted/update?commit=true' -H 'Content-Type: application/json' --data-binary '[ |
就地更新
就地更新与原子更新非常相似;某种意义上,这是原子更新的子集。在常规原子更新中,整个文档在应用更新期间内部重新索引。然而,在这种方法中,只有要更新的字段受到影响,文档的其余部分不会内部重新索引。因此,就地更新的效率不受要更新文档大小的影响(即字段数量、字段大小等)。
就地更新条件
仅当要更新的字段满足这三个条件时,才使用就地方法执行原子更新操作:
- 是非索引(
indexed="false"
)、非存储(stored="false"
)、单值(multiValued="false"
)数值docValues(docValues="true"
)字段 _version_
字段也是非索引、非存储单值docValues字段- 更新字段的复制目标(如果有)也是非索引、非存储单值数值docValues字段
就地更新操作符
set:
用指定值设置或替换字段值。可以指定为单个值。
inc:
将数值字段的值按特定数量递增或递减,指定为单个整数或浮点数。正值递增字段值,负值递减。
防止非就地原子更新
由于确保满足所有必要条件以确保更新可以就地完成可能很棘手,Solr支持名为update.partial.requireInPlace
的请求参数选项。设置为true
时,无法就地完成的原子更新将失败。用户可以在希望更新请求在无法就地完成时”快速失败”时指定此选项。
就地更新示例
如果价格和人气字段在模式中定义为:
1 | <field name="price" type="float" indexed="false" stored="false" docValues="true"/> |
如果集合中存在以下文档:
1 | { |
应用以下更新命令:
1 | { |
集合中的结果文档将是:
1 | { |
乐观并发控制
乐观并发是Solr的一个特性,可被更新/替换文档的客户端应用程序使用,以确保他们正在替换/更新的文档没有被另一个客户端应用程序同时修改。
工作流程
使用乐观并发的一般工作流程包括以下步骤:
- 客户端读取文档
- 客户端本地更改文档
- 客户端将更改后的文档重新提交到Solr
- 如果存在版本冲突(HTTP错误代码409),客户端重新开始该过程
版本控制语义
当客户端将更改后的文档重新提交给Solr时,可以包含_version_
来调用乐观并发控制。使用特定语义来定义何时应该更新文档或何时报告冲突:
- 如果
_version_
字段的内容大于’1’(即’12345’),则文档中的_version_
必须匹配索引中的_version_
- 如果
_version_
字段的内容等于’1’,则文档必须简单存在。在这种情况下,不进行版本匹配,但如果文档不存在,更新将被拒绝 - 如果
_version_
字段的内容小于’0’(即’-1’),则文档必须不存在。在这种情况下,不进行版本匹配,但如果文档存在,更新将被拒绝 - 如果
_version_
字段的内容等于’0’,则版本是否匹配或文档是否存在都无关紧要。如果存在,将被覆盖;如果不存在,将被添加
批处理和版本冲突
当文档批量添加/更新时,即使单个版本冲突也可能导致拒绝整个批次。使用参数failOnVersionConflicts=false
来避免当批次中一个或多个文档的版本约束失败时整个批次失败。
获取新版本
使用乐观并发时,客户端可以包含可选的versions=true
请求参数,以指示响应中应包含正在添加的文档的新版本。这允许客户端立即知道每个添加文档的_version_
,而无需进行冗余的/get
请求。
乐观并发示例
添加文档并获取版本
1 | curl -X POST -H 'Content-Type: application/json' 'http://localhost:8983/solr/techproducts/update?versions=true&omitHeader=true' --data-binary ' |
响应:
1 | { |
版本冲突示例
1 | curl -X POST -H 'Content-Type: application/json' 'http://localhost:8983/solr/techproducts/update?_version_=999999&versions=true&omitHeader=true' --data-binary ' |
错误响应:
1 | { |
正确版本更新
1 | curl -X POST -H 'Content-Type: application/json' 'http://localhost:8983/solr/techproducts/update?_version_=1632740120218042368&versions=true&commit=true&omitHeader=true' --data-binary ' |
成功响应:
1 | { |
文档中嵌入版本
1 | curl -X POST -H 'Content-Type: application/json' 'http://localhost:8983/solr/techproducts/update?&versions=true&commit=true&omitHeader=true' --data-binary ' |
批处理中的版本控制
1 | curl -X POST -H 'Content-Type: application/json' 'http://localhost:8983/solr/techproducts/update?versions=true&_version_=-1&failOnVersionConflicts=false&omitHeader=true' --data-binary ' |
响应:
1 | { |
在这个例子中,由于指定了_version_=-1
,不应添加已存在的文档”aaa”。请求成功且不抛出错误,因为指定了failOnVersionConflicts=false
参数。响应显示只添加了文档”ccc”,”aaa”被静默忽略。
基于文档的版本约束
在某些情况下,用户可能希望配置自己的文档特定版本字段,其中版本值由外部系统按每个文档分配,让Solr拒绝尝试用”较旧”版本替换文档的更新。在这种情况下,DocBasedVersionConstraintsProcessorFactory
可能很有用。
基本配置
在solrconfig.xml
中配置DocBasedVersionConstraintsProcessorFactory
作为UpdateRequestProcessorChain的一部分:
1 | <processor class="solr.DocBasedVersionConstraintsProcessorFactory"> |
配置参数
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
。
实践建议
选择合适的更新策略
- 大量字段更新:使用原子更新
- 数值字段频繁更新:配置就地更新
- 并发环境:使用乐观并发控制
- 批处理场景:结合版本控制和failOnVersionConflicts参数
性能优化
- 字段配置:为就地更新正确配置docValues字段
- 批处理大小:平衡批处理大小和内存使用
- 版本检查:仅在必要时使用版本控制
- 存储策略:合理配置字段存储属性
错误处理
- 版本冲突:实现重试机制
- 网络故障:使用适当的超时和重试策略
- 批处理失败:根据业务需求选择失败策略
监控和调试
- 更新性能:监控更新延迟和吞吐量
- 版本冲突率:跟踪并发冲突频率
- 内存使用:监控就地更新的内存效率
总结
Solr的部分文档更新功能为不同场景提供了灵活的解决方案。原子更新适合一般的字段更新需求,就地更新为特定类型的字段提供高效的更新机制,而乐观并发控制确保在并发环境中的数据一致性。
正确选择和配置这些更新策略,可以显著提高应用程序的性能和可靠性。结合适当的错误处理和监控机制,可以构建健壮的文档更新系统。