Postgres Full-Text Search: A search engine in a database
blog.crunchydata.com
blog.crunchydata.com
That is, what tends to be the "killer feature" that makes teams groan and set up Elasticsearch because you just can't do it in Postgres and your business needs it?
Having dealt with ES, I'd really like to avoid the operational burden if possible, but I wouldn't want to choose an intermediary solution without being able to say, "keep in mind we'll need to budget a 3-mo transition to ES once we need X, Y, or Z".
I will say, if you use open source libraries like pg_search, you are unlikely to ever have performant full-text search. Most full-text queries need to be written by hand to actually utilize indexes, instead of the query-soup that these types of libraries output. (No offense to the maintainers -- it's just how it be when you create a "general" solution.)
Find me some results in my area that contain these categoryIds and are slotted to start between now and next 10 days.
Since its already quite a filtered set of data, would that mean I should have little issues adding pg text search because with correct indexing and all, it will usually be applied to a small set of data?
Thanks
The GIN indexes for FTS don't really work in conjunction with other indices, which is why https://github.com/postgrespro/rum exists. Luckily, it sounds like you can use your existing indices to filter and let postgres scan for matches on the tsvector. The GIN tsvector indices are quite expensive to build, so don't add one if postgres can't make use of it!
We used es or solr for those cases. For English FTS with 100k documents doing it in PG is super easy and one less dependency
Edit: if any of the Crunchy Data people are reading this: support for RUM indexes would be super cool to have in your managed service.
If I could have your other ear for a moment: support for any EU-based cloud provider would be super nice. Ever since the Privacy Shield fig leaf was removed, questions about whether we store any data under US jurisdiction have become a lot more frequent.
We're currently on the big 3 US ones, in process of working on our 4th provider, our goal is very much to deliver the best Postgres experience whether on bare metal/on-premise or in the cloud, self hosted or fully managed.
Edit: Always feel free to reach out directly, I'm always happy to spend time with anyone that has questions and usually pretty easy to track me down.
My company relied on PG as its search engine and everything went well from POC to production. After a few years of production and new clients requiring volumes of data an order of magnitude above our comfort zone, things went south pretty fast.
Not many months later but many sweaty weeks of engineering after, we switched to ES and we're not looking back.
tl;dr; even with great DB engineers (which we had), I'd suggest that scale is a strong limiting factor on this feature.
There are in my mind two reasons to not use PG's search.
1. Elasticsearch allows you to build sophisticated linguistic and feature scoring pipelines to optimize your search quality. This is not a typical use case in PG.
2. Your primary database is usually your scaling bottleneck even without adding a relatively expensive search workload into the mix. A full-text search tends to be around as expensive as a 5% table scan of the related table. Most DBAs don't like large scan workloads.
"The only limit, is yourself…"
Doing so would require ElasticSearch to reach consensus on every read/write, which would remove most of the point of a distributed cluster. Despite this, ZomboDB's documentation says "complex aggregate queries can be answered in parallel across your ElasticSearch cluster".
They also claim that transactions will abort if ElasticSearch runs into network trouble, but the ElasticSearch documentation notes that writes during network partitions don't wait for confirmation of success[1], so I'm not sure how they would be able to detect that.
In short: I'll wait for the Jepsen analysis.
[1] https://www.elastic.co/blog/tracking-in-sync-shard-copies#:~...
ZomboDB only requires that ES have a view of its index that's consistent with the active Postgres transaction snapshot. ZDB handles this by ensuring that the ES index is fully refreshed after writes.
This doesn't necessarily make ZDB great for high-update loads, but that's not ZDB's target usage.
> They also claim that transactions will abort if ElasticSearch runs into network trouble...
I had to search my own repo to see where I make this claim. I don't. I do note that network failures between PG & ES will cause the active Postgres xact to abort. On top of that, any error that ES is capable of reporting back to the client will cause the PG xact to abort -- ensuring consistency between the two.
Because the ES index is properly refreshed as it relates to the active Postgres transaction, all of ES' aggregate search functions are capable of providing proper MVCC-correct results, using the parallelism provided by the ES cluster.
I don't have the time to detail everything that ZDB does to project Postgres xact snapshots on top of ES, but the above two points are the easy ones to solve.
While I'm here might I ask, are you finding the hosted PostgreSQL services (AWS, Azure, etc.) growing or shrinking your market opportunities? Also, does it play nice with Citus?
ZDB is still alive and well but I have a real job now too.
> As such, any sort of failure either with ZomboDB itself, between Postgres and Elasticsearch (network layer), or within Elasticsearch will cause the operating Postgres transaction to ABORT. [1]
In my defense, there is a fairly important distinction between "any error that ES is capable of reporting back" and "any sort of failure within Elasticsearch".
That said, I and trust that you're more familiar with consistency levels than me, so I'll bow out here.
It’s a difference between having separate OLAP and OLTP databases, which is what the parent post suggested.
For example, if a place has developed fairly good knowledge of PG already, they can "just" (!) continue developing their PG knowledge.
Adding ES into the mix though, introduces a whole new thing that needs to be learned, optimised, etc.
You CAN score results using setweight, although it's likely not as sophisticated as Elasticsearch's
https://www.postgresql.org/docs/9.1/textsearch-controls.html
Disclaimer: I use Postgres fulltext search in production, very happy with it although maintaining the various triggers and stored procs it requires to work becomes cumbersome whenever you have to write a migration that alters any of them (or that in fact touches any related field, as you may be required to drop and recreate all of the parts in order not to violate referential integrity)
It is certainly nice having not to worry about 1 additional dependency when deploying, though
Whether or not it works in your specific situation depends on your use case.
I'm using search_type=websearch https://github.com/simonw/simonwillisonblog/blob/a5b53a24b00...
That's using websearch_to_tsquery() which was added in PostgreSQL 11: https://www.postgresql.org/docs/11/textsearch-controls.html#...
Are there any workarounds for that?
Looks like we get 37 results, of which 2 are true positives.
Looks like "your" and "own" are both contained in the english.stop stopwords list. So you could fix this by removing stopwords from your dictionary.
While disabling the stemmer is relatively easy (use the 'simple' language setting for your ts_query), altering the stopword dictionaries is more involved, and not easy to maintain or pass between developers/environments, and not at all easy to share between queries.
And so the most common suggestion is to use ILIKE.
Lucene has no problems with any of this.
It returns only results that contain the exact multi word sequence in exactly the same order.
If migrating to ES makes you groan you can use a managed service like Pinecone[3] (disclaimer: I work there) just for storing and searching through text embeddings in-memory through an API while keeping the rest of your data in PG.
[1] Nearest-neighbor searches in Open Distro: https://opendistro.github.io/for-elasticsearch-docs/docs/knn...
[2] More on how semantic similarity is measured: https://www.pinecone.io/learn/semantic-search/
that's where it cant be used for anything serious.
I really think the is a neglected area and if PG was able to merge in TF-IDF and BM25 there would be little reason to use a separate search db / engine and many advantages with it being integrated.
https://github.com/zombodb/zombodb
It basically gives you a new kind of index (create index .. using zombodb(..) ..)
Our case was a domain specific knowledge base, with certain terms occurring often in many articles. Searching for a term could bring up thousands of results, but few of them were actually relevant to show in context of the search, they just happened to use the term.
1. PostgreSQL has a cap on column length, and the search index has to be stored in a column. The length of the column is indeterminate - it is storing every word in the document and where it's located, so a short document with very many unique words (numbers are treated as words too) can easily burst the cap. This means you have to truncate each document before indexing it, and pray that your cap is set low enough. You can use multiple columns but that slows down search and makes ranking a lot more complicated. I truncate documents at 4MB.
2. PostgreSQL supports custom dictionaries for specific languages, stemmers, and other nice tricks, but none of those are supported by AWS because the dictionary gets stored as a file on the filesystem (it's not a config setting). You can still have custom rules like whether or not numbers count as words.
That said, simonw has been on the case[2] demonstrating an implementation of that using Django and PostgreSQL.
[1] - https://en.wikipedia.org/wiki/Faceted_search
[2] - https://simonwillison.net/2017/Oct/5/django-postgresql-facet...
We begrudgingly use Solr instead (we started before ES was really a thing and haven't found a need to switch yet)
When you get more than about two different types of filters (e.g. types of filters could be 'tags', 'categories', 'geotags', 'media type', 'author' etc), the combinatorial explosion of Postgres queries required to provide facet counts gets unmanageable.
For example, when I do a query filtered by `?tag=abc&category=category1`, I need to do these queries:
- `... where tag = 'abc' and category_id = 1` (the current results)
- `count(*) ... where tag = 'def' and category_id = 1` (for each other tag present in the results)
- `count(*) ... where tag = 'abc' and category_id = 2` (for each other category present in the results)
- `count(*) ... where category_id = 1`
- `count(*) ... where tag = 'abc'`
- `count(*)`
There are certainly smarter ways to do this than lots of tiny queries, but all this complexity still ends up somewhere and isn't likely to be great for performance.Whereas solr/elasticsearch have faceting built in and handle this with ease.
counting by groups is still relatively expensive though in postgres; I built an implementation which uses estimate counting (via query planner) for larger buckets, and exact counting for only smaller buckets, but my use case allowed for some degree of inconsistency.
A few years ago we added yet-another part to our product and, whilst ES worked "okay", we got a bit weary of ES due to "some issues" (some bug in the architecture keeping things not perfect in sync, certain queries with "joins" of types taking long, demand on HW due to the size of database, no proper multi-node setup due to $$$ and time constraint, etc.; small things piling up over time).
Bright idea: let's see how far Postgres, which is our primary datastore, can take us!
Unfortunately, the feature never made it fully into production.
We thought that on paper, the basic requirements were ideal:
- although the table has multiple hundreds of millions of entries, natural segmentation by customer IDs made possible individual results much smaller
- no weighted search result needed: datetime based is perfect enough for this use-case, we thought it would be easy to come up with the "perfect index [tm]"
Alas, we didn't even get that far:
- we identified ("only") 2 columns necessary for the search => "yay, easy"
- one of those columns was multi-language; though we didn't have specific requirements and did not have to deal with language specific behaviour in ES, we had to decide on one for the TS vectorization (details elude me why "simple" wasn't appropriate for this one column; it was certainly for the other one)
- unsure which one, or both, we would need, for one of the columns we created both indices (difference being the "language")
- we started out with a GIN index (see https://www.postgresql.org/docs/9.6/textsearch-indexes.html )
- creating a single index took > 15 hours
But once the second index was done, and had not even rolled out the feature in the app itself (which at this point was still an ever changing MVP), unrelated we suddenly got hit by lot of customer complains that totally different operations on this table (INSERTs and UPDATEs) started to be getting slow (like 5-15 seconds slow, something which usually takes tiny ms).
Backend developer eyes were wide open O_O
But since we knew that second index just finished, after checking the Posgres logs we decided to drop the FTS indices and, lo' and behold, "performance problem solved".
Communication lines were very short back then (still are today, actually) and it was promptly decided we just cut the search functionality from this new part of the product and be done with it. This also solved the problem, basically (guess there's some "business lesson" to be learned here too, not just technical ones).
Since no one within the company counter argued this decision, we did not spend more time analyzing the details of the performance issue though I would have loved to dig into this and get an expert on board to dissect this.
--
A year later or so I had a bit free time and analyzed one annoying recurring slow UPDATE query problem on a completely different table, but also involving FTS on a single column there also using a GIN index. That's when I stumble over https://www.postgresql.org/docs/9.6/gin-implementation.html
> Updating a GIN index tends to be slow because of the intrinsic nature of inverted indexes: inserting or updating one heap row can cause many inserts into the index (one for each key extracted from the indexed item). As of PostgreSQL 8.4, GIN is capable of postponing much of this work by inserting new tuples into a temporary, unsorted list of pending entries. When the table is vacuumed or autoanalyzed, or when gin_clean_pending_list function is called, or if the pending list becomes larger than gin_pending_list_limit, the entries are moved to the main GIN data structure using the same bulk insert techniques used during initial index creation. This greatly improves GIN index update speed, even counting the additional vacuum overhead. Moreover the overhead work can be done by a background process instead of in foreground query processing.
In this particular case I was able to solve the occasional slow UPDATE queries with "FASTUPDATE=OFF" on that table and, thinking back about the other issue, it might have solved or minimized the impact.
Back to the original story: yep, this one table can have "peaks" of inserts but it's far from "facebook scale" or whatever, basically 1.5k inserts / second were the absolute rare peak I measured and usually it's in the <500 area. But I guess it was enough for this scenario to add latency within the database.
--
Turning back my memory further, I was always "pro" trying to minimize / get rid of ES after learning about http://rachbelaid.com/postgres-full-text-search-is-good-enou... even before we used any FTS feature. At also mentions the GIN/GiST issue but alas, in our case: ElasticSearch is good enough and, besides the thwarts we've with it, actually easier to reason about (so far).
The big problem with all the off-the-shelf search solutions (RDBMS full-text search, ES, Algolia) is that search ranking is a complicated and subtle problem, and frequently depends on signals that are not in the document itself. Google's big insight is that how other people talk about a website is more important than how the website talks about itself, and its ranking algorithm weights accordingly.
ES has the basic building blocks to construct such a ranking algorithm. In terms of fundamental infrastructure I found ES to be just as good as Google, and better in some ways. But its out-of-the-box ranking function sucks. Expect to put a domain expert just on search ranking and evaluation to get decent results, and they're going to have to delve pretty deeply into advanced features of ES to get there.
AFAICT Postgres search only lets you tweak the ranking algorithm by assigning different weights to fields, assuming that the final document score is a linear combination of individual fields. This is usually not what you want - it's pretty common to have non-linear terms from different signals.
I'm having trouble believing that seeing how top results on opinionated keywords are all SEO spam of websites no one visits by themselves.
In recent years however I tend to massively agree with your sentiment and experience. Every day I do not find the things that are really helpful on page 1 - 3, sadly.
You too can build your very own search engine. Unfortunately, the result quality is roughly what AltaVista was like in 1995. That is why people keep going back to Google.
Absolutely ! I think never before the saying "the devil is in the details"... is more appropriate than here.
Sure most users agree on the extreme's the REALLY bad search result (putting in 'apple' getting out LCD TV's [maybe the tv's are in parent-category 'Electronics' and you have category and popularity boost to high' ?]) and the REALLY good (results that you expected)
Search Results are commonly evaluated with Recall(did ALL the documents that are relevant got returned) vs Precision (how many of the results are 'correct')
But that is "one" of many-many metrics.
The biggest issues are non-tech ppl (like your boss or manager) walking in and "discussing" his/her pet-peeve-search query, cause in his mind when he put in APPLE iPhone we should be returning ONLY apple-iphones and NOT apple-iPhone accessories) or maybe we should be returning ONLY the "latest iphone" not the model from 2 generations back
I've commented this before, you can usually only shoot for an "average amount of happiness" (sounds like Arthur Schopenhauer ? :P) for most users. Never "ok ppl, search works perfect" for everyone one now
As to the "practical matters", we found that building a "search-test-suite" where you put in the "manager's pet-peeve" as well as any angry-emails about search queries, and whenever do you search-tuning it's easy to see any-query-regression oh and of course this needs to be an automated search-test-suite.
There is also an incredibly large difference between effective scoring for data that has deep relationships and data that does not. Most people's full text data doesn't have deep relationships like web pages do with inbound links.
Might you or anyone else have some recommendations for books or other resource on large scale search architecture that you think are are worthy reads on the subject?
I would also be interested in hearing if you or anyone else might ave any similar resources you could recommend on the subject of "search ranking"?
Actually, it is possible, but doing a search on a particular segment of rows is a very slow operation - say text search for all employees with name matching 'x', in organization id 'y'.
It is not able to utilise the index on organization id in this case, and it results in a full scan.
However search tech is pretty mature with Lucene at the core and there are many better options [2] from in-process libraries to simple standalone servers to full distributed systems like Elastic. There are also other databases (relational like MemSQL, or documentstores like MongoDB/RavenDB) that are adding search as native querying functions with most of the abilities of ES. If search is a core or complex part of your application (like patterns in raw image data or similarities in audio waveforms) then that's where ES will excel.
1. https://stackoverflow.com/questions/46122175/fulltext-search...
2. https://gist.github.com/manigandham/58320ddb24fed654b57b4ba2...
* One can cheaply compose full-text search with other search operators by just doing normal joins on database indexes, which means we can cheaply and performantly support tons of useful operators (https://zulip.com/help/search-for-messages).
* We don't have to build a pipeline to synchronize data between the real database and the search database. Being a chat product, a lot of the things users search for are things that changed recently; so lag, races, and inconsistencies are important to avoid. With the Postgres full-text search, all one needs to do is commit database transactions as usual, and we know that all future searches will return correct results.
* We don't have to operate, manage, and scale a separate service just to support search. And neither do the thousands of self-hosted Zulip installations.
Responding to the "Scaling bottleneck" concerns in comments below, one can send search traffic (which is fundamentally read-only) to a replica, with much less complexity than a dedicated search service.
Doing fancy scoring pipelines is a good reason to use a specialized search service over the Postgres feature.
I should also mention that a weakness of Postgres full-text search is that it only supports doing stemming for one language. The excellent PGroonga extension (https://pgroonga.github.io/) supports search in all languages; it's a huge improvement especially for character-based languages like Japanese. We're planning to migrate Zulip to using it by default; right now it's available as an option.
More details are available here: https://zulip.readthedocs.io/en/latest/subsystems/full-text-...
https://austingwalters.com/fast-full-text-search-in-postgres...
Worked quite well and still use it daily. Basically doing weighted searches on vectors is slower than my approach, but definitely good enough.
Currently, I can search around 50m HN & Reddit comments in 200ms on the postgresql running on my machine.
Super good if you’re at a company or something
https://twitter.com/austingwalters/status/104189476543920128...
They also have a free API.
If you need data dumps, maybe look into Google BigQuery.
[0] https://jcuenod.github.io/bibletech/2021/07/26/full-text-sea...
Trigrams (pg_trgm) are practically needed for usable search when it comes to misspellings and compound words (e.g. a search for "down loads" won't return "downloads").
I also recommend using websearch_to_tsquery instead of using the cryptic syntax of to_tsquery.
https://about.gitlab.com/blog/2016/03/18/fast-search-using-p...
You can also always read the official docs:
Curious there's no mention of zombodb[0] though, which gives you the full power of elasticsearch from within postgres (with consistency no, less!). You have to be willing to tolerate slow writes, of course, so using postgres' built-in search functionality still makes sense for a lot of cases.
My ideal is always though to start with Postgres, and then see if it can solve my problem. I would never Postgres is the best at everything it can do, but for most things it is good enough without having another system to maintain and wear a pager for.
The performance and cost implications of Zombo are more salient tradeoffs in my mind – if you want to index one of the main tables in your app, you'll have to wait for a network roundtrip and a multi-node write consensus on every update (~150ms or more[0]), you can't `CREATE INDEX CONCURRENTLY`, etc.
All that said, IMO the fact that Zombo exists makes it easier to pitch "hey lets just build search with postgres for now and if we ever need ES's features, we can easily port it to Zombo without rearchitecting our product".
A good implementation will weigh verbatim results highest before considering the stop-word stripped or stemmed version. Configuring to_tsvector() to not strip stop words or using a stemming dictionary is, in my opinion, a little clunky in Postgres: You'll want to make a new [language] dictionary and then call to_tsvector() using your new dictionary as the first parameter.
After you've set up the dictionary globally, this would look something like:
setweight(to_tsvector('english_no_stem_stop', col), 'A') || setweight(to_tsvector('english', col), 'B'))
I think blaming Postgres for adding stemming/stop-word support because it can be [ab]used for a poor search user experience is like blaming a hammer for a poorly built home. It is just a tool, it can be used for good or evil.
PS - You can do a verbatim search without using to_tsvector(), but that cannot be easily passed into setweight() and you cannot use features like ts_rank().
When you have 12 locales (kr/ru/cn/jp/..) it's not that fun anymore. Especially on a one man project :)
I'm slowly transitioning from MariaDB to Postgres - again as a learning experience. There is cool stuff and there is annoying stuff to reproduce things like case-insensitive + ignore accents (utf8_general_ci) in Postgres.
I've looked into FTS and searching for missing dictionaries to support all the locales but Chinese is one of the harder ones.
Essentially allow arbitraty query in from/to/subject/body. One thing that make full-text serch work great for me is that I don't need to sort or rank the relevant of query. I just show a list of email that match the query order by their id.
I also don't do pagination and counting, instead users has to load more paged and the ID of the email is pass to the query as a point to compare( where id < requests.get.before).
And with those strategy, full text search works great for us since we don't really want to bring in ElasticSearch because only about 20% of users use this features.
I had to install software but on Cloud SQL you can't. You have to do it on your instances.
But it does not know how to deal with languages like Chinese, Japanese and Thai.
For that you have to use something like PGroonga extension.
The rest of PostgreSQL mostly handles things ok, unless you try to sort on one of these languages and the same things happen again.
There are all ways around these problems. But it’s not as easy as turning on Unicode and just expect everything to work!
Yes I’m native English speaker who started to develop in Asia and discovered all of this recently.
The additional complexity is usually incurred when the data in postgresql changes, and those changes need to be mirrored up to Elasticsearch. Elasticsearch obviously has its uses, but for some cases, postgresql's built in full-text search can make more sense.
Also, with Elastic Cloud you get some access to useful features for logging (like life cycle management and data streams) that will help you scale the setup.
Kibana in recent iterations has actually improved quite a bit. The version you are getting from Amazon is probably a bit bare bones in comparison. One nice thing with Elastic is that going with the defaults gets you some useful dashboards out of the box if you use e.g. file or docker beats for collecting logs.
> you use e.g. file or docker beats for collecting logs.
My setup is custom app reading from CW Logs.
But can Elastic cluster scale automatically on indexing latency spikes, so my apps writes do not time out? If yes then how, please?
If you use life cycle management and data streams (which AWS doesn't have), you'd be able to control the sizes of your hot indices (i.e. the ones you write to). Basically keeping your hot indices small helps keeping things fast. If you have issues with app writes spiking, I'd use some queuing solution in between. Plenty of solutions for that.
The rest is just a matter of configuring things right in terms of number of shards and setting up properly. Basically, you get what you pay for in the end.
AWS has lifecycle stuff, a year ago. And index management is not cluster scaling, its a good advice, but not what I asked about.
And what text search engine?
(No full text search though, you have to define indexed labels from which to start your search when ingesting)