On 9th of April, Tom Lane committed patch:

Support indexing of regular-expression searches in contrib/pg_trgm.
 
This works by extracting trigrams from the given regular expression,
in generally the same spirit as the previously-existing support for
LIKE searches, though of course the details are far more complicated.
 
Currently, only GIN indexes are supported.  We might be able to make
it work with GiST indexes later.
 
The implementation includes adding API functions to backend/regex/
to provide a view of the search NFA created from a regular expression.
These functions are meant to be generic enough to be supportable in
a standalone version of the regex library, should that ever happen.
 
Alexander Korotkov, reviewed by Heikki Linnakangas and Tom Lane

One day later Tom Lane added support for the same operations using GiST indexes (original patch was working only with GIN).

Ever since 9.1 we knew it is possible to speed up substring searches. Now – the same thing can be used to speed up regexps.

How does it work?

I made a test table:

$ \d man_lines
  Table "public.man_lines"
  Column  | Type | Modifiers 
----------+------+-----------
 manpage  | text | 
 man_line | text | 
Indexes:
    "trgm_idx" gin (man_line gin_trgm_ops)

It contains manpages, split by line. Looks like this:

$ SELECT * FROM man_lines ORDER BY random() limit 10;
            manpage             |                                    man_line                                     
--------------------------------+---------------------------------------------------------------------------------
 Digest::MD5.3                  |        Or we can use the addfile method for more efficient reading of the file:
 perl5160delta.1                |        This policy is described in greater detail in perlpolicy.
 perltoc.1.gz                   |        DESCRIPTION
 HTML::Tree::AboutTrees.3       |              grow($s, $depth + 1);
 Mojolicious::Guides::Routing.3 |          # /bye -> {controller => 'foo', action => 'bye', mymessage => 'Bye'}
 foo2zjs-wrapper.1.gz           |        the Foomatic printer database.
 perltoc.1.gz                   |            config()
 bc.1.gz                        |                 "current balance = "; bal
 apt.conf.5.gz                  | LES OPTIONS DE DéBOGAGE
 mplayer.1.gz                   |        vhq
(10 rows)

Index that is created on the man_line column was done using:

$ create extension pg_trgm;
$ CREATE INDEX trgm_idx ON man_lines USING gin (man_line gin_trgm_ops);

Now, I can use this index in case of some regexps. Not all of course.

Some examples:

$ explain select * from man_lines where man_line ~ 'a..c';
                            QUERY PLAN
------------------------------------------------------------------
 Seq Scan on man_lines  (cost=0.00..56251.53 rows=34387 width=79)
   Filter: (man_line ~ 'a..c'::text)
(2 rows)

As you can see – index was not used. Because it is not possible to generate list of all trigrams that would match such expression. On the other hand, this, very simplistic, regexp – can use index:

$ explain select * from man_lines where man_line ~ 'abc';
                                QUERY PLAN
--------------------------------------------------------------------------
 Bitmap Heap Scan on man_lines  (cost=17.32..662.42 rows=170 width=79)
   Recheck Cond: (man_line ~ 'abc'::text)
   ->  Bitmap Index Scan on trgm_idx  (cost=0.00..17.28 rows=170 width=0)
         Index Cond: (man_line ~ 'abc'::text)
(4 rows)

What about something more complicated?

$ explain analyze select * from man_lines where man_line ~ 'a{3}';
                                                      QUERY PLAN                                                      
----------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on man_lines  (cost=17.32..662.42 rows=170 width=79) (actual time=8.003..10.376 rows=232 loops=1)
   Recheck Cond: (man_line ~ 'a{3}'::text)
   Rows Removed by Index Recheck: 74
   ->  Bitmap Index Scan on trgm_idx  (cost=0.00..17.28 rows=170 width=0) (actual time=7.912..7.912 rows=306 loops=1)
         Index Cond: (man_line ~ 'a{3}'::text)
 Total runtime: 10.503 ms
(6 rows)

Nice. What about something even more complicated:

$ explain analyze select * from man_lines where man_line ~ '(a|b)(cd|e)[fg]';
                                                         QUERY PLAN                                                          
-----------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on man_lines  (cost=281.25..28377.26 rows=17193 width=79) (actual time=16.620..60.913 rows=14328 loops=1)
   Recheck Cond: (man_line ~ '(a|b)(cd|e)[fg]'::text)
   Rows Removed by Index Recheck: 2640
   ->  Bitmap Index Scan on trgm_idx  (cost=0.00..276.95 rows=17193 width=0) (actual time=13.567..13.567 rows=16968 loops=1)
         Index Cond: (man_line ~ '(a|b)(cd|e)[fg]'::text)
 Total runtime: 61.497 ms
(6 rows)

In case regexp contains other operators – it also can be indexed:

$ explain analyze select * from man_lines where man_line ~* 'postgre.*sql';
                                                        QUERY PLAN                                                         
---------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on man_lines  (cost=89.32..734.42 rows=170 width=79) (actual time=308.631..315.839 rows=1198 loops=1)
   Recheck Cond: (man_line ~* 'postgre.*sql'::text)
   Rows Removed by Index Recheck: 12
   ->  Bitmap Index Scan on trgm_idx  (cost=0.00..89.28 rows=170 width=0) (actual time=308.554..308.554 rows=1210 loops=1)
         Index Cond: (man_line ~* 'postgre.*sql'::text)
 Total runtime: 315.934 ms
(6 rows)

All in all – it looks simply amazing. Almost magical:

$ explain analyze select * from man_lines where man_line ~* '(?:(?:p(?:ostgres(?:ql)?|g?sql)|sql)) (?:(?:(?:mak|us)e|do|is))';
                                                         QUERY PLAN                                                         
----------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on man_lines  (cost=285.32..930.42 rows=170 width=79) (actual time=579.765..592.435 rows=123 loops=1)
   Recheck Cond: (man_line ~* '(?:(?:p(?:ostgres(?:ql)?|g?sql)|sql)) (?:(?:(?:mak|us)e|do|is))'::text)
   Rows Removed by Index Recheck: 1909
   ->  Bitmap Index Scan on trgm_idx  (cost=0.00..285.28 rows=170 width=0) (actual time=578.780..578.780 rows=2032 loops=1)
         Index Cond: (man_line ~* '(?:(?:p(?:ostgres(?:ql)?|g?sql)|sql)) (?:(?:(?:mak|us)e|do|is))'::text)
 Total runtime: 592.474 ms
(6 rows)

Just in case: above regexp matches strings which contain one of:

  • postgresql
  • sql
  • postgres
  • pgsql
  • psql

then a space, and then one of:

  • do
  • is
  • use
  • make

Of course it's not even close to what TSearch2 can do, but still – it's a really cool thing. Thanks Alexander.

  1. 3 comments

  2. # Alexander Korotkov
    Apr 11, 2013

    A little note about regexp search vs tsearch2. I would prefere to not compare them, because they are different features for different use cases. Imagine you’re searching in logs or in some scientific data like DNA/protein sequences or chemical formulas. You probably could write a special parsers and special dictionaries for tsearch2. But you might as well use regexp search without such preliminary work.

  3. Apr 11, 2013

    @Alexander:

    Of course. The thing is – just like people use LIKE/ILIKE for full text searching, they also (though to lesser extent) use regexps.

    Anyway – I really like what you did. Great stuff.

  4. # Alexander Korotkov
    Apr 11, 2013

    @depesz:

    Just to be clear. I’m not encourage people to do full text searching with regexps: they might do it on their own risk :)

    Thank you for appreciating my work!

Leave a comment