一、简介
Elasticsearch中,脚本是一种强大的工具,可以在查询和索引操作中执行动态计算和数据处理。从Elasticsearch 7.6版本开始,脚本功能得到了进一步的优化和提升,提供了更加灵活和高效的数据处理方式。
二、脚本使用
根据商品文档中的多个字段来动态调整搜索结果的排序。脚本考虑了商品的价格、评分、库存以及销售情况,并且加入了一些条件逻辑来进一步细化排序逻辑。
{
"query": {
"function_score": {
"query": {
"match_all": {} // 匹配所有文档,实际使用时可能会替换为更具体的查询
},
"functions": [
{
"script_score": {
"script": {
"source": """
// 定义基础得分,这里使用文档的_score,即相关性得分
double baseScore = _score;
// 获取文档字段值
double price = doc['price'].value;
double rating = doc['rating'].value;
int stock = doc['stock'].value;
long salesCount = doc['salesCount'].value;
// 定义权重和因子,可以根据实际需要调整
double priceWeight = 0.1;
double ratingWeight = 0.3;
double stockWeight = 0.2;
double salesWeight = 0.1;
double freshnessFactor = 0.3; // 假设这是一个新鲜度因子,用于考虑商品的更新或上架时间
// 计算价格得分,价格越低得分越高
double priceScore = 1.0 / (price / 100.0 + 1.0); // 假设价格是以分为单位,避免除以0
// 计算评分得分,评分越高得分越高
double ratingScore = rating;
// 计算库存得分,库存越高得分越高,但也要考虑避免过量库存的影响
double stockScore = stock < 10 ? 1.0 : (stock < 100 ? 0.9 : (stock < 500 ? 0.8 : 0.5));
// 计算销售得分,销售数量越高得分越高
double salesScore = Math.log10(salesCount + 1); // 使用对数来平滑销售得分的影响
// 假设有一个外部数据源提供了商品的新鲜度评分,这里我们使用一个假设的值
double freshness = params.freshness; // 这个值通常会在查询时作为参数传入
// 计算总得分,基于权重和各个因素的得分
double totalScore = baseScore * freshnessFactor * freshness
+ priceScore * priceWeight
+ ratingScore * ratingWeight
+ stockScore * stockWeight
+ salesScore * salesWeight;
// 返回计算后的总得分
return totalScore;
""",
"params": {
"freshness": 0.9 // 假设的新鲜度评分,实际使用时可能会根据商品的上架时间或其他因素动态计算
}
}
}
}
],
"score_mode": "sum", // 指定得分计算模式,这里使用加和模式
"boost_mode": "replace" // 指定得分增强模式,这里用自定义得分替换原始得分
}
}
}
这个查询中的脚本做了以下几件事情:
初始化了一个基于文档原始相关性得分( _score
)的基础得分。从文档中提取了价格( price
)、评分(rating
)、库存(stock
)和销售数量(salesCount
)等字段的值。定义了一系列权重和因子,用于在计算最终得分时调整各个因素的重要性。 根据提取的字段值和定义的权重,计算了价格、评分、库存和销售的得分。其中价格得分通过反比关系计算(价格越低得分越高),评分得分直接使用评分字段的值,库存得分使用了一个分段函数来考虑不同库存水平的影响,销售得分使用了对数函数来平滑销售数量的影响。 引入了一个外部参数 freshness
,代表商品的新鲜度评分。这个值在实际使用时可能会根据商品的上架时间、更新频率或其他业务逻辑动态计算得出。将所有因素的得分按照定义的权重加权求和,计算出最终的总得分,并返回这个得分作为文档的排序依据。
再看一个聚合中使用脚本的例子:用于计算每个产品类别的加权平均销售额的:
POST /sales_records/_search
{
"size": 0, // 设置返回文档数为0,因为我们只关心聚合结果
"aggs": { // 定义聚合
"categories": { // 按产品类别进行聚合
"terms": {
"field": "product_category.keyword", // 使用product_category字段的值作为分组的关键字
"size": 10 // 指定返回的类别数量上限为10
},
"aggs": { // 在每个产品类别内部进行子聚合
"weighted_sales": { // 计算加权销售额
"sum": { // 使用求和聚合
"script": { // 使用脚本进行计算,将每个文档的sales_amount乘以sales_weight
"source": "doc['sales_amount'].value * doc['sales_weight'].value"
}
}
},
"sum_of_weights": { // 计算销售记录的总权重
"sum": { // 使用求和聚合
"field": "sales_weight" // 指定sales_weight字段进行求和
}
},
"weighted_average_sales": { // 计算加权平均销售额
"bucket_script": { // 使用bucket_script聚合来根据已有的聚合结果进行计算
"buckets_path": { // 指定需要引用的其他聚合结果的路径
"weightedSales": "weighted_sales", // 引用weighted_sales聚合的结果
"totalWeight": "sum_of_weights" // 引用sum_of_weights聚合的结果
},
"script": "params.weightedSales / params.totalWeight" // 计算加权平均销售额的脚本,即加权销售额除以总权重
}
}
}
}
}
}
设置"size": 0
,只会返回聚合的结果。按product_category
字段对销售记录进行分组,并在每个分组内部计算加权销售额和总权重。最后用bucket_script
聚合来计算每个类别的加权平均销售额,并将结果作为该类别的一个聚合指标返回。
三、脚本的执行过程
脚本解析:当接收到包含脚本的请求时,首先需要对脚本进行解析。解析器会根据所选的脚本语言(如Painless)的语法规则对脚本进行词法分析和语法分析,确保脚本的合法性和正确性。
脚本编译:某些脚本语言需对解析后的脚本进行编译。提高脚本的执行效率。Elasticsearch中默认的脚本语言Painless是解释执行的,不需要编译步骤。但即使是解释执行的脚本,Elasticsearch也会对其进行一定程度的优化,以提高执行性能。
脚本执行:成功解析 ,它可在查询或索引操作中被执行了。Elasticsearch为脚本提供了一个安全的执行环境,限制了脚本对系统资源的访问权限,以防止恶意脚本的执行。在执行过程中,脚本可以访问文档的字段、执行数学运算、调用内置函数等,以满足用户的数据处理需求。脚本的执行结果可以被用于影响查询结果、修改文档内容或计算得分等。
脚本缓存:为了提高脚本的执行性能,Elasticsearch会对解析和编译后的脚本进行缓存。当相同的脚本在多个请求中被使用时,Elasticsearch可以直接从缓存中获取已解析和编译的脚本,避免了重复的解析和编译开销。这大大提高了脚本的执行效率和响应速度。
四、脚本的一些常见使用场景
Elasticsearch脚本允许在查询和索引文档时执行复杂的操作。脚本可以用于计算字段的值、自定义排序逻辑、以及在更新和删除文档时应用业务逻辑等。Elasticsearch支持多种脚本语言,但默认推荐使用Painless脚本语言,因为它是一种安全、简单且功能强大的脚本语言
4.1. 脚本字段
动态生成查询结果中的字段。这些字段不是文档的实际部分,而是在查询时通过脚本计算得出的。如包含价格和数量的文档使用脚本来计算总价:
GET /my_index/_search
{
"query": { "match_all": {} },
"script_fields": {
"total_price": {
"script": {
"source": "doc['price'].value * doc['quantity'].value"
}
}
}
}
4.2. 脚本计算得分
使用脚本来自定义文档的得分计算方式。这对于实现复杂的搜索排名逻辑非常有用。如使用脚本来根据文档的某个字段的值来调整得分:
GET /my_index/_search
{
"query": {
"function_score": {
"query": { "match": { "content": "elasticsearch" } },
"script_score": {
"script": {
"source": "doc['likes'].value * params.weight",
"params": { "weight": 2 }
}
}
}
}
}
script_score
参数定义了一个脚本,该脚本将likes
字段的值与参数weight
相乘来计算得分。参数weight
的值设置为2,因此likes
字段的值将乘以2来计算最终的得分。
4.3. 更新脚本
使用脚本来应用复杂的业务逻辑。如使用脚本来增加文档中的某个字段的值:
POST /my_index/_update_by_query
{
"script": {
"source": "ctx._source.counter += params.count",
"params": { "count": 1 }
},
"query": { "term": { "user": "kimchy" } }
}
_update_by_query
API来更新文档。script
参数定义了一个脚本,该脚本将counter
字段的值增加参数count
的值。参数count
的值设置为1,因此counter
字段的值将增加1。
五、脚本最佳实践
尽量使用简单的脚本:复杂的脚本可能导致性能下降和难以调试的问题。脚本应尽量保持简单和清晰,避免使用过于复杂的逻辑和运算。
避免在脚本中执行耗时的操作:脚本的执行时间会影响查询的响应速度。应避免在脚本中执行耗时的操作,如复杂的计算、外部资源访问等。如果确实需要执行耗时操作,可以考虑将其移至应用程序端处理。
充分利用脚本缓存:Elasticsearch对解析和编译后的脚本进行缓存,以提高性能。避免在每次请求中都重新解析和编译相同的脚本。可以通过将脚本作为参数传递给查询或索引操作来实现脚本的重用。
注意脚本的安全性:使用经过验证和安全的脚本语言,限制脚本的访问权限,并定期审查和监控脚本的执行情况。
太强 ! SpringBoot中出入参增强的5种方法 : 加解密、脱敏、格式转换、时间时区处理
太强 ! SpringBoot中优化if-else语句的七种绝佳方法实战
SpringBoot使用EasyExcel并行导出多个excel文件并压缩zip下载
从MySQL行格式原理看:为什么开发规范中不推荐NULL?数据是如何在磁盘上存储的?
SpringBoot中使用Jackson实现自定义序列化和反序列化控制的5种方式总结
SpringBoot中基于JWT的双token(access_token+refresh_token)授权和续期方案
SpringBoot中基于JWT的单token授权和续期方案
SpringBoot中Token登录授权、续期和主动终止的方案(Redis+Token)
SpringBoot中大量数据导出方案:使用EasyExcel并行导出多个excel文件并压缩zip后下载
Elasticsearch揭秘:高效写入与精准检索的流程原理全解析
Spring Boot中Druid连接池与多数据源切换的方案