Solr配置:脚本更新处理器详解与多语言脚本实例

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; // org.apache.solr.common.SolrInputDocument
id = doc.getFieldValue("id");
logger.info("update-script#processAdd: id=" + id);

// 设置字段值:
// doc.setField("foo_s", "whatever");

// 获取配置参数:
// config_param = params.get('config_param'); // "params"仅在处理器配置了<lst name="params">时存在

// 获取请求参数:
// some_param = req.getParams().get("some_param")

// 添加匹配模式的字段名字段:
// - 通过在field_name_ss上进行分面,可能对确定结果集中表示的字段/属性很有用
// field_names = doc.getFieldNames().toArray();
// for(i=0; i < field_names.length; i++) {
// field_name = field_names[i];
// if (/attr_.*/.test(field_name)) { doc.addField("attribute_ss", field_names[i]); }
// }
}

function processDelete(cmd) {
// no-op
}

function processMergeIndexes(cmd) {
// no-op
}

function processCommit(cmd) {
// no-op
}

function processRollback(cmd) {
// no-op
}

function finish() {
// no-op
}

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 # org.apache.solr.common.SolrInputDocument
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)
# no-op
end

def processMergeIndexes(cmd)
# no-op
end

def processCommit(cmd)
# no-op
end

def processRollback(cmd)
# no-op
end

def finish()
# no-op
end

已知问题

在JRuby中以下内容不能按预期工作,尽管它在JavaScript中工作正常:

1
2
# $logger.info "update-script#processAdd: request_param=#{$req.params.get('request_param')}"
# $rsp.add('script_processed',id)

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 // org.apache.solr.common.SolrInputDocument
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) {
// no-op
}

def processMergeIndexes(cmd) {
// no-op
}

def processCommit(cmd) {
// no-op
}

def processRollback(cmd) {
// no-op
}

def finish() {
// no-op
}

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 coordinates = geocodeService.getCoordinates(fullAddress)
// doc.setField('location', "${coordinates.lat},${coordinates.lng}")
}

// 时区处理
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);
// 可以选择返回false来跳过这个文档
// return false;
}
}

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) { // 超过100ms记录警告
logger.warn("Slow script processing: " + processingTime + "ms for doc " +
doc.getFieldValue("id"));
}
}

总结

Solr脚本更新处理器提供了强大的文档处理能力,支持多种脚本语言。通过合理使用脚本,可以实现复杂的数据转换、验证和增强功能。在使用时需要注意性能影响和安全性,建议在开发环境中充分测试后再部署到生产环境。

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