Optimizing Postgres text search with trigrams
alexklibisz.com
alexklibisz.com
I'll have to play around with this at some time, but I am wondering how GiST and GIN compare if I don't order by similarity. I was also surprised I never heard about the siglen option, but it turns out this is new since Postgres 13. It looks like this has a big effect and is a useful knob to tune this.
FWIW, I've found GIN is a bit faster if you're just looking to filter. IIRC it was maybe 10-15% faster for the particular use-case I was looking at. So, worth a try, but don't expect a 10x improvement.
For the solution at the end, you’ll often find yourself wanting to query across multiple tables instead of multiple columns in a single table, in which case creating a materialised view with a single search term column and a GIST index is the best I’ve come up with. Obviously you then have a refresh to do instead of the expression being maintained as part of the index.
But no matter what, if your search isn’t fast, you’re probably doing something wrong. Postgres is great for this stuff (obviously for longer documents and fuzzier searches we can argue about quality).
Not that Postgres probably wouldn't handle the full table refresh effortlessly as it generally does - at least at my scale. But that feels kind of icky to me and I'm surprised there doesn't seem to be a better solution for this yet - everyone building a search function for their site must run into this and I'd expect it to scale terribly at dimensions startups generally aim at.
In any case, there's an interesting feature called Incremental View Maintenance that is being worked on by some Postgres developers: https://wiki.postgresql.org/wiki/Incremental_View_Maintenanc...
This would let us define a materialized view that gets automatically updated as the source tables change. When I last checked (late 2021), they were saying it might land in PG15.
I've deployed a solution that uses roughly this same method with multiple tables. I experimented with a materialized view that would centralize all the text columns, but ultimately found that it was much simpler and fast enough to have a single expression index in each of the tables. Then the query either joins the tables and checks each column, or you can run a separate query for each of the tables and stitch together the results in app code.
One thing that bothered me was how cumbersome the queries become once you start having to concatenate and coalesce a whole bunch of columns together. Postgres has a variadic function called "concat_ws" which ought to be useful here, especially as it implicitly coalesces NULL inputs to empty string. However, it's not declared to be immutable, so can't be used in an expression index.
The solution (from Erwin Brandstetter's answer on Stack Overflow) is to use an immutable variant of this, constrained to only handle data of type "text".
https://stackoverflow.com/questions/54372666/create-an-immut...
With this immutable concat function in place, and a trigram expression index making use of it, the WHERE clauses become a lot more concise, for example, the clause in the final query of the article reduces to just:
WHERE immutable_concat_ws(' ', asin, reviewer_id, reviewer_name, summary) ilike '%' || input.q || '%'I've had success using Slick, an ORM-ish Scala library, to abstract away this tedious concatenation in app code.
Also, there is this presentation that might be complementary to the one shared here.
http://matheusoliveira.s3-website-us-east-1.amazonaws.com/pr...
The presentation above is really good for people interested in FTS on PostgreSQL. The company behind this presentation does tens of thousands of sophisticated searches per second. And it's 100% powered by PostgreSQL.
Thanks