swap quote search to trigram ilike, search ui stub, more

This commit is contained in:
2026-04-26 11:44:45 +02:00
parent 84e2ad3918
commit a80a64ceec
4 changed files with 31 additions and 13 deletions

View File

@@ -37,7 +37,7 @@ pub async fn get_by_query(
) -> Result<Response, CompositeError> { ) -> Result<Response, CompositeError> {
let mut conn = state.pool.acquire().await?; let mut conn = state.pool.acquire().await?;
User::authenticate(&mut conn, &headers).await?.required()?; 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)] #[derive(Deserialize)]

View File

@@ -1,4 +1,6 @@
CREATE EXTENSION citext; CREATE EXTENSION citext;
CREATE EXTENSION pg_trgm;
CREATE TABLE users( CREATE TABLE users(
id UUID NOT NULL PRIMARY KEY, id UUID NOT NULL PRIMARY KEY,
handle CITEXT NOT NULL UNIQUE, handle CITEXT NOT NULL UNIQUE,
@@ -35,9 +37,10 @@ CREATE TABLE quotes (
context TEXT DEFAULT NULL, context TEXT DEFAULT NULL,
created_by UUID REFERENCES users(id), created_by UUID REFERENCES users(id),
public BOOLEAN DEFAULT FALSE, 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_by_creation_user ON quotes(created_by);
CREATE INDEX quotes_fts_trgm_idx ON quotes USING gin (fts gin_trgm_ops);
CREATE TABLE persons ( CREATE TABLE persons (
id UUID NOT NULL PRIMARY KEY, id UUID NOT NULL PRIMARY KEY,
bio TEXT DEFAULT NULL, bio TEXT DEFAULT NULL,
@@ -116,9 +119,9 @@ BEGIN
UPDATE quotes UPDATE quotes
SET fts = SET fts =
setweight(to_tsvector(COALESCE(quote_lines_content, '')), 'A') || COALESCE(quote_lines_content, '') || ' ' ||
setweight(to_tsvector(COALESCE(context, '')), 'B') || COALESCE(context, '') || ' ' ||
setweight(to_tsvector(COALESCE(location, '')), 'C') COALESCE(location, '')
WHERE id = affected_quote_id; WHERE id = affected_quote_id;
RETURN COALESCE(NEW, OLD); RETURN COALESCE(NEW, OLD);

View File

@@ -177,13 +177,15 @@ impl Quote {
pub async fn get_by_search_query( pub async fn get_by_search_query(
conn: &mut PgConnection, conn: &mut PgConnection,
query: &str, query: &str,
offset: i64,
limit: i64,
) -> Result<Vec<Quote>, QuoteError> { ) -> Result<Vec<Quote>, QuoteError> {
let ids: Vec<Uuid> = sqlx::query_scalar( let ids: Vec<Uuid> = 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(query)
.bind(20) .bind(limit)
.bind(0) .bind(offset)
.fetch_all(&mut *conn) .fetch_all(&mut *conn)
.await?; .await?;

View File

@@ -22,6 +22,7 @@ pub mod add;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct PageQuery { pub struct PageQuery {
page: Option<i64>, page: Option<i64>,
s: Option<String>,
} }
pub async fn page( pub async fn page(
@@ -39,10 +40,20 @@ pub async fn page(
let per_page = 10; let per_page = 10;
let offset = (page - 1) * per_page; 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_quotes = Quote::total_count(&mut *conn).await?;
let total_pages = (total_quotes as f64 / per_page as f64).ceil() as i64; 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( Ok(base(
"Quotes | Mnemosyne", "Quotes | Mnemosyne",
html!( html!(
@@ -59,8 +70,10 @@ pub async fn page(
span class="text-neutral-300 group-hover:text-neutral-200" {"Add quote"} 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" form method="get" action="/quotes" {
placeholder="Search not yet implemented."; 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" { div class="my-2 w-full" {
p class="ml-auto w-fit text-neutral-500 text-sm flex items-center" { p class="ml-auto w-fit text-neutral-500 text-sm flex items-center" {
span class="scale-[.6]" {(PreEscaped(icons::CALENDAR_ARROW_DOWN))} 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" { div class="flex justify-between items-center mt-4 text-neutral-400" {
@if page > 1 { @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" "Previous"
} }
} @else { } @else {
@@ -86,7 +99,7 @@ pub async fn page(
} }
@if page < total_pages { @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" "Next"
} }
} @else { } @else {