From a75bf093cc42e2d548f67f2279ff4b67a3895e06 Mon Sep 17 00:00:00 2001 From: Luca Soldaini Date: Wed, 6 Mar 2024 11:57:52 -0800 Subject: [PATCH] Added JQ syntax for replacements + min/max score. (#133) * added score, bugfix name * allow jq in spans, min score * e2e tests --- python/dolma/cli/mixer.py | 30 ++- src/filters.rs | 257 ++++++++++++++++++++- src/shard.rs | 245 ++++++++++++-------- tests/config/email-spans-jq.yaml | 27 +++ tests/data/expected/email-spans-jq.json.gz | Bin 0 -> 26104 bytes tests/python/test_mixer.py | 9 + 6 files changed, 456 insertions(+), 112 deletions(-) create mode 100644 tests/config/email-spans-jq.yaml create mode 100644 tests/data/expected/email-spans-jq.json.gz diff --git a/python/dolma/cli/mixer.py b/python/dolma/cli/mixer.py index 3b81d870..d425da64 100644 --- a/python/dolma/cli/mixer.py +++ b/python/dolma/cli/mixer.py @@ -32,7 +32,14 @@ class FilterConfig: @dataclass class SpanReplacementConfig: span: str = field(help="JSONPath expression for the span to replace") - min_score: float = field(default=0.5, help="Minimum score for the span to be replaced") + min_score: Optional[float] = field( + default=None, + help="Minimum score for the span to be replaced. Either min_score or max_score must be specified.", + ) + max_score: Optional[float] = field( + default=None, + help="Maximum score for the span to be replaced. Either min_score or max_score must be specified.", + ) replacement: str = field(default="", help="Replacement for the span") syntax: str = field( default="jsonpath", @@ -97,15 +104,30 @@ def run(cls, parsed_config: MixerConfig): } for span_replacement in stream_config.span_replacement: - if span_replacement.syntax not in ["jsonpath"]: - raise DolmaConfigError("Invalid span_replacement syntax; must be 'jsonpath'") + if span_replacement.syntax not in ["jsonpath", "jq"]: + raise DolmaConfigError("Invalid span_replacement syntax; must be 'jsonpath' or 'jq'") + + if span_replacement.min_score is None and span_replacement.max_score is None: + raise DolmaConfigError( + "Either min_score or max_score must be specified for span_replacement" + ) + + # add min_score and max_score to the config if they are specified + min_score_config = ( + {"min_score": span_replacement.min_score} if span_replacement.min_score is not None else {} + ) + max_score_config = ( + {"max_score": span_replacement.max_score} if span_replacement.max_score is not None else {} + ) # TODO: note that we are not using the syntax here yet; adding it later stream_config_dict.setdefault("span_replacement", []).append( { "span": str(span_replacement.span), - "min_score": float(span_replacement.min_score), "replacement": str(span_replacement.replacement), + "syntax": span_replacement.syntax, + **min_score_config, + **max_score_config, } ) diff --git a/src/filters.rs b/src/filters.rs index 21b7e44c..30f4c290 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -1,11 +1,252 @@ use std::io; -use crate::shard::shard_config::FilterConfig; +use crate::shard::shard_config::{FilterConfig, SpanReplacementConfig}; use jaq_interpret::{Ctx, Filter, FilterT, ParseCtx, RcIter, Val}; use jaq_std; use jsonpath_rust::JsonPathFinder; use serde_json::Value; +pub struct JqSelector { + pub selector: Filter, +} + +impl JqSelector { + pub fn new(selector_string: &str) -> Result { + let mut defs = ParseCtx::new(Vec::new()); + defs.insert_natives(jaq_core::core()); + defs.insert_defs(jaq_std::std()); + assert!(defs.errs.is_empty()); + + let (selector, errs) = jaq_parse::parse(selector_string, jaq_parse::main()); + if !errs.is_empty() { + return Err(io::Error::new( + io::ErrorKind::Other, + format!( + "Error parsing '{:?}' into filter: {:?}", + selector_string, errs + ), + )); + } + match selector { + Some(selector) => { + let selector: jaq_interpret::Filter = defs.compile(selector); + if !defs.errs.is_empty() { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("Error compiling '{:?}' into filter.", selector_string), + )); + } + + Ok(JqSelector { selector: selector }) + } + None => { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("Parsing '{:?}' resulted in no filter", selector_string), + )); + } + } + } + + // select returns array of results if the filter matches multiple elements, + // or a single result if the filter matches a single element. + // in case of no match, it returns null + pub fn select(&self, json: &Value) -> Result { + let inputs: RcIter> = RcIter::new(core::iter::empty()); + let out: Vec> = self + .selector + .run((Ctx::new(Vec::new(), &inputs), Val::from(json.clone()))) + .collect(); + if out.is_empty() { + return Ok(Value::Null); + } + let mut result = Vec::new(); + for resp in out { + match resp { + Ok(val) => result.push(val), + Err(e) => { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("Error evaluating filter: {:?}", e), + )) + } + } + } + + match result.len() { + 0 => Ok(Value::Null), + 1 => Ok(Value::from(result[0].clone())), + _ => Ok(Value::from(result)), + } + } +} + +pub struct JsonPathSelector { + pub path: String, +} + +impl JsonPathSelector { + pub fn new(path: &str) -> Result { + Ok(JsonPathSelector { + path: path.to_string(), + }) + } + + pub fn select(&self, json: &Value) -> Result { + match JsonPathFinder::from_str("{}", &self.path) { + Ok(mut finder) => { + finder.set_json(Box::new(json.clone())); + match finder.find() { + Value::Array(arr) => match arr.len() { + 0 => Ok(Value::Null), + 1 => Ok(arr[0].clone()), + _ => Ok(Value::from(arr)), + }, + Value::Null => Ok(Value::Null), + _ => Err(io::Error::new( + io::ErrorKind::Other, + format!("Error evaluating filter: {:?}", self.path), + )), + } + } + Err(e) => Err(io::Error::new( + io::ErrorKind::Other, + format!("Error evaluating filter: {:?}", e), + )), + } + } +} + +pub enum Selector { + JqSelector(JqSelector), + JsonPathSelector(JsonPathSelector), +} + +impl Selector { + pub fn new(selector_config: &SpanReplacementConfig) -> Result { + match selector_config.syntax.as_deref() { + Some("jq") => Ok(Selector::JqSelector(JqSelector::new( + &selector_config.span, + )?)), + Some("jsonpath") | None => Ok(Selector::JsonPathSelector(JsonPathSelector::new( + &selector_config.span, + )?)), + _ => Err(io::Error::new( + io::ErrorKind::Other, + format!("Unknown selector syntax: {:?}", selector_config.syntax), + )), + } + } + + pub fn select(&self, json: &Value) -> Result { + match self { + Selector::JqSelector(selector) => selector.select(json), + Selector::JsonPathSelector(selector) => selector.select(json), + } + } +} + +#[cfg(test)] +pub mod selector_tests { + use super::*; + use serde_json::json; + + #[test] + fn test_select() { + let doc = json!({ + "attributes": { + "foo": "bar", + "baz": "qux" + } + }); + let expected = json!("bar"); + + let jq_selector = JqSelector::new(".attributes.foo").unwrap(); + assert_eq!(jq_selector.select(&doc).unwrap(), expected); + + let jsonpath_selector = JsonPathSelector::new("$.attributes.foo").unwrap(); + assert_eq!(jsonpath_selector.select(&doc).unwrap(), expected); + } + + #[test] + fn test_select_array() { + let doc = json!({ + "attributes": { + "foo": [1, 2, 3], + "baz": "qux" + } + }); + let expected = json!([1, 2, 3]); + + let jq_selector = JqSelector::new(".attributes.foo").unwrap(); + assert_eq!(jq_selector.select(&doc).unwrap(), expected); + + let jsonpath_selector = JsonPathSelector::new("$.attributes.foo").unwrap(); + assert_eq!(jsonpath_selector.select(&doc).unwrap(), expected); + } + + #[test] + fn test_select_object() { + let jq_selector = JqSelector::new(".attributes").unwrap(); + let doc = json!({ + "attributes": { + "foo": "bar", + "baz": "qux" + } + }); + assert_eq!( + jq_selector.select(&doc).unwrap(), + json!({"foo": "bar", "baz": "qux"}) + ); + } + + #[test] + fn test_select_null() { + let doc = json!({ + "attributes": { + "baz": "qux" + } + }); + let expected = json!(null); + + let jq_selector = JqSelector::new(".attributes.foo").unwrap(); + assert_eq!(jq_selector.select(&doc).unwrap(), expected); + + let jsonpath_selector = JsonPathSelector::new("$.attributes.foo").unwrap(); + assert_eq!(jsonpath_selector.select(&doc).unwrap(), expected); + } + + #[test] + fn test_nested_select_null() { + let doc = json!({ + "attributes": { + "not_foo": { + "baz": "qux" + } + } + }); + let expected = json!(null); + + let jq_selector = JqSelector::new(".attributes?.foo?.baz?").unwrap(); + assert_eq!(jq_selector.select(&doc).unwrap(), expected); + + let jsonpath_selector = JsonPathSelector::new("$.attributes.foo.baz").unwrap(); + assert_eq!(jsonpath_selector.select(&doc).unwrap(), expected); + } + + #[test] + fn test_select_error() { + let doc = json!({ + "attributes": { + "foo": ["water", " & ", "bread"], + } + }); + + let jq_selector = JqSelector::new(".attributes.foo | add").unwrap(); + assert_eq!(jq_selector.select(&doc).unwrap(), json!("water & bread")); + } +} + pub struct JqDocFilter { pub include: Vec, pub exclude: Vec, @@ -157,7 +398,7 @@ impl DocFilter { pub fn new(filter_config: Option<&FilterConfig>) -> Result { match filter_config { Some(filter_config) => match filter_config.syntax.as_deref() { - Some("jaq") => Ok(DocFilter::JqDocFilter(JqDocFilter::new(filter_config)?)), + Some("jq") => Ok(DocFilter::JqDocFilter(JqDocFilter::new(filter_config)?)), Some("jsonpath") | None => Ok(DocFilter::JsonPathFilter(JsonPathFilter::new( filter_config, )?)), @@ -179,7 +420,7 @@ impl DocFilter { } #[cfg(test)] -mod tests { +mod filter_tests { use super::*; use serde_json::json; @@ -188,7 +429,7 @@ mod tests { let filter_config = FilterConfig { include: vec![".attributes.foo".to_string()], exclude: vec![r#".attributes.baz == "quac""#.to_string()], - syntax: Some("jaq".to_string()), + syntax: Some("jq".to_string()), }; let filters = DocFilter::new(Some(&filter_config)).unwrap(); let doc = json!({ @@ -205,7 +446,7 @@ mod tests { let filter_config = FilterConfig { include: vec![".attributes.foo".to_string()], exclude: vec![r#".attributes.baz == "qux""#.to_string()], - syntax: Some("jaq".to_string()), + syntax: Some("jq".to_string()), }; let filters = DocFilter::new(Some(&filter_config)).unwrap(); let doc = json!({ @@ -222,7 +463,7 @@ mod tests { let filter_config = FilterConfig { include: vec![".attributes.foo | length >= 3".to_string()], exclude: vec![], - syntax: Some("jaq".to_string()), + syntax: Some("jq".to_string()), }; let filters = DocFilter::new(Some(&filter_config)).unwrap(); let doc = json!({ @@ -293,7 +534,7 @@ mod tests { let filter_config = FilterConfig { include: vec![".attributes.foo | add >= 6".to_string()], exclude: vec![], - syntax: Some("jaq".to_string()), + syntax: Some("jq".to_string()), }; let filters = DocFilter::new(Some(&filter_config)).unwrap(); let doc = json!({ @@ -318,7 +559,7 @@ mod tests { let filter_config = FilterConfig { include: vec![".x | sum".to_string()], exclude: vec![], - syntax: Some("jaq".to_string()), + syntax: Some("jq".to_string()), }; let result = DocFilter::new(Some(&filter_config)); diff --git a/src/shard.rs b/src/shard.rs index bc50429a..175c0ece 100644 --- a/src/shard.rs +++ b/src/shard.rs @@ -175,7 +175,18 @@ impl Shard { let mut line_number = 0; let mut lines_written = 0; - let filter_tool = DocFilter::new(self.filter.as_ref())?; + // using the doc filters later to determine if we should keep the document + let doc_filters = DocFilter::new(self.filter.as_ref())?; + + // we have to create list of span replaces, potentially dealing with the fact + // there might not be any span replacements + let span_replacers = self + .span_replacements + .as_ref() + .unwrap_or(&Vec::new()) + .iter() + .map(|cfg| SpanReplacer::new(cfg)) + .collect::>(); for line in reader.lines() { match line { @@ -268,86 +279,93 @@ impl Shard { } } - let should_write = filter_tool + let should_write = doc_filters .should_keep(&data) .map_err(|s| io::Error::new(io::ErrorKind::Other, s))?; if should_write { - if self.span_replacements.is_some() { - let mut replacements = self - .span_replacements - .as_ref() - .unwrap() - .iter() - .flat_map(|r| r.find_spans_to_replace(&data).unwrap()) - .collect::>(); - if !replacements.is_empty() { - replacements.sort_by(|a, b| a.start.cmp(&b.start)); - - let mut new_text = String::new(); - let old_text = data["text"].as_str().unwrap().to_owned(); - let mut span_index = 0; - let mut i = 0; - let mut span_start_byte_index = 0; - let mut chars = old_text.char_indices(); - let mut byte_index_with_char = chars.next(); - while byte_index_with_char.is_some() { - let (byte_index, c) = byte_index_with_char.unwrap(); - if span_index < replacements.len() { - let is_inside_span = i >= replacements[span_index].start - && i < replacements[span_index].end; - if i == replacements[span_index].start { - span_start_byte_index = byte_index; - } - if !is_inside_span { - if i == replacements[span_index].end { - if !replacements[span_index].replacement.is_empty() - { - let replacement_text = replacements[span_index] - .replacement - .to_owned() - .replace( - "{}", - old_text - [span_start_byte_index..byte_index] - .to_owned() - .as_str(), - ); - new_text.push_str(&replacement_text); - } - while span_index < replacements.len() - && replacements[span_index].start < i - { - span_index += 1; - } + // if self.span_replacements.is_some() { + // let mut replacements = self + // .span_replacements + // .as_ref() + // .unwrap() + // .iter() + // .flat_map(|r| r.find_spans_to_replace(&data).unwrap()) + // .collect::>(); + + let mut replacements = span_replacers + .iter() + .map(|replacer| replacer.find_spans_to_replace(&data)) + .collect::>, io::Error>>()? + .into_iter() + .flatten() + .collect::>(); + + if !replacements.is_empty() { + replacements.sort_by(|a, b| a.start.cmp(&b.start)); + + let mut new_text = String::new(); + let old_text = data["text"].as_str().unwrap().to_owned(); + let mut span_index = 0; + let mut i = 0; + let mut span_start_byte_index = 0; + let mut chars = old_text.char_indices(); + let mut byte_index_with_char = chars.next(); + while byte_index_with_char.is_some() { + let (byte_index, c) = byte_index_with_char.unwrap(); + if span_index < replacements.len() { + let is_inside_span = i >= replacements[span_index].start + && i < replacements[span_index].end; + if i == replacements[span_index].start { + span_start_byte_index = byte_index; + } + if !is_inside_span { + if i == replacements[span_index].end { + if !replacements[span_index].replacement.is_empty() { + let replacement_text = replacements[span_index] + .replacement + .to_owned() + .replace( + "{}", + old_text[span_start_byte_index..byte_index] + .to_owned() + .as_str(), + ); + new_text.push_str(&replacement_text); } - if span_index < replacements.len() - && replacements[span_index].start == i + while span_index < replacements.len() + && replacements[span_index].start < i { - span_start_byte_index = byte_index; - } else { - new_text.push(c); + span_index += 1; } } - } else { - new_text.push(c); + if span_index < replacements.len() + && replacements[span_index].start == i + { + span_start_byte_index = byte_index; + } else { + new_text.push(c); + } } - i += 1; - byte_index_with_char = chars.next(); + } else { + new_text.push(c); } - if span_index < replacements.len() - && !replacements[span_index].replacement.is_empty() - { - let replacement_text = - replacements[span_index].replacement.to_owned().replace( - "{}", - old_text[span_start_byte_index..].to_owned().as_str(), - ); - new_text.push_str(&replacement_text); - } - data["text"] = Value::String(new_text); + i += 1; + byte_index_with_char = chars.next(); + } + if span_index < replacements.len() + && !replacements[span_index].replacement.is_empty() + { + let replacement_text = + replacements[span_index].replacement.to_owned().replace( + "{}", + old_text[span_start_byte_index..].to_owned().as_str(), + ); + new_text.push_str(&replacement_text); } + data["text"] = Value::String(new_text); } + // } for f in self.discard_fields.iter().flatten() { data.as_object_mut().unwrap().remove(f); } @@ -407,9 +425,11 @@ impl Shard { } pub mod shard_config { + use crate::filters::Selector; use jsonpath_rust::JsonPathFinder; use serde::{Deserialize, Serialize}; use serde_json::Value; + use std::io; #[derive(Serialize, Deserialize, Clone)] pub struct StreamConfig { @@ -449,8 +469,10 @@ pub mod shard_config { #[derive(Serialize, Deserialize, Clone)] pub struct SpanReplacementConfig { pub span: String, - pub min_score: f64, + pub min_score: Option, + pub max_score: Option, pub replacement: String, + pub syntax: Option, } pub struct SpanReplacement { @@ -459,40 +481,63 @@ pub mod shard_config { pub replacement: String, } - impl SpanReplacementConfig { + pub struct SpanReplacer { + selector: Selector, + min_score: f64, + max_score: f64, + replacement: String, + } + + impl SpanReplacer { + // Create a new SpanReplacer from a SpanReplacementConfig + pub fn new(config: &SpanReplacementConfig) -> SpanReplacer { + SpanReplacer { + selector: Selector::new(&config).unwrap(), + min_score: config.min_score.unwrap_or(f64::NEG_INFINITY), + max_score: config.max_score.unwrap_or(f64::INFINITY), + replacement: config.replacement.clone(), + } + } + // Search for the configured attribute name in the given json // Attribute must contains a list of [start, end, score] spans. // Return a list of spans to be replaced. - pub fn find_spans_to_replace(&self, json: &Value) -> Result, String> { - let mut finder = JsonPathFinder::from_str("{}", &self.span)?; - finder.set_json(Box::new(json.clone())); - let spans = finder.find(); - if spans == Value::Null { - return Ok(Vec::new()); + pub fn find_spans_to_replace( + &self, + json: &Value, + ) -> Result, io::Error> { + match self.selector.select(json) { + // we found an array of spans; we process them one by one + Ok(Value::Array(spans)) => { + let replacements: Vec = spans + .iter() + .filter_map(|span| { + let span = span.as_array().unwrap(); + let start = span[0].as_u64().unwrap(); + let end = span[1].as_u64().unwrap(); + let score = span[2].as_f64().unwrap(); + if score >= self.min_score && score < self.max_score { + let replacement = SpanReplacement { + start: start as usize, + end: end as usize, + replacement: self.replacement.clone(), + }; + Some(replacement) + } else { + None + } + }) + .collect::>(); + Ok(replacements) + } + // we found no spans, so it's okay to return empty array + Ok(Value::Null) => Ok(Vec::new()), + Err(e) => Err(e), + Ok(spans) => Err(io::Error::new( + io::ErrorKind::Other, + format!("Invalid span type: {}; expected array or null.", spans), + )), } - let replacements: Vec = spans - .as_array() - .unwrap() - .iter() - .flat_map(|span| span.as_array().unwrap().iter()) - .filter_map(|span| { - let span = span.as_array().unwrap(); - let start = span[0].as_u64().unwrap(); - let end = span[1].as_u64().unwrap(); - let score = span[2].as_f64().unwrap(); - if score >= self.min_score { - let replacement = SpanReplacement { - start: start as usize, - end: end as usize, - replacement: self.replacement.clone(), - }; - Some(replacement) - } else { - None - } - }) - .collect::>(); - Ok(replacements) } } diff --git a/tests/config/email-spans-jq.yaml b/tests/config/email-spans-jq.yaml new file mode 100644 index 00000000..544b930d --- /dev/null +++ b/tests/config/email-spans-jq.yaml @@ -0,0 +1,27 @@ +streams: + - name: email-spans-jq + documents: + - tests/data/provided/documents/*.json.gz + output: + max_size_in_bytes: 100000 + path: tests/work/output/email-spans-jq + attributes: + - pii + span_replacement: + - min_score: 0.5 + max_score: 0.9 + replacement: '[B-EMAIL]{}[E-EMAIL]' + span: .attributes?.pii?.email? + syntax: jq + - min_score: 0.5 + max_score: 0.9 + replacement: '' + span: .attributes?.pii?.company_name? + syntax: jq + +work_dir: + input: tests/work/temp/email-spans-jq/input + output: tests/work/temp/email-spans-jq/output + + +processes: 1 diff --git a/tests/data/expected/email-spans-jq.json.gz b/tests/data/expected/email-spans-jq.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..773e520d58e331544ab91674518eaca9fe004be3 GIT binary patch literal 26104 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 P|9