Although often the problem is producing many rows in the first place, it is made worse by returning all the unneeded rows to the client. This is especially true if client and server are not on the same host.
Here are some ways to reduce the traffic between the client and server.
In this case, order the documents by ranking on the server, and return only the top 20 actually displayed
SELECT title, ts_rank_cd(body_tsv, query, 20) AS text_rank FROM articles, plainto_tsquery('spicy potatoes') AS query WHERE body_tsv @@ query ORDER BY rank DESC LIMIT 20 ;
If you need the next 20, don't just query with limit 40 and throw away the ...