Painless 常用示例

Painless 常用示例 #

本页收集了各种场景下常用的 Painless 脚本示例,可直接复制使用或按需修改。

脚本字段(Script Fields) #

在搜索结果中返回动态计算的字段值。

计算折扣价格 #

GET products/_search
{
  "query": { "match_all": {} },
  "script_fields": {
    "discounted_price": {
      "script": {
        "source": "doc['price'].value * (1 - params.discount)",
        "params": {
          "discount": 0.2
        }
      }
    }
  }
}

拼接姓名字段 #

GET employees/_search
{
  "script_fields": {
    "full_name": {
      "script": {
        "source": """
          def first = doc['first_name.keyword'].size() > 0 ? doc['first_name.keyword'].value : '';
          def last = doc['last_name.keyword'].size() > 0 ? doc['last_name.keyword'].value : '';
          return last + first;
        """
      }
    }
  }
}

计算年龄 #

GET users/_search
{
  "script_fields": {
    "age": {
      "script": {
        "source": """
          if (doc['birthday'].size() == 0) return null;
          ZonedDateTime birthday = doc['birthday'].value;
          ZonedDateTime now = ZonedDateTime.ofInstant(
            Instant.ofEpochMilli(System.currentTimeMillis()),
            ZoneId.of('UTC')
          );
          return ChronoUnit.YEARS.between(birthday, now);
        """
      }
    }
  }
}

文档更新 #

计数器递增 #

POST page_views/_update/homepage
{
  "script": {
    "source": "ctx._source.view_count += params.count",
    "params": { "count": 1 }
  },
  "upsert": {
    "view_count": 1
  }
}

向数组追加唯一元素 #

POST articles/_update/1
{
  "script": {
    "source": """
      if (ctx._source.tags == null) {
        ctx._source.tags = [];
      }
      for (tag in params.new_tags) {
        if (!ctx._source.tags.contains(tag)) {
          ctx._source.tags.add(tag);
        }
      }
    """,
    "params": {
      "new_tags": ["featured", "trending"]
    }
  }
}

从数组移除元素 #

POST articles/_update/1
{
  "script": {
    "source": """
      if (ctx._source.tags != null) {
        ctx._source.tags.removeIf(tag -> tag == params.remove_tag);
      }
    """,
    "params": {
      "remove_tag": "outdated"
    }
  }
}

条件删除文档 #

POST orders/_update/123
{
  "script": {
    "source": """
      if (ctx._source.status == 'cancelled' && ctx._source.refunded == true) {
        ctx.op = 'delete';
      } else {
        ctx.op = 'noop';
      }
    """
  }
}

嵌套对象更新 #

POST users/_update/1
{
  "script": {
    "source": """
      if (ctx._source.profile == null) {
        ctx._source.profile = [:];
      }
      ctx._source.profile.bio = params.bio;
      ctx._source.profile.updated_at = params.now;
    """,
    "params": {
      "bio": "Easysearch 用户",
      "now": "2025-06-15T10:30:00Z"
    }
  }
}

批量更新(Update By Query) #

为所有文档添加新字段 #

POST my_index/_update_by_query
{
  "script": {
    "source": "ctx._source.version = params.version",
    "params": { "version": 2 }
  }
}

根据条件批量分类 #

POST products/_update_by_query
{
  "script": {
    "source": """
      if (ctx._source.price < 100) {
        ctx._source.tier = 'budget';
      } else if (ctx._source.price < 500) {
        ctx._source.tier = 'mid-range';
      } else {
        ctx._source.tier = 'premium';
      }
    """
  }
}

批量格式转换 #

POST logs/_update_by_query
{
  "script": {
    "source": """
      if (ctx._source.ip != null) {
        ctx._source.ip_parts = ctx._source.ip.splitOnToken('.');
      }
    """
  },
  "query": {
    "exists": { "field": "ip" }
  }
}

自定义评分 #

热度加权评分 #

GET articles/_search
{
  "query": {
    "script_score": {
      "query": { "match": { "title": "技术" } },
      "script": {
        "source": """
          double textScore = _score;
          double popularity = Math.log(2 + doc['views'].value);
          double recency = 1.0;

          if (doc['publish_date'].size() > 0) {
            long ageInDays = (System.currentTimeMillis() - doc['publish_date'].value.toInstant().toEpochMilli()) / 86400000L;
            recency = 1.0 / (1 + ageInDays * 0.01);
          }

          return textScore * popularity * recency;
        """
      }
    }
  }
}

