This is an automated email from the ASF dual-hosted git repository.
dheres pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/arrow-datafusion.git
The following commit(s) were added to refs/heads/master by this push:
new 15c19c314 Optimize `regex_replace` for scalar patterns (#3614)
15c19c314 is described below
commit 15c19c3140f4a03b169416ac88074065da5946e7
Author: Batuhan Taskaya <[email protected]>
AuthorDate: Tue Sep 27 09:32:42 2022 +0300
Optimize `regex_replace` for scalar patterns (#3614)
* Optimize `regex_replace` for scalar patterns
* Change the hot-path on `regexp_replace` to only variadic source (#2)
---
datafusion/physical-expr/src/functions.rs | 14 +-
datafusion/physical-expr/src/regex_expressions.rs | 288 ++++++++++++++++++++--
2 files changed, 282 insertions(+), 20 deletions(-)
diff --git a/datafusion/physical-expr/src/functions.rs
b/datafusion/physical-expr/src/functions.rs
index a34c1ab90..6997adc46 100644
--- a/datafusion/physical-expr/src/functions.rs
+++ b/datafusion/physical-expr/src/functions.rs
@@ -500,20 +500,22 @@ pub fn create_physical_fun(
BuiltinScalarFunction::RegexpReplace => {
Arc::new(|args| match args[0].data_type() {
DataType::Utf8 => {
- let func = invoke_if_regex_expressions_feature_flag!(
- regexp_replace,
+ let specializer_func =
invoke_if_regex_expressions_feature_flag!(
+ specialize_regexp_replace,
i32,
"regexp_replace"
);
- make_scalar_function(func)(args)
+ let func = specializer_func(args)?;
+ func(args)
}
DataType::LargeUtf8 => {
- let func = invoke_if_regex_expressions_feature_flag!(
- regexp_replace,
+ let specializer_func =
invoke_if_regex_expressions_feature_flag!(
+ specialize_regexp_replace,
i64,
"regexp_replace"
);
- make_scalar_function(func)(args)
+ let func = specializer_func(args)?;
+ func(args)
}
other => Err(DataFusionError::Internal(format!(
"Unsupported data type {:?} for function regexp_replace",
diff --git a/datafusion/physical-expr/src/regex_expressions.rs
b/datafusion/physical-expr/src/regex_expressions.rs
index c09c3c265..89805cade 100644
--- a/datafusion/physical-expr/src/regex_expressions.rs
+++ b/datafusion/physical-expr/src/regex_expressions.rs
@@ -21,16 +21,32 @@
//! Regex expressions
-use arrow::array::{ArrayRef, GenericStringArray, OffsetSizeTrait};
+use arrow::array::{
+ new_null_array, Array, ArrayRef, GenericStringArray, OffsetSizeTrait,
+};
use arrow::compute;
use datafusion_common::{DataFusionError, Result};
+use datafusion_expr::{ColumnarValue, ScalarFunctionImplementation};
use hashbrown::HashMap;
use lazy_static::lazy_static;
use regex::Regex;
use std::any::type_name;
use std::sync::Arc;
-macro_rules! downcast_string_arg {
+use crate::functions::make_scalar_function;
+
+macro_rules! fetch_string_arg {
+ ($ARG:expr, $NAME:expr, $T:ident, $EARLY_ABORT:ident) => {{
+ let array = downcast_string_array_arg!($ARG, $NAME, $T);
+ if array.is_null(0) {
+ return $EARLY_ABORT(array);
+ } else {
+ array.value(0)
+ }
+ }};
+}
+
+macro_rules! downcast_string_array_arg {
($ARG:expr, $NAME:expr, $T:ident) => {{
$ARG.as_any()
.downcast_ref::<GenericStringArray<T>>()
@@ -48,14 +64,14 @@ macro_rules! downcast_string_arg {
pub fn regexp_match<T: OffsetSizeTrait>(args: &[ArrayRef]) -> Result<ArrayRef>
{
match args.len() {
2 => {
- let values = downcast_string_arg!(args[0], "string", T);
- let regex = downcast_string_arg!(args[1], "pattern", T);
+ let values = downcast_string_array_arg!(args[0], "string", T);
+ let regex = downcast_string_array_arg!(args[1], "pattern", T);
compute::regexp_match(values, regex,
None).map_err(DataFusionError::ArrowError)
}
3 => {
- let values = downcast_string_arg!(args[0], "string", T);
- let regex = downcast_string_arg!(args[1], "pattern", T);
- let flags = Some(downcast_string_arg!(args[2], "flags", T));
+ let values = downcast_string_array_arg!(args[0], "string", T);
+ let regex = downcast_string_array_arg!(args[1], "pattern", T);
+ let flags = Some(downcast_string_array_arg!(args[2], "flags", T));
compute::regexp_match(values, regex,
flags).map_err(DataFusionError::ArrowError)
}
other => Err(DataFusionError::Internal(format!(
@@ -80,14 +96,17 @@ fn regex_replace_posix_groups(replacement: &str) -> String {
///
/// example: `regexp_replace('Thomas', '.[mN]a.', 'M') = 'ThM'`
pub fn regexp_replace<T: OffsetSizeTrait>(args: &[ArrayRef]) ->
Result<ArrayRef> {
+ // Default implementation for regexp_replace, assumes all args are arrays
+ // and args is a sequence of 3 or 4 elements.
+
// creating Regex is expensive so create hashmap for memoization
let mut patterns: HashMap<String, Regex> = HashMap::new();
match args.len() {
3 => {
- let string_array = downcast_string_arg!(args[0], "string", T);
- let pattern_array = downcast_string_arg!(args[1], "pattern", T);
- let replacement_array = downcast_string_arg!(args[2],
"replacement", T);
+ let string_array = downcast_string_array_arg!(args[0], "string",
T);
+ let pattern_array = downcast_string_array_arg!(args[1], "pattern",
T);
+ let replacement_array = downcast_string_array_arg!(args[2],
"replacement", T);
let result = string_array
.iter()
@@ -120,10 +139,10 @@ pub fn regexp_replace<T: OffsetSizeTrait>(args:
&[ArrayRef]) -> Result<ArrayRef>
Ok(Arc::new(result) as ArrayRef)
}
4 => {
- let string_array = downcast_string_arg!(args[0], "string", T);
- let pattern_array = downcast_string_arg!(args[1], "pattern", T);
- let replacement_array = downcast_string_arg!(args[2],
"replacement", T);
- let flags_array = downcast_string_arg!(args[3], "flags", T);
+ let string_array = downcast_string_array_arg!(args[0], "string",
T);
+ let pattern_array = downcast_string_array_arg!(args[1], "pattern",
T);
+ let replacement_array = downcast_string_array_arg!(args[2],
"replacement", T);
+ let flags_array = downcast_string_array_arg!(args[3], "flags", T);
let result = string_array
.iter()
@@ -178,10 +197,125 @@ pub fn regexp_replace<T: OffsetSizeTrait>(args:
&[ArrayRef]) -> Result<ArrayRef>
}
}
+fn _regexp_replace_early_abort<T: OffsetSizeTrait>(
+ input_array: &GenericStringArray<T>,
+) -> Result<ArrayRef> {
+ // Mimicking the existing behavior of regexp_replace, if any of the scalar
arguments
+ // are actuall null, then the result will be an array of the same size but
with nulls.
+ Ok(new_null_array(input_array.data_type(), input_array.len()))
+}
+
+/// Special cased regex_replace implementation for the scenerio where
+/// the pattern, replacement and flags are static (arrays that are derived
+/// from scalars). This means we can skip regex caching system and basically
+/// hold a single Regex object for the replace operation. This also speeds
+/// up the pre-processing time of the replacement string, since it only
+/// needs to processed once.
+fn _regexp_replace_static_pattern_replace<T: OffsetSizeTrait>(
+ args: &[ArrayRef],
+) -> Result<ArrayRef> {
+ let string_array = downcast_string_array_arg!(args[0], "string", T);
+ let pattern = fetch_string_arg!(args[1], "pattern", T,
_regexp_replace_early_abort);
+ let replacement =
+ fetch_string_arg!(args[2], "replacement", T,
_regexp_replace_early_abort);
+ let flags = match args.len() {
+ 3 => None,
+ 4 => Some(fetch_string_arg!(args[3], "flags", T,
_regexp_replace_early_abort)),
+ other => {
+ return Err(DataFusionError::Internal(format!(
+ "regexp_replace was called with {} arguments. It requires at
least 3 and at most 4.",
+ other
+ )))
+ }
+ };
+
+ // Embed the flag (if it exists) into the pattern. Limit will determine
+ // whether this is a global match (as in replace all) or just a single
+ // replace operation.
+ let (pattern, limit) = match flags {
+ Some("g") => (pattern.to_string(), 0),
+ Some(flags) => (
+ format!("(?{}){}", flags.to_string().replace('g', ""), pattern),
+ !flags.contains('g') as usize,
+ ),
+ None => (pattern.to_string(), 1),
+ };
+
+ let re = Regex::new(&pattern)
+ .map_err(|err| DataFusionError::Execution(err.to_string()))?;
+
+ // Replaces the posix groups in the replacement string
+ // with rust ones.
+ let replacement = regex_replace_posix_groups(replacement);
+
+ let result = string_array
+ .iter()
+ .map(|string| {
+ string.map(|string| re.replacen(string, limit,
replacement.as_str()))
+ })
+ .collect::<GenericStringArray<T>>();
+ Ok(Arc::new(result) as ArrayRef)
+}
+
+/// Determine which implementation of the regexp_replace to use based
+/// on the given set of arguments.
+pub fn specialize_regexp_replace<T: OffsetSizeTrait>(
+ args: &[ColumnarValue],
+) -> Result<ScalarFunctionImplementation> {
+ // This will serve as a dispatch table where we can
+ // leverage it in order to determine whether the scalarity
+ // of the given set of arguments fits a better specialized
+ // function.
+ let (is_source_scalar, is_pattern_scalar, is_replacement_scalar,
is_flags_scalar) = (
+ matches!(args[0], ColumnarValue::Scalar(_)),
+ matches!(args[1], ColumnarValue::Scalar(_)),
+ matches!(args[2], ColumnarValue::Scalar(_)),
+ // The forth argument (flags) is optional; so in the event that
+ // it is not available, we'll claim that it is scalar.
+ matches!(args.get(3), Some(ColumnarValue::Scalar(_)) | None),
+ );
+
+ match (
+ is_source_scalar,
+ is_pattern_scalar,
+ is_replacement_scalar,
+ is_flags_scalar,
+ ) {
+ // This represents a very hot path for the case where the there is
+ // a single pattern that is being matched against and a single
replacement.
+ // This is extremely important to specialize on since it removes the
overhead
+ // of DF's in-house regex pattern cache (since there will be at most a
single
+ // pattern) and the pre-processing of the same replacement pattern at
each
+ // query.
+ //
+ // The flags needs to be a scalar as well since each pattern is
actually
+ // constructed with the flags embedded into the pattern itself. This
means
+ // even if the pattern itself is scalar, if the flags are an array then
+ // we will create many regexes and it is best to use the implementation
+ // that caches it. If there are no flags, we can simply ignore it here,
+ // and let the specialized function handle it.
+ (_, true, true, true) => {
+ // We still don't know the scalarity of source, so we need the
adapter
+ // even if it will do some extra work for the pattern and the
flags.
+ //
+ // TODO: maybe we need a way of telling the adapter on which
arguments
+ // it can skip filling (so that we won't create N - 1 redundant
cols).
+ Ok(make_scalar_function(
+ _regexp_replace_static_pattern_replace::<T>,
+ ))
+ }
+
+ // If there are no specialized implementations, we'll fall back to the
+ // generic implementation.
+ (_, _, _, _) => Ok(make_scalar_function(regexp_replace::<T>)),
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
use arrow::array::*;
+ use datafusion_common::ScalarValue;
#[test]
fn test_case_sensitive_regexp_match() {
@@ -231,4 +365,130 @@ mod tests {
assert_eq!(re.as_ref(), &expected);
}
+
+ #[test]
+ fn test_static_pattern_regexp_replace() {
+ let values = StringArray::from(vec!["abc"; 5]);
+ let patterns = StringArray::from(vec!["b"; 5]);
+ let replacements = StringArray::from(vec!["foo"; 5]);
+ let expected = StringArray::from(vec!["afooc"; 5]);
+
+ let re = _regexp_replace_static_pattern_replace::<i32>(&[
+ Arc::new(values),
+ Arc::new(patterns),
+ Arc::new(replacements),
+ ])
+ .unwrap();
+
+ assert_eq!(re.as_ref(), &expected);
+ }
+
+ #[test]
+ fn test_static_pattern_regexp_replace_with_flags() {
+ let values = StringArray::from(vec!["abc", "ABC", "aBc", "AbC",
"aBC"]);
+ let patterns = StringArray::from(vec!["b"; 5]);
+ let replacements = StringArray::from(vec!["foo"; 5]);
+ let flags = StringArray::from(vec!["i"; 5]);
+ let expected =
+ StringArray::from(vec!["afooc", "AfooC", "afooc", "AfooC",
"afooC"]);
+
+ let re = _regexp_replace_static_pattern_replace::<i32>(&[
+ Arc::new(values),
+ Arc::new(patterns),
+ Arc::new(replacements),
+ Arc::new(flags),
+ ])
+ .unwrap();
+
+ assert_eq!(re.as_ref(), &expected);
+ }
+
+ #[test]
+ fn test_static_pattern_regexp_replace_early_abort() {
+ let values = StringArray::from(vec!["abc"; 5]);
+ let patterns = StringArray::from(vec![None; 5]);
+ let replacements = StringArray::from(vec!["foo"; 5]);
+ let expected = StringArray::from(vec![None; 5]);
+
+ let re = _regexp_replace_static_pattern_replace::<i32>(&[
+ Arc::new(values),
+ Arc::new(patterns),
+ Arc::new(replacements),
+ ])
+ .unwrap();
+
+ assert_eq!(re.as_ref(), &expected);
+ }
+
+ #[test]
+ fn test_static_pattern_regexp_replace_early_abort_flags() {
+ let values = StringArray::from(vec!["abc"; 5]);
+ let patterns = StringArray::from(vec!["a"; 5]);
+ let replacements = StringArray::from(vec!["foo"; 5]);
+ let flags = StringArray::from(vec![None; 5]);
+ let expected = StringArray::from(vec![None; 5]);
+
+ let re = _regexp_replace_static_pattern_replace::<i32>(&[
+ Arc::new(values),
+ Arc::new(patterns),
+ Arc::new(replacements),
+ Arc::new(flags),
+ ])
+ .unwrap();
+
+ assert_eq!(re.as_ref(), &expected);
+ }
+
+ #[test]
+ fn test_static_pattern_regexp_replace_pattern_error() {
+ let values = StringArray::from(vec!["abc"; 5]);
+ // Delibaretely using an invalid pattern to see how the single pattern
+ // error is propagated on regexp_replace.
+ let patterns = StringArray::from(vec!["["; 5]);
+ let replacements = StringArray::from(vec!["foo"; 5]);
+
+ let re = _regexp_replace_static_pattern_replace::<i32>(&[
+ Arc::new(values),
+ Arc::new(patterns),
+ Arc::new(replacements),
+ ]);
+ let pattern_err = re.expect_err("broken pattern should have failed");
+ assert_eq!(
+ pattern_err.to_string(),
+ "Execution error: regex parse error:\n [\n ^\nerror:
unclosed character class"
+ );
+ }
+
+ #[test]
+ fn test_regexp_can_specialize_all_cases() {
+ macro_rules! make_scalar {
+ () => {
+
ColumnarValue::Scalar(ScalarValue::Utf8(Some("foo".to_string())))
+ };
+ }
+
+ macro_rules! make_array {
+ () => {
+ ColumnarValue::Array(
+ Arc::new(StringArray::from(vec!["bar"; 2])) as ArrayRef
+ )
+ };
+ }
+
+ for source in [make_scalar!(), make_array!()] {
+ for pattern in [make_scalar!(), make_array!()] {
+ for replacement in [make_scalar!(), make_array!()] {
+ for flags in [Some(make_scalar!()), Some(make_array!()),
None] {
+ let mut args =
+ vec![source.clone(), pattern.clone(),
replacement.clone()];
+ if let Some(flags) = flags {
+ args.push(flags.clone());
+ }
+ let regex_func =
specialize_regexp_replace::<i32>(&args);
+ assert!(regex_func.is_ok());
+ }
+ }
+ }
+ }
+ }
}