feat(wfe-opensearch): add OpenSearch search index provider
OpenSearchIndex implementing SearchIndex trait. Index mapping for workflow documents. Bool queries with multi_match for terms, term filters for status/reference, range filters for dates. Pagination support.
This commit is contained in:
198
wfe-opensearch/tests/search.rs
Normal file
198
wfe-opensearch/tests/search.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
use chrono::Utc;
|
||||
use opensearch::http::transport::Transport;
|
||||
use opensearch::OpenSearch;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
use wfe_core::models::{WorkflowInstance, WorkflowStatus};
|
||||
use wfe_core::traits::search::{SearchFilter, SearchIndex};
|
||||
use wfe_opensearch::OpenSearchIndex;
|
||||
|
||||
const OPENSEARCH_URL: &str = "http://localhost:9200";
|
||||
|
||||
/// Check if OpenSearch is reachable, skip test if not.
|
||||
async fn opensearch_available() -> bool {
|
||||
let transport = Transport::single_node(OPENSEARCH_URL);
|
||||
if transport.is_err() {
|
||||
return false;
|
||||
}
|
||||
let client = OpenSearch::new(transport.unwrap());
|
||||
client
|
||||
.ping()
|
||||
.send()
|
||||
.await
|
||||
.map(|r| r.status_code().is_success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Helper to create a unique test index and return the provider + cleanup handle.
|
||||
async fn setup() -> Option<(OpenSearchIndex, String)> {
|
||||
if !opensearch_available().await {
|
||||
eprintln!("OpenSearch not available, skipping test");
|
||||
return None;
|
||||
}
|
||||
let index_name = format!("wfe_test_{}", Uuid::new_v4());
|
||||
let provider = OpenSearchIndex::new(OPENSEARCH_URL, &index_name).unwrap();
|
||||
provider.start().await.unwrap();
|
||||
Some((provider, index_name))
|
||||
}
|
||||
|
||||
/// Refresh the index so documents become searchable immediately.
|
||||
async fn refresh_index(provider: &OpenSearchIndex) {
|
||||
let url = format!("/{}/_refresh", provider.index_name());
|
||||
provider
|
||||
.client()
|
||||
.send(
|
||||
opensearch::http::Method::Post,
|
||||
&url,
|
||||
opensearch::http::headers::HeaderMap::new(),
|
||||
Option::<&serde_json::Value>::None,
|
||||
Some(b"".as_ref()),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Delete the test index.
|
||||
async fn cleanup(provider: &OpenSearchIndex) {
|
||||
let _ = provider
|
||||
.client()
|
||||
.indices()
|
||||
.delete(opensearch::indices::IndicesDeleteParts::Index(&[
|
||||
provider.index_name(),
|
||||
]))
|
||||
.send()
|
||||
.await;
|
||||
}
|
||||
|
||||
fn make_instance(description: Option<&str>, reference: Option<&str>) -> WorkflowInstance {
|
||||
let mut instance = WorkflowInstance::new("test-workflow", 1, json!({"key": "value"}));
|
||||
instance.description = description.map(String::from);
|
||||
instance.reference = reference.map(String::from);
|
||||
instance
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn index_and_search_by_terms() {
|
||||
let Some((provider, _index)) = setup().await else {
|
||||
return;
|
||||
};
|
||||
|
||||
let instance = make_instance(Some("Process the quarterly financial report"), None);
|
||||
provider.index_workflow(&instance).await.unwrap();
|
||||
refresh_index(&provider).await;
|
||||
|
||||
let page = provider
|
||||
.search("quarterly financial", 0, 10, &[])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(page.total, 1);
|
||||
assert_eq!(page.data.len(), 1);
|
||||
assert_eq!(page.data[0].id, instance.id);
|
||||
assert_eq!(
|
||||
page.data[0].description.as_deref(),
|
||||
Some("Process the quarterly financial report")
|
||||
);
|
||||
|
||||
cleanup(&provider).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn search_with_status_filter() {
|
||||
let Some((provider, _index)) = setup().await else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut runnable = make_instance(Some("Runnable workflow"), None);
|
||||
runnable.status = WorkflowStatus::Runnable;
|
||||
|
||||
let mut complete = make_instance(Some("Complete workflow"), None);
|
||||
complete.status = WorkflowStatus::Complete;
|
||||
complete.complete_time = Some(Utc::now());
|
||||
|
||||
provider.index_workflow(&runnable).await.unwrap();
|
||||
provider.index_workflow(&complete).await.unwrap();
|
||||
refresh_index(&provider).await;
|
||||
|
||||
let page = provider
|
||||
.search("", 0, 10, &[SearchFilter::Status(WorkflowStatus::Complete)])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(page.total, 1);
|
||||
assert_eq!(page.data[0].id, complete.id);
|
||||
assert_eq!(page.data[0].status, WorkflowStatus::Complete);
|
||||
|
||||
cleanup(&provider).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn search_with_no_results() {
|
||||
let Some((provider, _index)) = setup().await else {
|
||||
return;
|
||||
};
|
||||
|
||||
let instance = make_instance(Some("A regular workflow"), None);
|
||||
provider.index_workflow(&instance).await.unwrap();
|
||||
refresh_index(&provider).await;
|
||||
|
||||
let page = provider
|
||||
.search("nonexistent-xyzzy-42", 0, 10, &[])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(page.total, 0);
|
||||
assert!(page.data.is_empty());
|
||||
|
||||
cleanup(&provider).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn index_multiple_and_paginate() {
|
||||
let Some((provider, _index)) = setup().await else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut instances = Vec::new();
|
||||
for i in 0..5 {
|
||||
let instance = make_instance(Some(&format!("Paginated workflow number {i}")), None);
|
||||
provider.index_workflow(&instance).await.unwrap();
|
||||
instances.push(instance);
|
||||
}
|
||||
refresh_index(&provider).await;
|
||||
|
||||
// Search all, but skip 2 and take 2
|
||||
let page = provider.search("Paginated workflow", 2, 2, &[]).await.unwrap();
|
||||
|
||||
assert_eq!(page.total, 5);
|
||||
assert_eq!(page.data.len(), 2);
|
||||
|
||||
cleanup(&provider).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn search_by_reference() {
|
||||
let Some((provider, _index)) = setup().await else {
|
||||
return;
|
||||
};
|
||||
|
||||
let inst1 = make_instance(Some("First workflow"), Some("REF-001"));
|
||||
let inst2 = make_instance(Some("Second workflow"), Some("REF-002"));
|
||||
|
||||
provider.index_workflow(&inst1).await.unwrap();
|
||||
provider.index_workflow(&inst2).await.unwrap();
|
||||
refresh_index(&provider).await;
|
||||
|
||||
let page = provider
|
||||
.search("", 0, 10, &[SearchFilter::Reference("REF-001".to_string())])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(page.total, 1);
|
||||
assert_eq!(page.data[0].id, inst1.id);
|
||||
assert_eq!(page.data[0].reference.as_deref(), Some("REF-001"));
|
||||
|
||||
cleanup(&provider).await;
|
||||
}
|
||||
Reference in New Issue
Block a user