地理位置加权 #

GET shops/_search
{
  "query": {
    "script_score": {
      "query": { "match_all": {} },
      "script": {
        "source": """
          double distance = doc['location'].arcDistance(params.lat, params.lon) / 1000.0;
          return 1.0 / (1 + distance);
        """,
        "params": {
          "lat": 39.9042,
          "lon": 116.4074
        }
      }
    }
  }
}

自定义排序 #

按优先级 + 时间综合排序 #

GET tasks/_search
{
  "sort": [
    {
      "_script": {
        "type": "number",
        "script": {
          "source": """
            def priorityScore = 0;
            switch (doc['priority.keyword'].value) {
              case 'critical': priorityScore = 1000; break;
              case 'high': priorityScore = 100; break;
              case 'medium': priorityScore = 10; break;
              default: priorityScore = 1;
            }
            long ageHours = (System.currentTimeMillis() - doc['created_at'].value.toInstant().toEpochMilli()) / 3600000L;
            return priorityScore + ageHours;
          """
        },
        "order": "desc"
      }
    }
  ]
}

摄入管道脚本 #

自动生成摘要 #

PUT _ingest/pipeline/auto_summary
{
  "processors": [
    {
      "script": {
        "source": """
          if (ctx.content != null && ctx.content.length() > 200) {
            ctx.summary = ctx.content.substring(0, 200) + '...';
          } else {
            ctx.summary = ctx.content;
          }
        """
      }
    }
  ]
}

IP 地址分类 #

PUT _ingest/pipeline/ip_classify
{
  "processors": [
    {
      "script": {
        "source": """
          if (ctx.client_ip != null) {
            if (ctx.client_ip.startsWith('10.') || ctx.client_ip.startsWith('192.168.')) {
              ctx.network_type = 'internal';
            } else {
              ctx.network_type = 'external';
            }
          }
        """
      }
    }
  ]
}

时间戳处理 #

PUT _ingest/pipeline/timestamp_process
{
  "processors": [
    {
      "script": {
        "source": """
          if (ctx.timestamp != null) {
            def sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
            def date = sdf.parse(ctx.timestamp);
            def cal = Calendar.getInstance();
            cal.setTime(date);
            ctx.hour_of_day = cal.get(Calendar.HOUR_OF_DAY);
            ctx.day_of_week = cal.get(Calendar.DAY_OF_WEEK);
            ctx.is_weekend = (ctx.day_of_week == 1 || ctx.day_of_week == 7);
          }
        """
      }
    }
  ]
}

Reindex 中使用脚本 #

重建索引时重命名字段 #

POST _reindex
{
  "source": {
    "index": "old_index"
  },
  "dest": {
    "index": "new_index"
  },
  "script": {
    "source": """
      ctx._source.user_name = ctx._source.remove('username');
      ctx._source.email_address = ctx._source.remove('email');
      if (ctx._source.status == 'inactive') {
        ctx.op = 'noop';
      }
    """
  }
}

重建索引时转换数据格式 #

POST _reindex
{
  "source": {
    "index": "raw_logs"
  },
  "dest": {
    "index": "processed_logs"
  },
  "script": {
    "source": """
      // 将字符串时间戳转为统一格式
      if (ctx._source.log_time != null) {
        ctx._source.log_time = ctx._source.log_time.replace(' ', 'T') + 'Z';
      }

      // 提取日志级别
      if (ctx._source.message != null) {
        def matcher = /^\[(\w+)\]/.matcher(ctx._source.message);
        if (matcher.find()) {
          ctx._source.log_level = matcher.group(1);
        }
      }
    """
  }
}

实用技巧 #

空值安全处理模式 #

// 方式 1:检查 size
def val = doc['field'].size() > 0 ? doc['field'].value : defaultValue;

// 方式 2:使用 containsKey(在 ctx._source 中)
def val = ctx._source.containsKey('field') ? ctx._source.field : defaultValue;

// 方式 3:Elvis 运算符(对 params)
def val = params.value ?: defaultValue;

Debug 输出(仅调试用) #

// 在 Painless Execute API 中使用 Debug.explain 查看变量值
Debug.explain(doc['field'].value);

提示Debug.explain 会抛出一个包含变量类型和值的异常,仅在调试时使用,不要用于生产脚本。