From fc79ff8d67536f9cffff4aa18be841b049070c42 Mon Sep 17 00:00:00 2001 From: dal Date: Tue, 25 Mar 2025 15:03:34 -0600 Subject: [PATCH] search functionality through rest --- .../handlers/src/search/search_handler.rs | 24 ++- api/libs/search/src/search.rs | 197 ++++-------------- api/libs/search/src/types.rs | 5 +- api/src/routes/rest/routes/search/mod.rs | 10 +- api/src/routes/rest/routes/search/search.rs | 12 +- .../routes/ws/collections/post_collection.rs | 10 +- 6 files changed, 61 insertions(+), 197 deletions(-) diff --git a/api/libs/handlers/src/search/search_handler.rs b/api/libs/handlers/src/search/search_handler.rs index 9b7ff1eb6..49d1bd101 100644 --- a/api/libs/handlers/src/search/search_handler.rs +++ b/api/libs/handlers/src/search/search_handler.rs @@ -1,23 +1,25 @@ -use anyhow::Result; -use uuid::Uuid; +use anyhow::{anyhow, Result}; +use middleware::AuthenticatedUser; use search::{search as search_lib, SearchObject, SearchObjectType, SearchOptions}; -use crate::utils::user::user_info::get_user_organization_id; pub async fn search_handler( - user_id: Uuid, + user: &AuthenticatedUser, query: String, num_results: Option, asset_types: Option>, ) -> Result> { let num_results = num_results.unwrap_or(50); let asset_types = asset_types.unwrap_or_else(Vec::new); - + let options = SearchOptions::with_custom_options(num_results, asset_types); - - let user_organization_id = get_user_organization_id(&user_id).await?; - - let results = search_lib(user_id, user_organization_id, query, options).await?; - + + let user_organization = match user.organizations.get(0) { + Some(org) => org, + None => return Err(anyhow!("User doesn't belong to an organization")), + }; + + let results = search_lib(user.id, user_organization.id, query, options).await?; + Ok(results) -} \ No newline at end of file +} diff --git a/api/libs/search/src/search.rs b/api/libs/search/src/search.rs index 1b079c070..4632fa400 100644 --- a/api/libs/search/src/search.rs +++ b/api/libs/search/src/search.rs @@ -5,9 +5,7 @@ use uuid::Uuid; use database::pool::get_sqlx_pool; -use crate::types::{ - GenericSearchResult, MessageSearchResult, SearchObject, SearchObjectType, SearchOptions, -}; +use crate::types::{GenericSearchResult, SearchObject, SearchObjectType, SearchOptions}; pub async fn search( user_id: Uuid, @@ -62,7 +60,7 @@ pub async fn search( let mut results = sqlx::query(&query).fetch(&mut *conn); let mut results_vec = Vec::new(); - + while let Some(row) = results.try_next().await? { let content: String = match row.try_get("content") { Ok(content) => content, @@ -97,33 +95,9 @@ pub async fn search( let highlights = find_highlights(&content, &search_terms); let search_object = match asset_type.as_str() { - "thread" => { - let content_json: serde_json::Value = - serde_json::from_str(&content).unwrap_or_default(); - - let title = content_json["title"] - .as_str() - .unwrap_or("Untitled Thread") - .to_string(); - - let summary_question = content_json["summary_question"] - .as_str() - .unwrap_or("") - .to_string(); - - SearchObject::Message(MessageSearchResult { - id, - title, - summary_question, - updated_at, - highlights, - score: rank, - type_: SearchObjectType::Thread, - }) - } "collection" => SearchObject::Collection(GenericSearchResult { id, - name: extract_name_from_content(&content), + name: content, updated_at, highlights, score: rank, @@ -131,51 +105,19 @@ pub async fn search( }), "dashboard" => SearchObject::Dashboard(GenericSearchResult { id, - name: extract_name_from_content(&content), + name: content, updated_at, highlights, score: rank, type_: SearchObjectType::Dashboard, }), - "data_source" => SearchObject::DataSource(GenericSearchResult { + "metric" => SearchObject::Metric(GenericSearchResult { id, - name: extract_name_from_content(&content), + name: content, updated_at, highlights, score: rank, - type_: SearchObjectType::DataSource, - }), - "dataset" => SearchObject::Dataset(GenericSearchResult { - id, - name: extract_name_from_content(&content), - updated_at, - highlights, - score: rank, - type_: SearchObjectType::Dataset, - }), - "permission_group" => SearchObject::PermissionGroup(GenericSearchResult { - id, - name: extract_name_from_content(&content), - updated_at, - highlights, - score: rank, - type_: SearchObjectType::PermissionGroup, - }), - "team" => SearchObject::Team(GenericSearchResult { - id, - name: extract_name_from_content(&content), - updated_at, - highlights, - score: rank, - type_: SearchObjectType::Team, - }), - "term" => SearchObject::Term(GenericSearchResult { - id, - name: extract_name_from_content(&content), - updated_at, - highlights, - score: rank, - type_: SearchObjectType::Term, + type_: SearchObjectType::Metric, }), _ => continue, }; @@ -198,6 +140,7 @@ pub async fn search( } SearchObject::Team(team) => !team.highlights.is_empty(), SearchObject::Term(term) => !term.highlights.is_empty(), + SearchObject::Metric(metric) => !metric.highlights.is_empty(), }) .collect(); } @@ -212,7 +155,11 @@ pub async fn list_recent_assets( let mut conn = get_sqlx_pool().acquire().await?; // Default to 50 results if not specified for empty query listing - let num_results = if options.num_results <= 0 { 50 } else { options.num_results }; + let num_results = if options.num_results <= 0 { + 50 + } else { + options.num_results + }; let query = format!( r#" @@ -246,7 +193,7 @@ pub async fn list_recent_assets( let mut results = sqlx::query(&query).fetch(&mut *conn); let mut results_vec = Vec::new(); - + while let Some(row) = results.try_next().await? { let id: Uuid = match row.try_get("asset_id") { Ok(id) => id, @@ -274,33 +221,9 @@ pub async fn list_recent_assets( }; let search_object = match asset_type.as_str() { - "thread" => { - let content_json: serde_json::Value = - serde_json::from_str(&content).unwrap_or_default(); - - let title = content_json["title"] - .as_str() - .unwrap_or("Untitled Thread") - .to_string(); - - let summary_question = content_json["summary_question"] - .as_str() - .unwrap_or("") - .to_string(); - - SearchObject::Message(MessageSearchResult { - id, - title, - summary_question, - updated_at, - highlights: vec![], - score: 0.0, - type_: SearchObjectType::Thread, - }) - } "collection" => SearchObject::Collection(GenericSearchResult { id, - name: extract_name_from_content(&content), + name: content.to_string(), updated_at, highlights: vec![], score: 0.0, @@ -308,51 +231,19 @@ pub async fn list_recent_assets( }), "dashboard" => SearchObject::Dashboard(GenericSearchResult { id, - name: extract_name_from_content(&content), + name: content.to_string(), updated_at, highlights: vec![], score: 0.0, type_: SearchObjectType::Dashboard, }), - "data_source" => SearchObject::DataSource(GenericSearchResult { + "metric" => SearchObject::Metric(GenericSearchResult { id, - name: extract_name_from_content(&content), + name: content.to_string(), updated_at, highlights: vec![], score: 0.0, - type_: SearchObjectType::DataSource, - }), - "dataset" => SearchObject::Dataset(GenericSearchResult { - id, - name: extract_name_from_content(&content), - updated_at, - highlights: vec![], - score: 0.0, - type_: SearchObjectType::Dataset, - }), - "permission_group" => SearchObject::PermissionGroup(GenericSearchResult { - id, - name: extract_name_from_content(&content), - updated_at, - highlights: vec![], - score: 0.0, - type_: SearchObjectType::PermissionGroup, - }), - "team" => SearchObject::Team(GenericSearchResult { - id, - name: extract_name_from_content(&content), - updated_at, - highlights: vec![], - score: 0.0, - type_: SearchObjectType::Team, - }), - "term" => SearchObject::Term(GenericSearchResult { - id, - name: extract_name_from_content(&content), - updated_at, - highlights: vec![], - score: 0.0, - type_: SearchObjectType::Term, + type_: SearchObjectType::Metric, }), _ => continue, }; @@ -363,65 +254,47 @@ pub async fn list_recent_assets( Ok(results_vec) } -fn extract_name_from_content(content: &str) -> String { - match serde_json::from_str::(content) { - Ok(json) => { - if let Some(name) = json["name"].as_str() { - return name.to_string(); - } - if let Some(title) = json["title"].as_str() { - return title.to_string(); - } - "Untitled".to_string() - } - Err(_) => "Untitled".to_string(), - } -} - fn find_highlights(content: &str, search_terms: &[String]) -> Vec { let mut highlights = Vec::new(); - + // Try to parse the content as JSON first if let Ok(json) = serde_json::from_str::(content) { // Convert the JSON back to a string for highlighting let content_str = json.to_string().to_lowercase(); - + for term in search_terms { if content_str.contains(term) { - // Here you would extract context around the match - let term_start = content_str.find(term).unwrap_or(0); - let context_start = term_start.saturating_sub(20); - let context_end = (term_start + term.len() + 20).min(content_str.len()); - let highlight = content_str[context_start..context_end].to_string(); - highlights.push(highlight); + highlights.push(term.clone()); } } } else { // If not JSON, treat as plain text let content_lower = content.to_lowercase(); - + for term in search_terms { if content_lower.contains(term) { - let term_start = content_lower.find(term).unwrap_or(0); - let context_start = term_start.saturating_sub(20); - let context_end = (term_start + term.len() + 20).min(content_lower.len()); - let highlight = content_lower[context_start..context_end].to_string(); - highlights.push(highlight); + highlights.push(term.clone()); } } } - + + highlights.dedup(); // Remove any duplicate matches highlights } fn sanitize_search_term(term: String) -> String { // Remove special characters that might interfere with the search - let term = term.replace(['(', ')', '[', ']', '{', '}', '\\', '*', '+', '.', '?', '^', '$', '|'], ""); - + let term = term.replace( + [ + '(', ')', '[', ']', '{', '}', '\\', '*', '+', '.', '?', '^', '$', '|', + ], + "", + ); + // If the term is now empty, use a default that will match nothing if term.is_empty() { return "NOMATCHPOSSIBLE".to_string(); } - + term -} \ No newline at end of file +} diff --git a/api/libs/search/src/types.rs b/api/libs/search/src/types.rs index 711ab541e..67ac17712 100644 --- a/api/libs/search/src/types.rs +++ b/api/libs/search/src/types.rs @@ -37,6 +37,7 @@ pub enum SearchObject { PermissionGroup(GenericSearchResult), Team(GenericSearchResult), Term(GenericSearchResult), + Metric(GenericSearchResult), } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -50,6 +51,7 @@ pub enum SearchObjectType { PermissionGroup, Team, Term, + Metric, } impl ToString for SearchObjectType { @@ -63,6 +65,7 @@ impl ToString for SearchObjectType { SearchObjectType::PermissionGroup => "permission_group".to_string(), SearchObjectType::Team => "team".to_string(), SearchObjectType::Term => "term".to_string(), + SearchObjectType::Metric => "metric".to_string(), } } } @@ -97,7 +100,7 @@ impl SearchOptions { pub fn asset_types_to_string(&self) -> String { if self.asset_types.is_empty() { // If no asset types specified, include all types - return "'thread', 'collection', 'dashboard', 'data_source', 'dataset', 'permission_group', 'team', 'term'".to_string(); + return "'collection', 'dashboard', 'metric'".to_string(); } self.asset_types diff --git a/api/src/routes/rest/routes/search/mod.rs b/api/src/routes/rest/routes/search/mod.rs index b729c419e..c20df7982 100644 --- a/api/src/routes/rest/routes/search/mod.rs +++ b/api/src/routes/rest/routes/search/mod.rs @@ -1,11 +1,7 @@ -use axum::{ - routing::get, - Router, -}; +use axum::{routing::post, Router}; mod search; pub fn router() -> Router { - Router::new() - .route("/", get(search::search)) -} \ No newline at end of file + Router::new().route("/", post(search::search)) +} diff --git a/api/src/routes/rest/routes/search/search.rs b/api/src/routes/rest/routes/search/search.rs index d8d4b1797..a702d9c32 100644 --- a/api/src/routes/rest/routes/search/search.rs +++ b/api/src/routes/rest/routes/search/search.rs @@ -1,8 +1,4 @@ -use axum::{ - extract::Query, - http::StatusCode, - Extension, -}; +use axum::{http::StatusCode, Extension, Json}; use serde::Deserialize; use handlers::search::search_handler; @@ -20,10 +16,10 @@ pub struct SearchQuery { pub async fn search( Extension(user): Extension, - Query(params): Query, + Json(params): Json, ) -> Result>, (StatusCode, &'static str)> { let results = match search_handler( - user.id, + &user, params.query, params.num_results, params.asset_types, @@ -38,4 +34,4 @@ pub async fn search( }; Ok(ApiResponse::JsonData(results)) -} \ No newline at end of file +} diff --git a/api/src/routes/ws/collections/post_collection.rs b/api/src/routes/ws/collections/post_collection.rs index a61cc1b30..f185ab409 100644 --- a/api/src/routes/ws/collections/post_collection.rs +++ b/api/src/routes/ws/collections/post_collection.rs @@ -121,13 +121,7 @@ async fn post_collection_handler( let insert_task_user_id = user_id.clone(); let insert_task_collection = collection.clone(); - let mut conn = match get_pg_pool().get().await { - Ok(conn) => conn, - Err(e) => { - tracing::error!("Error getting pg connection: {}", e); - return; - } - }; + let mut conn = get_pg_pool().get().await?; let asset_permissions = AssetPermission { identity_id: insert_task_user_id, @@ -166,7 +160,7 @@ async fn post_collection_handler( } } - k(CollectionState { + Ok(CollectionState { collection, assets: None, permission: AssetPermissionRole::Owner,