Merge branch 'fts'
This commit is contained in:
@@ -50,4 +50,5 @@ pub fn api_router() -> Router<MnemoState> {
|
|||||||
// quotes
|
// quotes
|
||||||
.route("/api/quotes", post(quotes::create))
|
.route("/api/quotes", post(quotes::create))
|
||||||
.route("/api/quotes/{id}", get(quotes::get_by_id))
|
.route("/api/quotes/{id}", get(quotes::get_by_id))
|
||||||
|
.route("/api/quotes/search", get(quotes::get_by_query))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,16 @@ pub async fn get_by_id(
|
|||||||
Ok(Json(Quote::get_by_id(&mut conn, id).await?).into_response())
|
Ok(Json(Quote::get_by_id(&mut conn, id).await?).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_by_query(
|
||||||
|
State(state): State<MnemoState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Json(q): Json<String>,
|
||||||
|
) -> Result<Response, CompositeError> {
|
||||||
|
let mut conn = state.pool.acquire().await?;
|
||||||
|
User::authenticate(&mut conn, &headers).await?.required()?;
|
||||||
|
Ok(Json(Quote::get_by_search_query(&mut conn, &q, 20, 0).await?).into_response())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct QuoteLineForm {
|
pub struct QuoteLineForm {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -96,3 +99,36 @@ CREATE TABLE logs (
|
|||||||
);
|
);
|
||||||
CREATE INDEX logs_by_actor ON logs(actor);
|
CREATE INDEX logs_by_actor ON logs(actor);
|
||||||
CREATE INDEX logs_by_target ON logs(target);
|
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 =
|
||||||
|
COALESCE(quote_lines_content, '') || ' ' ||
|
||||||
|
COALESCE(context, '') || ' ' ||
|
||||||
|
COALESCE(location, '')
|
||||||
|
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();
|
||||||
|
|||||||
@@ -174,6 +174,29 @@ impl Quote {
|
|||||||
Ok(quotes)
|
Ok(quotes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_by_search_query(
|
||||||
|
conn: &mut PgConnection,
|
||||||
|
query: &str,
|
||||||
|
offset: i64,
|
||||||
|
limit: i64,
|
||||||
|
) -> Result<Vec<Quote>, QuoteError> {
|
||||||
|
let ids: Vec<Uuid> = sqlx::query_scalar(
|
||||||
|
"SELECT id FROM quotes WHERE fts ILIKE '%' || $1 || '%' LIMIT $2 OFFSET $3",
|
||||||
|
)
|
||||||
|
.bind(query)
|
||||||
|
.bind(limit)
|
||||||
|
.bind(offset)
|
||||||
|
.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(
|
pub async fn create(
|
||||||
conn: &mut PgConnection,
|
conn: &mut PgConnection,
|
||||||
lines: Vec<(String, Vec<Name>)>,
|
lines: Vec<(String, Vec<Name>)>,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user