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:
Sonetto 2026-06-08 18:26:35 +08:00 committed by GitHub
parent e74b9f3c39
commit 959d813630
2 changed files with 22 additions and 6 deletions

View 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);

View File

@ -801,22 +801,27 @@ pub async fn get_post_stats() -> Result<PostStatsResponse, ServerFnError> {
pub async fn search_posts(query: String) -> Result<PostListResponse, ServerFnError> {
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
.query(
"SELECT
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,
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
LEFT JOIN post_tags pt ON p.id = pt.post_id
LEFT JOIN tags t ON pt.tag_id = t.id
WHERE p.status = 'published' AND p.deleted_at IS NULL
AND (p.title ILIKE $1 OR p.content_md ILIKE $1)
GROUP BY p.id
ORDER BY p.published_at DESC",
&[&search_pattern],
AND p.search_text ILIKE '%' || $1 || '%'
GROUP BY p.id, p.search_text
ORDER BY sml DESC, p.published_at DESC
LIMIT 50",
&[&q],
)
.await
.map_err(query_error)?;