datafusion_opensearch/
udf.rs1use std::sync::{Arc, OnceLock};
24
25use datafusion::arrow::array::{Array, BooleanArray, StringArray};
26use datafusion::arrow::datatypes::DataType;
27use datafusion::common::Result;
28use datafusion::logical_expr::{ColumnarValue, ScalarFunctionArgs, ScalarUDF, ScalarUDFImpl, Signature, Volatility};
29use datafusion::scalar::ScalarValue;
30
31pub const OS_MATCH: &str = "os_match";
33pub const OS_GEO_BBOX: &str = "os_geo_bbox";
35pub const OS_GEO_DISTANCE: &str = "os_geo_distance";
37pub const OS_GEO_POLYGON: &str = "os_geo_polygon";
39
40pub fn tokens(s: &str) -> Vec<String> {
44 s.split(|c: char| !c.is_alphanumeric())
45 .filter(|t| !t.is_empty())
46 .map(str::to_lowercase)
47 .collect()
48}
49
50fn matches(doc: &str, query: &str) -> bool {
52 let q = tokens(query);
53 !q.is_empty() && {
54 let d = tokens(doc);
55 q.iter().any(|t| d.contains(t))
56 }
57}
58
59#[derive(Debug, PartialEq, Eq, Hash)]
61struct MatchUdf {
62 signature: Signature,
63}
64
65impl MatchUdf {
66 fn new() -> Self {
67 Self {
68 signature: Signature::exact(vec![DataType::Utf8, DataType::Utf8], Volatility::Immutable),
69 }
70 }
71}
72
73impl ScalarUDFImpl for MatchUdf {
74 fn name(&self) -> &str {
75 OS_MATCH
76 }
77 fn signature(&self) -> &Signature {
78 &self.signature
79 }
80 fn return_type(&self, _: &[DataType]) -> Result<DataType> {
81 Ok(DataType::Boolean)
82 }
83 fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
86 let n = args.number_rows;
87 let query = match args.args.get(1) {
88 Some(ColumnarValue::Scalar(ScalarValue::Utf8(Some(q)))) => q.clone(),
89 _ => return Ok(ColumnarValue::Array(Arc::new(BooleanArray::from(vec![false; n])))),
90 };
91 let col = args.args[0].clone().into_array(n)?;
92 let out: BooleanArray = match col.as_any().downcast_ref::<StringArray>() {
93 Some(sa) => (0..sa.len())
94 .map(|i| Some(!sa.is_null(i) && matches(sa.value(i), &query)))
95 .collect(),
96 None => BooleanArray::from(vec![false; n]),
97 };
98 Ok(ColumnarValue::Array(Arc::new(out)))
99 }
100}
101
102pub fn os_match_udf() -> Arc<ScalarUDF> {
104 static UDF: OnceLock<Arc<ScalarUDF>> = OnceLock::new();
105 UDF.get_or_init(|| Arc::new(ScalarUDF::from(MatchUdf::new()))).clone()
106}
107
108#[derive(Debug, PartialEq, Eq, Hash)]
113struct GeoUdf {
114 name: &'static str,
115 signature: Signature,
116}
117
118impl ScalarUDFImpl for GeoUdf {
119 fn name(&self) -> &str {
120 self.name
121 }
122 fn signature(&self) -> &Signature {
123 &self.signature
124 }
125 fn return_type(&self, _: &[DataType]) -> Result<DataType> {
126 Ok(DataType::Boolean)
127 }
128 fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
129 Ok(ColumnarValue::Array(Arc::new(BooleanArray::from(vec![
130 false;
131 args.number_rows
132 ]))))
133 }
134}
135
136fn geo_udf(name: &'static str, arity: usize) -> Arc<ScalarUDF> {
137 Arc::new(ScalarUDF::from(GeoUdf {
140 name,
141 signature: Signature::any(arity, Volatility::Immutable),
142 }))
143}
144
145pub fn os_geo_bbox_udf() -> Arc<ScalarUDF> {
147 static UDF: OnceLock<Arc<ScalarUDF>> = OnceLock::new();
148 UDF.get_or_init(|| geo_udf(OS_GEO_BBOX, 5)).clone()
149}
150
151pub fn os_geo_distance_udf() -> Arc<ScalarUDF> {
153 static UDF: OnceLock<Arc<ScalarUDF>> = OnceLock::new();
154 UDF.get_or_init(|| geo_udf(OS_GEO_DISTANCE, 4)).clone()
155}
156
157pub fn os_geo_polygon_udf() -> Arc<ScalarUDF> {
160 static UDF: OnceLock<Arc<ScalarUDF>> = OnceLock::new();
161 UDF.get_or_init(|| geo_udf(OS_GEO_POLYGON, 2)).clone()
162}
163
164pub fn all_udfs() -> [Arc<ScalarUDF>; 4] {
166 [
167 os_match_udf(),
168 os_geo_bbox_udf(),
169 os_geo_distance_udf(),
170 os_geo_polygon_udf(),
171 ]
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn tokenise_splits_lowercases_and_drops_empties() {
180 assert_eq!(tokens("Engine Overheating!"), vec!["engine", "overheating"]);
181 assert_eq!(tokens("fault(sensor,7)"), vec!["fault", "sensor", "7"]);
182 assert!(tokens(" ").is_empty());
183 }
184
185 #[test]
186 fn or_token_match_is_case_insensitive_and_not_substring() {
187 assert!(matches("engine overheating detected", "ENGINE")); assert!(matches("brakes worn", "engine brakes")); assert!(!matches("engine", "eng")); assert!(!matches("anything", " ")); }
192
193 #[test]
194 fn udfs_are_named_and_boolean() {
195 let udf = os_match_udf();
196 assert_eq!(udf.name(), OS_MATCH);
197 assert_eq!(
198 udf.return_type(&[DataType::Utf8, DataType::Utf8]).unwrap(),
199 DataType::Boolean
200 );
201 let names: Vec<String> = all_udfs().iter().map(|u| u.name().to_string()).collect();
202 assert_eq!(names, [OS_MATCH, OS_GEO_BBOX, OS_GEO_DISTANCE, OS_GEO_POLYGON]);
203 }
204}