LP#1998363: Reduce churn caused by bib updates user/miker/lp-1998363-less-ingest-churn-mfe-edition
authorMike Rylander <mrylander@gmail.com>
Thu, 1 Dec 2022 22:23:04 +0000 (17:23 -0500)
committerMike Rylander <mrylander@gmail.com>
Thu, 1 Dec 2022 22:23:04 +0000 (17:23 -0500)
This commit causes reingest of the core search data to check for
prexisting values that match exactly after pre-storage normalizers are
run.  When such rows exist, it is reused and rewritten so that changes
to post-storage normalizer configuration will have the intended effects.

This will have downstream benefits for the symspell subsystem that
supports the Did You Mean functionality, allowing it to avoid
reprocessing rows where the data to be processed has not changed in a
way material to that logic.

Signed-off-by: Mike Rylander <mrylander@gmail.com>
Open-ILS/src/sql/Pg/030.schema.metabib.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.function.metabib_field_entry_churn_reduction.sql [new file with mode: 0644]

index 40cd60e..293324c 100644 (file)
@@ -1047,7 +1047,6 @@ CREATE OR REPLACE FUNCTION metabib.reingest_metabib_field_entries(
     only_fields INT[] DEFAULT '{}'::INT[]
 ) RETURNS VOID AS $func$
 DECLARE
-    fclass          RECORD;
     ind_data        metabib.field_entry_template%ROWTYPE;
     mbe_row         metabib.browse_entry%ROWTYPE;
     mbe_id          BIGINT;
@@ -1057,6 +1056,12 @@ DECLARE
     b_skip_search   BOOL;
     value_prepped   TEXT;
     field_list      INT[] := only_fields;
+    extant_mtfe     BIGINT[];
+    extant_mafe     BIGINT[];
+    extant_msfe     BIGINT[];
+    extant_msefe    BIGINT[];
+    extant_mife     BIGINT[];
+    extant_mkfe     BIGINT[];
     field_types     TEXT[] := '{}'::TEXT[];
 BEGIN
 
@@ -1064,6 +1069,13 @@ BEGIN
         SELECT ARRAY_AGG(id) INTO field_list FROM config.metabib_field;
     END IF;
 
+    SELECT ARRAY_AGG(id) INTO extant_mtfe FROM metabib.title_field_entry WHERE source = bib_id AND field = ANY ( field_list );
+    SELECT ARRAY_AGG(id) INTO extant_mafe FROM metabib.author_field_entry WHERE source = bib_id AND field = ANY ( field_list );
+    SELECT ARRAY_AGG(id) INTO extant_msfe FROM metabib.subject_field_entry WHERE source = bib_id AND field = ANY ( field_list );
+    SELECT ARRAY_AGG(id) INTO extant_msefe FROM metabib.series_field_entry WHERE source = bib_id AND field = ANY ( field_list );
+    SELECT ARRAY_AGG(id) INTO extant_mife FROM metabib.identifier_field_entry WHERE source = bib_id AND field = ANY ( field_list );
+    SELECT ARRAY_AGG(id) INTO extant_mkfe FROM metabib.keyword_field_entry WHERE source = bib_id AND field = ANY ( field_list );
+
     SELECT COALESCE(NULLIF(skip_facet, FALSE), EXISTS (SELECT enabled FROM config.internal_flag WHERE name =  'ingest.skip_facet_indexing' AND enabled)) INTO b_skip_facet;
     SELECT COALESCE(NULLIF(skip_display, FALSE), EXISTS (SELECT enabled FROM config.internal_flag WHERE name =  'ingest.skip_display_indexing' AND enabled)) INTO b_skip_display;
     SELECT COALESCE(NULLIF(skip_browse, FALSE), EXISTS (SELECT enabled FROM config.internal_flag WHERE name =  'ingest.skip_browse_indexing' AND enabled)) INTO b_skip_browse;
@@ -1076,12 +1088,6 @@ BEGIN
 
     PERFORM * FROM config.internal_flag WHERE name = 'ingest.assume_inserts_only' AND enabled;
     IF NOT FOUND THEN
-        IF NOT b_skip_search THEN
-            FOR fclass IN SELECT * FROM config.metabib_class LOOP
-                -- RAISE NOTICE 'Emptying out %', fclass.name;
-                EXECUTE $$DELETE FROM metabib.$$ || fclass.name || $$_field_entry WHERE source = $$ || bib_id;
-            END LOOP;
-        END IF;
         IF NOT b_skip_facet THEN
             DELETE FROM metabib.facet_entry WHERE source = bib_id;
         END IF;
