This patch adds the following to the FTI module:
authorBruce Momjian
Sat, 4 Aug 2001 19:41:00 +0000 (19:41 +0000)
committerBruce Momjian
Sat, 4 Aug 2001 19:41:00 +0000 (19:41 +0000)
* The ability to index more than one column in a table with a single
trigger.
* All uses of sprintf changed to snprintf to prevent users from crashing
Postgres.
* Error messages made more consistent
* Some changes made to bring it into line with coding requirements for
triggers specified in the docs.  (ie. check you're a trigger before casting
your context)
* The perl script that generate indices has been updated to support indexing
multiple columns in a table.
* Fairly well tested in our development environment indexing a food
database's brand and description fields.  The size of the fti index is
around 300,000 rows.
* All docs and examples upgraded.  This includes specifying more efficient
index usage that was specified before, better examples that don't produce
duplicates, etc.

Christopher Kings-Lynne & Brett

contrib/fulltextindex/README.fti
contrib/fulltextindex/fti.c
contrib/fulltextindex/fti.pl

index 10838db6912641d129643c6abff8025551523d1b..5c3d6362a6b49ee88f3127b07fa306dfface9024 100644 (file)
@@ -3,11 +3,13 @@ An attempt at some sort of Full Text Indexing for PostgreSQL.
 The included software is an attempt to add some sort of Full Text Indexing
 support to PostgreSQL. I mean by this that we can ask questions like:
 
-   Give me all rows that have 'still' and 'nash' in the 'artist' field.
+   Give me all rows that have 'still' and 'nash' in the 'artist' or 'title'
+   fields.
 
 Ofcourse we can write this as:
 
-   select * from cds where artist ~* 'stills' and artist ~* 'nash';
+   select * from cds where (artist ~* 'stills' or title ~* 'stills') and 
+   (artist ~* 'nash' or title ~* 'nash');
 
 But this does not use any indices, and therefore, if your database
 gets very large, it will not have very high performance (the above query
@@ -15,8 +17,8 @@ requires at least one sequential scan, it probably takes 2 due to the
 self-join).
 
 The approach used by this add-on is to define a trigger on the table and
-column you want to do this queries on. On every insert in the table, it
-takes the value in the specified column, breaks the text in this column
+columns you want to do this queries on. On every insert in the table, it
+takes the value in the specified columns, breaks the text in these columns
 up into pieces, and stores all sub-strings into another table, together
 with a reference to the row in the original table that contained this
 sub-string (it uses the oid of that row).
@@ -24,8 +26,8 @@ sub-string (it uses the oid of that row).
 By now creating an index over the 'fti-table', we can search for
 substrings that occur in the original table. By making a join between
 the fti-table and the orig-table, we can get the actual rows we want
