diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index 7150130..986f95e 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -1,9 +1,17 @@ name: Pull Request Actions on: + push: + branches: + - master + pull_request: branches: - master + types: + - opened + - reopened + - synchronize env: CARGO_TERM_COLOR: always @@ -20,8 +28,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Test - run: git status - name: Fmt run: cargo fmt --all --verbose --check @@ -43,3 +49,23 @@ jobs: - uses: actions/checkout@v4 - name: Test run: cargo test --all --verbose + + build-platforms: + if: github.event_name == 'push' + runs-on: ubuntu-latest + strategy: + matrix: + rust-target: [ + 'x86_64-unknown-linux-gnu', + 'aarch64-apple-darwin', + 'x86_64-apple-darwin' + ] + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + target: ${{ matrix.rust-target }} + - name: Add rustup target ${{ matrix.rust-target }} + run: rustup target add ${{ matrix.rust-target }} + - name: Build app for ${{ matrix.rust-target }} + run: cargo build --release --target ${{ matrix.rust-target }} Cargo.toml diff --git a/Cargo.toml b/Cargo.toml index 7a423de..fdeee58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" maintenance = { status = "actively-developed" } [features] +enable-cacher = ["dep:redis"] default = [] [dependencies] @@ -37,7 +38,8 @@ features = ["rustc-serialize", "serde"] [dependencies.redis] version = "0.27.3" -features = ["aio", "tokio-comp", "connection-manager"] +features = ["aio", "tokio-comp", "connection-manager", "serde_json", "json"] +optional = true [dependencies.reqwest] version = "0.12.8" diff --git a/config/development.toml b/config/development.toml index ff941b2..ce96e93 100644 --- a/config/development.toml +++ b/config/development.toml @@ -12,8 +12,8 @@ allowed = "*" max_age = 3600 [elastic] -address = "localhost:9200" -enabled_tls = "false" +address = "130.193.37.41:9200" +enabled_tls = "true" username = "elastic" password = "elastic" diff --git a/crates/elquery/src/filter/must_filter.rs b/crates/elquery/src/filter/must_filter.rs index a85adf3..91916c0 100644 --- a/crates/elquery/src/filter/must_filter.rs +++ b/crates/elquery/src/filter/must_filter.rs @@ -1,6 +1,7 @@ use serde_derive::Serialize; use serde_json::{json, Value}; +#[allow(dead_code)] trait MustFilterItemTrait {} impl MustFilterItemTrait for TermFilterItem {} impl MustFilterItemTrait for RangeFilterItem {} diff --git a/src/bin/doc-searcher-run.rs b/src/bin/doc-searcher-run.rs index 4e87421..7ea0b97 100644 --- a/src/bin/doc-searcher-run.rs +++ b/src/bin/doc-searcher-run.rs @@ -2,7 +2,6 @@ extern crate doc_search; use actix_web::middleware::Logger; use actix_web::{web, App, HttpServer}; -use doc_search::cacher::redis::RedisClient; use doc_search::cors::build_cors; use doc_search::elastic::ElasticClient; use doc_search::embeddings::native::EmbeddingsClient; @@ -15,6 +14,9 @@ use doc_search::storage::endpoints::build_scope as build_storage_scope; use doc_search::storage::{DocumentService, FolderService}; use doc_search::{config, swagger, Connectable}; +#[cfg(feature = "enable-cacher")] +use doc_search::cacher; + #[actix_web::main] async fn main() -> Result<(), anyhow::Error> { let s_config = config::ServiceConfig::new()?; @@ -24,14 +26,15 @@ async fn main() -> Result<(), anyhow::Error> { let logger_config = s_config.logger(); init_logger(logger_config)?; - let cacher_service = RedisClient::connect(s_config.cacher())?; let search_service = ElasticClient::connect(s_config.elastic())?; let embeddings_service = EmbeddingsClient::connect(s_config.embeddings())?; + #[cfg(feature = "enable-cacher")] + let cacher_service = cacher::redis::RedisClient::connect(s_config.cacher())?; + HttpServer::new(move || { let cors = build_cors(&cors_config.clone()); let logger = Logger::default(); - let cacher_cxt = Box::new(cacher_service.clone()); let documents_cxt: Box = Box::new(search_service.clone()); let folders_cxt: Box = Box::new(search_service.clone()); @@ -39,14 +42,23 @@ async fn main() -> Result<(), anyhow::Error> { let searcher_cxt: Box = Box::new(search_service.clone()); let embeddings_cxt: Box = Box::new(embeddings_service.clone()); - App::new() + let app = App::new() .app_data(web::Data::new(documents_cxt)) .app_data(web::Data::new(folders_cxt)) .app_data(web::Data::new(paginator_cxt)) - .app_data(web::Data::new(cacher_cxt)) .app_data(web::Data::new(searcher_cxt)) - .app_data(web::Data::new(embeddings_cxt)) - .wrap(logger) + .app_data(web::Data::new(embeddings_cxt)); + + #[cfg(feature = "enable-cacher")] + let cacher_search_cxt: cacher::redis::SearchParamsCached = Box::new(cacher_service.clone()); + #[cfg(feature = "enable-cacher")] + let cacher_paginate_cxt: cacher::redis::PaginatedCached = Box::new(cacher_service.clone()); + #[cfg(feature = "enable-cacher")] + let app = app + .app_data(web::Data::new(cacher_search_cxt)) + .app_data(web::Data::new(cacher_paginate_cxt)); + + app.wrap(logger) .wrap(cors) .service(build_metrics_scope()) .service(build_storage_scope()) diff --git a/src/cacher/mod.rs b/src/cacher/mod.rs index a1d25a7..e41cb1e 100644 --- a/src/cacher/mod.rs +++ b/src/cacher/mod.rs @@ -1,4 +1,6 @@ pub mod config; + +#[cfg(feature = "enable-cacher")] pub mod redis; #[async_trait::async_trait] diff --git a/src/cacher/redis/mod.rs b/src/cacher/redis/mod.rs index fc214fe..d38edc6 100644 --- a/src/cacher/redis/mod.rs +++ b/src/cacher/redis/mod.rs @@ -1,17 +1,23 @@ +mod models; + use crate::cacher::config::CacherConfig; use crate::cacher::CacherService; +use crate::searcher::forms::PaginateNextForm; +use crate::searcher::models::{Paginated, SearchParams}; use crate::Connectable; use getset::CopyGetters; use redis::{AsyncCommands, Client, RedisError, RedisResult}; +use serde_json::Value; use std::sync::Arc; use tokio::sync::RwLock; +pub type SearchParamsCached = Box>>>; +pub type PaginatedCached = Box>>>; + #[derive(Clone, CopyGetters)] pub struct RedisClient { - // #[getset(get_copy = "pub")] options: Arc, - // #[getset(get_copy = "pub")] client: Arc>, } diff --git a/src/cacher/redis/models.rs b/src/cacher/redis/models.rs new file mode 100644 index 0000000..92fcd75 --- /dev/null +++ b/src/cacher/redis/models.rs @@ -0,0 +1,91 @@ +use crate::searcher::forms::PaginateNextForm; +use crate::searcher::models::{Paginated, SearchParams}; +use crate::storage::models::Document; + +use redis::{RedisError, RedisResult, RedisWrite, Value}; +use serde::ser::Error; + +impl redis::ToRedisArgs for PaginateNextForm { + fn write_redis_args(&self, out: &mut W) + where + W: ?Sized + RedisWrite, + { + match serde_json::to_string(self) { + Ok(json_str) => out.write_arg_fmt(json_str), + Err(err) => { + tracing::error!("cacher: failed to serialize paginate form: {err:#?}"); + } + } + } +} + +impl redis::ToRedisArgs for SearchParams { + fn write_redis_args(&self, out: &mut W) + where + W: ?Sized + RedisWrite, + { + match serde_json::to_string(self) { + Ok(json_str) => out.write_arg_fmt(json_str), + Err(err) => { + tracing::error!("cacher: failed to serialize search parameters: {err:#?}"); + } + } + } +} + +impl redis::ToRedisArgs for Document { + fn write_redis_args(&self, out: &mut W) + where + W: ?Sized + RedisWrite, + { + match serde_json::to_string(self) { + Ok(json_str) => out.write_arg_fmt(json_str), + Err(err) => { + tracing::error!("cacher: failed to serialize document: {err:#?}"); + } + } + } +} + +impl redis::ToRedisArgs for Paginated> { + fn write_redis_args(&self, out: &mut W) + where + W: ?Sized + RedisWrite, + { + match serde_json::to_string(self) { + Ok(json_str) => out.write_arg_fmt(json_str), + Err(err) => { + tracing::error!("cacher: failed to serialize paginated docs: {err:#?}"); + } + } + } +} + +impl redis::FromRedisValue for Document { + fn from_redis_value(v: &Value) -> RedisResult { + match v { + Value::BulkString(data) => { + serde_json::from_slice::(data.as_slice()).map_err(RedisError::from) + } + _ => { + let err = serde_json::Error::custom("failed to extract redis value type"); + Err(RedisError::from(err)) + } + } + } +} + +impl redis::FromRedisValue for Paginated> { + fn from_redis_value(v: &Value) -> RedisResult { + match v { + Value::BulkString(data) => { + serde_json::from_slice::>>(data.as_slice()) + .map_err(RedisError::from) + } + _ => { + let err = serde_json::Error::custom("failed to extract redis value type"); + Err(RedisError::from(err)) + } + } + } +} diff --git a/src/searcher/elastic/extractor.rs b/src/searcher/elastic/extractor.rs index 7caf0e7..99abc59 100644 --- a/src/searcher/elastic/extractor.rs +++ b/src/searcher/elastic/extractor.rs @@ -204,76 +204,3 @@ impl SearcherTrait for InfoFolder { InfoFolder::deserialize(source_value).map_err(WebError::from) } } - -#[cfg(test)] -mod test_all_search_queries { - use super::*; - - #[tokio::test] - async fn test_document_build_query() -> Result<(), anyhow::Error> { - let s_params = build_search_params(); - let build_query = Document::build_query(&s_params).await; - println!("{}", serde_json::to_string_pretty(&build_query).unwrap()); - - Ok(()) - } - - #[tokio::test] - async fn test_document_preview_build_query() -> Result<(), anyhow::Error> { - let s_params = build_search_params(); - let build_query = DocumentPreview::build_query(&s_params).await; - println!("{}", serde_json::to_string_pretty(&build_query).unwrap()); - - Ok(()) - } - - #[tokio::test] - async fn test_document_vectors_build_query() -> Result<(), anyhow::Error> { - let s_params = build_search_params(); - let build_query = DocumentVectors::build_query(&s_params).await; - println!("{}", serde_json::to_string_pretty(&build_query).unwrap()); - - Ok(()) - } - - #[tokio::test] - async fn test_info_folder_system_build_query() -> Result<(), anyhow::Error> { - let s_params = build_search_params(); - let build_query = InfoFolder::build_query(&s_params).await; - println!("{}", serde_json::to_string_pretty(&build_query).unwrap()); - - Ok(()) - } - - #[tokio::test] - async fn test_info_folder_build_query() -> Result<(), anyhow::Error> { - let mut s_params = build_search_params(); - s_params.set_show_all(false); - - let build_query = InfoFolder::build_query(&s_params).await; - println!("{}", serde_json::to_string_pretty(&build_query).unwrap()); - - Ok(()) - } - - fn build_search_params() -> SearchParams { - SearchParams::builder() - .query("Some query".to_string()) - .query_tokens(Some(Vec::default())) - .folder_ids(Some("test-folder-id".to_string())) - .document_type("document".to_string()) - .document_extension("txt".to_string()) - .created_date_to("2025-04-26T11:14:55Z".to_string()) - .created_date_from("2024-04-26T11:14:55Z".to_string()) - .document_size_to(37000) - .document_size_from(0) - .result_size(25) - .result_offset(0) - .scroll_lifetime("1m".to_string()) - .knn_amount(Some(5)) - .knn_candidates(Some(100)) - .show_all(Some(true)) - .build() - .unwrap() - } -} diff --git a/src/searcher/elastic/helper.rs b/src/searcher/elastic/helper.rs index 6be212a..92c47bc 100644 --- a/src/searcher/elastic/helper.rs +++ b/src/searcher/elastic/helper.rs @@ -118,7 +118,7 @@ pub async fn search( indexes: &[&str], ) -> Result>, WebError> where - T: DocumentsTrait + SearcherTrait, + T: DocumentsTrait + SearcherTrait + serde::Serialize, { let body_value = T::build_query(s_params).await; let response = send_search_request(elastic, s_params, &body_value, indexes).await?; @@ -137,7 +137,7 @@ pub async fn search_all( indexes: &[&str], ) -> Result>, WebError> where - T: DocumentsTrait + SearcherTrait, + T: DocumentsTrait + SearcherTrait + serde::Serialize, { let body_value = DocumentPreview::build_query(s_params).await; let response = send_search_request(elastic, s_params, &body_value, indexes).await?; @@ -171,7 +171,7 @@ pub async fn send_search_request( pub async fn extract_elastic_response(response: Response) -> Paginated> where - T: DocumentsTrait + SearcherTrait, + T: DocumentsTrait + SearcherTrait + serde::Serialize, { let common_object = response.json::().await.unwrap(); let document_json = &common_object[&"hits"][&"hits"]; diff --git a/src/searcher/endpoints.rs b/src/searcher/endpoints.rs index e449335..72d6aea 100644 --- a/src/searcher/endpoints.rs +++ b/src/searcher/endpoints.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "enable-cacher")] +use crate::cacher::CacherService; + use crate::embeddings::EmbeddingsService; use crate::errors::{ErrorResponse, JsonResponse, PaginateResponse, Successful}; use crate::searcher::forms::{ @@ -18,6 +21,11 @@ type EmbeddingsContext = Data>; type SearchContext = Data>; type PaginateContext = Data>; +#[cfg(feature = "enable-cacher")] +type CacherSearchContext = Data>>>>; +#[cfg(feature = "enable-cacher")] +type CacherPaginateContext = Data>>>>; + pub fn build_scope() -> Scope { web::scope("/search") .service(search_fulltext) @@ -75,13 +83,25 @@ pub fn build_scope() -> Scope { #[post("/fulltext")] async fn search_fulltext( cxt: SearchContext, + #[cfg(feature = "enable-cacher")] cacher: CacherSearchContext, form: Json, document_type: Query, ) -> PaginateResponse> { let client = cxt.get_ref(); let search_form = SearchParams::from(form.0); + + #[cfg(feature = "enable-cacher")] + if let Some(docs) = cacher.load(&search_form).await { + tracing::info!("loaded from cache by params: {:?}", &search_form); + return Ok(Json(docs)); + } + let doc_type = document_type.0.get_type(); let documents = client.search_fulltext(&search_form, &doc_type).await?; + + #[cfg(feature = "enable-cacher")] + cacher.insert(&search_form, &documents).await; + Ok(Json(documents)) } @@ -134,6 +154,7 @@ async fn search_fulltext( #[post("/semantic")] async fn search_semantic( cxt: SearchContext, + #[cfg(feature = "enable-cacher")] cacher: CacherSearchContext, em_cxt: EmbeddingsContext, form: Json, document_type: Query, @@ -145,7 +166,17 @@ async fn search_semantic( let query_tokens = em_cxt.load_from_text(search_form.query()).await?; search_form.set_tokens(query_tokens); + #[cfg(feature = "enable-cacher")] + if let Some(docs) = cacher.load(&search_form).await { + tracing::info!("loaded from cache by params: {:?}", &search_form); + return Ok(Json(docs)); + } + let documents = client.search_semantic(&search_form, &doc_type).await?; + + #[cfg(feature = "enable-cacher")] + cacher.insert(&search_form, &documents).await; + Ok(Json(documents)) } @@ -202,15 +233,27 @@ async fn search_semantic( )] #[post("/folders/{folder_id}/documents")] async fn get_index_records( - cxt: Data>, + cxt: SearchContext, + #[cfg(feature = "enable-cacher")] cacher: CacherSearchContext, form: Json, document_type: Query, ) -> PaginateResponse> { let client = cxt.get_ref(); let search_form = SearchParams::from(form.0); + + #[cfg(feature = "enable-cacher")] + if let Some(docs) = cacher.load(&search_form).await { + tracing::info!("loaded from cache by params: {:?}", &search_form); + return Ok(Json(docs)); + } + let doc_type = document_type.0.get_type(); - let folder_documents = client.search_records(&search_form, &doc_type).await?; - Ok(Json(folder_documents)) + let documents = client.search_records(&search_form, &doc_type).await?; + + #[cfg(feature = "enable-cacher")] + cacher.insert(&search_form, &documents).await; + + Ok(Json(documents)) } #[utoipa::path( @@ -318,11 +361,23 @@ async fn delete_paginate_sessions( #[post("/paginate/next")] async fn paginate_next( cxt: PaginateContext, + #[cfg(feature = "enable-cacher")] cacher: CacherPaginateContext, form: Json, document_type: Query, ) -> PaginateResponse> { let client = cxt.get_ref(); let pag_form = form.0; - let founded_docs = client.paginate(&pag_form, &document_type).await?; - Ok(Json(founded_docs)) + + #[cfg(feature = "enable-cacher")] + if let Some(docs) = cacher.load(&pag_form).await { + tracing::info!("loaded from cache by paginate form: {:?}", &pag_form); + return Ok(Json(docs)); + } + + let documents = client.paginate(&pag_form, &document_type).await?; + + #[cfg(feature = "enable-cacher")] + cacher.insert(&pag_form, &documents).await; + + Ok(Json(documents)) } diff --git a/src/searcher/forms.rs b/src/searcher/forms.rs index fa3b78b..ca7a46e 100644 --- a/src/searcher/forms.rs +++ b/src/searcher/forms.rs @@ -48,6 +48,7 @@ impl From for SearchParams { .scroll_lifetime(value.scroll_lifetime) .knn_amount(None) .knn_candidates(None) + .show_all(None) .build() .unwrap() } @@ -169,7 +170,7 @@ impl SearchQuery { } } -#[derive(Builder, Deserialize, Serialize, IntoParams, ToSchema)] +#[derive(Builder, Debug, Deserialize, Serialize, IntoParams, ToSchema)] pub struct PaginateNextForm { #[schema(example = "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmOSWhk")] scroll_id: String, diff --git a/src/searcher/models.rs b/src/searcher/models.rs index 0d3cf91..87b2c91 100644 --- a/src/searcher/models.rs +++ b/src/searcher/models.rs @@ -5,7 +5,7 @@ use getset::{Getters, Setters}; use serde::{Deserialize, Serialize}; use utoipa::{IntoParams, ToSchema}; -#[derive(Builder, Clone, Deserialize, Serialize, IntoParams, ToSchema, Getters, Setters)] +#[derive(Builder, Clone, Debug, Deserialize, Serialize, IntoParams, ToSchema, Getters, Setters)] pub struct SearchParams { #[getset(get = "pub")] #[schema(example = "Hello world")] @@ -128,8 +128,11 @@ impl Default for SearchParams { } } -#[derive(Serialize, Builder, ToSchema)] -pub struct Paginated { +#[derive(Builder, Deserialize, Serialize, ToSchema)] +pub struct Paginated +where + D: serde::Serialize, +{ #[schema(value_type = Paginated>)] founded: D, #[schema(example = "10m")] @@ -137,7 +140,10 @@ pub struct Paginated { scroll_id: Option, } -impl Paginated { +impl Paginated +where + D: serde::Serialize, +{ pub fn new(founded: D) -> Self { Paginated { founded, diff --git a/src/swagger/examples.rs b/src/swagger/examples.rs index b2e810e..d7fd36b 100644 --- a/src/swagger/examples.rs +++ b/src/swagger/examples.rs @@ -174,7 +174,7 @@ impl TestExample for DocumentVectors { impl TestExample>> for Paginated> where - T: DocumentsTrait + TestExample, + T: DocumentsTrait + TestExample + serde::Serialize, { fn test_example(_value: Option<&str>) -> Paginated> { Paginated::new_with_id(vec![T::test_example(None)], PAGINATE_HASH_ID.to_string()) diff --git a/tests/test_cacher.rs b/tests/test_cacher.rs new file mode 100644 index 0000000..9246069 --- /dev/null +++ b/tests/test_cacher.rs @@ -0,0 +1,29 @@ +#[cfg(test)] +#[cfg(feature = "enable-cacher")] +mod test_redis_client { + use doc_search::cacher::redis::RedisClient; + use doc_search::cacher::CacherService; + use doc_search::config::ServiceConfig; + use doc_search::storage::models::Document; + use doc_search::swagger::examples::TestExample; + use doc_search::Connectable; + + #[tokio::test] + async fn test_redis_cacher_client() -> Result<(), anyhow::Error> { + let s_config = ServiceConfig::new()?; + + let cacher_config = s_config.cacher(); + let cacher = RedisClient::connect(cacher_config)?; + + let test_doc = Document::test_example(None); + cacher.insert(test_doc.document_id(), &test_doc).await; + + let cached_doc_opt: Option = cacher.load(test_doc.document_id()).await; + assert!(cached_doc_opt.is_some()); + + let cached_doc = cached_doc_opt.unwrap(); + assert_eq!(cached_doc.document_id(), "98ac9896be35f47fb8442580cd9839b4"); + + Ok(()) + } +} diff --git a/tests/test_searcher_extractor.rs b/tests/test_searcher_extractor.rs new file mode 100644 index 0000000..0d7f7a3 --- /dev/null +++ b/tests/test_searcher_extractor.rs @@ -0,0 +1,78 @@ +use doc_search::searcher::models::SearchParams; +use doc_search::searcher::SearcherTrait; +use doc_search::storage::models::{Document, DocumentPreview, DocumentVectors, InfoFolder}; + +const DOCUMENT_QUERY: &str = "{\"_source\":{\"exclude\":[\"embeddings\"]},\"highlight\":{\"fields\":{\"content\":{\"post_tags\":[\"\"],\"pre_tags\":[\"\"]}},\"order\":\"\"},\"query\":{\"bool\":{\"filter\":{\"bool\":{\"must\":[{\"range\":{\"document_created\":{\"gte\":\"2024-04-26T11:14:55Z\",\"lte\":\"2025-04-26T11:14:55Z\"}}},{\"range\":{\"document_size\":{\"gte\":0,\"lte\":37000}}},{\"term\":{\"document_extension\":\"txt\"}},{\"term\":{\"document_type\":\"document\"}}]}},\"must\":{\"multi_match\":{\"fields\":[\"content\",\"document_path\"],\"query\":\"Some query\"}}}}}"; +const PREVIEW_QUERY: &str = "{\"_source\":{\"exclude\":[\"embeddings\"]},\"query\":{\"bool\":{\"filter\":{\"bool\":{\"must\":[{\"range\":{\"document_created\":{\"gte\":\"2024-04-26T11:14:55Z\",\"lte\":\"2025-04-26T11:14:55Z\"}}},{\"range\":{\"document_size\":{\"gte\":0,\"lte\":37000}}},{\"term\":{\"document_extension\":\"txt\"}}]}},\"should\":[{\"multi_match\":{\"fields\":[\"document_name\",\"document_path\"],\"minimum_should_match\":\"50%\",\"query\":\"Some query\",\"type\":\"phrase_prefix\"}}]}},\"sort\":[{\"document_created\":{\"format\":\"strict_date_optional_time_nanos\",\"order\":\"desc\"}}]}"; +const SEMANTIC_QUERY: &str = "{\"knn\":{\"field\":\"embeddings.vector\",\"k\":5,\"num_candidates\":100,\"query_vector\":[]},\"size\":0}"; +const ALL_RECORDS_QUERY: &str = "{\"query\":{\"bool\":{\"filter\":{\"bool\":{\"must\":[{\"exists\":{\"field\":\"folder_type\"}}]}},\"must\":{\"match_all\":{}}}}}"; +const NON_SYSTEM_RECORDS_QUERY: &str = "{\"query\":{\"bool\":{\"filter\":{\"bool\":{\"must\":[{\"term\":{\"is_system\":\"false\"}},{\"exists\":{\"field\":\"folder_type\"}}]}},\"must\":{\"match_all\":{}}}}}"; + +#[tokio::test] +async fn test_document_build_query() -> Result<(), anyhow::Error> { + let s_params = build_search_params(); + let build_query = Document::build_query(&s_params).await; + let query = serde_json::to_string(&build_query)?; + assert_eq!(DOCUMENT_QUERY, query); + Ok(()) +} + +#[tokio::test] +async fn test_document_preview_build_query() -> Result<(), anyhow::Error> { + let s_params = build_search_params(); + let build_query = DocumentPreview::build_query(&s_params).await; + let query = serde_json::to_string(&build_query)?; + assert_eq!(PREVIEW_QUERY, query); + Ok(()) +} + +#[tokio::test] +async fn test_document_vectors_build_query() -> Result<(), anyhow::Error> { + let s_params = build_search_params(); + let build_query = DocumentVectors::build_query(&s_params).await; + let query = serde_json::to_string(&build_query)?; + assert_eq!(SEMANTIC_QUERY, query); + Ok(()) +} + +#[tokio::test] +async fn test_info_folder_system_build_query() -> Result<(), anyhow::Error> { + let s_params = build_search_params(); + let build_query = InfoFolder::build_query(&s_params).await; + let query = serde_json::to_string(&build_query)?; + assert_eq!(ALL_RECORDS_QUERY, query); + Ok(()) +} + +#[tokio::test] +async fn test_info_folder_build_query() -> Result<(), anyhow::Error> { + let mut s_params = build_search_params(); + s_params.set_show_all(false); + + let build_query = InfoFolder::build_query(&s_params).await; + let query = serde_json::to_string(&build_query)?; + assert_eq!(NON_SYSTEM_RECORDS_QUERY, query); + + Ok(()) +} + +fn build_search_params() -> SearchParams { + SearchParams::builder() + .query("Some query".to_string()) + .query_tokens(Some(Vec::default())) + .folder_ids(Some("test-folder-id".to_string())) + .document_type("document".to_string()) + .document_extension("txt".to_string()) + .created_date_to("2025-04-26T11:14:55Z".to_string()) + .created_date_from("2024-04-26T11:14:55Z".to_string()) + .document_size_to(37000) + .document_size_from(0) + .result_size(25) + .result_offset(0) + .scroll_lifetime("1m".to_string()) + .knn_amount(Some(5)) + .knn_candidates(Some(100)) + .show_all(Some(true)) + .build() + .unwrap() +}