feat(opensearch): add amount and customer_id as filters and handle name for different indexes (#7073)

This commit is contained in:
Sandeep Kumar
2025-02-07 17:10:12 +05:30
committed by GitHub
parent 7dfe400401
commit df328c5e52
3 changed files with 162 additions and 65 deletions

View File

@ -456,7 +456,7 @@ pub struct OpenSearchQueryBuilder {
pub query: String, pub query: String,
pub offset: Option<i64>, pub offset: Option<i64>,
pub count: Option<i64>, pub count: Option<i64>,
pub filters: Vec<(String, Vec<String>)>, pub filters: Vec<(String, Vec<Value>)>,
pub time_range: Option<OpensearchTimeRange>, pub time_range: Option<OpensearchTimeRange>,
search_params: Vec<AuthInfo>, search_params: Vec<AuthInfo>,
case_sensitive_fields: HashSet<&'static str>, case_sensitive_fields: HashSet<&'static str>,
@ -477,6 +477,8 @@ impl OpenSearchQueryBuilder {
"search_tags.keyword", "search_tags.keyword",
"card_last_4.keyword", "card_last_4.keyword",
"payment_id.keyword", "payment_id.keyword",
"amount",
"customer_id.keyword",
]), ]),
} }
} }
@ -492,7 +494,7 @@ impl OpenSearchQueryBuilder {
Ok(()) Ok(())
} }
pub fn add_filter_clause(&mut self, lhs: String, rhs: Vec<String>) -> QueryResult<()> { pub fn add_filter_clause(&mut self, lhs: String, rhs: Vec<Value>) -> QueryResult<()> {
self.filters.push((lhs, rhs)); self.filters.push((lhs, rhs));
Ok(()) Ok(())
} }
@ -505,9 +507,18 @@ impl OpenSearchQueryBuilder {
} }
} }
pub fn get_amount_field(&self, index: SearchIndex) -> &str {
match index {
SearchIndex::Refunds | SearchIndex::SessionizerRefunds => "refund_amount",
SearchIndex::Disputes | SearchIndex::SessionizerDisputes => "dispute_amount",
_ => "amount",
}
}
pub fn build_filter_array( pub fn build_filter_array(
&self, &self,
case_sensitive_filters: Vec<&(String, Vec<String>)>, case_sensitive_filters: Vec<&(String, Vec<Value>)>,
index: SearchIndex,
) -> Vec<Value> { ) -> Vec<Value> {
let mut filter_array = Vec::new(); let mut filter_array = Vec::new();
if !self.query.is_empty() { if !self.query.is_empty() {
@ -522,7 +533,14 @@ impl OpenSearchQueryBuilder {
let case_sensitive_json_filters = case_sensitive_filters let case_sensitive_json_filters = case_sensitive_filters
.into_iter() .into_iter()
.map(|(k, v)| json!({"terms": {k: v}})) .map(|(k, v)| {
let key = if *k == "amount" {
self.get_amount_field(index).to_string()
} else {
k.clone()
};
json!({"terms": {key: v}})
})
.collect::<Vec<Value>>(); .collect::<Vec<Value>>();
filter_array.extend(case_sensitive_json_filters); filter_array.extend(case_sensitive_json_filters);
@ -542,7 +560,7 @@ impl OpenSearchQueryBuilder {
pub fn build_case_insensitive_filters( pub fn build_case_insensitive_filters(
&self, &self,
mut payload: Value, mut payload: Value,
case_insensitive_filters: &[&(String, Vec<String>)], case_insensitive_filters: &[&(String, Vec<Value>)],
auth_array: Vec<Value>, auth_array: Vec<Value>,
index: SearchIndex, index: SearchIndex,
) -> Value { ) -> Value {
@ -690,22 +708,16 @@ impl OpenSearchQueryBuilder {
/// Ensure that the input data and the structure of the query are valid and correctly handled. /// Ensure that the input data and the structure of the query are valid and correctly handled.
pub fn construct_payload(&self, indexes: &[SearchIndex]) -> QueryResult<Vec<Value>> { pub fn construct_payload(&self, indexes: &[SearchIndex]) -> QueryResult<Vec<Value>> {
let mut query_obj = Map::new(); let mut query_obj = Map::new();
let mut bool_obj = Map::new(); let bool_obj = Map::new();
let (case_sensitive_filters, case_insensitive_filters): (Vec<_>, Vec<_>) = self let (case_sensitive_filters, case_insensitive_filters): (Vec<_>, Vec<_>) = self
.filters .filters
.iter() .iter()
.partition(|(k, _)| self.case_sensitive_fields.contains(k.as_str())); .partition(|(k, _)| self.case_sensitive_fields.contains(k.as_str()));
let filter_array = self.build_filter_array(case_sensitive_filters);
if !filter_array.is_empty() {
bool_obj.insert("filter".to_string(), Value::Array(filter_array));
}
let should_array = self.build_auth_array(); let should_array = self.build_auth_array();
query_obj.insert("bool".to_string(), Value::Object(bool_obj)); query_obj.insert("bool".to_string(), Value::Object(bool_obj.clone()));
let mut sort_obj = Map::new(); let mut sort_obj = Map::new();
sort_obj.insert( sort_obj.insert(
@ -724,6 +736,16 @@ impl OpenSearchQueryBuilder {
Value::Object(sort_obj.clone()) Value::Object(sort_obj.clone())
] ]
}); });
let filter_array = self.build_filter_array(case_sensitive_filters.clone(), *index);
if !filter_array.is_empty() {
payload
.get_mut("query")
.and_then(|query| query.get_mut("bool"))
.and_then(|bool_obj| bool_obj.as_object_mut())
.map(|bool_map| {
bool_map.insert("filter".to_string(), Value::Array(filter_array));
});
}
payload = self.build_case_insensitive_filters( payload = self.build_case_insensitive_filters(
payload, payload,
&case_insensitive_filters, &case_insensitive_filters,

View File

@ -5,12 +5,17 @@ use api_models::analytics::search::{
use common_utils::errors::{CustomResult, ReportSwitchExt}; use common_utils::errors::{CustomResult, ReportSwitchExt};
use error_stack::ResultExt; use error_stack::ResultExt;
use router_env::tracing; use router_env::tracing;
use serde_json::Value;
use crate::{ use crate::{
enums::AuthInfo, enums::AuthInfo,
opensearch::{OpenSearchClient, OpenSearchError, OpenSearchQuery, OpenSearchQueryBuilder}, opensearch::{OpenSearchClient, OpenSearchError, OpenSearchQuery, OpenSearchQueryBuilder},
}; };
pub fn convert_to_value<T: Into<Value>>(items: Vec<T>) -> Vec<Value> {
items.into_iter().map(|item| item.into()).collect()
}
pub async fn msearch_results( pub async fn msearch_results(
client: &OpenSearchClient, client: &OpenSearchClient,
req: GetGlobalSearchRequest, req: GetGlobalSearchRequest,
@ -38,21 +43,24 @@ pub async fn msearch_results(
if let Some(currency) = filters.currency { if let Some(currency) = filters.currency {
if !currency.is_empty() { if !currency.is_empty() {
query_builder query_builder
.add_filter_clause("currency.keyword".to_string(), currency.clone()) .add_filter_clause("currency.keyword".to_string(), convert_to_value(currency))
.switch()?; .switch()?;
} }
}; };
if let Some(status) = filters.status { if let Some(status) = filters.status {
if !status.is_empty() { if !status.is_empty() {
query_builder query_builder
.add_filter_clause("status.keyword".to_string(), status.clone()) .add_filter_clause("status.keyword".to_string(), convert_to_value(status))
.switch()?; .switch()?;
} }
}; };
if let Some(payment_method) = filters.payment_method { if let Some(payment_method) = filters.payment_method {
if !payment_method.is_empty() { if !payment_method.is_empty() {
query_builder query_builder
.add_filter_clause("payment_method.keyword".to_string(), payment_method.clone()) .add_filter_clause(
"payment_method.keyword".to_string(),
convert_to_value(payment_method),
)
.switch()?; .switch()?;
} }
}; };
@ -61,15 +69,17 @@ pub async fn msearch_results(
query_builder query_builder
.add_filter_clause( .add_filter_clause(
"customer_email.keyword".to_string(), "customer_email.keyword".to_string(),
customer_email convert_to_value(
.iter() customer_email
.filter_map(|email| { .iter()
// TODO: Add trait based inputs instead of converting this to strings .filter_map(|email| {
serde_json::to_value(email) // TODO: Add trait based inputs instead of converting this to strings
.ok() serde_json::to_value(email)
.and_then(|a| a.as_str().map(|a| a.to_string())) .ok()
}) .and_then(|a| a.as_str().map(|a| a.to_string()))
.collect(), })
.collect(),
),
) )
.switch()?; .switch()?;
} }
@ -79,15 +89,17 @@ pub async fn msearch_results(
query_builder query_builder
.add_filter_clause( .add_filter_clause(
"feature_metadata.search_tags.keyword".to_string(), "feature_metadata.search_tags.keyword".to_string(),
search_tags convert_to_value(
.iter() search_tags
.filter_map(|search_tag| { .iter()
// TODO: Add trait based inputs instead of converting this to strings .filter_map(|search_tag| {
serde_json::to_value(search_tag) // TODO: Add trait based inputs instead of converting this to strings
.ok() serde_json::to_value(search_tag)
.and_then(|a| a.as_str().map(|a| a.to_string())) .ok()
}) .and_then(|a| a.as_str().map(|a| a.to_string()))
.collect(), })
.collect(),
),
) )
.switch()?; .switch()?;
} }
@ -95,7 +107,7 @@ pub async fn msearch_results(
if let Some(connector) = filters.connector { if let Some(connector) = filters.connector {
if !connector.is_empty() { if !connector.is_empty() {
query_builder query_builder
.add_filter_clause("connector.keyword".to_string(), connector.clone()) .add_filter_clause("connector.keyword".to_string(), convert_to_value(connector))
.switch()?; .switch()?;
} }
}; };
@ -104,7 +116,7 @@ pub async fn msearch_results(
query_builder query_builder
.add_filter_clause( .add_filter_clause(
"payment_method_type.keyword".to_string(), "payment_method_type.keyword".to_string(),
payment_method_type.clone(), convert_to_value(payment_method_type),
) )
.switch()?; .switch()?;
} }
@ -112,21 +124,47 @@ pub async fn msearch_results(
if let Some(card_network) = filters.card_network { if let Some(card_network) = filters.card_network {
if !card_network.is_empty() { if !card_network.is_empty() {
query_builder query_builder
.add_filter_clause("card_network.keyword".to_string(), card_network.clone()) .add_filter_clause(
"card_network.keyword".to_string(),
convert_to_value(card_network),
)
.switch()?; .switch()?;
} }
}; };
if let Some(card_last_4) = filters.card_last_4 { if let Some(card_last_4) = filters.card_last_4 {
if !card_last_4.is_empty() { if !card_last_4.is_empty() {
query_builder query_builder
.add_filter_clause("card_last_4.keyword".to_string(), card_last_4.clone()) .add_filter_clause(
"card_last_4.keyword".to_string(),
convert_to_value(card_last_4),
)
.switch()?; .switch()?;
} }
}; };
if let Some(payment_id) = filters.payment_id { if let Some(payment_id) = filters.payment_id {
if !payment_id.is_empty() { if !payment_id.is_empty() {
query_builder query_builder
.add_filter_clause("payment_id.keyword".to_string(), payment_id.clone()) .add_filter_clause(
"payment_id.keyword".to_string(),
convert_to_value(payment_id),
)
.switch()?;
}
};
if let Some(amount) = filters.amount {
if !amount.is_empty() {
query_builder
.add_filter_clause("amount".to_string(), convert_to_value(amount))
.switch()?;
}
};
if let Some(customer_id) = filters.customer_id {
if !customer_id.is_empty() {
query_builder
.add_filter_clause(
"customer_id.keyword".to_string(),
convert_to_value(customer_id),
)
.switch()?; .switch()?;
} }
}; };
@ -211,21 +249,24 @@ pub async fn search_results(
if let Some(currency) = filters.currency { if let Some(currency) = filters.currency {
if !currency.is_empty() { if !currency.is_empty() {
query_builder query_builder
.add_filter_clause("currency.keyword".to_string(), currency.clone()) .add_filter_clause("currency.keyword".to_string(), convert_to_value(currency))
.switch()?; .switch()?;
} }
}; };
if let Some(status) = filters.status { if let Some(status) = filters.status {
if !status.is_empty() { if !status.is_empty() {
query_builder query_builder
.add_filter_clause("status.keyword".to_string(), status.clone()) .add_filter_clause("status.keyword".to_string(), convert_to_value(status))
.switch()?; .switch()?;
} }
}; };
if let Some(payment_method) = filters.payment_method { if let Some(payment_method) = filters.payment_method {
if !payment_method.is_empty() { if !payment_method.is_empty() {
query_builder query_builder
.add_filter_clause("payment_method.keyword".to_string(), payment_method.clone()) .add_filter_clause(
"payment_method.keyword".to_string(),
convert_to_value(payment_method),
)
.switch()?; .switch()?;
} }
}; };
@ -234,15 +275,17 @@ pub async fn search_results(
query_builder query_builder
.add_filter_clause( .add_filter_clause(
"customer_email.keyword".to_string(), "customer_email.keyword".to_string(),
customer_email convert_to_value(
.iter() customer_email
.filter_map(|email| { .iter()
// TODO: Add trait based inputs instead of converting this to strings .filter_map(|email| {
serde_json::to_value(email) // TODO: Add trait based inputs instead of converting this to strings
.ok() serde_json::to_value(email)
.and_then(|a| a.as_str().map(|a| a.to_string())) .ok()
}) .and_then(|a| a.as_str().map(|a| a.to_string()))
.collect(), })
.collect(),
),
) )
.switch()?; .switch()?;
} }
@ -252,15 +295,17 @@ pub async fn search_results(
query_builder query_builder
.add_filter_clause( .add_filter_clause(
"feature_metadata.search_tags.keyword".to_string(), "feature_metadata.search_tags.keyword".to_string(),
search_tags convert_to_value(
.iter() search_tags
.filter_map(|search_tag| { .iter()
// TODO: Add trait based inputs instead of converting this to strings .filter_map(|search_tag| {
serde_json::to_value(search_tag) // TODO: Add trait based inputs instead of converting this to strings
.ok() serde_json::to_value(search_tag)
.and_then(|a| a.as_str().map(|a| a.to_string())) .ok()
}) .and_then(|a| a.as_str().map(|a| a.to_string()))
.collect(), })
.collect(),
),
) )
.switch()?; .switch()?;
} }
@ -268,7 +313,7 @@ pub async fn search_results(
if let Some(connector) = filters.connector { if let Some(connector) = filters.connector {
if !connector.is_empty() { if !connector.is_empty() {
query_builder query_builder
.add_filter_clause("connector.keyword".to_string(), connector.clone()) .add_filter_clause("connector.keyword".to_string(), convert_to_value(connector))
.switch()?; .switch()?;
} }
}; };
@ -277,7 +322,7 @@ pub async fn search_results(
query_builder query_builder
.add_filter_clause( .add_filter_clause(
"payment_method_type.keyword".to_string(), "payment_method_type.keyword".to_string(),
payment_method_type.clone(), convert_to_value(payment_method_type),
) )
.switch()?; .switch()?;
} }
@ -285,21 +330,47 @@ pub async fn search_results(
if let Some(card_network) = filters.card_network { if let Some(card_network) = filters.card_network {
if !card_network.is_empty() { if !card_network.is_empty() {
query_builder query_builder
.add_filter_clause("card_network.keyword".to_string(), card_network.clone()) .add_filter_clause(
"card_network.keyword".to_string(),
convert_to_value(card_network),
)
.switch()?; .switch()?;
} }
}; };
if let Some(card_last_4) = filters.card_last_4 { if let Some(card_last_4) = filters.card_last_4 {
if !card_last_4.is_empty() { if !card_last_4.is_empty() {
query_builder query_builder
.add_filter_clause("card_last_4.keyword".to_string(), card_last_4.clone()) .add_filter_clause(
"card_last_4.keyword".to_string(),
convert_to_value(card_last_4),
)
.switch()?; .switch()?;
} }
}; };
if let Some(payment_id) = filters.payment_id { if let Some(payment_id) = filters.payment_id {
if !payment_id.is_empty() { if !payment_id.is_empty() {
query_builder query_builder
.add_filter_clause("payment_id.keyword".to_string(), payment_id.clone()) .add_filter_clause(
"payment_id.keyword".to_string(),
convert_to_value(payment_id),
)
.switch()?;
}
};
if let Some(amount) = filters.amount {
if !amount.is_empty() {
query_builder
.add_filter_clause("amount".to_string(), convert_to_value(amount))
.switch()?;
}
};
if let Some(customer_id) = filters.customer_id {
if !customer_id.is_empty() {
query_builder
.add_filter_clause(
"customer_id.keyword".to_string(),
convert_to_value(customer_id),
)
.switch()?; .switch()?;
} }
}; };

View File

@ -14,6 +14,8 @@ pub struct SearchFilters {
pub card_network: Option<Vec<String>>, pub card_network: Option<Vec<String>>,
pub card_last_4: Option<Vec<String>>, pub card_last_4: Option<Vec<String>>,
pub payment_id: Option<Vec<String>>, pub payment_id: Option<Vec<String>>,
pub amount: Option<Vec<u64>>,
pub customer_id: Option<Vec<String>>,
} }
impl SearchFilters { impl SearchFilters {
pub fn is_all_none(&self) -> bool { pub fn is_all_none(&self) -> bool {
@ -27,6 +29,8 @@ impl SearchFilters {
&& self.card_network.is_none() && self.card_network.is_none()
&& self.card_last_4.is_none() && self.card_last_4.is_none()
&& self.payment_id.is_none() && self.payment_id.is_none()
&& self.amount.is_none()
&& self.customer_id.is_none()
} }
} }