@@ -1095,7 +1101,7 @@ BEGIN
 
     FOR ind_data IN SELECT * FROM biblio.extract_metabib_field_entry( bib_id, ' ', field_types, field_list ) LOOP
 
-       -- don't store what has been normalized away
+           -- don't store what has been normalized away
         CONTINUE WHEN ind_data.value IS NULL;
 
         IF ind_data.field < 0 THEN
@@ -1148,25 +1154,50 @@ BEGIN
         END IF;
 
         IF ind_data.search_field AND NOT b_skip_search THEN
+            -- This function processes stored-value (negative) normalizers so we can properly check for duplicates
+            value_prepped := metabib.browse_normalize(ind_data.value, ind_data.field);
+
             -- Avoid inserting duplicate rows
             EXECUTE 'SELECT 1 FROM metabib.' || ind_data.field_class ||
                 '_field_entry WHERE field = $1 AND source = $2 AND value = $3'
-                INTO mbe_id USING ind_data.field, ind_data.source, ind_data.value;
+                INTO mbe_id USING ind_data.field, ind_data.source, value_prepped;
                 -- RAISE NOTICE 'Search for an already matching row returned %', mbe_id;
-            IF mbe_id IS NULL THEN
+
+            IF mbe_id IS NULL THEN -- No existing field entry for the to-be-stored value
                 EXECUTE $$
                 INSERT INTO metabib.$$ || ind_data.field_class || $$_field_entry (field, source, value)
                     VALUES ($$ ||
                         quote_literal(ind_data.field) || $$, $$ ||
                         quote_literal(ind_data.source) || $$, $$ ||
-                        quote_literal(ind_data.value) ||
+                        quote_literal(value_prepped) ||
                     $$);$$;
+            ELSE -- update the row so that the positive normalizers can run
+
+                -- reusing this row, consider it new instead of extant for the deletes below
+                extant_mtfe := ARRAY_REMOVE(extant_mtfe,mbe_id);
+                extant_mafe := ARRAY_REMOVE(extant_mafe,mbe_id);
+                extant_msfe := ARRAY_REMOVE(extant_msfe,mbe_id);
+                extant_msefe := ARRAY_REMOVE(extant_msefe,mbe_id);
+                extant_mife := ARRAY_REMOVE(extant_mife,mbe_id);
+                extant_mkfe := ARRAY_REMOVE(extant_mkfe,mbe_id);
+
+                EXECUTE 'UPDATE metabib.' || ind_data.field_class ||
+                    '_field_entry SET value = $1 WHERE id = $2'
+                    USING value_prepped, mbe_id;
             END IF;
         END IF;
 
     END LOOP;
 
     IF NOT b_skip_search THEN
+        -- Flush old, un-updated entries
+        DELETE FROM metabib.title_field_entry WHERE id = ANY (extant_mtfe);
+        DELETE FROM metabib.author_field_entry WHERE id = ANY (extant_mafe);
+        DELETE FROM metabib.subject_field_entry WHERE id = ANY (extant_msfe);
+        DELETE FROM metabib.series_field_entry WHERE id = ANY (extant_msefe);
+        DELETE FROM metabib.identifier_field_entry WHERE id = ANY (extant_mife);
+        DELETE FROM metabib.keyword_field_entry WHERE id = ANY (extant_mkfe);
+
         PERFORM metabib.update_combined_index_vectors(bib_id);
         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_symspell_reification' AND enabled;
         IF NOT FOUND THEN
@@ -1894,6 +1925,7 @@ $func$ LANGUAGE PLPGSQL;
 CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
 DECLARE
     tmp_bool BOOL;
+    fclass   RECORD;
 BEGIN
 
     IF NEW.deleted THEN -- If this bib is deleted
@@ -1910,6 +1942,10 @@ BEGIN
             -- with the #deleted modifier, so one should turn on the named
             -- internal flag for that functionality.
             DELETE FROM metabib.record_attr_vector_list WHERE source = NEW.id;
+
+            FOR fclass IN SELECT * FROM config.metabib_class LOOP
+                EXECUTE $$DELETE FROM metabib.$$ || fclass.name || $$_field_entry WHERE source = $$ || NEW.id;
+            END LOOP;
         END IF;
 
         DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
