Compare commits

...

2 Commits

Author SHA1 Message Date
Jiang Bohan
3c4ac3b455 fix(search): use LOWER()+LIKE instead of ILIKE for pg_bigm index compat
ILIKE bypasses pg_bigm GIN indexes. Instead, lowercase the query in Go
and use LOWER(column) LIKE in SQL. Rebuild bigram indexes on LOWER()
expressions to match.
2026-04-09 19:03:03 +08:00
Jiang Bohan
d02051b338 fix(search): make Cmd+K search case-insensitive
Replace LIKE with ILIKE in SearchIssues query so that search matches
regardless of case for titles, descriptions, and comments.
2026-04-09 18:52:12 +08:00
5 changed files with 64 additions and 21 deletions

View File

@@ -203,7 +203,7 @@ func (h *Handler) SearchIssues(w http.ResponseWriter, r *http.Request) {
includeClosed := r.URL.Query().Get("include_closed") == "true"
wsUUID := parseUUID(workspaceID)
queryText := strToText(escapeLike(q))
queryText := strToText(strings.ToLower(escapeLike(q)))
rows, err := h.Queries.SearchIssues(ctx, db.SearchIssuesParams{
WorkspaceID: wsUUID,

View File

@@ -0,0 +1,20 @@
-- Revert to original (non-LOWER) bigram indexes.
DO $$
BEGIN
DROP INDEX IF EXISTS idx_issue_title_bigm;
DROP INDEX IF EXISTS idx_issue_description_bigm;
CREATE INDEX idx_issue_title_bigm ON issue USING gin (title gin_bigm_ops);
CREATE INDEX idx_issue_description_bigm ON issue USING gin (COALESCE(description, '') gin_bigm_ops);
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'skipping bigram index rebuild (pg_bigm not installed)';
END
$$;
DO $$
BEGIN
DROP INDEX IF EXISTS idx_comment_content_bigm;
CREATE INDEX idx_comment_content_bigm ON comment USING gin (content gin_bigm_ops);
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'skipping bigram index rebuild on comment (pg_bigm not installed)';
END
$$;

View File

@@ -0,0 +1,21 @@
-- Replace bigram indexes with LOWER() versions for case-insensitive search.
-- Only created when pg_bigm is installed.
DO $$
BEGIN
DROP INDEX IF EXISTS idx_issue_title_bigm;
DROP INDEX IF EXISTS idx_issue_description_bigm;
CREATE INDEX idx_issue_title_bigm ON issue USING gin (LOWER(title) gin_bigm_ops);
CREATE INDEX idx_issue_description_bigm ON issue USING gin (LOWER(COALESCE(description, '')) gin_bigm_ops);
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'skipping bigram index rebuild (pg_bigm not installed)';
END
$$;
DO $$
BEGIN
DROP INDEX IF EXISTS idx_comment_content_bigm;
CREATE INDEX idx_comment_content_bigm ON comment USING gin (LOWER(content) gin_bigm_ops);
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'skipping bigram index rebuild on comment (pg_bigm not installed)';
END
$$;

View File

@@ -431,16 +431,16 @@ const searchIssues = `-- name: SearchIssues :many
SELECT i.id, i.workspace_id, i.title, i.description, i.status, i.priority, i.assignee_type, i.assignee_id, i.creator_type, i.creator_id, i.parent_issue_id, i.acceptance_criteria, i.context_refs, i.position, i.due_date, i.created_at, i.updated_at, i.number, i.project_id,
COUNT(*) OVER() AS total_count,
CASE
WHEN i.title LIKE '%' || $1 || '%' THEN 'title'
WHEN COALESCE(i.description, '') LIKE '%' || $1 || '%' THEN 'description'
WHEN LOWER(i.title) LIKE '%' || $1 || '%' THEN 'title'
WHEN LOWER(COALESCE(i.description, '')) LIKE '%' || $1 || '%' THEN 'description'
ELSE 'comment'
END AS match_source,
CASE
WHEN i.title LIKE '%' || $1 || '%' THEN ''
WHEN COALESCE(i.description, '') LIKE '%' || $1 || '%' THEN ''
WHEN LOWER(i.title) LIKE '%' || $1 || '%' THEN ''
WHEN LOWER(COALESCE(i.description, '')) LIKE '%' || $1 || '%' THEN ''
ELSE COALESCE(
(SELECT c.content FROM comment c
WHERE c.issue_id = i.id AND c.content LIKE '%' || $1 || '%'
WHERE c.issue_id = i.id AND LOWER(c.content) LIKE '%' || $1 || '%'
ORDER BY c.created_at DESC LIMIT 1),
''
)
@@ -448,18 +448,18 @@ SELECT i.id, i.workspace_id, i.title, i.description, i.status, i.priority, i.ass
FROM issue i
WHERE i.workspace_id = $2
AND (
i.title LIKE '%' || $1 || '%'
OR COALESCE(i.description, '') LIKE '%' || $1 || '%'
LOWER(i.title) LIKE '%' || $1 || '%'
OR LOWER(COALESCE(i.description, '')) LIKE '%' || $1 || '%'
OR EXISTS (
SELECT 1 FROM comment c
WHERE c.issue_id = i.id AND c.content LIKE '%' || $1 || '%'
WHERE c.issue_id = i.id AND LOWER(c.content) LIKE '%' || $1 || '%'
)
)
AND ($3::boolean OR i.status NOT IN ('done', 'cancelled'))
ORDER BY
CASE
WHEN i.title LIKE '%' || $1 || '%' THEN 0
WHEN COALESCE(i.description, '') LIKE '%' || $1 || '%' THEN 1
WHEN LOWER(i.title) LIKE '%' || $1 || '%' THEN 0
WHEN LOWER(COALESCE(i.description, '')) LIKE '%' || $1 || '%' THEN 1
ELSE 2
END,
i.updated_at DESC
@@ -499,6 +499,7 @@ type SearchIssuesRow struct {
MatchedCommentContent interface{} `json:"matched_comment_content"`
}
// @query is expected to be pre-lowercased by the caller.
func (q *Queries) SearchIssues(ctx context.Context, arg SearchIssuesParams) ([]SearchIssuesRow, error) {
rows, err := q.db.Query(ctx, searchIssues,
arg.Query,

View File

@@ -81,19 +81,20 @@ WHERE parent_issue_id = $1
ORDER BY position ASC, created_at DESC;
-- name: SearchIssues :many
-- @query is expected to be pre-lowercased by the caller.
SELECT i.*,
COUNT(*) OVER() AS total_count,
CASE
WHEN i.title LIKE '%' || @query || '%' THEN 'title'
WHEN COALESCE(i.description, '') LIKE '%' || @query || '%' THEN 'description'
WHEN LOWER(i.title) LIKE '%' || @query || '%' THEN 'title'
WHEN LOWER(COALESCE(i.description, '')) LIKE '%' || @query || '%' THEN 'description'
ELSE 'comment'
END AS match_source,
CASE
WHEN i.title LIKE '%' || @query || '%' THEN ''
WHEN COALESCE(i.description, '') LIKE '%' || @query || '%' THEN ''
WHEN LOWER(i.title) LIKE '%' || @query || '%' THEN ''
WHEN LOWER(COALESCE(i.description, '')) LIKE '%' || @query || '%' THEN ''
ELSE COALESCE(
(SELECT c.content FROM comment c
WHERE c.issue_id = i.id AND c.content LIKE '%' || @query || '%'
WHERE c.issue_id = i.id AND LOWER(c.content) LIKE '%' || @query || '%'
ORDER BY c.created_at DESC LIMIT 1),
''
)
@@ -101,18 +102,18 @@ SELECT i.*,
FROM issue i
WHERE i.workspace_id = @workspace_id
AND (
i.title LIKE '%' || @query || '%'
OR COALESCE(i.description, '') LIKE '%' || @query || '%'
LOWER(i.title) LIKE '%' || @query || '%'
OR LOWER(COALESCE(i.description, '')) LIKE '%' || @query || '%'
OR EXISTS (
SELECT 1 FROM comment c
WHERE c.issue_id = i.id AND c.content LIKE '%' || @query || '%'
WHERE c.issue_id = i.id AND LOWER(c.content) LIKE '%' || @query || '%'
)
)
AND (@include_closed::boolean OR i.status NOT IN ('done', 'cancelled'))
ORDER BY
CASE
WHEN i.title LIKE '%' || @query || '%' THEN 0
WHEN COALESCE(i.description, '') LIKE '%' || @query || '%' THEN 1
WHEN LOWER(i.title) LIKE '%' || @query || '%' THEN 0
WHEN LOWER(COALESCE(i.description, '')) LIKE '%' || @query || '%' THEN 1
ELSE 2
END,
i.updated_at DESC