-(this can also be done by using subselects, and maybe there're other
-ways too).
+(this can also be done by using subselects - but subselects are currently
+inefficient in Postgres, and maybe there're other ways too).
 
 The trigger code also allows an array called StopWords, that prevents
 certain words from being indexed.
@@ -62,20 +64,22 @@ The create the function that contains the trigger::
 And finally define the trigger on the 'cds' table:
 
    create trigger cds-fti-trigger after update or insert or delete on cds
-       for each row execute procedure fti(cds-fti, artist);
+       for each row execute procedure fti(cds-fti, artist, title);
 
 Here, the trigger will be defined on table 'cds', it will create
-sub-strings from the field 'artist', and it will place those sub-strings
-in the table 'cds-fti'.
+sub-strings from the fields 'artist' and 'title', and it will place 
+those sub-strings in the table 'cds-fti'.
 
 Now populate the table 'cds'. This will also populate the table 'cds-fti'.
-It's fastest to populate the table *before* you create the indices.
+It's fastest to populate the table *before* you create the indices.  Use the
+supplied 'fti.pl' to assist you with this.
 
 Before you start using the system, you should at least have the following
 indices:
 
-   create index cds-fti-idx on cds-fti (string, id);
-   create index cds-oid-idx on cds (oid);
+   create index cds-fti-idx on cds-fti (string); -- String matching
+   create index cds-fti-idx on cds-fti (id);     -- For deleting a cds row
+   create index cds-oid-idx on cds (oid);           -- For joining cds to cds-fti
 
 To get the most performance out of this, you should have 'cds-fti'
 clustered on disk, ie. all rows with the same sub-strings should be
@@ -109,7 +113,7 @@ clustered  : same as above, only clustered : 4.501.321 rows
 A sequential scan of the artist_fti table (and thus also the clustered table)
 takes around 6:16 minutes....
 
-Unfortunately I cannot probide anybody else with this test-date, since I
+Unfortunately I cannot provide anybody else with this test-data, since I
 am not allowed to redistribute the data (it's a database being sold by
 a couple of wholesale companies). Anyways, it's megabytes, so you probably
 wouldn't want it in this distribution anyways.
index a797505e7016dbb56b36d90fc99fe769568bdf82..10e40fd5dd2a3801c5212dbe5822f8e2bd875a17 100644 (file)
@@ -6,61 +6,87 @@
 #include "commands/trigger.h"
 
 /*
- * Trigger function takes 2 arguments:
-       1. relation in which to store the substrings
-       2. field to extract substrings from
-
-   The relation in which to insert *must* have the following layout:
-
-       string      varchar(#)
-       id          oid
-
-   Example:
-
-create function fti() returns opaque as
-'/home/boekhold/src/postgresql-6.2/contrib/fti/fti.so' language 'C';
-
-create table title_fti (string varchar(25), id oid);
-create index title_fti_idx on title_fti (string);
-
-create trigger title_fti_trigger after update or insert or delete on product
-for each row execute procedure fti(title_fti, title);
-                                  ^^^^^^^^^
-                                  where to store index in
-                                             ^^^^^
-                                             which column to index
-
-ofcourse don't forget to create an index on title_idx, column string, else
-you won't notice much speedup :)
-
-After populating 'product', try something like:
-
-select p.* from product p, title_fti f1, title_fti f2 where
-   f1.string='slippery' and f2.string='wet' and f1.id=f2.id and p.oid=f1.id;
-*/
-
-/*
-   march 4 1998 Changed breakup() to return less substrings. Only breakup
-                in word parts which are in turn shortened from the start
-                of the word (ie. word, ord, rd)
-                Did allocation of substring buffer outside of breakup()
-   oct. 5 1997, fixed a bug in string breakup (where there are more nonalpha
-                characters between words then 1).
-
-   oct 4-5 1997 implemented the thing, at least the basic functionallity
-                of it all....
-*/
-
-/* IMPROVEMENTS:
-
-   save a plan for deletes
-   create a function that will make the index *after* we have populated
-   the main table (probably first delete all contents to be sure there's
-   nothing in it, then re-populate the fti-table)
-
-   can we do something with operator overloading or a seperate function
-   that can build the final query automatigally?
-   */
+ *  Trigger function accepts variable number of arguments:
+ *
+ *     1. relation in which to store the substrings
+ *     2. fields to extract substrings from
+ *
+ *  The relation in which to insert *must* have the following layout:
+ *
+ *     string      varchar(#)
+ *     id          oid
+ *
+ *   where # is the largest size of the varchar columns being indexed
+ *
+ * Example:
+ *
+ *  -- Create the SQL function based on the compiled shared object
+ *  create function fti() returns opaque as
+ *    '/usr/local/pgsql/lib/contrib/fti.so' language 'C';
+ *
+ *  -- Create the FTI table
+ *  create table product_fti (string varchar(255), id oid);
+ *
+ *  -- Create an index to assist string matches
+ *  create index product_fti_string_idx on product_fti (string);
+ *
+ *  -- Create an index to assist trigger'd deletes
+ *  create index product_fti_id_idx on product_fti (id);
+ *
+ *  -- Create an index on the product oid column to assist joins 
+ *  -- between the fti table and the product table
+ *  create index product_oid_idx on product (oid);
+ *
+ *  -- Create the trigger to perform incremental changes to the full text index.
+ *  create trigger product_fti_trig after update or insert or delete on product
+ *  for each row execute procedure fti(product_fti, title, artist);
+ *                                    ^^^^^^^^^^^
+ *                                    table where full text index is stored
+ *                                                 ^^^^^^^^^^^^^
+ *                                                 columns to index in the base table
+ *
+ *  After populating 'product', try something like:
+ *
+ *  SELECT DISTINCT(p.*) FROM product p, product_fti f1, product_fti f2 WHERE
+ * f1.string ~ '^slippery' AND f2.string ~ '^wet' AND p.oid=f1.id AND p.oid=f2.id;
+ *
+ *  To check that your indicies are being used correctly, make sure you
+ *  EXPLAIN SELECT ... your test query above.
+ *
+ * CHANGELOG
+ * ---------
+ *
+ * august 3 2001
+ *              Extended fti function to accept more than one column as a
+ *              parameter and all specified columns are indexed.  Changed
+ *              all uses of sprintf to snprintf.  Made error messages more
+ *              consistent.
+ *
+ * march 4 1998 Changed breakup() to return less substrings. Only breakup
+ *              in word parts which are in turn shortened from the start
+ *              of the word (ie. word, ord, rd)
+ *              Did allocation of substring buffer outside of breakup()
+ *
+ * oct. 5 1997, fixed a bug in string breakup (where there are more nonalpha
+ *              characters between words then 1).
+ *
+ * oct 4-5 1997 implemented the thing, at least the basic functionallity
+ *              of it all....
+ *
+ * TODO
+ * ----
+ *
+ *   prevent generating duplicate words for an oid in the fti table
+ *   save a plan for deletes
+ *   create a function that will make the index *after* we have populated
+ *   the main table (probably first delete all contents to be sure there's
+ *   nothing in it, then re-populate the fti-table)
+ *
+ *   can we do something with operator overloading or a seperate function
+ *   that can build the final query automatigally?
+ */
+
+#define MAX_FTI_QUERY_LENGTH 8192
 
 extern Datum fti(PG_FUNCTION_ARGS);
 static char *breakup(char *, char *);
@@ -81,10 +107,10 @@ char      *StopWords[] = {     /* list of words to skip in indexing */
 /* stuff for caching query-plans, stolen from contrib/spi/\*.c */
 typedef struct
 {
-   char       *ident;
-   int         nplans;
-   void      **splan;
-}          EPlan;
+   char    *ident;
+   int     nplans;
+   void    **splan;
+} EPlan;
 
 static EPlan *InsertPlans = NULL;
 static EPlan *DeletePlans = NULL;
@@ -99,7 +125,7 @@ PG_FUNCTION_INFO_V1(fti);
 Datum
 fti(PG_FUNCTION_ARGS)
 {
-   TriggerData *trigdata = (TriggerData *) fcinfo->context;
+   TriggerData *trigdata;
    Trigger    *trigger;        /* to get trigger name */
    int         nargs;          /* # of arguments */
    char      **args;           /* arguments */
@@ -111,7 +137,7 @@ fti(PG_FUNCTION_ARGS)
    bool        isinsert = false;
    bool        isdelete = false;
    int         ret;
-   char        query[8192];
+   char        query[MAX_FTI_QUERY_LENGTH];
    Oid         oid;
 
    /*
@@ -124,11 +150,15 @@ fti(PG_FUNCTION_ARGS)
     */
 
    if (!CALLED_AS_TRIGGER(fcinfo))
-       elog(ERROR, "Full Text Indexing: not fired by trigger manager");
+       elog(ERROR, "Full Text Indexing: Not fired by trigger manager");
+
+   /* It's safe to cast now that we've checked */
+   trigdata = (TriggerData *) fcinfo->context;
+
    if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
-       elog(ERROR, "Full Text Indexing: can't process STATEMENT events");
+       elog(ERROR, "Full Text Indexing: Can't process STATEMENT events");
    if (TRIGGER_FIRED_BEFORE(trigdata->tg_event))
-       elog(ERROR, "Full Text Indexing: must be fired AFTER event");
+       elog(ERROR, "Full Text Indexing: Must be fired AFTER event");
 
    if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
        isinsert = true;
@@ -148,11 +178,11 @@ fti(PG_FUNCTION_ARGS)
        rettuple = trigdata->tg_newtuple;
 
    if ((ret = SPI_connect()) < 0)
-       elog(ERROR, "Full Text Indexing: SPI_connect failed, returned %d\n", ret);
+       elog(ERROR, "Full Text Indexing: SPI_connect: Failed, returned %d\n", ret);
 
    nargs = trigger->tgnargs;
-   if (nargs != 2)
-       elog(ERROR, "Full Text Indexing: trigger can only have 2 arguments");
+   if (nargs < 2)
+       elog(ERROR, "Full Text Indexing: Trigger must have at least 2 arguments\n");
 
    args = trigger->tgargs;
    indexname = args[0];
@@ -161,7 +191,7 @@ fti(PG_FUNCTION_ARGS)
    /* get oid of current tuple, needed by all, so place here */
    oid = rettuple->t_data->t_oid;
    if (!OidIsValid(oid))
-       elog(ERROR, "Full Text Indexing: oid of current tuple is NULL");
+       elog(ERROR, "Full Text Indexing: Oid of current tuple is invalid");
 
    if (isdelete)
    {
@@ -169,8 +199,14 @@ fti(PG_FUNCTION_ARGS)
        Oid        *argtypes;
        Datum       values[1];
        EPlan      *plan;
+       int i;
+
+       snprintf(query, MAX_FTI_QUERY_LENGTH, "D%s", indexname);
+       for (i = 1; i < nargs; i++)
+       {
+           snprintf(query, MAX_FTI_QUERY_LENGTH, "%s$%s", query, args[i]);
+       }
 
-       sprintf(query, "D%s$%s", args[0], args[1]);
        plan = find_plan(query, &DeletePlans, &nDeletePlans);
        if (plan->nplans <= 0)
        {
@@ -178,15 +214,13 @@ fti(PG_FUNCTION_ARGS)
 
            argtypes[0] = OIDOID;
 
-           sprintf(query, "DELETE FROM %s WHERE id = $1", indexname);
+           snprintf(query, MAX_FTI_QUERY_LENGTH, "DELETE FROM %s WHERE id = $1", indexname);
            pplan = SPI_prepare(query, 1, argtypes);
            if (!pplan)
-               elog(ERROR, "Full Text Indexing: SPI_prepare returned NULL "
-                    "in delete");
+               elog(ERROR, "Full Text Indexing: SPI_prepare: Returned NULL in delete");
            pplan = SPI_saveplan(pplan);
            if (pplan == NULL)
-               elog(ERROR, "Full Text Indexing: SPI_saveplan returned NULL "
-                    "in delete");
+               elog(ERROR, "Full Text Indexing: SPI_saveplan: Returned NULL in delete");
 
            plan->splan = (void **) malloc(sizeof(void *));
            *(plan->splan) = pplan;
@@ -197,21 +231,29 @@ fti(PG_FUNCTION_ARGS)
 
        ret = SPI_execp(*(plan->splan), values, NULL, 0);
        if (ret != SPI_OK_DELETE)
-           elog(ERROR, "Full Text Indexing: error executing plan in delete");
+           elog(ERROR, "Full Text Indexing: SPI_execp: Error executing plan in delete");
    }
 
    if (isinsert)
    {
-       char       *substring,
-                  *column;
-       void       *pplan;
-       Oid        *argtypes;
+       char        *substring;
+       char        *column;
+       void        *pplan;
+       Oid         *argtypes;
        Datum       values[2];
        int         colnum;
-       struct varlena *data;
-       EPlan      *plan;
+       struct  varlena *data;
+       EPlan       *plan;
+       int         i;
+       char        *buff;
+       char        *string;
+
+       snprintf(query, MAX_FTI_QUERY_LENGTH, "I%s", indexname);
+       for (i = 1; i < nargs; i++)
+       {
+           snprintf(query, MAX_FTI_QUERY_LENGTH, "%s$%s", query, args[i]);
+       }
 
-       sprintf(query, "I%s$%s", args[0], args[1]);
        plan = find_plan(query, &InsertPlans, &nInsertPlans);
 
        /* no plan yet, so allocate mem for argtypes */
@@ -224,67 +266,65 @@ fti(PG_FUNCTION_ARGS)
            argtypes[1] = OIDOID;       /* id     oid);    */
 
            /* prepare plan to gain speed */
-           sprintf(query, "INSERT INTO %s (string, id) VALUES ($1, $2)",
+           snprintf(query, MAX_FTI_QUERY_LENGTH, "INSERT INTO %s (string, id) VALUES ($1, $2)",
                    indexname);
            pplan = SPI_prepare(query, 2, argtypes);
            if (!pplan)
-               elog(ERROR, "Full Text Indexing: SPI_prepare returned NULL "
-                    "in insert");
+               elog(ERROR, "Full Text Indexing: SPI_prepare: Returned NULL in insert");
 
            pplan = SPI_saveplan(pplan);
            if (pplan == NULL)
-               elog(ERROR, "Full Text Indexing: SPI_saveplan returned NULL"
-                    " in insert");
+               elog(ERROR, "Full Text Indexing: SPI_saveplan: Returned NULL in insert");
 
            plan->splan = (void **) malloc(sizeof(void *));
            *(plan->splan) = pplan;
            plan->nplans = 1;
        }
 
-
        /* prepare plan for query */
-       colnum = SPI_fnumber(tupdesc, args[1]);
-       if (colnum == SPI_ERROR_NOATTRIBUTE)
-           elog(ERROR, "Full Text Indexing: column '%s' of '%s' not found",
-                args[1], args[0]);
-
-       /* Get the char* representation of the column with name args[1] */
-       column = SPI_getvalue(rettuple, tupdesc, colnum);
-
-       if (column)
-       {                       /* make sure we don't try to index NULL's */
-           char       *buff;
-           char       *string = column;
-
-           while (*string != '\0')
-           {
-               *string = tolower((unsigned char) *string);
-               string++;
-           }
+       for (i = 0; i < nargs - 1; i++)
+       {
+           colnum = SPI_fnumber(tupdesc, args[i + 1]);
+           if (colnum == SPI_ERROR_NOATTRIBUTE)
+               elog(ERROR, "Full Text Indexing: SPI_fnumber: Column '%s' of '%s' not found", args[i + 1], indexname);
 
-           data = (struct varlena *) palloc(sizeof(int32) + strlen(column) +1);
-           buff = palloc(strlen(column) + 1);
-           /* saves lots of calls in while-loop and in breakup() */
+           /* Get the char* representation of the column */
+           column = SPI_getvalue(rettuple, tupdesc, colnum);
 
-           new_tuple = true;
-           while ((substring = breakup(column, buff)))
+           /* make sure we don't try to index NULL's */
+           if (column)
            {
-               int         l;
-
-               l = strlen(substring);
-
-               data->vl_len = l + sizeof(int32);
-               memcpy(VARDATA(data), substring, l);
-               values[0] = PointerGetDatum(data);
-               values[1] = oid;
-
-               ret = SPI_execp(*(plan->splan), values, NULL, 0);
-               if (ret != SPI_OK_INSERT)
-                   elog(ERROR, "Full Text Indexing: error executing plan "
-                        "in insert");
+               string = column;
+               while (*string != '\0')
+               {
+                   *string = tolower((unsigned char) *string);
+                   string++;
+               }
+
+               data = (struct varlena *) palloc(sizeof(int32) + strlen(column) + 1);
+               buff = palloc(strlen(column) + 1);
+               /* saves lots of calls in while-loop and in breakup() */
+
+               new_tuple = true;
+
+               while ((substring = breakup(column, buff)))
+               {
+                   int         l;
+
+                   l = strlen(substring);
+
+                   data->vl_len = l + sizeof(int32);
+                   memcpy(VARDATA(data), substring, l);
+                   values[0] = PointerGetDatum(data);
+                   values[1] = oid;
+
+                   ret = SPI_execp(*(plan->splan), values, NULL, 0);
+                   if (ret != SPI_OK_INSERT)
+                       elog(ERROR, "Full Text Indexing: SPI_execp: Error executing plan in insert");
+               }
+               pfree(buff);
+               pfree(data);
            }
-           pfree(buff);
-           pfree(data);
        }
    }
 
index 230ba92703352a41ce9f8fb29a4d569d5e82abdb..58080a5425cbfae55473a1ef9422b964ee741b5b 100644 (file)
@@ -17,7 +17,7 @@
 #
 # Example:
 #
-#  fti.pl -u -d mydb -t mytable -c mycolumn -f myfile
+#  fti.pl -u -d mydb -t mytable -c mycolumn,mycolumn2 -f myfile
 #  sort -o myoutfile myfile
 #  uniq myoutfile sorted-file
 #
@@ -140,11 +140,13 @@ sub main {
    getopts('d:t:c:f:u');
 
    if (!$opt_d || !$opt_t || !$opt_c || !$opt_f) {
-       print STDERR "usage: $0 [-u] -d database -t table -c column ".
+       print STDERR "usage: $0 [-u] -d database -t table -c column[,column...] ".
          "-f output-file\n";
        return 1;
    }
 
+   @cols = split(/,/, $opt_c);
+
    if (defined($opt_u)) {
        $uname = get_username();
        $pwd   = get_password();
@@ -166,7 +168,9 @@ sub main {
 
    PQexec($PG_CONN, "begin");
 
-   $query = "declare C cursor for select $opt_c, oid from $opt_t";
+   $query = "declare C cursor for select (\"";
+   $query .= join("\" || ' ' || \"", @cols);
+   $query .= "\") as string, oid from $opt_t";
    $res = PQexec($PG_CONN, $query);
    if (!$res || (PQresultStatus($res) != $PGRES_COMMAND_OK)) {
        print STDERR "Error declaring cursor!\n";