opensearch_dsl/search/queries/params/script_object.rs
1//! With scripting, you can evaluate custom expressions in OpenSearch. For
2//! example, you can use a script to return a computed value as a field or
3//! evaluate a custom score for a query.
4//!
5//! The default scripting language is [Painless](https://www.elastic.co/guide/en/opensearch/reference/current/modules-scripting-painless.html).
6//! Additional `lang` plugins are available to run scripts written in other
7//! languages. You can specify the language of the script anywhere that scripts
8//! run.
9//!
10//! <https://www.elastic.co/guide/en/opensearch/reference/current/modules-scripting.html>
11
12use std::fmt;
13
14use serde::{
15 de::{self, Deserialize, Deserializer, Visitor},
16 Serialize, Serializer,
17};
18
19use crate::{util::*, Map};
20
21/// Wherever scripting is supported in the OpenSearch APIs, the syntax follows
22/// the same pattern; you specify the language of your script, provide the
23/// script logic (or source, and add parameters that are passed into the script.
24///
25/// <https://www.elastic.co/guide/en/opensearch/reference/current/modules-scripting-using.html>
26#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
27pub struct Script {
28 #[serde(flatten)]
29 source: ScriptSource,
30
31 #[serde(default, skip_serializing_if = "ShouldSkip::should_skip")]
32 lang: Option<ScriptLang>,
33
34 #[serde(default, skip_serializing_if = "ShouldSkip::should_skip")]
35 params: Map<String, serde_json::Value>,
36}
37
38/// The script itself, which you specify as `source` for an inline script or
39/// `id` for a stored script. Use the
40/// [stored script APIs](https://www.elastic.co/guide/en/opensearch/reference/current/modules-scripting-using.html#prefer-params)
41/// to create and manage stored scripts.
42#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
43#[serde(rename_all = "snake_case")]
44pub enum ScriptSource {
45 /// Inline script
46 Source(String),
47
48 /// Stored script
49 Id(String),
50}
51
52impl Script {
53 /// Creates an instance of inlined [`Script`]
54 pub fn source<S>(source: S) -> Self
55 where
56 S: ToString,
57 {
58 Self {
59 source: ScriptSource::Source(source.to_string()),
60 lang: None,
61 params: Map::new(),
62 }
63 }
64
65 /// Creates an instance of stored [`Script`]
66 pub fn id<S>(id: S) -> Self
67 where
68 S: ToString,
69 {
70 Self {
71 source: ScriptSource::Id(id.to_string()),
72 lang: None,
73 params: Map::new(),
74 }
75 }
76
77 /// Specifies the language the script is written in. Defaults to `painless`.
78 pub fn lang<S>(mut self, lang: S) -> Self
79 where
80 S: Into<ScriptLang>,
81 {
82 self.lang = Some(lang.into());
83 self
84 }
85
86 /// Specifies any named parameters that are passed into the script as variables. [Use parameters](https://www.elastic.co/guide/en/opensearch/reference/current/modules-scripting-using.html#prefer-params)
87 /// instead of hard-coded values to decrease compile time.
88 pub fn param<T, S>(mut self, name: S, param: T) -> Self
89 where
90 S: ToString,
91 T: Serialize,
92 {
93 if let Ok(param) = serde_json::to_value(param) {
94 let _ = self.params.entry(name.to_string()).or_insert(param);
95 }
96 self
97 }
98}
99/// Available scripting language
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub enum ScriptLang {
102 /// ***Painless*** is a performant, secure scripting language designed
103 /// specifically for OpenSearch. You can use Painless to safely write inline
104 /// and stored scripts anywhere scripts are supported in OpenSearch.
105 ///
106 /// Painless provides numerous capabilities that center around the following
107 /// core principles:
108 ///
109 /// - **Safety**: Ensuring the security of your cluster is of utmost
110 /// importance. To that end, Painless
111 /// uses a fine-grained allowlist with a granularity down to the members of a
112 /// class. Anything that is not part of the allowlist results in a
113 /// compilation error. See the [Painless API Reference](https://www.elastic.co/guide/en/opensearch/painless/7.15/painless-api-reference.html)
114 /// for a complete list of available classes, methods, and fields per script
115 /// context.
116 ///
117 /// - **Performance**: Painless compiles directly into JVM bytecode to take
118 /// advantage of all possible
119 /// optimizations that the JVM provides. Also, Painless typically avoids
120 /// features that require additional slower checks at runtime.
121 ///
122 /// - **Simplicity**: Painless implements a syntax with a natural familiarity
123 /// to anyone with some
124 /// basic coding experience. Painless uses a subset of Java syntax with some
125 /// additional improvements to enhance readability and remove boilerplate.
126 ///
127 /// <https://www.elastic.co/guide/en/opensearch/reference/current/modules-scripting-painless.html>
128 Painless,
129
130 /// Lucene’s expressions compile a `javascript` expression to bytecode. They
131 /// are designed for high-performance custom ranking and sorting functions
132 /// and are enabled for `inline` and `stored` scripting by default.
133 ///
134 /// <https://www.elastic.co/guide/en/opensearch/reference/current/modules-scripting-expression.html>
135 Expression,
136
137 /// A template language to search with templates
138 ///
139 /// <https://www.elastic.co/guide/en/opensearch/reference/current/search-template.html>
140 Mustache,
141
142 /// Custom language refer to [Advanced scripts using script engines](https://www.elastic.co/guide/en/opensearch/reference/current/modules-scripting-engine.html)
143 Custom(String),
144}
145
146impl Serialize for ScriptLang {
147 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
148 where
149 S: Serializer,
150 {
151 match self {
152 Self::Painless => serializer.serialize_str("painless"),
153 Self::Expression => serializer.serialize_str("expression"),
154 Self::Mustache => serializer.serialize_str("mustache"),
155 Self::Custom(lang) => lang.serialize(serializer),
156 }
157 }
158}
159
160impl<'de> Deserialize<'de> for ScriptLang {
161 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
162 where
163 D: Deserializer<'de>,
164 {
165 struct ScriptLangVisitor;
166
167 impl<'de> Visitor<'de> for ScriptLangVisitor {
168 type Value = ScriptLang;
169
170 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
171 formatter.write_str("a string representing a script language")
172 }
173
174 fn visit_str<E>(self, value: &str) -> Result<ScriptLang, E>
175 where
176 E: de::Error,
177 {
178 match value {
179 "painless" => Ok(ScriptLang::Painless),
180 "expression" => Ok(ScriptLang::Expression),
181 "mustache" => Ok(ScriptLang::Mustache),
182 _ => Ok(ScriptLang::Custom(value.to_string())),
183 }
184 }
185 }
186
187 deserializer.deserialize_str(ScriptLangVisitor)
188 }
189}
190
191impl<T> From<T> for ScriptLang
192where
193 T: ToString,
194{
195 fn from(value: T) -> Self {
196 let value = value.to_string();
197
198 match value.as_str() {
199 "painless" => Self::Painless,
200 "expression" => Self::Expression,
201 "mustache" => Self::Mustache,
202 _ => Self::Custom(value),
203 }
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn serialization() {
213 assert_serialize(
214 Script::source("Math.log(_score * 2) * params['multiplier'].len()")
215 .param("multiplier", [1, 2, 3])
216 .lang(ScriptLang::Painless),
217 json!({
218 "source": "Math.log(_score * 2) * params['multiplier'].len()",
219 "lang": "painless",
220 "params": {
221 "multiplier": [1, 2, 3]
222 }
223 }),
224 );
225
226 assert_serialize(
227 Script::source("doc['my_field'].value * params['multiplier']")
228 .param("multiplier", 1)
229 .lang("my_lang"),
230 json!({
231 "source": "doc['my_field'].value * params['multiplier']",
232 "lang": "my_lang",
233 "params": {
234 "multiplier": 1
235 }
236 }),
237 );
238
239 assert_serialize(
240 Script::id(123).param("multiplier", [1, 2, 3]),
241 json!({
242 "id": "123",
243 "params": {
244 "multiplier": [1, 2, 3]
245 }
246 }),
247 );
248 }
249}