From a80a64ceec808fec4a909dcc9821456658fd8a8a Mon Sep 17 00:00:00 2001 From: jmanczak Date: Sun, 26 Apr 2026 11:44:45 +0200 Subject: [PATCH] swap quote search to trigram ilike, search ui stub, more --- src/api/quotes.rs | 2 +- src/database/migrations/0001_init.sql | 11 +++++++---- src/quotes/mod.rs | 8 +++++--- src/web/pages/quotes.rs | 23 ++++++++++++++++++----- 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/api/quotes.rs b/src/api/quotes.rs index 13e4c8d..f22d257 100644 --- a/src/api/quotes.rs +++ b/src/api/quotes.rs @@ -37,7 +37,7 @@ pub async fn get_by_query( ) -> 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()) + Ok(Json(Quote::get_by_search_query(&mut conn, &q, 20, 0).await?).into_response()) } #[derive(Deserialize)] diff --git a/src/database/migrations/0001_init.sql b/src/database/migrations/0001_init.sql index 0a20091..02ac9b0 100644 --- a/src/database/migrations/0001_init.sql +++ b/src/database/migrations/0001_init.sql @@ -1,4 +1,6 @@ CREATE EXTENSION citext; +CREATE EXTENSION pg_trgm; + CREATE TABLE users( id UUID NOT NULL PRIMARY KEY, handle CITEXT NOT NULL UNIQUE, @@ -35,9 +37,10 @@ CREATE TABLE quotes ( context TEXT DEFAULT NULL, created_by UUID REFERENCES users(id), public BOOLEAN DEFAULT FALSE, - fts TSVECTOR NOT NULL DEFAULT ''::tsvector -- TODO: trigger?? + fts TEXT NOT NULL DEFAULT '' ); CREATE INDEX quotes_by_creation_user ON quotes(created_by); +CREATE INDEX quotes_fts_trgm_idx ON quotes USING gin (fts gin_trgm_ops); CREATE TABLE persons ( id UUID NOT NULL PRIMARY KEY, bio TEXT DEFAULT NULL, @@ -116,9 +119,9 @@ BEGIN UPDATE quotes SET fts = - setweight(to_tsvector(COALESCE(quote_lines_content, '')), 'A') || - setweight(to_tsvector(COALESCE(context, '')), 'B') || - setweight(to_tsvector(COALESCE(location, '')), 'C') + COALESCE(quote_lines_content, '') || ' ' || + COALESCE(context, '') || ' ' || + COALESCE(location, '') WHERE id = affected_quote_id; RETURN COALESCE(NEW, OLD); diff --git a/src/quotes/mod.rs b/src/quotes/mod.rs index bcee39e..234286d 100644 --- a/src/quotes/mod.rs +++ b/src/quotes/mod.rs @@ -177,13 +177,15 @@ impl Quote { pub async fn get_by_search_query( conn: &mut PgConnection, query: &str, + offset: i64, + limit: i64, ) -> Result, QuoteError> { let ids: Vec = sqlx::query_scalar( - "SELECT id FROM quotes WHERE fts @@ phraseto_tsquery($1) LIMIT $2 OFFSET $3", + "SELECT id FROM quotes WHERE fts ILIKE '%' || $1 || '%' LIMIT $2 OFFSET $3", ) .bind(query) - .bind(20) - .bind(0) + .bind(limit) + .bind(offset) .fetch_all(&mut *conn) .await?; diff --git a/src/web/pages/quotes.rs b/src/web/pages/quotes.rs index 2c4452e..b4a2323 100644 --- a/src/web/pages/quotes.rs +++ b/src/web/pages/quotes.rs @@ -22,6 +22,7 @@ pub mod add; #[derive(Deserialize)] pub struct PageQuery { page: Option, + s: Option, } pub async fn page( @@ -39,10 +40,20 @@ pub async fn page( let per_page = 10; let offset = (page - 1) * per_page; - let quotes = Quote::get_chronological_offset(&mut *conn, offset, per_page).await?; + let search = query.s.as_deref().unwrap_or(""); + let quotes = match search { + "" => Quote::get_chronological_offset(&mut *conn, offset, per_page).await?, + _ => Quote::get_by_search_query(&mut *conn, search, offset, per_page).await?, + }; let total_quotes = Quote::total_count(&mut *conn).await?; let total_pages = (total_quotes as f64 / per_page as f64).ceil() as i64; + let s_qs = if search.is_empty() { + String::new() + } else { + format!("&s={}", search) + }; + Ok(base( "Quotes | Mnemosyne", html!( @@ -59,8 +70,10 @@ pub async fn page( span class="text-neutral-300 group-hover:text-neutral-200" {"Add quote"} } } - input class="border w-full border-neutral-200/25 hover:border-neutral-200/45 bg-neutral-950/50 p-2 rounded" - placeholder="Search not yet implemented."; + form method="get" action="/quotes" { + input type="text" name="s" class="border w-full border-neutral-200/25 hover:border-neutral-200/45 bg-neutral-950/50 p-2 rounded" + placeholder="Search quotes..." value={(search)}; + } div class="my-2 w-full" { p class="ml-auto w-fit text-neutral-500 text-sm flex items-center" { span class="scale-[.6]" {(PreEscaped(icons::CALENDAR_ARROW_DOWN))} @@ -74,7 +87,7 @@ pub async fn page( div class="flex justify-between items-center mt-4 text-neutral-400" { @if page > 1 { - a href=(format!("/quotes?page={}", (page - 1).max(1))) class="px-4 py-2 border border-neutral-200/25 hover:border-neutral-200/45 bg-neutral-200/5 hover:bg-neutral-200/15 rounded" { + a href=(format!("/quotes?page={}{}", (page - 1).max(1), s_qs)) class="px-4 py-2 border border-neutral-200/25 hover:border-neutral-200/45 bg-neutral-200/5 hover:bg-neutral-200/15 rounded" { "Previous" } } @else { @@ -86,7 +99,7 @@ pub async fn page( } @if page < total_pages { - a href=(format!("/quotes?page={}", page + 1)) class="px-4 py-2 border border-neutral-200/25 hover:border-neutral-200/45 bg-neutral-200/5 hover:bg-neutral-200/15 rounded" { + a href=(format!("/quotes?page={}{}", page + 1, s_qs)) class="px-4 py-2 border border-neutral-200/25 hover:border-neutral-200/45 bg-neutral-200/5 hover:bg-neutral-200/15 rounded" { "Next" } } @else {