========================================================================== This module is deprecated in 7.4 version of PostgreSQL and will be obsoleted in 7.5. Please, use new tsearch2 contrib module, it works with 7.3.X (http://www.sai.msu.su/~megera/postgres/gist/tsearch/V2/) ========================================================================== Tsearch contrib module contains implementation of new data type txtidx - a searchable data type (textual) with indexed access. All work was done by Teodor Sigaev (teodor@stack.net) and Oleg Bartunov (oleg@sai.msu.su). CHANGES: August 29, 2002 Space usage and using CLUSTER command documented August 22, 2002 Fix works with 'bad' queries August 13, 2002 Use parser of OpenFTS v0.33. IMPORTANT NOTICE: This is a first step of our work on integration of OpenFTS full text search engine (http://openfts.sourceforge.net/) into PostgreSQL. It's based on our recent development of GiST (Generalized Search Tree) for PostgreSQL 7.2 (see our GiST page at http://www.sai.msu.su/~megera/postgres/gist/ for info about GiST) and will works only for PostgreSQL version 7.2 and later. We didn't try to implement a full-featured search engine with stable interfaces but rather experiment with various approaches. There are many issues remains (most of them just not documented or implemented) but we'd like to present a working prototype of full text search engine fully integrated into PostgreSQL to collect user's feedback and recommendations. INSTALLATION: cd contrib/tsearch gmake gmake install REGRESSION TEST: gmake installcheck USAGE: psql DATABASE < tsearch.sql (from contrib/tsearch) INTRODUCTION: This module provides an implementation of a new data type 'txtidx' which is a string of a space separated "words". "Words" with spaces should be enclosed in apostrophes and apostrophes inside a "word" should be escaped by backslash. This is quite different from OpenFTS approach which uses array of integers (ID of lexems) and requires storing of lexem-id pairs in database. One of the prominent benefit of this new approach is that it's possible now to perform full text search in a 'natural' way. Some examples: create table foo ( titleidx txtidx ); 2 regular words: insert into foo values ( 'the are' ); Word with space: insert into foo values ( 'the\\ are' ); Words with apostrophe: insert into foo values ( 'value\'s this' ); Complex word with apostrophe: insert into foo values ( 'value\'s this we \'PostgreSQL site\'' ); select * from foo where titleidx @@ '\'PostgreSQL site\' | this'; select * from foo where titleidx @@ 'value\'s | this'; select * from foo where titleidx @@ '(the|this)&!we'; test=# select 'two words'::txtidx; txtidx --------------- 'two' 'words' (1 row) test=# select 'single\\ word'::txtidx; txtidx --------------- 'single word' (1 row) FULL TEXT SEARCH: The basic idea of this data type is to use it for full text search inside database. If you have a 'text' column title and corresponding column titleidx of type 'txtidx', which contains the same information from text column, then search on title could be replaced by searching on titleidx which would be fast because of indexed access. As a real life example consider database with table 'titles' containing titles of mailing list postings in column 'title': create table titles ( title text ); Suppose, you already have a lot of titles and want to do full text search on them. First, you need to install contrib/tsearch module (see INSTALLATION and USAGE). Add column 'titleidx' of type txtidx, containing space separated words from title. It's possible to use function txt2txtidx(title) to fill 'titleidx' column (see notice 1): -- add titleidx column of type txtidx alter table titles add titleidx txtidx; update titles set titleidx=txt2txtidx(title); Create index on titleidx: create index t_idx on titles using gist(titleidx); and now you can search all titles with words 'patch' and 'gist': select title from titles where titleidx ## 'patch&gist'; Here, ## is a new operation defined for type 'txtidx' which could use index (if exists) built on titleidx. This operator uses morphology to expand query, i.e. ## 'patches&gist' will find titles with 'patch' and 'gist' also. If you want to provide query as is, use operator @@ instead: select title from titles where titleidx @@ 'patch&gist'; but remember, that function txt2txtidx does uses morphology, so you need to fill column 'titleidx' using some another way. We hope in future releases provide more consistent and convenient interfaces. Query could contains boolean operators &,|,!,() with their usual meaning, for example: 'patch&gist&!cvs', 'patch|cvs'. Each operation ( ##, @@ ) requires appropriate query type - txtidx ## mquery_txt txtidx @@ query_txt To see what query actually will be used : test=# select 'patches&gist'::mquery_txt; mquery_txt ------------------ 'patch' & 'gist' (1 row) test=# select 'patches&gist'::query_txt; query_txt -------------------- 'patches' & 'gist' (1 row) Notice the difference ! You could use trigger to be sure column 'titleidx' is consistent with any changes in column 'title': create trigger txtidxupdate before update or insert on titles for each row execute procedure tsearch(titleidx, title); This trigger uses the same parser, dictionaries as function txt2txtidx (see notice 1). Current syntax allows creating trigger for several columns you want to be searchable: create trigger txtidxupdate before update or insert on titles for each row execute procedure tsearch(titleidx, title1, title2,... ); Use function txtidxsize(titleidx) to get the number of "words" in column titleidx. To get total number of words in table titles: test=# select sum(txtidxsize(titleidx)) from titles; sum --------- 1917182 (1 row) NOTICES: 1. function txt2txtidx and trigger use parser, dictionaries coming with this contrib module on default. Parser is mostly the same as in OpenFTS and dictionaries are simple stemmers (sort of Lovin's stemmer which uses a longest match algorithm.) for english and russian languages. There is a perl script makedict/makedict.pl, which could be used to create specific dictionaries from files with endings and stop-words. Example files for english and russian languages are available from http://www.sai.msu.su/~megera/postgres/gist/tsearch/. Run script without parameters to see information about arguments and options. Example: cd makedict ./makedict.pl -l LOCALNAME -e FILEENDINGS -s FILESTOPWORD \ -o ../dict/YOURDICT.dct Another options of makedict.pl: -f do not execute tolower for any char -a function of checking stopword will be work after lemmatize, default is before You need to edit dict.h to use your dictionary and, probably, morph.c to change mapdict array. Don't forget to do make clean; make; make install 2. txtidx doesn't preserve words ordering (this is not critical for searching) for performance reason, for example: test=# select 'page two'::txtidx; txtidx -------------- 'two' 'page' (1 row) 3. Indexed access provided by txtidx data type isn't always good because of internal data structure we use (RD-Tree). Particularly, queries like '!gist' will be slower than just a sequential scan, because for such queries RD-Tree doesn't provides selectivity on internal nodes and all checks should be processed at leaf nodes, i.t. scan of full index. You may play with function query_tree to see how effective will be index usage: test=# select querytree( 'patch&gist'::query_txt ); querytree ------------------ 'patch' & 'gist' (1 row) This is an example of "good" query - index will effective for both words. test=# select querytree( 'patch&!gist'::query_txt ); querytree ----------- 'patch' (1 row) This means that index is effective only to search word 'patch' and resulted rows will be checked against '!gist'. test=# select querytree( 'patch|!gist'::query_txt ); querytree ----------- T (1 row) test=# select querytree( '!gist'::query_txt ); querytree ----------- T (1 row) These two queries will be processed by scanning of full index ! Very slow ! 4. Following selects produce the same result select title from titles where titleidx @@ 'patch&gist'; select title from titles where titleidx @@ 'patch' and titleidx @@ 'gist'; but the former will be more effective, because of internal optimization of query executor. TODO: Better configurability (as in OpenFTS) User's interfaces to parser, dictionaries ... Write documentation BENCHMARKS: We use test collection in our experiments which contains 377905 titles from various mailing lists stored in our mailware project. All runs were performed on IBM ThinkPad T21 notebook with PIII 733 Mhz, 256 RAM, 20 Gb HDD, Linux 2.2.19, postgresql 7.2.dev We didn't do extensive benchmarking and all numbers provide for illustration. Actual performance is strongly depends on many factors (query, collection, dictionaries and hardware). Collection is available for download from http://www.sai.msu.su/~megera/postgres/gist/tsearch/mw_titles.gz (377905 titles from postgresql mailing lists, about 3Mb). 0. install contrib/tsearch module 1. createdb test 2. psql test < tsearch.sql (from contrib/tsearch) 3. zcat mw_titles.gz | psql test (it will creates table, copy test data and creates index) Database contains one table: test=# \d titles Table "titles" Column | Type | Modifiers ----------+------------------------+----------- title | character varying(256) | titleidx | txtidx | Indexes: t_idx Index was created as: create index t_idx on titles using gist(titleidx); (notice: this operation takes about 14 minutes on my notebook) Typical select looks like: select title from titles where titleidx @@ 'patch&gist'; Total number of lexems in collection : 1917182 1. We trust index - we consider index is exact and no checking against tuples is necessary. update pg_amop set amopreqcheck = false where amopclaid = (select oid from pg_opclass where opcname = 'gist_txtidx_ops'); using gist indices 1: titleidx @@ 'patch&gist' 0.000u 0.000s 0m0.054s 0.00% 2: titleidx @@ 'patch&gist' 0.020u 0.000s 0m0.045s 44.82% 3: titleidx @@ 'patch&gist' 0.000u 0.000s 0m0.044s 0.00% using gist indices (morph) 1: titleidx ## 'patch&gist' 0.000u 0.010s 0m0.046s 21.62% 2: titleidx ## 'patch&gist' 0.010u 0.010s 0m0.046s 43.47% 3: titleidx ## 'patch&gist' 0.000u 0.000s 0m0.046s 0.00% disable gist index 1: titleidx @@ 'patch&gist' 0.000u 0.010s 0m1.601s 0.62% 2: titleidx @@ 'patch&gist' 0.000u 0.000s 0m1.607s 0.00% 3: titleidx @@ 'patch&gist' 0.010u 0.000s 0m1.607s 0.62% traditional like 1: title ~* 'gist' and title ~* 'patch' 0.010u 0.000s 0m9.206s 0.10% 2: title ~* 'gist' and title ~* 'patch' 0.000u 0.010s 0m9.205s 0.10% 3: title ~* 'gist' and title ~* 'patch' 0.010u 0.000s 0m9.208s 0.10% 2. Need to check results against tuples to avoid possible hash collision. update pg_amop set amopreqcheck = true where amopclaid = (select oid from pg_opclass where opcname = 'gist_txtidx_ops'); using gist indices 1: titleidx @@ 'patch&gist' 0.010u 0.000s 0m0.052s 19.26% 2: titleidx @@ 'patch&gist' 0.000u 0.000s 0m0.045s 0.00% 3: titleidx @@ 'patch&gist' 0.010u 0.000s 0m0.045s 22.39% using gist indices (morph) 1: titleidx ## 'patch&gist' 0.000u 0.000s 0m0.046s 0.00% 2: titleidx ## 'patch&gist' 0.000u 0.010s 0m0.046s 21.75% 3: titleidx ## 'patch&gist' 0.020u 0.000s 0m0.047s 42.13% There are no visible difference between these 2 cases but your mileage may vary. NOTES: 1. The size of txtidx column should be lesser than size of corresponding column. Below some real numbers from test database (link above). a) After loading data -rw------- 1 postgres users 23191552 Aug 29 14:08 53016937 -rw------- 1 postgres users 81059840 Aug 29 14:08 52639027 Table titles (52639027) occupies 80Mb, index on txtidx column (53016937) occupies 22Mb. Use contrib/oid2name to get mappings from oid to names. After doing test=# select title into titles_tmp from titles; SELECT I got size of table 'titles' without txtidx field -rw------- 1 postgres users 30105600 Aug 29 14:14 53016938 So, txtidx column itself occupies about 50Mb. b) after running 'vacuum full analyze' I got: -rw------- 1 postgres users 30105600 Aug 29 14:26 53016938 -rw------- 1 postgres users 36880384 Aug 29 14:26 53016937 -rw------- 1 postgres users 51494912 Aug 29 14:26 52639027 53016938 = titles_tmp So, actual size of 'txtidx' field is 20 Mb ! "quod erat demonstrandum" 2. CLUSTER command is highly recommended if you need fast searching. For example: test=# cluster t_idx on titles; BUT ! In 7.2 CLUSTER command forgets about other indices and permissions, so you need be carefull and rebuild these indices and restore permissions after clustering. Also, clustering isn't dynamic, so you'd need to use CLUSTER from time to time. In 7.3 CLUSTER command should works fine. after clustering: -rw------- 1 postgres users 23404544 Aug 29 14:59 53394850 -rw------- 1 postgres users 30105600 Aug 29 14:26 53016938 -rw------- 1 postgres users 50995200 Aug 29 14:45 53394845 pg@zen:/usr/local/pgsql/data/base/52638986$ oid2name -d test All tables from database "test": --------------------------------- 53394850 = t_idx 53394845 = titles 53016938 = titles_tmp