feat(api): on-write search reindex after catalogue writes (#17)
Wire best-effort Meilisearch index sync into the admin write paths (create/update/delete/set_fields/set_visibility). Adds SearchClient::sync_object (reindex if the object exists, remove if gone — one uniform path), an optional AppState.search client, and a reindex helper that logs failures via tracing without failing the committed write. Server gains MEILI_URL/MEILI_MASTER_KEY/MEILI_INDEX config; search stays disabled (no-op) when unset. reindex_all remains the recovery path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -160,6 +160,8 @@ pub(crate) async fn set_visibility(
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
crate::reindex(&state, object_id).await;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
Err(db::catalog::VisibilityError::ObjectNotFound) => Err(StatusCode::NOT_FOUND),
|
||||
|
||||
@@ -234,6 +234,8 @@ pub(crate) async fn create_object(
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
crate::reindex(&state, id).await;
|
||||
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
Json(CreatedObject { id: id.to_string() }),
|
||||
@@ -299,6 +301,8 @@ pub(crate) async fn update_object(
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
if existed {
|
||||
crate::reindex(&state, object_id).await;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
} else {
|
||||
Err(StatusCode::NOT_FOUND)
|
||||
@@ -339,6 +343,8 @@ pub(crate) async fn delete_object(
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
if existed {
|
||||
crate::reindex(&state, object_id).await;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
} else {
|
||||
Err(StatusCode::NOT_FOUND)
|
||||
@@ -443,6 +449,8 @@ pub(crate) async fn set_fields(
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
crate::reindex(&state, object_id).await;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
Err(db::catalog::FieldError::ObjectNotFound) => Err(StatusCode::NOT_FOUND),
|
||||
|
||||
@@ -26,6 +26,23 @@ pub struct AppState {
|
||||
/// Whether the session cookie carries the `Secure` attribute (default true;
|
||||
/// disable only for plain-HTTP self-hosting).
|
||||
pub cookie_secure: bool,
|
||||
/// Search client for on-write index sync. `None` disables indexing (search is a
|
||||
/// best-effort feature; absent when Meilisearch is not configured).
|
||||
pub search: Option<search::SearchClient>,
|
||||
}
|
||||
|
||||
/// Best-effort: keep the search index in step with a catalogue write that has already
|
||||
/// committed. Re-projects and indexes the object, or removes it if it no longer exists.
|
||||
/// Never fails the request — a search outage must not undo a committed write, and
|
||||
/// `reindex_all` is the recovery path. A no-op when search is not configured.
|
||||
pub(crate) async fn reindex(state: &AppState, id: domain::ObjectId) {
|
||||
let Some(search) = &state.search else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(err) = search.sync_object(&state.db, id).await {
|
||||
tracing::error!(?err, object_id = %id, "search reindex after write failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the application router from shared state.
|
||||
|
||||
Reference in New Issue
Block a user