Postgres Full Text Search vs. the Rest
supabase.com
supabase.com
A surprising (to me) thing about PostgreSQL FTS is that it doesn't do TF/IDF or BM25 relevance calculations.
These calculations take statistics about the entire corpus into account - they do things like ranking a document higher if it contains terms from the search which are statistically rare in the overall corpus.
PostgreSQL FTS uses how often the search terms appear in the document as part of the ranking score, but doesn't look at how common/rare individual terms are. https://www.postgresql.org/docs/current/textsearch-controls....
OpenSearch/Elasticsearch uses BM25: https://opensearch.org/docs/latest/opensearch/rest-api/expla...
SQLite stores these kinds of statistics and can support BM25 or TF/IDF or custom ranking functions. I wrote more about that here: https://simonwillison.net/2019/Jan/7/exploring-search-releva...
Meilisearch does something a bit different (on reading this I don't think it considers full corpus statistics, but I may be misinterpreting that): https://docs.meilisearch.com/learn/core_concepts/relevancy.h...
It looks like Typesense only considers the documents, not the overall corpus: https://typesense.org/docs/guide/ranking-and-relevance.html
Seems like an excellent weekend project.
[0]: https://www.postgresql.org/docs/current/textsearch-features....
Basically, I read on the internet (and was surprised by) the fact that setweight can be used and combined with individual terms on tsvectors, and then those tsvectors can be combined and they keep their weightings.
Some code from that project to illustrate:
UPDATE podcasts
SET fts_doc = setweight(to_tsvector(COALESCE(title, ' ')), 'A')
|| setweight(to_tsvector(COALESCE(homepage_url, ' ')), 'A')
|| setweight(to_tsvector(COALESCE(podcast_idx_itunes_author, ' ')), 'A')
|| setweight(to_tsvector(COALESCE(podcast_idx_itunes_ownername, ' ')), 'A')
|| setweight(to_tsvector(COALESCE(podcast_idx_host, ' ')), 'A')
|| setweight(to_tsvector(array_to_string(categories, ' ')), 'B')
|| setweight(to_tsvector(COALESCE(description_html, ' ')), 'D')
Basically I'm making tsvectors out of chunks of the document, weighting them differently then recombining with other vectors without losing the weightings -- I'm thinking this could be applied to words identified by the corpus-level algos.So my simplistic thinking here is that if you've done the corpus level processing, you could build an intermediate data structure and re-evaluate each search document with the appropriate weighting. It would likely be quite the lengthy stored procedure, but seems like setweight could support the usecase? Maybe I'm being a bit optimistic.
PS podcastsaver looks neat!
some quick feedback:
1) your "switch back to light mode" icon looks a LOT like a gear for a settings menu. I turned on dark mode, did a search, saw the "back to light mode" icon and thought "huh, the dark mode toggle is settings now? Weird choice, let's see what's there..."
2) the show notes seem truncated. It would be helpful for me to be able to search the show notes for a defined set of podcasts. Sometimes I remember that a podcast mentioned a product or service that I wanted to check out, but I can't remember the name of the product or the overall episode, and it's painful to find the right one by scrolling back through everything in my pod catcher.
3) are you tracking Podcasts 2.0? Some interesting additional stuff to index there. https://origin.fm/blog/podcasting-2point0/
On (1) I can definitely see that — will fix!
(2) yeah I need to go to the source for that, I think podcast index data might have been why? I’m going to double check.
(3) no I’m not! Thank you for the pointer!
I’m going to work on all of this (and tackle the speed issue)
Yep, I mean this is always the case for corpus-level algos right?
No reason you can’t do it iteratively —- postgres has triggers…
Oh but actually thinking about it, it could be a function! You’d just need access to that intermediate representation.
> I suspect most people would not be interested in such an approach, instead either making do without TF/IDF, or moving to a non-pg solution.
Well people would be happy if it was there at all, I think. Then they could at least make the choice or have a decent option.
It probably won’t be as performant as other solutions which can make more drastic architecture changes but… might still be worth having
I am not sure which parts of which calculations lucene (Elastic Search and Solr) does on the fly vs pre-calculates after any change to corpus, because it's more or less transparent. I mean, I guess that's not entirely true -- there are definitely index-rebuilds that happen after updates, and for larger-scale things they can be resource-intensive enough that you have to account for them (for very small-scale things you can more or less ignore them), maybe it's just that Solr/ES have architectures built around accounting for that and giving you tools to deal with it with various approaches.
> InnoDB full-text search is modeled on the Sphinx full-text search engine, and the algorithms used are based on BM25 and TF-IDF ranking algorithms. For these reasons, relevancy rankings for InnoDB boolean full-text search may differ from MyISAM relevancy rankings.
> InnoDB uses a variation of the “term frequency-inverse document frequency” (TF-IDF) weighting system to rank a document's relevance for a given full-text search query. The TF-IDF weighting is based on how frequently a word appears in a document, offset by how frequently the word appears in all documents in the collection. In other words, the more frequently a word appears in a document, and the less frequently the word appears in the document collection, the higher the document is ranked.
MySQL's FTS is fine. We're using it at work for fairly basic boolean searches on millions of documents, also retrieving the relevancy score, and it's plenty fast. We'll outgrow it one day, but for now it's pretty easy and does well enough.
https://dev.mysql.com/doc/refman/8.0/en/fulltext-boolean.htm...
Edit: keep the index in memory
It’s also a reason why MyIsam separated data and index files. InnoDb is a clustered index, so all the data is packed next to the primary key.
In practice these are nearly worthless. Useful relevance ranking is difficult. Google sort of gamed it with PageRank (using inbound links). "Information Retrieval" by Buettcher et al is a good book on search implementation with a decent amount of info about relevance ranking, though maybe it no longer up to date: https://mitpress.mit.edu/9780262528870/information-retrieval...
Are you suggesting that you can do just as well without TF/IDF, or with postgres fts specifically? In some/all circumstances? I'd be interested in hearing more about that, if it comes from experience.
Looking at the table of contents for the Büttcher et al book, it looks to me like it covers TF/IDF-based algorithms pretty extensively. BM25 is in the table of contents specifically. Büttcher's own pedagogical search engine, _Wumpus_, includes a BM25 implementation. http://stefan.buettcher.org/cs/wumpus/docs/relevance.html
Yes that book described tf/idf and bm25 but a bunch of other stuff too that was somewhat more promising. Really though, IMHO there is no getting away from understanding the actual documents, and possibly their connection with the outer world (pagerank being an example of the latter). For web search it's now probably worse than before, since instead of merely being overwhelmed by noise, the data is now actually adverserial in the sense of having SEO trying to game your ranking.
Even without that though, go on any retail site and try a search. The relevance ranking will be so awful that sorting by price or age or alphabetically will work a lot better. Same thing with the Algolia search here on HN. Chronological is almost always more useful than the search engine's idea of relevance. For automating relevance the scoring system needs much more semantic understanding of the data. Maybe that is more feasible with recent advances in NLP. I don't know whether that is good or bad.
https://xapian.org/docs/bm25.html
https://getting-started-with-xapian.readthedocs.io/en/latest...
Also, it is used by the the public-inbox Perl scripts. See https://public-inbox.org
That includes the Linux kernel mailing list, Git mailing list and other lists available via lore.kernel.org:
For example, to search the BPF mailing list for "bpf" and sort results by relevance:
https://lore.kernel.org/bpf/?q=bpf&r
To view the Xapian query parser operators:
Overall, pg_search is an amazing tool for low-complexity systems. Once you need really complex searching going on, a dedicated tool wins by a lot.
- MeiliSearch: https://www.meilisearch.com/
- OpenSearch: https://opensearch.org/
- SQLite FTS: https://www.sqlite.org/fts5.html
- Typesense: https://typesense.org/
Some of the callouts from the results: - Even when consuming similar content, engines can produce different results, but generally ratios between queries on the same engine should be consistent.
- Postgres FTS is quite close performance-wise to many other solutions, at least in their default configuration.
- Only Typesense and MeiliSearch properly handled mis-spellings (the "suprman" query).
- Typesense was relatively strict with matches compared to other engines.
- OpenSearch was very fast with ingest, but also failed with the misspelling out of the box.
- In-memory SQLite is by far the fastest, and PG isn't too far behind for this small data set.different strengths and best use cases
Sonic[2] I know much less about but it also seems good. Honestly anything except ES is what I like to hear about (though OpenSearch is interesting).
Another thing I think the world really needs is a CLI +/- API tool (ideally rust lib + CLI + API) that unifies interacting with these things. I got REALLY close to writing it while working on this article, but I was already running late and I have a penchant for yak shaving.
This won't be the last thing I write about search engines -- there's been a LOT of movement in the space that has nothing to do with the elastic/opensearch debacle and I don't see enough tires getting kicked.
[0]: https://github.com/quickwit-oss/tantivy
We support almost all languages. The only thing is that in some languages, without help and intervention from the community, we stop when our level of comprehension is not enough. Today we handle perfectly Latin-based languages and all languages that are space separated. We have also worked with the community to improve Chinese, Japanese, Thai, and Korean, which is under review.
Thanks @qdequelen for pointing out Quickwit :)
I didn't dive into the various engines, but I was looking for one that would support Russian in a small side-project, and Meilisearch was the only one [1] that had it right there out of the box
[1] Criteria for "only one" where "out of the box, ease of operation, no fiddling with configs, if not directly inside DB then with an easy HTTP API"
Moreover, it's Hacktoberfest. If you want to help us improve the language support, it would be awesome!
edit: fixed https://github.com/supabase/supabase/pull/9565
Gist/Gin indexes are great and do a fine job to make millions of records searchable in very few milliseconds.
The problem is accuracy.
I’ve tried a few of these and accuracy is wildly different with most of them.
Accuracy depends on how much you index.
For example, even with decently designed weighting: If you index title, subtitle, tags and content — too much of content ruin relevancy.
And yes, we have proper relevancy sorting setup nicely.
The best is something like elastic search, but it does not integrate nicely with PG for our use case. Because of the multi tenant nature of our data setup.
PG will get us to that magic 80% but that leaves the all important 20% which is not great.
Not to mention the inability to index and search Asian character sets.
So even though we exclusively use it, it’s not great and every time our team can’t find something that drives my team into the Psql CLI — I start searching for alternatives again.
The big brick wall is updating, inserting and deleting from an external solution fast enough so we don’t miss stuff.
And quickly searching multi tenant records in that external solution.
And no I’m not ready to use ES as my primary database.
Quite deep Postgres/Elasticsearch integration
I haven't used ZomboDB but I have managed plenty of applications where Postgres was the main db and elastic was used for FTS. Zombo looks like it makes it easier to do that type of setup, but Postgres is so high performance (at least at parity on speed w/ Elastic) and Elastic is such a pain in the ass to manage from a DevOps perspective that I'd like to eliminate the need for Elastic by investing some time into Postgres Extensions.
Plenty of great db solutions have come out of Postgres in the last few years based on extensions and fulltext search is one of the areas that has been very quiet, I think we can do better and I'd like to try.
I think in the interim Zombo looks like a really good stopgap though!
Here I'm listing engines based on https://github.com/quickwit-oss/tantivy - tantivy is comparable to Lucene in its scope - but I'm sure there are other engines that could tackle ElasticSearch.
Another thing that could happen is maybe directly embed tantivy in Postgres using an extension, perhaps this could be an option too.
It does take some migration but we are looking at that as an add-on.
- CJK support is tricky for almost all of these search products, but postgresql is probably the only one that needs additional extensions, and some extensions are not updated for a while.
- Scaling is tricky, bc on one hand, your search and other normal queries may not have same load pattern, but you only have one schema in multiple nodes, so sometimes you have to waste some resource; on the other hand, dependencies of the 3rd party CJK extensions makes it hard to use managed instances and maintaining your own is time consuming.
- Most other search engines have well tuned APIs for typical use cases like filters/typo/etc, but you have to build your own in PostgreSQL. Of course it’s not entirely bad thing bc PostgreSQL does have most flexible query capabilities for those less than typical use cases.
End of day, search for end users have many semi-edge cases, one have to try all these engines to find the best fit. For large use cases, operational cost and user tolerance of search errors are all part of consideration. A conventional wisdom is do less work in the early days , and find better solutions later if have to.
With some tuning and memory page size adjustments in Postgres you get very compelling speeds.
I’ve actually previously written a full twitter thread on a somewhat inactive account about why we don't use existing dbs for search more.
https://twitter.com/kinglycrow/status/1533270619353231360?s=...
That thread led me to a project/product idea where you take an existing Postgres instance used for normal products or whatever, replicate it to various read only clusters with a custom search extension loaded and some orchestrator sitting on top (I’ve written most of one in rust that uses 0mq to communicate with it’s nodes) and create drop in search from existing databases with a nice guided web gui for automatic tuning suitable for most business use cases.
It fell off when my friend who wanted to help work on it went off to Google and you know working on a search engine for them became a bit of a no no. I still think it's a great idea with a lot of value, I should circle back.
I also write (at length) about how ease of use will win search here: https://twitter.com/KinglyCrow/status/1532402654218964993
Very interesting idea -- just want to add one thing, write it in rust (with pgx?[0]) :)
They're fundamentally different products, but sometimes "pg full text search is good enough" isn't true.
I would say, frankly, that if you already have a PG database and
1. you want better full text search than a non-existing solution.
2. you don't need the excellent searching like we've come to expect from search engines.
then use the full text search feature your PG database already has. I wouldn't necessarily go out of my way to use a PG database for FTS but I think its a good solution when you're in the right position to use it and its not too shabby.
"for item in ... if item.containsSubstring(query)"
I would say it's definitely useful for real apps.Once you start exiting that and start touching document search, being able to sort on relevance OR date with indexes, or any data ingestion killing search latency, you will have wished you went to the vast and uniform area to the right of db fts called elastic search. That is where I found myself.
If search is not integral to your product, by all means avoid the complexity and curve that is elastic and be happy that you did.
It's not even close: Postgres Full Text Search is a kludge compared to Lucene-based services. It is better than nothing, so, yes, I agree with the Pareto improvement idea. But it is also much, much worse than a Lucene-based alternative. If you need something and can't support having ElasticSearch/OpenSearch (heck, even Solr) running at the same time to support full text search, then sure, use Postgres FTS. But if the queries are still too slow or you need ranking or other text/information retrieval/NLP-type features, you'll want to give up on Postgres FTS and move to something else.
Feel free to ask me anything and let me know just how many postgres features I missed out on (pg_trgm is definitely one) -- I love to learn about corners of Postgres I've missed.
> Typesense was relatively strict with matches compared to other engines.
Perhaps an example to clarify the statement?
- cross-tables FTS requires at least materialized views, which lock data for writing at refresh. This was too much worrying for us.
- sorting by rank is not indexable, so we can't sort our dataset and have acceptable timings at the same time. Our dataset isn't enormous, but neither small (~1.5m records)
Spoiler, the adapter is Searchkick: https://github.com/ankane/searchkick
- Does the searcher already know the result they are looking for? (If yes, much easier)
- Are there subjective and objective qualities of the results which should alter the search score, sometimes separate from the text being indexed? (If yes, much harder)
- What is the quality of the text being indexed? (If end-user provided, this will vary widely)
Ultimately, building good search is often a struggle against providing the best possible results between searcher intent and incomplete document evaluation criteria. People never really think about when a search is working really well, but they definitely know and complain when it's working poorly.I do wonder how much deep search really matters when people only really expect to look at the first page.
[0]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/da...
In my experience, you don't have to spend a lot of time thinking about scoring and relevancy for these types of search. Generally you only want to include a small edit distance in the results at all to handle misspellings.
This is so vastly different when you have a corpus of millions of documents about an encyclopedia's worth of topics.
> I do wonder how much deep search really matters when people only really expect to look at the first page.
Getting the first page to have the best quality and relevancy is much more difficult if the user is searching through something like scientific papers, stock video footage. It is a challenge in bridging the distance between ideas and expectations.
That's where trigrams come in. pg_trgm can fix mis-spellings, and even compound words ("super man" to "superman"). I opt for performing the search as entered, and use trigrams to offer suggestions to the user, e.g. do the search for "suprman", but show an option to search for "superman".
Gitlab actually has a great article about this for those who want to read more:
https://about.staging.gitlab.com/blog/2016/03/18/fast-search...
I'm not sure how it compares but in my use case with 200,000 rows it has been "good enough" for <100ms autocomplete.
You can even combine it with a sqlite vfs[0] to run full text searches against a sqlite db stored in s3 relatively efficiently.
This is a lot harder to evaluate and compare because it's so not-black-and-white, but... I don't think most people are choosing a full-text search option based on latency alone, are they? While some products may have unsatisfactory latency, most popular products probably do okay, and once you have good enough latency, the issue is results quality.
Getting an overview of pg's fts capabilities, and a list of other products with similar, is I guess a useful starting point. But the article's focus on performance is not too useful to me; are there really an audience of people choosing an fts solution based mostly on performance? I want to know if pg can provide good enough results compared to fulltext-search-focused products -- it's true it's less clear how to measure that, and may depend on your exact situation. Which is why I'd be interested in reading from someone who has something to say on it!
If you have a large-ish data set with lots of similar data (4M addresses and location names was the test case), Postgres FTS just doesn't perform.
There is no index that helps scoring results. You would have to install an extension like RUM index (https://github.com/postgrespro/rum) to improve this, which may or may not be an option (often not if you use managed databases).
If you want a best of both worlds, one could investigate this extension (again, often not an option for managed databases): https://github.com/matthewfranglen/postgres-elasticsearch-fd...
Either way, writing something that indexes your postgres database into elastic/opensearch is a one time investment that usually pays off in the long run.
From this it seems like the best solution would be to use SQLite's FTS and replicate the database using something like rqlite, litestream or mvsqlite, and then loadbalance the requests to SQLite.
Given that SQLite is serverless in the classic sense, seems like a nobrainer.
https://github.com/benbjohnson/litestream/pull/411
[EDIT] - I indulged myself: https://news.ycombinator.com/item?id=33204347
You might want to give that a chance.
1) It’s based on tokenizing input, and ‘english’ config will tokenize it in unexpected ways. We had people’s name being tokenized like “eddie” to “eddi” and searching for “eddie:*” doesn’t work. Even the ‘simple’ config will tokenize it undesirably. I couldn’t find the exact config to turn it off.
2) you can index multiple columns by concat strings, but when results come back, no easy way of knowing which fields matched where to highlight the search.
3) Typo tolerance isn’t there. You have to send possible typos with OR in query. Typesense achieves this with ART index where it’s very cheap to explore more paths of a trie.
4) it’s fast, but not get results on every keystroke fast. Prefix based searching is still fairly expensive compared to token based search.
Postgres FTS is good usecase if you want to search for whole words in large document.
Typesense is nice, but I wish they built an auto-sync with postgres so one doesn’t have to deal with ETL headache.
Been trying to find an alternative to mysql fts, and some typo tolerance / did you mean mechanism is needed for some of our clients.
There's an FDW for Elastic, so they work together quite nicely and you can have the best of both worlds.
You can however implement a full text algorithm such as tf-idf atop indexedDB. It had reasonable performance when I made a toy version and fed Shakespeare's plays in as a corpus. Fun way to learn about stemming and building queries.
I know of at least 5 more search engines I can't wait to share and they're burning a hole in my bookmarks folder.
Also, donating 60% of subscription revenue towards supporting the open source project of your choice (or one that was featured if you have no preference) -- you can sign up for that as well[2].
[0]: https://awsmfoss.com
[1]: https://twitter.com/awsmfoss
[2]: https://baserow.vadosware.io/form/Xv5rChuZb-YodDOKjpJJpuDhrE...
I briefly looked into for storing long form text archive records for my company two years ago. There are EXTREME limitations in the source code around it that no one really talks about but have important implications.
Phrase searching doesn't really work the way our analysts would have liked and needed. There are a bunch of technical limitations in the source about how much data is tracked about the tokens. I can't remember exactly but there was something weird about stemming or lemming in the phrase search too.
The following variables need to be bumped up to get phrase searching more accurate.
- MAXSTRLEN (2047) https://github.com/postgres/postgres/blob/master/src/include...
- MAXSTRPOS (1048575) https://github.com/postgres/postgres/blob/master/src/include...
- MAXENTRYPOS (16363) https://github.com/postgres/postgres/blob/master/src/include...
- MAXNUMPOS (256) https://github.com/postgres/postgres/blob/master/src/include...
TsHeadline for highlighting doesn't consider phrase searching so you can get weird results. It probably needs to be rewritten to match websearch_to_tsquery.
The accuracy issue drained the blood from my BA's faces. I eventually just went with on-premise SOLR because it's easier to add new hardware for it than elasticsearch.
TLDR: postgres search is probably fine for short-form content, but major gotcha's once you go past those max limits. Also phrase searching will probably not work the way people are used too.