Question

I'm working on a pretty simple blog in Ruby on Rails and is currently implementing the search functionality using pg_search for PostgreSQL full text search. Unfortunately, I'm having a problem with (I believe) my trigger for updating the tsvector column in the posts table. After several hours of searching, I've not been able to resolve the issue myself - even though I suspect it's easy if you've done similar things before.

Anyway, the error reads as follows:

irb(main):001:0> post = FactoryGirl.build :post
=> #<Post id: nil, title: "Optimized uniform infrastructure", body: "<p>Inventore consectetur culpa nulla eius voluptati...", published: true, created_at: "2012-09-09  04:08:51", updated_at: nil, tsv: nil>
irb(main):002:0> post.save
ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR:  column "tsv" does not exist
    LINE 1: SELECT tsvector_update_trigger(tsv, 'pg_catalog.english', ti...
    QUERY:  SELECT tsvector_update_trigger(tsv, 'pg_catalog.english', title, body)
    CONTEXT:  PL/pgSQL function "posts_before_insert_update_row_tr" line 3 at assignment
    : INSERT INTO "posts" ("body", "created_at", "published", "title", "updated_at") VALUES
    ($1, $2, $3, $4, $5) RETURNING "id"

The tsv column is made in a migration like so:

class AddTsvToPosts < ActiveRecord::Migration
    def change
        add_column :posts, :tsv, :tsvector
        add_index(:posts, :tsv, using: 'gin')
    end
end

The trigger is defined using hair_trigger in this way:

class Post < ActiveRecord::Base
  include PgSearch

  pg_search_scope :search, against: [:title, :body],
    using: {
      tsearch: {
        dictionary: 'english',
        prefix: true,
        tsvector_column: 'tsv'
      }
    }

  trigger.before(:insert, :update) do
    "new.tsv := tsvector_update_trigger(tsv, 'pg_catalog.english', title, body);"
  end
end

The whole thing produces the following sql schema (with config.active_record.schema_format = :sql) -- -- PostgreSQL database dump --

SET statement_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SET check_function_bodies = false;
SET client_min_messages = warning;

--
-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: -
--

CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;


--
-- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: -
--

COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';


SET search_path = public, pg_catalog;

--
-- Name: posts_before_insert_update_row_tr(); Type: FUNCTION; Schema: public; Owner: -
--

CREATE FUNCTION posts_before_insert_update_row_tr() RETURNS trigger
    LANGUAGE plpgsql
    AS $$
BEGIN
    new.tsv := tsvector_update_trigger(tsv, 'pg_catalog.english', title, body);
    RETURN NEW;
END;
$$;


SET default_tablespace = '';

SET default_with_oids = false;

--
-- Name: posts; Type: TABLE; Schema: public; Owner: -; Tablespace: 
--

CREATE TABLE posts (
    id integer NOT NULL,
    title character varying(255),
    body text,
    published boolean,
    created_at timestamp without time zone,
    updated_at timestamp without time zone,
    tsv tsvector
);


--
-- Name: posts_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--

CREATE SEQUENCE posts_id_seq
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


--
-- Name: posts_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--

ALTER SEQUENCE posts_id_seq OWNED BY posts.id;


--
-- Name: schema_migrations; Type: TABLE; Schema: public; Owner: -; Tablespace: 
--

CREATE TABLE schema_migrations (
    version character varying(255) NOT NULL
);


--
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--

ALTER TABLE ONLY posts ALTER COLUMN id SET DEFAULT nextval('posts_id_seq'::regclass);


--
-- Name: posts_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: 
--

ALTER TABLE ONLY posts
    ADD CONSTRAINT posts_pkey PRIMARY KEY (id);


--
-- Name: index_posts_on_tsv; Type: INDEX; Schema: public; Owner: -; Tablespace: 
--

CREATE INDEX index_posts_on_tsv ON posts USING gin (tsv);


--
-- Name: unique_schema_migrations; Type: INDEX; Schema: public; Owner: -; Tablespace: 
--

CREATE UNIQUE INDEX unique_schema_migrations ON schema_migrations USING btree (version);


--
-- Name: posts_before_insert_update_row_tr; Type: TRIGGER; Schema: public; Owner: -
--

CREATE TRIGGER posts_before_insert_update_row_tr BEFORE INSERT OR UPDATE ON posts FOR EACH ROW EXECUTE PROCEDURE posts_before_insert_update_row_tr();


--
-- PostgreSQL database dump complete
--

SET search_path TO "$user",public;

INSERT INTO schema_migrations (version) VALUES ('20131230213035');

INSERT INTO schema_migrations (version) VALUES ('20140115101632');

INSERT INTO schema_migrations (version) VALUES ('20140115183846');

If someone could help me to figure out on why rails doesn't acknowledges the existance of the tsv column I would be soooooo happy! :)

Was it helpful?

Solution

If someone could help me to figure out on why rails doesn't acknowledges the existance of the tsv column

It's not rails, but this code:

   new.tsv := tsvector_update_trigger(tsv, 'pg_catalog.english', title, body);

Aside from the syntactic problem that tsv in this context cannot be interpreted, the real concern is that tsvector_update_trigger returns trigger, not tsvector, so it's supposed to be called by the SQL engine following an insert or update, not explicitly by user code.

In fact the reason of being for a pre-existing tsvector_update_trigger is precisely for the programmer to avoid writing a trigger in the first place. You're expected to refer to it directly with a CREATE TRIGGER statement, like this:

CREATE TRIGGER trigger_name 
BEFORE INSERT OR UPDATE ON posts FOR EACH ROW EXECUTE PROCEDURE 
tsvector_update_trigger(tsv, 'pg_catalog.english', title, body);

and then your function posts_before_insert_update_row_tr() is not needed.

See Triggers for Automatic Updates in the doc.

OTHER TIPS

Based on Daniel Vérité's answer I've written a custom trigger (put it in a separate migration instead of in the model).

class CreateUpdateTsvPostsTrigger < ActiveRecord::Migration
  def up
    create_trigger(compatibility: 1).name('update_tsv_posts').on(:posts).before(:insert, :update) do
      "new.tsv :=
          setweight(to_tsvector('pg_catalog.english', coalesce(new.title,'')), 'A') ||
          setweight(to_tsvector('pg_catalog.english', coalesce(new.body,'')), 'D');"
    end
  end

  def down
    drop_trigger("update_tsv_posts", "posts")
  end
end

The whole thing works like a charm. :)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top