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:
2026-06-02 23:25:43 +02:00
parent c4e0c4c834
commit d15afda9b2
17 changed files with 299 additions and 0 deletions
+18
View File
@@ -40,6 +40,7 @@ pub struct SearchDocument {
}
/// A Meilisearch-backed search client scoped to one index.
#[derive(Clone)]
pub struct SearchClient {
client: meilisearch_sdk::client::Client,
index_uid: String,
@@ -146,6 +147,23 @@ impl SearchClient {
.collect()
}
/// Sync a single object's index entry with the database after a catalogue write
/// commits: re-project and index it if it still exists, otherwise remove it. This
/// is the uniform on-write path for create/update/delete/field/visibility changes —
/// a delete (object gone) removes the entry; everything else re-indexes the current
/// projection. Best-effort: callers invoke it after the DB transaction commits and
/// log (not propagate) any error, since `reindex_all` is the recovery path.
pub async fn sync_object(&self, db: &Db, id: ObjectId) -> Result<(), SearchError> {
match db::catalog::object_by_id(db.pool(), id).await? {
Some(object) => {
let document = build_document(db, &object).await?;
self.index_object(&document).await
}
None => self.remove_object(id).await,
}
}
/// Rebuild the whole index from the database (clears then re-adds all objects).
pub async fn reindex_all(&self, db: &Db) -> Result<(), SearchError> {
let index = self.client.index(&self.index_uid);