feat: pg_trgm full-text search for posts (#2)
* feat(db): add pg_trgm search index on posts * feat(api): use pg_trgm similarity search for posts * fix(api): use ILIKE + word_similarity instead of % operator for search
This commit is contained in:
parent
e74b9f3c39
commit
959d813630
11
migrations/003_search_trgm.sql
Normal file
11
migrations/003_search_trgm.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||||
|
|
||||||
|
ALTER TABLE posts ADD COLUMN IF NOT EXISTS search_text TEXT
|
||||||
|
GENERATED ALWAYS AS (
|
||||||
|
COALESCE(title, '') || ' ' ||
|
||||||
|
COALESCE(summary, '') || ' ' ||
|
||||||
|
COALESCE(content_md, '')
|
||||||
|
) STORED;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_posts_search_trgm
|
||||||
|
ON posts USING GIN (search_text gin_trgm_ops);
|
||||||
@ -801,22 +801,27 @@ pub async fn get_post_stats() -> Result<PostStatsResponse, ServerFnError> {
|
|||||||
pub async fn search_posts(query: String) -> Result<PostListResponse, ServerFnError> {
|
pub async fn search_posts(query: String) -> Result<PostListResponse, ServerFnError> {
|
||||||
let client = get_conn().await.map_err(db_conn_error)?;
|
let client = get_conn().await.map_err(db_conn_error)?;
|
||||||
|
|
||||||
let search_pattern = format!("%{}%", query);
|
let q = query.trim();
|
||||||
|
if q.is_empty() {
|
||||||
|
return Ok(PostListResponse { posts: Vec::new() });
|
||||||
|
}
|
||||||
|
|
||||||
let rows = client
|
let rows = client
|
||||||
.query(
|
.query(
|
||||||
"SELECT
|
"SELECT
|
||||||
p.id, p.author_id, p.title, p.slug, p.summary, p.content_md, p.content_html,
|
p.id, p.author_id, p.title, p.slug, p.summary, p.content_md, p.content_html,
|
||||||
p.status, p.published_at, p.created_at, p.updated_at, p.cover_image,
|
p.status, p.published_at, p.created_at, p.updated_at, p.cover_image,
|
||||||
COALESCE(array_agg(t.name) FILTER (WHERE t.name IS NOT NULL), '{}') as tags
|
COALESCE(array_agg(t.name) FILTER (WHERE t.name IS NOT NULL), '{}') as tags,
|
||||||
|
word_similarity(p.search_text, $1) AS sml
|
||||||
FROM posts p
|
FROM posts p
|
||||||
LEFT JOIN post_tags pt ON p.id = pt.post_id
|
LEFT JOIN post_tags pt ON p.id = pt.post_id
|
||||||
LEFT JOIN tags t ON pt.tag_id = t.id
|
LEFT JOIN tags t ON pt.tag_id = t.id
|
||||||
WHERE p.status = 'published' AND p.deleted_at IS NULL
|
WHERE p.status = 'published' AND p.deleted_at IS NULL
|
||||||
AND (p.title ILIKE $1 OR p.content_md ILIKE $1)
|
AND p.search_text ILIKE '%' || $1 || '%'
|
||||||
GROUP BY p.id
|
GROUP BY p.id, p.search_text
|
||||||
ORDER BY p.published_at DESC",
|
ORDER BY sml DESC, p.published_at DESC
|
||||||
&[&search_pattern],
|
LIMIT 50",
|
||||||
|
&[&q],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(query_error)?;
|
.map_err(query_error)?;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user