Solr配置:脚本更新处理器详解与多语言脚本实例
ScriptUpdateProcessorFactory允许在Solr文档更新处理期间使用Java脚本引擎,为在索引之前表达自定义文档处理逻辑提供了极大的灵活性。
它有提交、删除、回滚等索引操作的钩子,但add是最常见的用法。它被实现为一个UpdateProcessor,放置在UpdateChain中。
⚠️ 提示:这以前被称为_StatelessScriptingUpdateProcessor_,重命名是为了澄清此更新处理器的关键方面是它启用了脚本功能。
功能特性
- 多语言支持:脚本可以用JVM支持的任何脚本语言编写(如JavaScript)
- 动态执行:动态执行,无需预编译
- 强大工具:有时被称为”脱困免费卡”,因为你可以用这种方式解决一些无法以其他方式解决的问题
⚠️ 安全警告:能够在索引管道中运行你选择的脚本是一个非常强大的工具,但也会引入一些潜在的安全漏洞。
模块要求
此功能通过需要在使用前启用的scripting
Solr模块提供。
启用脚本更新处理器和脚本引擎
JavaScript引擎
- Java 11及之前版本带有名为Nashorn的JavaScript引擎
- Java 12将需要你添加自己的JavaScript引擎
其他脚本引擎
其他支持的脚本引擎如JRuby、Jython、Groovy,都需要你将JAR文件添加到Solr中。
了解更多关于将任何其他所需的JAR文件(取决于你的脚本引擎)添加到Solr的Lib目录中。
配置
基本配置
1 2 3 4 5 6 7 8 9 10 11
| <updateRequestProcessorChain name="script"> <processor class="org.apache.solr.scripting.update.ScriptUpdateProcessorFactory"> <str name="script">update-script.js</str> </processor> <lst name="params"> <str name="config_param">example config parameter</str> </lst> <processor class="solr.LogUpdateProcessorFactory" /> <processor class="solr.RunUpdateProcessorFactory" /> </updateRequestProcessorChain>
|
⚠️ 注意:处理器支持defaults/appends/invariants概念的配置。但是,也可以跳过这个级别,直接在<processor>
标签下配置参数。
配置参数
script (必需)
- 脚本文件名
- 脚本文件必须放置在
conf/
目录中
- 可以指定一个或多个”script”参数;多个脚本按指定顺序执行
engine (可选)
- 可选地指定要使用的脚本引擎
- 仅当脚本文件的扩展名不是到脚本引擎的标准映射时才需要
- 例如,如果你的脚本文件用JavaScript编写但文件名叫
update-script.foo
,使用javascript
作为引擎名
params (可选)
- 传递到脚本执行上下文中的可选参数
- 指定为具有嵌套类型参数的命名列表(
<lst>
)结构
- 如果指定,脚本上下文将获得一个”params”对象,否则将没有”params”对象可用
脚本执行上下文
每个脚本都有一些提供给它的变量:
- logger:Logger (org.slf4j.Logger) 实例。对于从脚本记录信息很有用
- req:SolrQueryRequest 实例
- rsp:SolrQueryResponse 实例
- params:来自配置的”params”对象(如果指定了任何)
快速体验
techproducts配置集中有一个JavaScript示例update-script.js
。
要试用脚本功能,在./server/solr/configsets/sample_techproducts_configs/conf/solrconfig.xml
文件中启用<updateRequestProcessorChain name="script">
配置。然后通过bin/solr start -e techproducts -Dsolr.modules=scripting
启动Solr。
示例URL:
1
| http://localhost:8983/solr/techproducts/update?commit=true&stream.contentType=text/csv&fieldnames=id,description&stream.body=1,foo&update.chain=script
|
这将记录以下内容:
1
| INFO: update-script#processAdd: id=1
|
脚本语言示例
JavaScript示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| function processAdd(cmd) { doc = cmd.solrDoc; id = doc.getFieldValue("id"); logger.info("update-script#processAdd: id=" + id);
}
function processDelete(cmd) { }
function processMergeIndexes(cmd) { }
function processCommit(cmd) { }
function processRollback(cmd) { }
function finish() { }
|
Ruby示例
Ruby支持通过JRuby项目实现。要使用JRuby作为脚本引擎,将jruby.jar
添加到Solr。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| def processAdd(cmd) doc = cmd.solrDoc id = doc.getFieldValue('id')
$logger.info "update-script#processAdd: id=#{id}"
doc.setField('source_s', 'ruby')
$logger.info "update-script#processAdd: config_param=#{$params.get('config_param')}" end
def processDelete(cmd) end
def processMergeIndexes(cmd) end
def processCommit(cmd) end
def processRollback(cmd) end
def finish() end
|
已知问题
在JRuby中以下内容不能按预期工作,尽管它在JavaScript中工作正常:
Groovy示例
将Groovy发行版的lib/
目录中的JAR添加到Solr。可能不需要Groovy发行版的所有JAR,但需要的不只是主要的groovy.jar
文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| def processAdd(cmd) { doc = cmd.solrDoc id = doc.getFieldValue('id')
logger.info "update-script#processAdd: id=" + id
doc.setField('source_s', 'groovy')
logger.info "update-script#processAdd: config_param=" + params.get('config_param')
logger.info "update-script#processAdd: request_param=" + req.params.get('request_param') rsp.add('script_processed',id) }
def processDelete(cmd) { }
def processMergeIndexes(cmd) { }
def processCommit(cmd) { }
def processRollback(cmd) { }
def finish() { }
|
Python示例
Python支持通过Jython项目实现。将独立的jython.jar
(包含所有依赖项的JAR)添加到Solr中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| def processAdd(cmd): doc = cmd.solrDoc id = doc.getFieldValue("id") logger.info("update-script#processAdd: id=" + id)
def processDelete(cmd): logger.info("update-script#processDelete")
def processMergeIndexes(cmd): logger.info("update-script#processMergeIndexes")
def processCommit(cmd): logger.info("update-script#processCommit")
def processRollback(cmd): logger.info("update-script#processRollback")
def finish(): logger.info("update-script#finish")
|
实际应用示例
1. 数据清洗和规范化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| function processAdd(cmd) { doc = cmd.solrDoc; var phone = doc.getFieldValue("phone"); if (phone) { var cleanPhone = phone.toString().replace(/\D/g, ''); doc.setField("phone_clean", cleanPhone); } var email = doc.getFieldValue("email"); if (email) { doc.setField("email_domain", email.toString().split('@')[1]); } var price = doc.getFieldValue("price"); var discount = doc.getFieldValue("discount"); if (price && discount) { var finalPrice = price * (1 - discount / 100); doc.setField("final_price", finalPrice); } }
|
2. 动态分类标记
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| function processAdd(cmd) { doc = cmd.solrDoc; var title = doc.getFieldValue("title"); var description = doc.getFieldValue("description"); var content = (title + " " + description).toLowerCase(); var categories = []; if (content.includes("java") || content.includes("spring")) { categories.push("java"); } if (content.includes("python") || content.includes("django")) { categories.push("python"); } if (content.includes("javascript") || content.includes("nodejs")) { categories.push("javascript"); } if (categories.length > 0) { doc.setField("auto_categories", categories); } if (content.includes("urgent") || content.includes("emergency")) { doc.setField("priority", "high"); } else if (content.includes("normal")) { doc.setField("priority", "medium"); } else { doc.setField("priority", "low"); } }
|
3. 地理位置处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| def processAdd(cmd) { doc = cmd.solrDoc def address = doc.getFieldValue('address') def city = doc.getFieldValue('city') def state = doc.getFieldValue('state') if (address && city && state) { def fullAddress = "${address}, ${city}, ${state}" doc.setField('full_address', fullAddress) } def timezone = doc.getFieldValue('timezone') if (timezone) { def currentTime = new Date() doc.setField('local_time', formatTimeForTimezone(currentTime, timezone)) } }
|
4. 内容增强
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| def processAdd(cmd) doc = cmd.solrDoc content = doc.getFieldValue('content') if content words = content.split(/\W+/).select { |w| w.length > 3 } keywords = words.group_by(&:downcase).map { |k, v| [k, v.size] } .sort_by { |_, count| -count } .first(10) .map { |word, _| word } doc.setField('keywords_ss', keywords) if keywords.any? doc.setField('word_count', words.size) doc.setField('char_count', content.length) if content.match(/[\u4e00-\u9fff]/) doc.setField('language', 'zh') elsif content.match(/[a-zA-Z]/) doc.setField('language', 'en') end end end
|
性能优化和最佳实践
1. 脚本优化
1 2 3 4 5 6 7 8 9 10 11 12 13
| var emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; var phoneRegex = /^\d{10,}$/;
function processAdd(cmd) { doc = cmd.solrDoc; var email = doc.getFieldValue("email"); if (email && emailRegex.test(email)) { doc.setField("email_valid", true); } }
|
2. 错误处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function processAdd(cmd) { try { doc = cmd.solrDoc; var price = doc.getFieldValue("price"); if (price && !isNaN(price)) { doc.setField("price_category", price > 100 ? "expensive" : "affordable"); } } catch (e) { logger.error("Script error processing document: " + e.message); } }
|
3. 配置管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <updateRequestProcessorChain name="enhanced-script"> <processor class="org.apache.solr.scripting.update.ScriptUpdateProcessorFactory"> <str name="script">data-cleaner.js</str> <str name="script">categorizer.js</str> <str name="script">validator.js</str> <lst name="params"> <str name="environment">production</str> <bool name="debug">false</bool> <arr name="allowed_categories"> <str>technology</str> <str>business</str> <str>science</str> </arr> </lst> </processor> <processor class="solr.LogUpdateProcessorFactory" /> <processor class="solr.RunUpdateProcessorFactory" /> </updateRequestProcessorChain>
|
故障排除
1. 脚本引擎问题
1 2 3 4 5 6 7 8
| java -cp "lib/*" -c " import javax.script.ScriptEngineManager; var manager = new ScriptEngineManager(); manager.getEngineFactories().forEach(factory -> { System.out.println(factory.getEngineName() + ' ' + factory.getEngineVersion()); }); "
|
2. 调试脚本
1 2 3 4 5 6 7 8 9 10 11 12 13
| function processAdd(cmd) { doc = cmd.solrDoc; logger.info("Processing document with ID: " + doc.getFieldValue("id")); var fieldNames = doc.getFieldNames().toArray(); for (var i = 0; i < fieldNames.length; i++) { logger.debug("Field: " + fieldNames[i] + " = " + doc.getFieldValue(fieldNames[i])); } }
|
3. 性能监控
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function processAdd(cmd) { var startTime = Date.now(); doc = cmd.solrDoc; var endTime = Date.now(); var processingTime = endTime - startTime; if (processingTime > 100) { logger.warn("Slow script processing: " + processingTime + "ms for doc " + doc.getFieldValue("id")); } }
|
总结
Solr脚本更新处理器提供了强大的文档处理能力,支持多种脚本语言。通过合理使用脚本,可以实现复杂的数据转换、验证和增强功能。在使用时需要注意性能影响和安全性,建议在开发环境中充分测试后再部署到生产环境。