package store import ( "context" ) // MessageHit is a search result: a message scoped to the member's // conversations, with its conversation context. type MessageHit struct { Message ConversationType string ConversationName string } // SearchMessages finds messages matching query in conversations the user // belongs to, newest first. ILIKE substring matching keeps the search // dependency-free at family scale; pg_trgm is the documented scaling path. func (s *Store) SearchMessages(ctx context.Context, userID int64, query string, limit int) ([]MessageHit, error) { if query == "" { return nil, nil } pattern := "%" + query + "%" rows, err := s.pool.Query(ctx, ` SELECT m.id, m.conversation_id, m.sender_id, m.body, m.media_id, m.created_at, m.edited_at, m.deleted_at, c.type, c.name FROM messages m JOIN conversation_members cm ON cm.conversation_id = m.conversation_id AND cm.user_id = $1 JOIN conversations c ON c.id = m.conversation_id WHERE m.body ILIKE $2 ORDER BY m.id DESC LIMIT $3 `, userID, pattern, limit) if err != nil { return nil, err } defer rows.Close() var hits []MessageHit for rows.Next() { var h MessageHit if err := rows.Scan( &h.ID, &h.ConversationID, &h.SenderID, &h.Body, &h.MediaID, &h.CreatedAt, &h.EditedAt, &h.DeletedAt, &h.ConversationType, &h.ConversationName, ); err != nil { return nil, err } hits = append(hits, h) } return hits, nil }