use axum::Json;
use axum::extract::{Query, State};
use serde::Deserialize;
use utoipa::IntoParams;
use yorishiro_core::ResultExt;
use yorishiro_core::repositories::search::{self, SearchHit};
use crate::error::ApiError;
use crate::http::middleware::auth::{ReadScope, Verified};
use crate::state::AppState;
#[derive(Debug, Deserialize, IntoParams)]
pub struct SearchEntitiesParams {
pub query_text: String,
pub entity_type: Option<String>,
pub filter: Option<String>,
pub limit: Option<i64>,
}
#[utoipa::path(
get,
path = "/api/search",
params(SearchEntitiesParams),
responses(
(status = 200, description = "Vector similarity search results for a natural-language query", body = Vec<SearchHit>),
(status = 401, description = "Invalid or missing credentials", body = crate::error::ApiErrorBody),
(status = 403, description = "Insufficient scope", body = crate::error::ApiErrorBody),
),
tag = "search",
)]
pub async fn search_entities(
State(state): State<AppState>,
verified: Verified<ReadScope>,
Query(params): Query<SearchEntitiesParams>,
) -> Result<Json<Vec<SearchHit>>, ApiError> {
let default = search::SearchQuery::default();
let query = search::SearchQuery {
entity_type: params.entity_type,
filter: crate::http::controllers::parse_filter_param(params.filter)?,
limit: params.limit.unwrap_or(default.limit),
};
let vector = search::embed_query(state.embedding_provider.as_ref(), ¶ms.query_text).await?;
let workspace_id = verified.ctx.workspace_id;
let mut conn = state
.tenant_db
.acquire_for_workspace(verified.ctx.tenant_id, workspace_id)
.await
.internal()?;
let hits = search::search_by_vector(&mut conn, workspace_id, vector, ¶ms.query_text, query)
.await?;
Ok(Json(hits))
}