RUM: a PostgreSQL index access method that keeps lexeme positions in the posting tree
RUM access method - inverted index with additional information in posting lists
At a glance
- What is it?
- RUM is a PostgreSQL extension providing an index access method built on the GIN code, but it stores positional and other extra information in the posting tree. That makes ranking, phrase search and ordering by timestamp cheaper than with GIN, at the cost of slower builds and inserts.
- Who is it for?
- RUM suits a PostgreSQL application that ranks or orders full text search results, or searches for phrases, and where the extra heap fetch after a GIN scan is measurable. It does not suit a workload that only matches documents without ordering them, because GIN already covers that case and RUM pays a slower build and insert for it.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
GIN discards positions, and RUM keeps them
The rum module provides an access method for working with RUM indexes, and it is based on the GIN access method code. That inheritance is the whole design.
GIN lets you run fast full text search using tsvector and tsquery types, but the documentation is blunt about the catch: full text search with a GIN index has performance issues because positional and other additional information is not stored. Ranking a full text result needs to know where in the document each lexeme occurred, and a GIN index cannot answer that from the index alone.
RUM solves that by storing the additional information in a posting tree. The four benefits the project lists all follow from that one change.
Ranking is faster, because ranking requires positional information, and after the index scan there is no additional heap scan to retrieve lexeme positions, since RUM stores them.
Phrase search is faster for the same reason, since phrase search also needs positional information.
Ordering by timestamp is faster, because RUM stores additional information together with lexemes, so it is not necessary to perform a heap scan.
And depth-first search becomes possible, which means the first results can be returned immediately instead of after the whole matching set has been walked.
That last one is the capability with the biggest architectural consequence. A GIN index scan is effectively a bitmap scan over the whole posting list, which is why the depth-first strategy that a B-tree gives you for equality lookups is not available. Once positions and extra columns live in the posting tree, the scan can walk entries in order and stop early.
The audience is a PostgreSQL DBA or application engineer whose search results need ordering by relevance, by phrase, or by recency, and who has measured the gap between what GIN returns and what the query actually needs.
The cost is in the build and the insert path
The drawback is stated in one sentence in the project documentation: RUM has slower build and insert time than GIN.
Two reasons are given, and the second one repeats the first in a way that reads like an editing slip in the original text. The first is that additional information has to be stored besides the keys. The second is that RUM stores that additional information together with the keys and uses generic WAL records.
The WAL detail is the one an operator should care about most. Generic WAL records are a Postgres write-ahead log format that carries the whole page rather than a targeted change, which is a heavier thing to write on every insert. A column that is updated often, in a table with a RUM index, pays that cost on every write.
The practical consequence is a specific profile. RUM is a good fit for a table where the indexed data is written once and read many times, especially if reads are ranked and the ranking work is expensive. It is a poor fit for an append-heavy ingest table, or for one where rows are updated constantly, because the insert path is where the extra information and the generic WAL records land.
This is a normal trade in index design rather than a defect. GIN keeps a small, fast structure and pays at query time with a heap fetch. RUM keeps a bigger, slower to build structure and pays at write time. The choice is which side of that curve your workload sits on.
The project includes a diagram of how RUM stores the additional information next to the GIN structure, linked from the Postgres Professional documentation.
Operator classes decide what you can order by
The module provides several operator classes, and the choice between them is what determines the queries you can run efficiently.
rum_tsvector_ops applies to tsvector and stores tsvector lexemes with positional information. It supports ordering by the distance operator and prefix search. rum_tsvector_hash_ops stores a hash of the tsvector lexemes with positional information, and it supports the distance ordering but does not support prefix search.
Then there is rum_TYPE_ops, a family that covers a long list of types: int2, int4, int8, float4, float8, money, oid, time, timetz, date, interval, macaddr, inet, cidr, text, varchar, char, bytea, bit, varbit, numeric, timestamp and timestamptz. For all of those it supports the ordinary comparison operators, and for int2, int4, int8, float4, float8, money, oid, timestamp and timestamptz it also supports the three distance operators.
There is also rum_tsvector_addon_ops, which stores tsvector lexemes together with any additional field supported by the module. That is the class you use when you want to order by a timestamp column alongside the text search, and the documentation gives an example that creates an index over both a tsvector column and a timestamp column.
CREATE TABLE tsts (id int, t tsvector, d timestamp);
\copy tsts from 'rum/data/tsts.data'
CREATE INDEX tsts_idx ON tsts USING rum (t rum_tsvector_addon_ops, d)The four distance operators are the interface. A tsvector against a tsquery returns a float4 distance, and a timestamp against a timestamp returns a float8 distance, with two asymmetric variants that return the distance for the left or the right side only. Those last three also work for timestamptz, int2, int4, int8, float4, float8, money and oid.
One constraint is worth noting. The hash variant of the tsvector operator class drops prefix search support, so if your workload needs prefix matching you have to use the non-hash class.
Building it needs PGXS, a PostgreSQL 12 base and a lot of C
Installation follows the usual PostgreSQL extension path, with one requirement that narrows the supported versions.
Before building, the documentation says to ensure PostgreSQL version 12 or higher is installed. Older versions from 9.6 to 11 are mentioned with a caveat: you need to transfer the tidbitmap.c file of the required version into the module source and include it in the corresponding file, so the older path involves manual patching rather than a plain build.
The typical procedure from a GitHub checkout is four commands.
$ git clone https://github.com/postgrespro/rum
$ cd rum
$ make USE_PGXS=1
$ make USE_PGXS=1 install
$ make USE_PGXS=1 installcheck
$ psql DB -c "CREATE EXTENSION rum;"There is also a PGXN path, which is one command.
$ USE_PGXS=1 pgxn install rumOne thing not to miss is the note about setting the PG_CONFIG variable if you want to test against a custom build of PostgreSQL, with a link to the PostgreSQL wiki page on building and installing extension modules.
The Makefile shows what you are compiling. The extension is a C module named rum, with an object list covering the TID bitmap, sorting, ts utilities, tsquery handling, the B-tree and bulk insert paths, data page, entry page, get, insert, scan, util, vacuum and validate modules, plus a btree_rum file. The SQL side is assembled from rum_init.sql into the extension script, and there are upgrade scripts for each step from 1.0 to 1.4, matching the EXTVERSION of 1.4 in the manifest.
There is also a meson.build in the repository root, so the build system supports both make and meson.
The module ships with a Travis CI configuration and a META.json for package metadata, and the tests are listed in the Makefile, including regression tests for each supported type, isolation tests for the predicate locking behaviour, and tap tests.
The test suite includes a 1GB download and a 3GB file
Running the tests is a single command, and the documentation is unusually forthcoming about what it costs.
$ make checkThat runs regression tests, isolation tests and tap tests.
The catch is in the tap tests. One of them downloads a 1GB archive and unpacks it into a file weighing almost 3GB, and it is disabled by default. To run it you set an environment variable.
$ export PG_TEST_EXTRA=big_valuesAnd to turn it off again, the variable is unset with the -n flag.
$ export -n PG_TEST_EXTRAThis is a small thing, but it is a good example of a project documenting its own resource needs rather than leaving a contributor to discover a 3GB file appearing in their working tree. If you are running the suite on a small CI runner, the default is the configuration you want.
The Makefile also contains a version specific workaround worth knowing about. On PostgreSQL 12 and 13 the isolation tests cannot be run with pgxs, so the installcheck target is overridden to run all the other tests instead, and the comment in the file explains that this is a shortcut version of the pgxs installcheck target.
The repository layout supports that level of testing detail. There are directories for the SQL tests, the expected output files, the tap tests under t/, the isolation test specifications, and a data directory, plus a logical.conf for the isolation test setup.
Where RUM is the wrong tool
The first wrong-tool case is a search that only matches. If your query filters with the tsvector match operator and returns rows in an arbitrary order, GIN already does that well, and RUM's extra information buys you nothing while slowing every insert. Choosing RUM for a pure filter is paying a build and write cost for ranking you never use.
The second case is a prefix search on a hashed tsvector. The hash operator class does not support prefix search, so if you need prefix matching you have to use the non-hash class with its larger postings.
The third case is an old PostgreSQL. The clean path needs version 12 or newer. The 9.6 to 11 range works only with the manual tidbitmap file transfer described in the installation section, which is a maintenance burden for a database that is itself out of support.
The fourth case is a write-heavy table. The slower insert time and the generic WAL records compound under a bulk load or under frequent updates, and a RUM index on a column that changes often is the worst case for the design.
On the practical side, this is a C extension that has to be compiled against your PostgreSQL. That means a build step in your deployment pipeline, a rebuild for every major version upgrade, and an extension that has to be present in every database that uses it. The upgrade scripts from 1.0 through 1.4 are provided, so path upgrades are handled by the extension scripts, but the compiled module itself is tied to the server version.
On licensing, the module is available under a licence similar to PostgreSQL's own, and the project records it as a custom licence that GitHub cannot classify, with the LICENSE file in the repository as the authority. That is the convention extension projects use when they want to stay compatible with the PostgreSQL licence, but it means a legal review reads a file rather than recognising a standard identifier.
The last point is about the release history. The recent tags are 1.3.15 from 2025-10-23, 1.3.13 from 2022-09-19 and 1.3.11 from 2022-05-27, while the last push was on 2026-10-19 and the Makefile already carries EXTVERSION 1.4 with the 1.3 to 1.4 upgrade script. The gap between the newest tag and the source tree means the tree is ahead of the packaged release.
Editorial conclusion
RUM suits a PostgreSQL application that ranks or orders full text search results, or searches for phrases, and where the extra heap fetch after a GIN scan is measurable. It does not suit a workload that only matches documents without ordering them, because GIN already covers that case and RUM pays a slower build and insert for it. Before adopting, build the extension against PostgreSQL 12 or newer with make USE_PGXS=1, create the extension in the target database, and measure the insert path on a copy of your data, since the additional information in the posting tree and the generic WAL records are where the cost lands.
Frequently asked questions
What does the RUM index access method add over GIN in PostgreSQL?
RUM stores positional and other additional information in the posting tree, while GIN does not. As a result RUM can rank results, run phrase searches and order by timestamp without an additional heap scan to retrieve lexeme positions.
How do I install the RUM extension?
Clone the repository, then run make USE_PGXS=1, make USE_PGXS=1 install, make USE_PGXS=1 installcheck, and create the extension in the database with CREATE EXTENSION rum. The PGXN path is a single command, USE_PGXS=1 pgxn install rum, and PostgreSQL 12 or newer is required.
What is the drawback of using RUM instead of GIN?
RUM has slower build and insert time than GIN, because it stores additional information besides the keys and uses generic WAL records. The documentation notes that no additional heap scan is needed after the index scan when ranking or ordering by timestamp.
Which operator classes does RUM provide?
They are rum_tsvector_ops, which stores lexemes with positional information and supports prefix search, rum_tsvector_hash_ops, which stores a hash of the lexemes and does not support prefix search, rum_TYPE_ops for a long list of scalar types, and rum_tsvector_addon_ops, which stores an additional field such as a timestamp alongside the lexemes.
How do I run the RUM test suite?
Run make check, which runs regression tests, isolation tests and tap tests. One tap test downloads a 1GB archive and unpacks it into a file of almost 3GB, and it is disabled by default; set PG_TEST_EXTRA=big_values to enable it and export -n PG_TEST_EXTRA to turn it off again.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/postgrespro-rum)Was this useful?