Skip to content

Commit a780cd0

Browse files
committed
handle nested paths
1 parent 0fee875 commit a780cd0

File tree

2 files changed

+251
-12
lines changed

2 files changed

+251
-12
lines changed

quickwit/quickwit-serve/src/elasticsearch_api/rest_handler.rs

Lines changed: 233 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -487,22 +487,114 @@ async fn es_compat_index_field_capabilities(
487487
Ok(search_response_rest)
488488
}
489489

490+
fn filter_source(
491+
value: &mut serde_json::Value,
492+
_source_excludes: &Option<Vec<String>>,
493+
_source_includes: &Option<Vec<String>>,
494+
) {
495+
fn navigate_and_remove(value: &mut serde_json::Value, path: &str) {
496+
for (prefix, suffix) in generate_path_variants_with_suffix(path) {
497+
match value {
498+
serde_json::Value::Object(ref mut map) => {
499+
if let Some(suffix) = suffix {
500+
if let Some(sub_value) = map.get_mut(prefix) {
501+
navigate_and_remove(sub_value, suffix);
502+
return;
503+
}
504+
} else {
505+
map.remove(prefix);
506+
}
507+
}
508+
_ => continue,
509+
}
510+
}
511+
}
512+
fn navigate_and_include(
513+
value: &mut serde_json::Value,
514+
current_path: &str,
515+
include_paths: &Vec<String>,
516+
) {
517+
if let Some(ref mut map) = value.as_object_mut() {
518+
map.retain(|key, sub_value| {
519+
let path = if current_path.is_empty() {
520+
key.to_string()
521+
} else {
522+
format!("{}.{}", current_path, key)
523+
};
524+
525+
if include_paths.contains(&path) {
526+
// Exact match keep whole node
527+
return true;
528+
}
529+
// check if the path is sub path of any allowed path
530+
for allowed_path in include_paths {
531+
if allowed_path.starts_with(path.as_str()) {
532+
navigate_and_include(sub_value, &path, include_paths);
533+
return true;
534+
}
535+
}
536+
false
537+
});
538+
}
539+
}
540+
541+
// Remove fields that are not included
542+
if let Some(includes) = _source_includes {
543+
navigate_and_include(value, "", includes);
544+
}
545+
546+
// Exclude fields
547+
if let Some(excludes) = _source_excludes {
548+
for exclude in excludes {
549+
navigate_and_remove(value, exclude);
550+
}
551+
}
552+
}
553+
554+
/// "app.id.name" -> [("app", Some("id.name")), ("app.id", Some("name")), ("app.id.name", None)]
555+
fn generate_path_variants_with_suffix(input: &str) -> Vec<(&str, Option<&str>)> {
556+
let mut variants = Vec::new();
557+
558+
// Iterate over each character in the input.
559+
for (idx, ch) in input.char_indices() {
560+
if ch == '.' {
561+
// If a dot is found, create a variant using the current slice and the remainder of the
562+
// string.
563+
let prefix = &input[0..idx];
564+
let suffix = if idx + 1 < input.len() {
565+
Some(&input[idx + 1..])
566+
} else {
567+
None
568+
};
569+
variants.push((prefix, suffix));
570+
}
571+
}
572+
573+
// Add the final variant, which includes the entire string as the prefix and None as the suffix.
574+
variants.push((&input[0..], None));
575+
576+
variants
577+
}
578+
490579
fn convert_hit(
491580
hit: quickwit_proto::search::Hit,
492581
append_shard_doc: bool,
493582
_source_excludes: &Option<Vec<String>>,
494583
_source_includes: &Option<Vec<String>>,
495584
) -> ElasticHit {
496-
let mut fields: BTreeMap<String, serde_json::Value> =
497-
serde_json::from_str(&hit.json).unwrap_or_default();
498-
if let Some(_source_includes) = _source_includes {
499-
fields.retain(|key, _| _source_includes.contains(key));
500-
}
501-
if let Some(_source_excludes) = _source_excludes {
502-
for exclude in _source_excludes {
503-
fields.remove(exclude);
585+
let mut json: serde_json::Value = serde_json::from_str(&hit.json).unwrap_or(json!({}));
586+
filter_source(&mut json, _source_excludes, _source_includes);
587+
let source =
588+
Source::from_string(serde_json::to_string(&json).unwrap_or_else(|_| "{}".to_string()))
589+
.unwrap_or_else(|_| Source::from_string("{}".to_string()).unwrap());
590+
591+
let mut fields: BTreeMap<String, serde_json::Value> = Default::default();
592+
if let serde_json::Value::Object(map) = json {
593+
for (key, val) in map {
594+
fields.insert(key, val);
504595
}
505596
}
597+
506598
let mut sort = Vec::new();
507599
if let Some(partial_hit) = hit.partial_hit {
508600
if let Some(sort_value) = partial_hit.sort_value {
@@ -517,9 +609,6 @@ fn convert_hit(
517609
));
518610
}
519611
}
520-
let source =
521-
Source::from_string(serde_json::to_string(&fields).unwrap_or_else(|_| "{}".to_string()))
522-
.unwrap_or_else(|_| Source::from_string("{}".to_string()).unwrap());
523612

524613
ElasticHit {
525614
fields,
@@ -745,7 +834,7 @@ pub(crate) fn str_lines(body: &str) -> impl Iterator<Item = &str> {
745834
mod tests {
746835
use hyper::StatusCode;
747836

748-
use super::partial_hit_from_search_after_param;
837+
use super::{partial_hit_from_search_after_param, *};
749838

750839
#[test]
751840
fn test_partial_hit_from_search_after_param_invalid_length() {
@@ -791,4 +880,136 @@ mod tests {
791880
u32}`"
792881
);
793882
}
883+
884+
#[test]
885+
fn test_single_element() {
886+
let input = "app";
887+
let expected = vec![("app", None)];
888+
assert_eq!(generate_path_variants_with_suffix(input), expected);
889+
}
890+
891+
#[test]
892+
fn test_two_elements() {
893+
let input = "app.id";
894+
let expected = vec![("app", Some("id")), ("app.id", None)];
895+
assert_eq!(generate_path_variants_with_suffix(input), expected);
896+
}
897+
898+
#[test]
899+
fn test_multiple_elements() {
900+
let input = "app.id.name";
901+
let expected = vec![
902+
("app", Some("id.name")),
903+
("app.id", Some("name")),
904+
("app.id.name", None),
905+
];
906+
assert_eq!(generate_path_variants_with_suffix(input), expected);
907+
}
908+
909+
#[test]
910+
fn test_include_fields1() {
911+
let mut fields = json!({
912+
"app": { "id": 123, "name": "Blub" },
913+
"user": { "id": 456, "name": "Fred" }
914+
});
915+
916+
let includes = Some(vec!["app.id".to_string()]);
917+
filter_source(&mut fields, &None, &includes);
918+
919+
let expected = json!({
920+
"app": { "id": 123 }
921+
});
922+
923+
assert_eq!(fields, expected);
924+
}
925+
#[test]
926+
fn test_include_fields2() {
927+
let mut fields = json!({
928+
"app": { "id": 123, "name": "Blub" },
929+
"app.id": { "id": 123, "name": "Blub" },
930+
"user": { "id": 456, "name": "Fred" }
931+
});
932+
933+
let includes = Some(vec!["app".to_string(), "app.id".to_string()]);
934+
filter_source(&mut fields, &None, &includes);
935+
936+
let expected = json!({
937+
"app": { "id": 123, "name": "Blub" },
938+
"app.id": { "id": 123, "name": "Blub" },
939+
});
940+
941+
assert_eq!(fields, expected);
942+
}
943+
944+
#[test]
945+
fn test_exclude_fields() {
946+
let mut fields = json!({
947+
"app": {
948+
"id": 123,
949+
"name": "Blub"
950+
},
951+
"user": {
952+
"id": 456,
953+
"name": "Fred"
954+
}
955+
});
956+
957+
let excludes = Some(vec!["app.name".to_string(), "user.id".to_string()]);
958+
filter_source(&mut fields, &excludes, &None);
959+
960+
let expected = json!({
961+
"app": {
962+
"id": 123
963+
},
964+
"user": {
965+
"name": "Fred"
966+
}
967+
});
968+
969+
assert_eq!(fields, expected);
970+
}
971+
972+
#[test]
973+
fn test_include_and_exclude_fields() {
974+
let mut fields = json!({
975+
"app": { "id": 123, "name": "Blub", "version": "1.0" },
976+
"user": { "id": 456, "name": "Fred", "email": "[email protected]" }
977+
});
978+
979+
let includes = Some(vec![
980+
"app".to_string(),
981+
"user.name".to_string(),
982+
"user.email".to_string(),
983+
]);
984+
let excludes = Some(vec!["app.version".to_string(), "user.email".to_string()]);
985+
filter_source(&mut fields, &excludes, &includes);
986+
987+
let expected = json!({
988+
"app": { "id": 123, "name": "Blub" },
989+
"user": { "name": "Fred" }
990+
});
991+
992+
assert_eq!(fields, expected);
993+
}
994+
995+
#[test]
996+
fn test_no_includes_or_excludes() {
997+
let mut fields = json!({
998+
"app": {
999+
"id": 123,
1000+
"name": "Blub"
1001+
}
1002+
});
1003+
1004+
filter_source(&mut fields, &None, &None);
1005+
1006+
let expected = json!({
1007+
"app": {
1008+
"id": 123,
1009+
"name": "Blub"
1010+
}
1011+
});
1012+
1013+
assert_eq!(fields, expected);
1014+
}
7941015
}

quickwit/rest-api-tests/scenarii/es_compatibility/0022-source.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,21 @@ expected:
4747
- _source:
4848
$expect: "len(val) == 1" # Contains only 'actor'
4949
id: 5688
50+
--- # _source_includes with path
51+
params:
52+
_source_includes: "actor.id"
53+
json:
54+
size: 1
55+
query:
56+
match_all: {}
57+
expected:
58+
hits:
59+
total:
60+
value: 100
61+
relation: eq
62+
hits:
63+
- _source:
64+
actor:
65+
$expect: "len(val) == 1" # Contains only 'actor'
66+
id: 5688
67+

0 commit comments

Comments
 (0)