use serde_json::{Value, json};
use crate::types::{
PageCursor, SearchModifier, SearchParamType, SearchParameter, SearchPrefix, SearchQuery,
SortDirection, SortDirective,
};
use super::fts;
use super::modifier_handlers;
use super::parameter_handlers::{composite, date, number, quantity, reference, string, token, uri};
#[derive(Debug, Clone)]
pub struct EsQuery {
pub body: Value,
pub index: String,
}
pub struct EsQueryBuilder<'a> {
tenant_id: &'a str,
#[allow(dead_code)]
resource_type: &'a str,
index: String,
}
impl<'a> EsQueryBuilder<'a> {
pub fn new(tenant_id: &'a str, resource_type: &'a str, index: String) -> Self {
Self {
tenant_id,
resource_type,
index,
}
}
pub fn build(&self, query: &SearchQuery) -> EsQuery {
let mut must_clauses: Vec<Value> = Vec::new();
let filter_clauses: Vec<Value> = vec![
json!({ "term": { "tenant_id": self.tenant_id } }),
json!({ "term": { "is_deleted": false } }),
];
for param in &query.parameters {
if let Some(clause) = self.build_parameter_clause(param) {
must_clauses.push(clause);
}
}
let mut bool_query = json!({
"filter": filter_clauses,
});
if !must_clauses.is_empty() {
bool_query["must"] = json!(must_clauses);
}
let mut body = json!({
"query": { "bool": bool_query },
});
let sort = self.build_sort(&query.sort);
body["sort"] = sort;
let count = query.count.unwrap_or(20);
body["size"] = json!(count);
if let Some(ref cursor_str) = query.cursor {
if let Ok(cursor) = PageCursor::decode(cursor_str) {
let search_after = self.build_search_after(&cursor);
body["search_after"] = search_after;
}
} else if let Some(offset) = query.offset {
body["from"] = json!(offset);
}
body["track_total_hits"] = json!(true);
EsQuery {
body,
index: self.index.clone(),
}
}
fn build_parameter_clause(&self, param: &SearchParameter) -> Option<Value> {
match param.name.as_str() {
"_id" => return self.build_id_clause(param),
"_lastUpdated" => return self.build_last_updated_clause(param),
"_text" => return fts::build_text_clause(param),
"_content" => return fts::build_content_clause(param),
_ => {}
}
if param.modifier == Some(SearchModifier::Missing) {
return modifier_handlers::build_missing_clause(param);
}
let clauses: Vec<Value> = param
.values
.iter()
.filter_map(|value| self.build_value_clause(param, &value.value, value.prefix))
.collect();
if clauses.is_empty() {
return None;
}
if clauses.len() == 1 {
Some(clauses.into_iter().next().unwrap())
} else {
Some(json!({
"bool": {
"should": clauses,
"minimum_should_match": 1
}
}))
}
}
fn build_value_clause(
&self,
param: &SearchParameter,
value: &str,
prefix: SearchPrefix,
) -> Option<Value> {
match param.param_type {
SearchParamType::String => string::build_clause(param, value),
SearchParamType::Token => token::build_clause(param, value),
SearchParamType::Date => date::build_clause(¶m.name, value, prefix),
SearchParamType::Number => number::build_clause(¶m.name, value, prefix),
SearchParamType::Quantity => quantity::build_clause(¶m.name, value, prefix),
SearchParamType::Reference => reference::build_clause(param, value),
SearchParamType::Uri => uri::build_clause(param, value),
SearchParamType::Composite => composite::build_clause(param, value),
SearchParamType::Special => None,
}
}
fn build_id_clause(&self, param: &SearchParameter) -> Option<Value> {
let ids: Vec<&str> = param.values.iter().map(|v| v.value.as_str()).collect();
if ids.len() == 1 {
Some(json!({ "term": { "resource_id": ids[0] } }))
} else {
Some(json!({ "terms": { "resource_id": ids } }))
}
}
fn build_last_updated_clause(&self, param: &SearchParameter) -> Option<Value> {
let mut range = serde_json::Map::new();
for value in ¶m.values {
match value.prefix {
SearchPrefix::Eq => {
range.insert("gte".to_string(), json!(value.value));
range.insert("lte".to_string(), json!(value.value));
}
SearchPrefix::Gt => {
range.insert("gt".to_string(), json!(value.value));
}
SearchPrefix::Lt => {
range.insert("lt".to_string(), json!(value.value));
}
SearchPrefix::Ge => {
range.insert("gte".to_string(), json!(value.value));
}
SearchPrefix::Le => {
range.insert("lte".to_string(), json!(value.value));
}
_ => {
range.insert("gte".to_string(), json!(value.value));
range.insert("lte".to_string(), json!(value.value));
}
}
}
if range.is_empty() {
None
} else {
Some(json!({ "range": { "last_updated": Value::Object(range) } }))
}
}
fn build_sort(&self, directives: &[SortDirective]) -> Value {
if directives.is_empty() {
return json!([
{ "last_updated": { "order": "desc" } },
{ "resource_id": { "order": "asc" } }
]);
}
let mut sort_clauses: Vec<Value> = Vec::new();
for directive in directives {
let order = match directive.direction {
SortDirection::Ascending => "asc",
SortDirection::Descending => "desc",
};
match directive.parameter.as_str() {
"_id" => {
sort_clauses.push(json!({ "resource_id": { "order": order } }));
}
"_lastUpdated" => {
sort_clauses.push(json!({ "last_updated": { "order": order } }));
}
name => {
sort_clauses.push(json!({
"search_params.string.value.keyword": {
"order": order,
"nested": {
"path": "search_params.string",
"filter": {
"term": { "search_params.string.name": name }
}
},
"missing": if order == "asc" { "_last" } else { "_first" }
}
}));
}
}
}
sort_clauses.push(json!({ "resource_id": { "order": "asc" } }));
Value::Array(sort_clauses)
}
fn build_search_after(&self, cursor: &PageCursor) -> Value {
let mut values: Vec<Value> = cursor
.sort_values()
.iter()
.map(|v| match v {
crate::types::CursorValue::String(s) => json!(s),
crate::types::CursorValue::Number(n) => json!(n),
crate::types::CursorValue::Decimal(d) => json!(d.to_string()),
crate::types::CursorValue::Boolean(b) => json!(b),
crate::types::CursorValue::Null => json!(null),
})
.collect();
values.push(json!(cursor.resource_id()));
Value::Array(values)
}
}
pub fn build_count_query(tenant_id: &str, resource_type: &str, query: &SearchQuery) -> Value {
let builder = EsQueryBuilder::new(tenant_id, resource_type, String::new());
let es_query = builder.build(query);
let mut body = es_query.body;
if let Some(obj) = body.as_object_mut() {
obj.remove("sort");
obj.remove("size");
obj.remove("from");
obj.remove("search_after");
}
body["size"] = json!(0);
body
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{SearchValue, SortDirection};
#[test]
fn test_basic_query_build() {
let query = SearchQuery::new("Patient");
let builder = EsQueryBuilder::new("acme", "Patient", "hfs_acme_patient".to_string());
let es_query = builder.build(&query);
let filters = &es_query.body["query"]["bool"]["filter"];
assert!(filters.is_array());
}
#[test]
fn test_id_parameter() {
let query = SearchQuery::new("Patient").with_parameter(SearchParameter {
name: "_id".to_string(),
param_type: SearchParamType::Token,
modifier: None,
values: vec![SearchValue::eq("123")],
chain: vec![],
components: vec![],
});
let builder = EsQueryBuilder::new("acme", "Patient", "hfs_acme_patient".to_string());
let es_query = builder.build(&query);
let body_str = serde_json::to_string(&es_query.body).unwrap();
assert!(body_str.contains("resource_id"));
}
#[test]
fn test_default_sort() {
let query = SearchQuery::new("Patient");
let builder = EsQueryBuilder::new("acme", "Patient", "hfs_acme_patient".to_string());
let es_query = builder.build(&query);
let sort = &es_query.body["sort"];
assert!(sort.is_array());
assert!(sort[0]["last_updated"]["order"].as_str() == Some("desc"));
}
#[test]
fn test_custom_sort() {
let query = SearchQuery::new("Patient").with_sort(SortDirective {
parameter: "_id".to_string(),
direction: SortDirection::Ascending,
});
let builder = EsQueryBuilder::new("acme", "Patient", "hfs_acme_patient".to_string());
let es_query = builder.build(&query);
let sort = &es_query.body["sort"];
assert!(sort[0]["resource_id"]["order"].as_str() == Some("asc"));
}
}