diff --git a/src/api/mod.rs b/src/api/mod.rs index 541a76f..1e74aaf 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -50,4 +50,5 @@ pub fn api_router() -> Router { // quotes .route("/api/quotes", post(quotes::create)) .route("/api/quotes/{id}", get(quotes::get_by_id)) + .route("/api/quotes/search", get(quotes::get_by_query)) } diff --git a/src/api/quotes.rs b/src/api/quotes.rs index 708becc..13e4c8d 100644 --- a/src/api/quotes.rs +++ b/src/api/quotes.rs @@ -30,6 +30,16 @@ pub async fn get_by_id( Ok(Json(Quote::get_by_id(&mut conn, id).await?).into_response()) } +pub async fn get_by_query( + State(state): State, + headers: HeaderMap, + Json(q): Json, +) -> Result { + let mut conn = state.pool.acquire().await?; + User::authenticate(&mut conn, &headers).await?.required()?; + Ok(Json(Quote::get_by_search_query(&mut conn, &q).await?).into_response()) +} + #[derive(Deserialize)] pub struct QuoteLineForm { pub content: String, diff --git a/src/database/migrations/0001_init.sql b/src/database/migrations/0001_init.sql index 54ce564..0a20091 100644 --- a/src/database/migrations/0001_init.sql +++ b/src/database/migrations/0001_init.sql @@ -96,3 +96,36 @@ CREATE TABLE logs ( ); CREATE INDEX logs_by_actor ON logs(actor); CREATE INDEX logs_by_target ON logs(target); + +CREATE OR REPLACE FUNCTION update_quote_fts_from_lines() +RETURNS TRIGGER AS $$ +DECLARE + affected_quote_id UUID; + quote_lines_content TEXT; +BEGIN + IF TG_OP = 'DELETE' THEN + affected_quote_id := OLD.quote_id; + ELSE + affected_quote_id := NEW.quote_id; + END IF; + + SELECT string_agg(content, ' ' ORDER BY ordering) + INTO quote_lines_content + FROM lines + WHERE quote_id = affected_quote_id; + + UPDATE quotes + SET fts = + setweight(to_tsvector(COALESCE(quote_lines_content, '')), 'A') || + setweight(to_tsvector(COALESCE(context, '')), 'B') || + setweight(to_tsvector(COALESCE(location, '')), 'C') + WHERE id = affected_quote_id; + + RETURN COALESCE(NEW, OLD); +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trigger_lines_update_quote_fts + AFTER INSERT OR UPDATE OR DELETE ON lines + FOR EACH ROW + EXECUTE FUNCTION update_quote_fts_from_lines(); diff --git a/src/quotes/mod.rs b/src/quotes/mod.rs index 434a0a3..bcee39e 100644 --- a/src/quotes/mod.rs +++ b/src/quotes/mod.rs @@ -174,6 +174,27 @@ impl Quote { Ok(quotes) } + pub async fn get_by_search_query( + conn: &mut PgConnection, + query: &str, + ) -> Result, QuoteError> { + let ids: Vec = sqlx::query_scalar( + "SELECT id FROM quotes WHERE fts @@ phraseto_tsquery($1) LIMIT $2 OFFSET $3", + ) + .bind(query) + .bind(20) + .bind(0) + .fetch_all(&mut *conn) + .await?; + + let mut quotes = Vec::with_capacity(ids.len()); + for id in ids { + quotes.push(Self::get_by_id(&mut *conn, id).await?); + } + + Ok(quotes) + } + pub async fn create( conn: &mut PgConnection, lines: Vec<(String, Vec)>,