mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 12:06:56 +08:00
feat(global-search): dashboard globalsearch apis (#3831)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -1,5 +1,5 @@
|
||||
use aws_config::{self, meta::region::RegionProviderChain};
|
||||
use aws_sdk_lambda::{config::Region, types::InvocationType::Event, Client};
|
||||
use aws_config::{self, meta::region::RegionProviderChain, Region};
|
||||
use aws_sdk_lambda::{types::InvocationType::Event, Client};
|
||||
use aws_smithy_types::Blob;
|
||||
use common_utils::errors::CustomResult;
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
|
||||
@ -12,6 +12,7 @@ pub mod connector_events;
|
||||
pub mod health_check;
|
||||
pub mod outgoing_webhook_event;
|
||||
pub mod sdk_events;
|
||||
pub mod search;
|
||||
mod sqlx;
|
||||
mod types;
|
||||
use api_event::metrics::{ApiEventMetric, ApiEventMetricRow};
|
||||
@ -664,3 +665,42 @@ pub struct ReportConfig {
|
||||
pub dispute_function: String,
|
||||
pub region: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
#[serde(tag = "auth")]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum OpensearchAuth {
|
||||
Basic { username: String, password: String },
|
||||
Aws { region: String },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
pub struct OpensearchIndexes {
|
||||
pub payment_attempts: String,
|
||||
pub payment_intents: String,
|
||||
pub refunds: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
pub struct OpensearchConfig {
|
||||
host: String,
|
||||
auth: OpensearchAuth,
|
||||
indexes: OpensearchIndexes,
|
||||
}
|
||||
|
||||
impl Default for OpensearchConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
host: "https://localhost:9200".to_string(),
|
||||
auth: OpensearchAuth::Basic {
|
||||
username: "admin".to_string(),
|
||||
password: "admin".to_string(),
|
||||
},
|
||||
indexes: OpensearchIndexes {
|
||||
payment_attempts: "hyperswitch-payment-attempt-events".to_string(),
|
||||
payment_intents: "hyperswitch-payment-intent-events".to_string(),
|
||||
refunds: "hyperswitch-refund-events".to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
150
crates/analytics/src/search.rs
Normal file
150
crates/analytics/src/search.rs
Normal file
@ -0,0 +1,150 @@
|
||||
use api_models::analytics::search::{
|
||||
GetGlobalSearchRequest, GetSearchRequestWithIndex, GetSearchResponse, OpenMsearchOutput,
|
||||
OpensearchOutput, SearchIndex,
|
||||
};
|
||||
use aws_config::{self, meta::region::RegionProviderChain, Region};
|
||||
use common_utils::errors::CustomResult;
|
||||
use opensearch::{
|
||||
auth::Credentials,
|
||||
cert::CertificateValidation,
|
||||
http::{
|
||||
request::JsonBody,
|
||||
transport::{SingleNodeConnectionPool, TransportBuilder},
|
||||
Url,
|
||||
},
|
||||
MsearchParts, OpenSearch, SearchParts,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::{errors::AnalyticsError, OpensearchAuth, OpensearchConfig, OpensearchIndexes};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum OpensearchError {
|
||||
#[error("Opensearch connection error")]
|
||||
ConnectionError,
|
||||
#[error("Opensearch NON-200 response content: '{0}'")]
|
||||
ResponseNotOK(String),
|
||||
#[error("Opensearch response error")]
|
||||
ResponseError,
|
||||
}
|
||||
|
||||
pub fn search_index_to_opensearch_index(index: SearchIndex, config: &OpensearchIndexes) -> String {
|
||||
match index {
|
||||
SearchIndex::PaymentAttempts => config.payment_attempts.clone(),
|
||||
SearchIndex::PaymentIntents => config.payment_intents.clone(),
|
||||
SearchIndex::Refunds => config.refunds.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_opensearch_client(config: OpensearchConfig) -> Result<OpenSearch, OpensearchError> {
|
||||
let url = Url::parse(&config.host).map_err(|_| OpensearchError::ConnectionError)?;
|
||||
let transport = match config.auth {
|
||||
OpensearchAuth::Basic { username, password } => {
|
||||
let credentials = Credentials::Basic(username, password);
|
||||
TransportBuilder::new(SingleNodeConnectionPool::new(url))
|
||||
.cert_validation(CertificateValidation::None)
|
||||
.auth(credentials)
|
||||
.build()
|
||||
.map_err(|_| OpensearchError::ConnectionError)?
|
||||
}
|
||||
OpensearchAuth::Aws { region } => {
|
||||
let region_provider = RegionProviderChain::first_try(Region::new(region));
|
||||
let sdk_config = aws_config::from_env().region(region_provider).load().await;
|
||||
let conn_pool = SingleNodeConnectionPool::new(url);
|
||||
TransportBuilder::new(conn_pool)
|
||||
.auth(
|
||||
sdk_config
|
||||
.clone()
|
||||
.try_into()
|
||||
.map_err(|_| OpensearchError::ConnectionError)?,
|
||||
)
|
||||
.service_name("es")
|
||||
.build()
|
||||
.map_err(|_| OpensearchError::ConnectionError)?
|
||||
}
|
||||
};
|
||||
Ok(OpenSearch::new(transport))
|
||||
}
|
||||
|
||||
pub async fn msearch_results(
|
||||
req: GetGlobalSearchRequest,
|
||||
merchant_id: &String,
|
||||
config: OpensearchConfig,
|
||||
) -> CustomResult<Vec<GetSearchResponse>, AnalyticsError> {
|
||||
let client = get_opensearch_client(config.clone())
|
||||
.await
|
||||
.map_err(|_| AnalyticsError::UnknownError)?;
|
||||
|
||||
let mut msearch_vector: Vec<JsonBody<Value>> = vec![];
|
||||
for index in SearchIndex::iter() {
|
||||
msearch_vector
|
||||
.push(json!({"index": search_index_to_opensearch_index(index,&config.indexes)}).into());
|
||||
msearch_vector.push(json!({"query": {"bool": {"must": {"query_string": {"query": req.query}}, "filter": {"match_phrase": {"merchant_id": merchant_id}}}}}).into());
|
||||
}
|
||||
|
||||
let response = client
|
||||
.msearch(MsearchParts::None)
|
||||
.body(msearch_vector)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| AnalyticsError::UnknownError)?;
|
||||
|
||||
let response_body = response
|
||||
.json::<OpenMsearchOutput<Value>>()
|
||||
.await
|
||||
.map_err(|_| AnalyticsError::UnknownError)?;
|
||||
|
||||
Ok(response_body
|
||||
.responses
|
||||
.into_iter()
|
||||
.zip(SearchIndex::iter())
|
||||
.map(|(index_hit, index)| GetSearchResponse {
|
||||
count: index_hit.hits.total.value,
|
||||
index,
|
||||
hits: index_hit
|
||||
.hits
|
||||
.hits
|
||||
.into_iter()
|
||||
.map(|hit| hit._source)
|
||||
.collect(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn search_results(
|
||||
req: GetSearchRequestWithIndex,
|
||||
merchant_id: &String,
|
||||
config: OpensearchConfig,
|
||||
) -> CustomResult<GetSearchResponse, AnalyticsError> {
|
||||
let search_req = req.search_req;
|
||||
|
||||
let client = get_opensearch_client(config.clone())
|
||||
.await
|
||||
.map_err(|_| AnalyticsError::UnknownError)?;
|
||||
|
||||
let response = client
|
||||
.search(SearchParts::Index(&[&search_index_to_opensearch_index(req.index.clone(),&config.indexes)]))
|
||||
.from(search_req.offset)
|
||||
.size(search_req.count)
|
||||
.body(json!({"query": {"bool": {"must": {"query_string": {"query": search_req.query}}, "filter": {"match_phrase": {"merchant_id": merchant_id}}}}}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| AnalyticsError::UnknownError)?;
|
||||
|
||||
let response_body = response
|
||||
.json::<OpensearchOutput<Value>>()
|
||||
.await
|
||||
.map_err(|_| AnalyticsError::UnknownError)?;
|
||||
|
||||
Ok(GetSearchResponse {
|
||||
count: response_body.hits.total.value,
|
||||
index: req.index,
|
||||
hits: response_body
|
||||
.hits
|
||||
.hits
|
||||
.into_iter()
|
||||
.map(|hit| hit._source)
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user