How can I make "phraseto_tsquery" as fast as "plainto_tsquery" in PostgreSQL
The phrase search has to inspect every document which contains both 'awesom' &amp; 'websit' to make sure the tokens occur in the correct order and proximity, because the order and proximity data is not present in the index.  Reparsing, tokenizing, stemming, etc. those documents is slow.  Also, since a large document is stored out-of-band, it has to read that large data in from a separate TOAST table before it can do any of that.  Without the the word order and proximity constraint, it doesn't have to do any recheck at all, as in this case the index only returns exact results, with no possible false positives (as long as work_mem is large enough).

Rather than using a functional index, you can store the tsvector in the table and keep that column populated (for example, a trigger on row insert and on update of body which sets tsv = to_tsvector('english', body)). Then you would build the index directly on "tsv" column, and write the query directly against that column: 

ALTER TABLE documents add column tsv tsvector;
update documents set tsv = to_tsvector('english', body);
CREATE INDEX documents_tsv_tsvector_index ON documents USING GIN (tsv);

SELECT...WHERE tsv @@ phraseto_tsquery('english', 'awesome website')


Here, it still needs to recheck the "tsv" for order and proximity, which means it needs to read that large column out of disk and back into memory, but it doesn't need to reparse it as it is already stored in tokenized form designed for efficiency.  How much of an improvement this makes will depend on whether your bottleneck is the IO for reading the documents back into memory, or the CPU for doing the parsing.

Of course that does mean you will be storing both "body" and "tsv" in the table, which will expand its size considerably, but that might be a low cost to pay for the improved performance.  If all you want to do is COUNT(*), then you could even store only the tsvector, and don't store the raw "body" at all.