use crate::core::model::Model;
use crate::core::query::QueryBuilder;
use crate::orm::pagination::{Page, SimplePage};
use crate::orm::postgres::model::PgModel;
pub struct SearchService<M>(std::marker::PhantomData<M>);
impl<M: PgModel + Sync + Clone + serde::Serialize> SearchService<M> {
pub async fn search(
term: &str,
cols: &[&str],
pool: &sqlx::PgPool,
) -> Result<Vec<M>, sqlx::Error> {
let effective = effective_cols::<M>(cols);
let builder = ilike_builder::<M>(term, &effective);
M::find_where(pool, builder).await
}
pub async fn search_paginated(
term: &str,
cols: &[&str],
page: u32,
per_page: u32,
pool: &sqlx::PgPool,
) -> Result<Page<M>, sqlx::Error> {
let cols_owned: Vec<String> = effective_cols::<M>(cols)
.into_iter()
.map(|s| s.to_owned())
.collect();
let term_owned = term.to_owned();
let query = M::all_query()
.and_where_group(move |b| ilike_into_builder::<M>(b, &term_owned, &cols_owned));
crate::orm::postgres::pool::with_pool(pool.clone(), query.paginate(per_page, page)).await
}
pub async fn search_simple_paginated(
term: &str,
cols: &[&str],
page: u32,
per_page: u32,
pool: &sqlx::PgPool,
) -> Result<SimplePage<M>, sqlx::Error> {
let cols_owned: Vec<String> = effective_cols::<M>(cols)
.into_iter()
.map(|s| s.to_owned())
.collect();
let term_owned = term.to_owned();
let query = M::all_query()
.and_where_group(move |b| ilike_into_builder::<M>(b, &term_owned, &cols_owned));
crate::orm::postgres::pool::with_pool(pool.clone(), query.simple_paginate(per_page, page))
.await
}
pub async fn fts(
term: &str,
cols: &[&str],
pool: &sqlx::PgPool,
) -> Result<Vec<M>, sqlx::Error> {
let effective = effective_cols::<M>(cols);
if effective.is_empty() {
return Self::search(term, &[], pool).await;
}
let builder = fts_builder::<M>(term, &effective);
M::find_where(pool, builder).await
}
}
fn effective_cols<'a, M: Model>(cols: &'a [&'a str]) -> Vec<&'a str> {
if !cols.is_empty() {
cols.to_vec()
} else {
M::searchable_columns().to_vec()
}
}
fn ilike_builder<M: Model>(term: &str, cols: &[&str]) -> QueryBuilder<M> {
let owned: Vec<String> = cols.iter().map(|&s| s.to_owned()).collect();
ilike_into_builder(M::query(), term, &owned)
}
fn ilike_into_builder<M: Model>(
mut builder: QueryBuilder<M>,
term: &str,
cols: &[String],
) -> QueryBuilder<M> {
if cols.is_empty() {
return builder;
}
let pattern = format!("%{term}%");
for (i, col) in cols.iter().enumerate() {
if i == 0 {
builder = builder.where_ilike(col, &pattern);
} else {
builder = builder.or_where_ilike(col, &pattern);
}
}
builder
}
fn fts_builder<M: Model>(term: &str, cols: &[&str]) -> QueryBuilder<M> {
let concat = cols.join(" || ' ' || ");
let safe_term = term.replace('\'', "''");
let raw =
format!("to_tsvector('english', {concat}) @@ plainto_tsquery('english', '{safe_term}')");
M::query().where_raw(&raw)
}