@@ -2136,28 +2172,7 @@ BEGIN
     NEW.index_vector = ''::tsvector;
 
     IF TG_TABLE_NAME::TEXT ~ 'field_entry$' THEN
-        FOR normalizer IN
-            SELECT  n.func AS func,
-                    n.param_count AS param_count,
-                    m.params AS params
-              FROM  config.index_normalizer n
-                    JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
-              WHERE field = NEW.field AND m.pos < 0
-              ORDER BY m.pos LOOP
-                EXECUTE 'SELECT ' || normalizer.func || '(' ||
-                    quote_literal( value ) ||
-                    CASE
-                        WHEN normalizer.param_count > 0
-                            THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
-                            ELSE ''
-                        END ||
-                    ')' INTO value;
-
-        END LOOP;
-
-        NEW.value = value;
-
-        FOR normalizer IN
+        FOR normalizer IN -- only process post-stored-value normalizers
             SELECT  n.func AS func,
                     n.param_count AS param_count,
                     m.params AS params
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.function.metabib_field_entry_churn_reduction.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.function.metabib_field_entry_churn_reduction.sql
new file mode 100644 (file)
index 0000000..f783f64
--- /dev/null
@@ -0,0 +1,349 @@
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
+DECLARE
+    tmp_bool BOOL;
+    fclass   RECORD;
+BEGIN
+
+    IF NEW.deleted THEN -- If this bib is deleted
+
+        PERFORM * FROM config.internal_flag WHERE
+            name = 'ingest.metarecord_mapping.preserve_on_delete' AND enabled;
+
+        tmp_bool := FOUND; -- Just in case this is changed by some other statement
+
+        PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint, TRUE, tmp_bool );
+
+        IF NOT tmp_bool THEN
+            -- One needs to keep these around to support searches
+            -- with the #deleted modifier, so one should turn on the named
+            -- internal flag for that functionality.
+            DELETE FROM metabib.record_attr_vector_list WHERE source = NEW.id;
+
+            FOR fclass IN SELECT * FROM config.metabib_class LOOP
+                EXECUTE $$DELETE FROM metabib.$$ || fclass.name || $$_field_entry WHERE source = $$ || NEW.id;
+            END LOOP;
+        END IF;
+
+        DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
+        DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = NEW.id; -- Separate any multi-homed items
+        DELETE FROM metabib.browse_entry_def_map WHERE source = NEW.id; -- Don't auto-suggest deleted bibs
+        RETURN NEW; -- and we're done
+    END IF;
+
+    IF TG_OP = 'UPDATE' AND OLD.deleted IS FALSE THEN -- re-ingest?
+        PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
+
+        IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
+            RETURN NEW;
+        END IF;
+    END IF;
+
+    -- Record authority linking
+    PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_linking' AND enabled;
+    IF NOT FOUND THEN
+        PERFORM biblio.map_authority_linking( NEW.id, NEW.marc );
+    END IF;
+
+    -- Flatten and insert the mfr data
+    PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_full_rec' AND enabled;
+    IF NOT FOUND THEN
+        PERFORM metabib.reingest_metabib_full_rec(NEW.id);
+
+        -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
+        PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
+        IF NOT FOUND THEN
+            PERFORM metabib.reingest_record_attributes(NEW.id, NULL, NEW.marc, TG_OP = 'INSERT' OR OLD.deleted);
+        END IF;
+    END IF;
+
+    -- Gather and insert the field entry data
+    PERFORM metabib.reingest_metabib_field_entries(NEW.id);
+
+    -- Located URI magic
+    PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
+    IF NOT FOUND THEN PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor ); END IF;
+
+    -- (re)map metarecord-bib linking
+    IF TG_OP = 'INSERT' THEN -- if not deleted and performing an insert, check for the flag
+        PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
+        IF NOT FOUND THEN
+            PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
+        END IF;
+    ELSE -- we're doing an update, and we're not deleted, remap
+        PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_update' AND enabled;
+        IF NOT FOUND THEN
+            PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
+        END IF;
+    END IF;
+
+    RETURN NEW;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION metabib.reingest_metabib_field_entries(
+    bib_id BIGINT,
+    skip_facet BOOL DEFAULT FALSE,
+    skip_display BOOL DEFAULT FALSE,
+    skip_browse BOOL DEFAULT FALSE,
+    skip_search BOOL DEFAULT FALSE,
+    only_fields INT[] DEFAULT '{}'::INT[]
+) RETURNS VOID AS $func$
+DECLARE
+    ind_data        metabib.field_entry_template%ROWTYPE;
+    mbe_row         metabib.browse_entry%ROWTYPE;
+    mbe_id          BIGINT;
+    b_skip_facet    BOOL;
+    b_skip_display    BOOL;
+    b_skip_browse   BOOL;
+    b_skip_search   BOOL;
+    value_prepped   TEXT;
+    field_list      INT[] := only_fields;
+    extant_mtfe     BIGINT[];
+    extant_mafe     BIGINT[];
+    extant_msfe     BIGINT[];
+    extant_msefe    BIGINT[];
+    extant_mife     BIGINT[];
+    extant_mkfe     BIGINT[];
+    field_types     TEXT[] := '{}'::TEXT[];
+BEGIN
+
+    IF field_list = '{}'::INT[] THEN
+        SELECT ARRAY_AGG(id) INTO field_list FROM config.metabib_field;
+    END IF;
+
+    SELECT ARRAY_AGG(id) INTO extant_mtfe FROM metabib.title_field_entry WHERE source = bib_id AND field = ANY ( field_list );
+    SELECT ARRAY_AGG(id) INTO extant_mafe FROM metabib.author_field_entry WHERE source = bib_id AND field = ANY ( field_list );
+    SELECT ARRAY_AGG(id) INTO extant_msfe FROM metabib.subject_field_entry WHERE source = bib_id AND field = ANY ( field_list );
+    SELECT ARRAY_AGG(id) INTO extant_msefe FROM metabib.series_field_entry WHERE source = bib_id AND field = ANY ( field_list );
+    SELECT ARRAY_AGG(id) INTO extant_mife FROM metabib.identifier_field_entry WHERE source = bib_id AND field = ANY ( field_list );
+    SELECT ARRAY_AGG(id) INTO extant_mkfe FROM metabib.keyword_field_entry WHERE source = bib_id AND field = ANY ( field_list );
+
+    SELECT COALESCE(NULLIF(skip_facet, FALSE), EXISTS (SELECT enabled FROM config.internal_flag WHERE name =  'ingest.skip_facet_indexing' AND enabled)) INTO b_skip_facet;
+    SELECT COALESCE(NULLIF(skip_display, FALSE), EXISTS (SELECT enabled FROM config.internal_flag WHERE name =  'ingest.skip_display_indexing' AND enabled)) INTO b_skip_display;
+    SELECT COALESCE(NULLIF(skip_browse, FALSE), EXISTS (SELECT enabled FROM config.internal_flag WHERE name =  'ingest.skip_browse_indexing' AND enabled)) INTO b_skip_browse;
+    SELECT COALESCE(NULLIF(skip_search, FALSE), EXISTS (SELECT enabled FROM config.internal_flag WHERE name =  'ingest.skip_search_indexing' AND enabled)) INTO b_skip_search;
+
+    IF NOT b_skip_facet THEN field_types := field_types || '{facet}'; END IF;
+    IF NOT b_skip_display THEN field_types := field_types || '{display}'; END IF;
+    IF NOT b_skip_browse THEN field_types := field_types || '{browse}'; END IF;
+    IF NOT b_skip_search THEN field_types := field_types || '{search}'; END IF;
+
+    PERFORM * FROM config.internal_flag WHERE name = 'ingest.assume_inserts_only' AND enabled;
+    IF NOT FOUND THEN
+        IF NOT b_skip_facet THEN
+            DELETE FROM metabib.facet_entry WHERE source = bib_id;
+        END IF;
+        IF NOT b_skip_display THEN
+            DELETE FROM metabib.display_entry WHERE source = bib_id;
+        END IF;
+        IF NOT b_skip_browse THEN
+            DELETE FROM metabib.browse_entry_def_map WHERE source = bib_id;
+        END IF;
+    END IF;
+
+    FOR ind_data IN SELECT * FROM biblio.extract_metabib_field_entry( bib_id, ' ', field_types, field_list ) LOOP
+
+        -- don't store what has been normalized away
+        CONTINUE WHEN ind_data.value IS NULL;
+
+        IF ind_data.field < 0 THEN
+            ind_data.field = -1 * ind_data.field;
+        END IF;
+
+        IF ind_data.facet_field AND NOT b_skip_facet THEN
+            INSERT INTO metabib.facet_entry (field, source, value)
+                VALUES (ind_data.field, ind_data.source, ind_data.value);
+        END IF;
+
+        IF ind_data.display_field AND NOT b_skip_display THEN
+            INSERT INTO metabib.display_entry (field, source, value)
+                VALUES (ind_data.field, ind_data.source, ind_data.value);
+        END IF;
+
+
+        IF ind_data.browse_field AND NOT b_skip_browse THEN
+            -- A caveat about this SELECT: this should take care of replacing
+            -- old mbe rows when data changes, but not if normalization (by
+            -- which I mean specifically the output of
+            -- evergreen.oils_tsearch2()) changes.  It may or may not be
+            -- expensive to add a comparison of index_vector to index_vector
+            -- to the WHERE clause below.
+
+            CONTINUE WHEN ind_data.sort_value IS NULL;
+
+            value_prepped := metabib.browse_normalize(ind_data.value, ind_data.field);
+            IF ind_data.browse_nocase THEN
+                SELECT INTO mbe_row * FROM metabib.browse_entry
+                    WHERE evergreen.lowercase(value) = evergreen.lowercase(value_prepped) AND sort_value = ind_data.sort_value
+                    ORDER BY sort_value, value LIMIT 1; -- gotta pick something, I guess
+            ELSE
+                SELECT INTO mbe_row * FROM metabib.browse_entry
+                    WHERE value = value_prepped AND sort_value = ind_data.sort_value;
+            END IF;
+
+            IF FOUND THEN
+                mbe_id := mbe_row.id;
+            ELSE
+                INSERT INTO metabib.browse_entry
+                    ( value, sort_value ) VALUES
+                    ( value_prepped, ind_data.sort_value );
+
+                mbe_id := CURRVAL('metabib.browse_entry_id_seq'::REGCLASS);
+            END IF;
+
+            INSERT INTO metabib.browse_entry_def_map (entry, def, source, authority)
+                VALUES (mbe_id, ind_data.field, ind_data.source, ind_data.authority);
+        END IF;
+
+        IF ind_data.search_field AND NOT b_skip_search THEN
+            -- This function processes stored-value (negative) normalizers so we can properly check for duplicates
+            value_prepped := metabib.browse_normalize(ind_data.value, ind_data.field);
+
+            -- Avoid inserting duplicate rows
+            EXECUTE 'SELECT 1 FROM metabib.' || ind_data.field_class ||
+                '_field_entry WHERE field = $1 AND source = $2 AND value = $3'
+                INTO mbe_id USING ind_data.field, ind_data.source, value_prepped;
+                -- RAISE NOTICE 'Search for an already matching row returned %', mbe_id;
+
+            IF mbe_id IS NULL THEN -- No existing field entry for the to-be-stored value
+                EXECUTE $$
+                INSERT INTO metabib.$$ || ind_data.field_class || $$_field_entry (field, source, value)
+                    VALUES ($$ ||
+                        quote_literal(ind_data.field) || $$, $$ ||
+                        quote_literal(ind_data.source) || $$, $$ ||
+                        quote_literal(value_prepped) ||
+                    $$);$$;
+            ELSE -- update the row so that the positive normalizers can run
+
+                -- reusing this row, consider it new instead of extant for the deletes below
+                extant_mtfe := ARRAY_REMOVE(extant_mtfe,mbe_id);
+                extant_mafe := ARRAY_REMOVE(extant_mafe,mbe_id);
+                extant_msfe := ARRAY_REMOVE(extant_msfe,mbe_id);
+                extant_msefe := ARRAY_REMOVE(extant_msefe,mbe_id);
+                extant_mife := ARRAY_REMOVE(extant_mife,mbe_id);
+                extant_mkfe := ARRAY_REMOVE(extant_mkfe,mbe_id);
+
+                EXECUTE 'UPDATE metabib.' || ind_data.field_class ||
+                    '_field_entry SET value = $1 WHERE id = $2'
+                    USING value_prepped, mbe_id;
+            END IF;
+        END IF;
+
+    END LOOP;
+
+    IF NOT b_skip_search THEN
+        -- Flush old, un-updated entries
+        DELETE FROM metabib.title_field_entry WHERE id = ANY (extant_mtfe);
+        DELETE FROM metabib.author_field_entry WHERE id = ANY (extant_mafe);
+        DELETE FROM metabib.subject_field_entry WHERE id = ANY (extant_msfe);
+        DELETE FROM metabib.series_field_entry WHERE id = ANY (extant_msefe);
+        DELETE FROM metabib.identifier_field_entry WHERE id = ANY (extant_mife);
+        DELETE FROM metabib.keyword_field_entry WHERE id = ANY (extant_mkfe);
+
+        PERFORM metabib.update_combined_index_vectors(bib_id);
+        PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_symspell_reification' AND enabled;
+        IF NOT FOUND THEN
+            PERFORM search.symspell_dictionary_reify();
+        END IF;
+    END IF;
+
+    RETURN;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION public.oils_tsearch2 () RETURNS TRIGGER AS $$
+DECLARE
+    normalizer      RECORD;
+    value           TEXT := '';
+    temp_vector     TEXT := '';
+    ts_rec          RECORD;
+    cur_weight      "char";
+BEGIN
+
+    value := NEW.value;
+    NEW.index_vector = ''::tsvector;
+
+    IF TG_TABLE_NAME::TEXT ~ 'field_entry$' THEN
+        FOR normalizer IN -- only process post-stored-value normalizers
+            SELECT  n.func AS func,
+                    n.param_count AS param_count,
+                    m.params AS params
+              FROM  config.index_normalizer n
+                    JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
+              WHERE field = NEW.field AND m.pos >= 0
+              ORDER BY m.pos LOOP
+                EXECUTE 'SELECT ' || normalizer.func || '(' ||
+                    quote_literal( value ) ||
+                    CASE
+                        WHEN normalizer.param_count > 0
+                            THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
+                            ELSE ''
+                        END ||
+                    ')' INTO value;
+
+        END LOOP;
+   END IF;
+
+    IF TG_TABLE_NAME::TEXT ~ 'browse_entry$' THEN
+
+        value :=  ARRAY_TO_STRING(
+            evergreen.regexp_split_to_array(value, E'\\W+'), ' '
+        );
+        value := public.search_normalize(value);
+        NEW.index_vector = to_tsvector(TG_ARGV[0]::regconfig, value);
+
+    ELSIF TG_TABLE_NAME::TEXT ~ 'field_entry$' THEN
+        FOR ts_rec IN
+
+            SELECT DISTINCT m.ts_config, m.index_weight
+            FROM config.metabib_class_ts_map m
+                 LEFT JOIN metabib.record_attr_vector_list r ON (r.source = NEW.source)
+                 LEFT JOIN config.coded_value_map ccvm ON (
+                    ccvm.ctype IN ('item_lang', 'language') AND
+                    ccvm.code = m.index_lang AND
+                    r.vlist @> intset(ccvm.id)
+                )
+            WHERE m.field_class = TG_ARGV[0]
+                AND m.active
+                AND (m.always OR NOT EXISTS (SELECT 1 FROM config.metabib_field_ts_map WHERE metabib_field = NEW.field))
+                AND (m.index_lang IS NULL OR ccvm.id IS NOT NULL)
+                        UNION
+            SELECT DISTINCT m.ts_config, m.index_weight
+            FROM config.metabib_field_ts_map m
+                 LEFT JOIN metabib.record_attr_vector_list r ON (r.source = NEW.source)
+                 LEFT JOIN config.coded_value_map ccvm ON (
+                    ccvm.ctype IN ('item_lang', 'language') AND
+                    ccvm.code = m.index_lang AND
+                    r.vlist @> intset(ccvm.id)
+                )
+            WHERE m.metabib_field = NEW.field
+                AND m.active
+                AND (m.index_lang IS NULL OR ccvm.id IS NOT NULL)
+            ORDER BY index_weight ASC
+
+        LOOP
+
+            IF cur_weight IS NOT NULL AND cur_weight != ts_rec.index_weight THEN
+                NEW.index_vector = NEW.index_vector || setweight(temp_vector::tsvector,cur_weight);
+                temp_vector = '';
+            END IF;
+
+            cur_weight = ts_rec.index_weight;
+            SELECT INTO temp_vector temp_vector || ' ' || to_tsvector(ts_rec.ts_config::regconfig, value)::TEXT;
+
+        END LOOP;
+        NEW.index_vector = NEW.index_vector || setweight(temp_vector::tsvector,cur_weight);
+    ELSE
+        NEW.index_vector = to_tsvector(TG_ARGV[0]::regconfig, value);
+    END IF;
+
+    RETURN NEW;
+END;
+$$ LANGUAGE PLPGSQL;
+
+COMMIT;
+