1 --Upgrade Script for 3.6.2 to 3.7-beta
2 \set eg_version '''3.7-beta'''
4 INSERT INTO config.upgrade_log (version, applied_to) VALUES ('3.7-beta', :eg_version);
6 SELECT evergreen.upgrade_deps_block_check('1247', :eg_version);
8 INSERT INTO permission.perm_list (id,code,description) VALUES (627,'SSO_ADMIN','Modify patron SSO settings');
10 INSERT INTO config.org_unit_setting_type
11 ( name, grp, label, description, datatype, update_perm )
13 ('opac.login.shib_sso.enable',
15 oils_i18n_gettext('opac.login.shib_sso.enable', 'Enable Shibboleth SSO for the OPAC', 'coust', 'label'),
16 oils_i18n_gettext('opac.login.shib_sso.enable', 'Enable Shibboleth SSO for the OPAC', 'coust', 'description'),
18 ('opac.login.shib_sso.entityId',
20 oils_i18n_gettext('opac.login.shib_sso.entityId', 'Shibboleth SSO Entity ID', 'coust', 'label'),
21 oils_i18n_gettext('opac.login.shib_sso.entityId', 'Which configured Entity ID to use for SSO when there is more than one available to Shibboleth', 'coust', 'description'),
23 ('opac.login.shib_sso.logout',
25 oils_i18n_gettext('opac.login.shib_sso.logout', 'Log out of the Shibboleth IdP', 'coust', 'label'),
26 oils_i18n_gettext('opac.login.shib_sso.logout', 'When logging out of Evergreen, also force a logout of the IdP behind Shibboleth', 'coust', 'description'),
28 ('opac.login.shib_sso.allow_native',
30 oils_i18n_gettext('opac.login.shib_sso.allow_native', 'Allow both Shibboleth and native OPAC authentication', 'coust', 'label'),
31 oils_i18n_gettext('opac.login.shib_sso.allow_native', 'When Shibboleth SSO is enabled, also allow native Evergreen authentication', 'coust', 'description'),
33 ('opac.login.shib_sso.evergreen_matchpoint',
35 oils_i18n_gettext('opac.login.shib_sso.evergreen_matchpoint', 'Evergreen SSO matchpoint', 'coust', 'label'),
36 oils_i18n_gettext('opac.login.shib_sso.evergreen_matchpoint',
37 'Evergreen-side field to match a patron against for Shibboleth SSO. Default is usrname. Other reasonable values would be barcode or email.',
38 'coust', 'description'),
40 ('opac.login.shib_sso.shib_matchpoint',
42 oils_i18n_gettext('opac.login.shib_sso.shib_matchpoint', 'Shibboleth SSO matchpoint', 'coust', 'label'),
43 oils_i18n_gettext('opac.login.shib_sso.shib_matchpoint',
44 'Shibboleth-side field to match a patron against for Shibboleth SSO. Default is uid; use eppn for Active Directory', 'coust', 'description'),
49 SELECT evergreen.upgrade_deps_block_check('1248', :eg_version);
51 DO LANGUAGE plpgsql $$
57 -- We only want to mess with gist indexes in stock Evergreen.
58 -- If you've added your own convert them or don't as you see fit.
61 JOIN pg_class cls ON cls.oid=idx.indexrelid
62 JOIN pg_namespace sc ON sc.oid = cls.relnamespace
63 JOIN pg_class tab ON tab.oid=idx.indrelid
64 JOIN pg_attribute at ON (at.attnum = ANY(idx.indkey) AND at.attrelid = tab.oid)
65 JOIN pg_am am ON am.oid=cls.relam
66 WHERE am.amname = 'gist'
68 'authority_full_rec_index_vector_idx',
69 'authority_simple_heading_index_vector_idx',
70 'metabib_identifier_field_entry_index_vector_idx',
71 'metabib_combined_identifier_field_entry_index_vector_idx',
72 'metabib_title_field_entry_index_vector_idx',
73 'metabib_combined_title_field_entry_index_vector_idx',
74 'metabib_author_field_entry_index_vector_idx',
75 'metabib_combined_author_field_entry_index_vector_idx',
76 'metabib_subject_field_entry_index_vector_idx',
77 'metabib_combined_subject_field_entry_index_vector_idx',
78 'metabib_keyword_field_entry_index_vector_idx',
79 'metabib_combined_keyword_field_entry_index_vector_idx',
80 'metabib_series_field_entry_index_vector_idx',
81 'metabib_combined_series_field_entry_index_vector_idx',
82 'metabib_full_rec_index_vector_idx'
91 RAISE NOTICE 'Converting GIST indexes into GIN indexes...';
93 FOR ind IN SELECT sc.nspname AS sch, tab.relname AS tab, cls.relname AS idx, at.attname AS col
95 JOIN pg_class cls ON cls.oid=idx.indexrelid
96 JOIN pg_namespace sc ON sc.oid = cls.relnamespace
97 JOIN pg_class tab ON tab.oid=idx.indrelid
98 JOIN pg_attribute at ON (at.attnum = ANY(idx.indkey) AND at.attrelid = tab.oid)
99 JOIN pg_am am ON am.oid=cls.relam
100 WHERE am.amname = 'gist'
102 'authority_full_rec_index_vector_idx',
103 'authority_simple_heading_index_vector_idx',
104 'metabib_identifier_field_entry_index_vector_idx',
105 'metabib_combined_identifier_field_entry_index_vector_idx',
106 'metabib_title_field_entry_index_vector_idx',
107 'metabib_combined_title_field_entry_index_vector_idx',
108 'metabib_author_field_entry_index_vector_idx',
109 'metabib_combined_author_field_entry_index_vector_idx',
110 'metabib_subject_field_entry_index_vector_idx',
111 'metabib_combined_subject_field_entry_index_vector_idx',
112 'metabib_keyword_field_entry_index_vector_idx',
113 'metabib_combined_keyword_field_entry_index_vector_idx',
114 'metabib_series_field_entry_index_vector_idx',
115 'metabib_combined_series_field_entry_index_vector_idx',
116 'metabib_full_rec_index_vector_idx'
119 -- Move existing index out of the way so there's no difference between new databases and upgraded databases
120 EXECUTE FORMAT('ALTER INDEX %I.%I RENAME TO %I_gist', ind.sch, ind.idx, ind.idx);
122 -- Meet the new index, same as the old index (almost)
123 EXECUTE FORMAT('CREATE INDEX %I ON %I.%I USING GIN (%I)', ind.idx, ind.sch, ind.tab, ind.col);
125 -- And drop the old index
126 EXECUTE FORMAT('DROP INDEX %I.%I_gist', ind.sch, ind.idx);
128 tablist := tablist || ' ' || ind.sch || '.' || ind.tab || E'\n';
132 RAISE NOTICE E'Conversion Complete.\n\n You should run a VACUUM ANALYZE on the following tables soon:\n%', tablist;
138 SELECT evergreen.upgrade_deps_block_check('1249', :eg_version);
140 INSERT INTO config.usr_setting_type (
148 'circ.default_overdue_notices_enabled',
151 'circ.default_overdue_notices_enabled',
152 'Receive Overdue and Courtesy Emails',
157 'circ.default_overdue_notices_enabled',
158 'Receive overdue and predue email notifications',
168 \qecho The following query will set the circ.default_overdue_notices_enabled
169 \qecho user setting to true (the default value) for all existing users,
170 \qecho ensuring they continue to receive overdue/predue emails.
172 \qecho INSERT INTO actor.usr_setting (usr, name, value)
175 \qecho 'circ.default_overdue_notices_enabled',
177 \qecho FROM actor.usr;
179 \qecho The following query will add the circ.default_overdue_notices_enabled
180 \qecho user setting as an opt-in setting for all action triggers that send
181 \qecho emails based on a circ being due (unless another opt-in setting is
182 \qecho already in use).
184 \qecho UPDATE action_trigger.event_definition
185 \qecho SET opt_in_setting = 'circ.default_overdue_notices_enabled',
186 \qecho usr_field = 'usr'
187 \qecho WHERE opt_in_setting IS NULL
188 \qecho AND hook = 'checkout.due'
189 \qecho AND reactor = 'SendEmail';
191 \qecho Evergreen admins who wish to use the new setting should run both of
192 \qecho the above queries. Admins who do not wish to use it, or who are
193 \qecho already using a custom opt-in setting of their own, do not need to
198 SELECT evergreen.upgrade_deps_block_check('1250', :eg_version);
200 CREATE TABLE action.batch_hold_event (
201 id SERIAL PRIMARY KEY,
202 staff INT NOT NULL REFERENCES actor.usr (id) ON UPDATE CASCADE ON DELETE CASCADE,
203 bucket INT NOT NULL REFERENCES container.user_bucket (id) ON UPDATE CASCADE ON DELETE CASCADE,
205 hold_type TEXT NOT NULL DEFAULT 'T', -- maybe different hold types in the future...
206 run_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
207 cancelled TIMESTAMP WITH TIME ZONE
210 CREATE TABLE action.batch_hold_event_map (
211 id SERIAL PRIMARY KEY,
212 batch_hold_event INT NOT NULL REFERENCES action.batch_hold_event (id) ON UPDATE CASCADE ON DELETE CASCADE,
213 hold INT NOT NULL REFERENCES action.hold_request (id) ON UPDATE CASCADE ON DELETE CASCADE
216 INSERT INTO container.user_bucket_type (code,label) VALUES ('hold_subscription','Hold Group Container');
218 INSERT INTO config.org_unit_setting_type
219 (name, label, description, grp, datatype)
221 'holds.subscription.randomize',
223 'holds.subscription.randomize',
224 'Randomize group hold order',
229 'holds.subscription.randomize',
230 'When placing a batch group hold, randomize the order of the patrons receiving the holds so they are not always in the same order.',
238 INSERT INTO permission.perm_list (id,code,description)
239 VALUES ( 628, 'MANAGE_HOLD_GROUPS', oils_i18n_gettext(628, 'Manage hold groups and hold group events', 'ppl', 'description'));
241 INSERT INTO action.hold_request_cancel_cause (id,label)
242 VALUES ( 8, oils_i18n_gettext(8, 'Hold Group Event rollback', 'ahrcc', 'label'));
244 INSERT INTO action_trigger.event_definition (active, owner, name, hook, validator, reactor, delay, delay_field, group_field, cleanup_success, template)
245 VALUES ('f', 1, 'Hold Group Hold Placed for Patron Email Notification', 'hold_request.success', 'NOOP_True', 'SendEmail', '30 minutes', 'request_time', 'usr', 'CreateHoldNotification',
248 [%- user = target.0.usr -%]
249 To: [%- params.recipient_email || user.email %]
250 From: [%- params.sender_email || default_sender %]
251 Date: [%- date.format(date.now, '%a, %d %b %Y %T -0000', gmt => 1) %]
252 Subject: Subcription Hold placed for you
253 Auto-Submitted: auto-generated
255 Dear [% user.family_name %], [% user.first_given_name %]
256 The following items have been placed on hold for you:
258 [% FOR hold IN target %]
259 [%- copy_details = helpers.get_copy_bib_basics(hold.current_copy.id) -%]
260 Title: [% copy_details.title %]
261 Author: [% copy_details.author %]
262 Call Number: [% hold.current_copy.call_number.label %]
263 Barcode: [% hold.current_copy.barcode %]
264 Library: [% hold.pickup_lib.name %]
269 INSERT INTO action_trigger.environment (event_def, path ) VALUES
270 ( currval('action_trigger.event_definition_id_seq'), 'usr' ),
271 ( currval('action_trigger.event_definition_id_seq'), 'pickup_lib' ),
272 ( currval('action_trigger.event_definition_id_seq'), 'current_copy.call_number' );
275 INSERT INTO action_trigger.event_definition (
276 active, owner, name, hook, validator, reactor, cleanup_success,
277 delay, delay_field, group_field, template
279 false, 1, 'Hold Group Hold Placed for Patron SMS Notification', 'hold_request.success', 'NOOP_True',
280 'SendSMS', 'CreateHoldNotification', '00:30:00', 'shelf_time', 'sms_notify',
282 [%- user = target.0.usr -%]
283 From: [%- params.sender_email || default_sender %]
284 Date: [%- date.format(date.now, ''%a, %d %b %Y %T -0000'', gmt => 1) %]
285 To: [%- params.recipient_email || helpers.get_sms_gateway_email(target.0.sms_carrier,target.0.sms_notify) %]
286 Subject: [% target.size %] subscription hold(s) placed for you
287 Auto-Submitted: auto-generated
289 [% FOR hold IN target %][%-
290 bibxml = helpers.xml_doc( hold.current_copy.call_number.record.marc );
292 FOR part IN bibxml.findnodes(''//*[@tag="245"]/*[@code="a"]'');
293 title = title _ part.textContent;
295 author = bibxml.findnodes(''//*[@tag="100"]/*[@code="a"]'').textContent;
296 %][% hold.usr.first_given_name %]:[% title %] @ [% hold.pickup_lib.name %]
301 INSERT INTO action_trigger.environment (
305 currval('action_trigger.event_definition_id_seq'),
306 'current_copy.call_number.record.simple_record'
308 currval('action_trigger.event_definition_id_seq'),
311 currval('action_trigger.event_definition_id_seq'),
312 'pickup_lib.billing_address'
315 INSERT INTO action_trigger.event_params (event_def, param, value)
316 VALUES (currval('action_trigger.event_definition_id_seq'), 'check_sms_notify', 1);
321 SELECT evergreen.upgrade_deps_block_check('1251', :eg_version);
323 INSERT INTO config.org_unit_setting_type
324 (grp, name, datatype, label, description)
327 'circ.renew.expired_patron_allow', 'bool',
329 'circ.renew.expired_patron_allow',
330 'Allow renewal request if renewal recipient privileges have expired',
335 'circ.renew.expired_patron_allow',
336 'If enabled, users within the org unit who are expired may still renew items.',
343 SELECT evergreen.upgrade_deps_block_check('1252', :eg_version);
345 INSERT INTO config.coded_value_map
346 (id, ctype, code, opac_visible, value, search_label)
347 SELECT 1738,'search_format','video', true,
348 oils_i18n_gettext(1738, 'All Videos', 'ccvm', 'value'),
349 oils_i18n_gettext(1738, 'All Videos', 'ccvm', 'search_label')
351 SELECT 1 FROM config.coded_value_map WHERE id=1738
352 OR value = 'All Videos' OR search_label = 'All Videos'
355 INSERT INTO config.composite_attr_entry_definition (coded_value, definition)
356 SELECT 1738, '{"_attr":"item_type","_val":"g"}'
358 SELECT 1 FROM config.composite_attr_entry_definition WHERE coded_value = 1738
362 SELECT evergreen.upgrade_deps_block_check('1253', :eg_version);
364 INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
366 'eg.print.template_context.booking_capture', 'gui', 'string',
368 'eg.print.template_context.booking_capture',
369 'Print Template Context: booking_capture',
375 SELECT evergreen.upgrade_deps_block_check('1254', :eg_version);
377 -- XXX Committer: confirm ID below is the next available!
378 INSERT INTO permission.perm_list (id, code, description)
379 VALUES ( 629, 'ADMIN_LIBRARY_GROUPS', 'Administer library groups');
381 INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
383 'eg.grid.admin.server.actor.org_lasso', 'gui', 'object',
385 'eg.grid.admin.server.actor.org_lasso',
386 'Grid Config: admin.server.actor.org_lasso',
390 'eg.grid.admin.server.actor.org_lasso_map', 'gui', 'object',
392 'eg.grid.admin.server.actor.org_lasso_map',
393 'Grid Config: admin.server.actor.org_lasso_map',
398 ALTER TABLE actor.org_lasso ADD COLUMN global BOOL NOT NULL DEFAULT FALSE;
402 -- check whether patch can be applied
403 SELECT evergreen.upgrade_deps_block_check('1255', :eg_version);
405 CREATE EXTENSION earthdistance CASCADE;
407 -- 005.schema.actors.sql
409 -- CREATE TABLE actor.org_address (
415 ALTER TABLE actor.org_address ADD COLUMN latitude FLOAT;
416 ALTER TABLE actor.org_address ADD COLUMN longitude FLOAT;
418 -- 002.schema.config.sql
420 CREATE TABLE config.geolocation_service (
421 id SERIAL PRIMARY KEY,
423 owner INT NOT NULL, -- REFERENCES actor.org_unit (id)
431 ALTER TABLE config.geolocation_service ADD CONSTRAINT cgs_owner_fkey
432 FOREIGN KEY (owner) REFERENCES actor.org_unit(id) DEFERRABLE INITIALLY DEFERRED;
434 -- 950.data.seed-values.sql
436 INSERT INTO config.global_flag (name, value, enabled, label)
438 'opac.use_geolocation',
442 'opac.use_geolocation',
443 'Offer use of geographic location services in the public catalog',
448 INSERT INTO config.org_unit_setting_type (name, label, grp, description, datatype)
450 'opac.holdings_sort_by_geographic_proximity',
451 oils_i18n_gettext('opac.holdings_sort_by_geographic_proximity',
452 'Enable Holdings Sort by Geographic Proximity',
455 oils_i18n_gettext('opac.holdings_sort_by_geographic_proximity',
456 'When set to TRUE, will cause the record details page to display the controls for sorting holdings by geographic proximity. This also depends on the global flag opac.use_geolocation being enabled.',
457 'coust', 'description'),
461 INSERT INTO config.org_unit_setting_type (name, label, grp, description, datatype)
463 'opac.geographic_proximity_in_miles',
464 oils_i18n_gettext('opac.geographic_proximity_in_miles',
465 'Show Geographic Proximity in Miles',
468 oils_i18n_gettext('opac.geographic_proximity_in_miles',
469 'When set to TRUE, will cause the record details page to show distances for geographic proximity in miles instead of kilometers.',
470 'coust', 'description'),
474 INSERT INTO config.org_unit_setting_type (name, label, grp, description, datatype, fm_class)
476 'opac.geographic_location_service_for_address',
477 oils_i18n_gettext('opac.geographic_location_service_for_address',
478 'Geographic Location Service to use for Addresses',
481 oils_i18n_gettext('opac.geographic_location_service_for_address',
482 'Specifies which geographic location service to use for converting address input to geographic coordinates.',
483 'coust', 'description'),
487 INSERT INTO permission.perm_list ( id, code, description ) VALUES
488 ( 630, 'VIEW_GEOLOCATION_SERVICES', oils_i18n_gettext(630,
489 'View geographic location services', 'ppl', 'description')),
490 ( 631, 'ADMIN_GEOLOCATION_SERVICES', oils_i18n_gettext(631,
491 'Administer geographic location services', 'ppl', 'description'))
494 -- geolocation-aware variant
495 CREATE OR REPLACE FUNCTION evergreen.rank_ou(lib INT, search_lib INT, pref_lib INT, plat FLOAT, plon FLOAT)
496 RETURNS INTEGER AS $$
499 -- lib matches search_lib
500 (SELECT CASE WHEN $1 = $2 THEN -20000 END),
502 -- lib matches pref_lib
503 (SELECT CASE WHEN $1 = $3 THEN -10000 END),
506 -- pref_lib is a child of search_lib and lib is a child of pref lib.
507 -- For example, searching CONS, pref lib is SYS1,
508 -- copies at BR1 and BR2 sort to the front.
509 (SELECT distance - 5000
510 FROM actor.org_unit_descendants_distance($3)
511 WHERE id = $1 AND $3 IN (
512 SELECT id FROM actor.org_unit_descendants($2))),
514 -- lib is a child of search_lib
515 (SELECT distance FROM actor.org_unit_descendants_distance($2) WHERE id = $1),
517 -- all others pay cash
519 ) + ((SELECT CASE WHEN addr.latitude IS NULL THEN 0 ELSE -20038 END) + (earth_distance( -- shortest GC distance is returned, only half the circumfrence is needed
521 COALESCE(addr.latitude,plat), -- if the org has no coords, we just
522 COALESCE(addr.longitude,plon) -- force 0 distance and let the above tie-break
523 ),ll_to_earth(plat,plon)
524 ) / 1000)::INT ) -- earth_distance is in meters, convert to kilometers and subtract from largest distance
525 FROM actor.org_unit org
526 LEFT JOIN actor.org_address addr ON (org.billing_address = addr.id)
528 $$ LANGUAGE SQL STABLE;
531 SELECT evergreen.upgrade_deps_block_check('1256', :eg_version);
533 CREATE EXTENSION IF NOT EXISTS fuzzystrmatch;
534 CREATE EXTENSION IF NOT EXISTS pg_trgm;
536 INSERT INTO config.internal_flag (name, value, enabled) VALUES ('symspell.prefix_length', '6', TRUE);
537 INSERT INTO config.internal_flag (name, value, enabled) VALUES ('symspell.max_edit_distance', '3', TRUE);
539 INSERT into config.org_unit_setting_type
540 ( name, grp, label, description, datatype )
542 ( 'opac.did_you_mean.max_suggestions', 'opac',
544 'opac.did_you_mean.max_suggestions',
545 'Maximum number of spelling suggestions that may be offered',
548 'opac.did_you_mean.max_suggestions',
549 'If set to -1, provide "best" suggestion if mispelled; if set higher than 0, the maximum suggestions that can be provided; if set to 0, disable suggestions.',
550 'coust', 'description'),
553 INSERT into config.org_unit_setting_type
554 ( name, grp, label, description, datatype )
556 ( 'opac.did_you_mean.low_result_threshold', 'opac',
558 'opac.did_you_mean.low_result_threshold',
559 'Maximum search result count at which spelling suggestions may be offered',
562 'opac.did_you_mean.low_result_threshold',
563 'If a search results in this number or fewer results, and there are correctable spelling mistakes, a suggested search may be provided.',
564 'coust', 'description'),
567 INSERT into config.org_unit_setting_type
568 ( name, grp, label, description, datatype )
570 ( 'search.symspell.min_suggestion_use_threshold', 'opac',
572 'search.symspell.min_suggestion_use_threshold',
573 'Minimum required uses of a spelling suggestions that may be offered',
576 'search.symspell.min_suggestion_use_threshold',
577 'The number of bibliographic records (more or less) that a spelling suggestion must appear in to be considered before offering it to a user. Defaults to 1 (must appear in the bib data).',
578 'coust', 'description'),
581 INSERT into config.org_unit_setting_type
582 ( name, grp, label, description, datatype )
584 ( 'search.symspell.soundex.weight', 'opac',
586 'search.symspell.soundex.weight',
587 'Soundex score weighting in OPAC spelling suggestions.',
590 'search.symspell.soundex.weight',
591 'Soundex, trgm, and keyboard distance similarity measures can be combined to form a secondary ordering parameter for spelling suggestions. This controls the relative weight of the scaled soundex component. Defaults to 0 for "off".',
592 'coust', 'description'),
595 INSERT into config.org_unit_setting_type
596 ( name, grp, label, description, datatype )
598 ( 'search.symspell.pg_trgm.weight', 'opac',
600 'search.symspell.pg_trgm.weight',
601 'Pg_trgm score weighting in OPAC spelling suggestions.',
604 'search.symspell.pg_trgm.weight',
605 'Soundex, pg_trgm, and keyboard distance similarity measures can be combined to form a secondary ordering parameter for spelling suggestions. This controls the relative weight of the scaled pg_trgm component. Defaults to 0 for "off".',
606 'coust', 'description'),
609 INSERT into config.org_unit_setting_type
610 ( name, grp, label, description, datatype )
612 ( 'search.symspell.keyboard_distance.weight', 'opac',
614 'search.symspell.keyboard_distance.weight',
615 'Keyboard distance score weighting in OPAC spelling suggestions.',
618 'search.symspell.keyboard_distance.weight',
619 'Soundex, trgm, and keyboard distance similarity measures can be combined to form a secondary ordering parameter for spelling suggestions. This controls the relative weight of the scaled keyboard distance component. Defaults to 0 for "off".',
620 'coust', 'description'),
623 CREATE OR REPLACE FUNCTION evergreen.uppercase( TEXT ) RETURNS TEXT AS $$
625 $$ LANGUAGE PLPERLU STRICT IMMUTABLE;
627 CREATE OR REPLACE FUNCTION evergreen.text_array_merge_unique (
629 ) RETURNS TEXT[] AS $F$
631 SELECT * FROM UNNEST($1) x WHERE x IS NOT NULL
633 SELECT * FROM UNNEST($2) y WHERE y IS NOT NULL
637 CREATE OR REPLACE FUNCTION evergreen.qwerty_keyboard_distance ( a TEXT, b TEXT ) RETURNS NUMERIC AS $F$
638 use String::KeyboardDistance qw(:all);
639 return qwerty_keyboard_distance(@_);
640 $F$ LANGUAGE PLPERLU STRICT IMMUTABLE;
642 CREATE OR REPLACE FUNCTION evergreen.qwerty_keyboard_distance_match ( a TEXT, b TEXT ) RETURNS NUMERIC AS $F$
643 use String::KeyboardDistance qw(:all);
644 return qwerty_keyboard_distance_match(@_);
645 $F$ LANGUAGE PLPERLU STRICT IMMUTABLE;
647 CREATE OR REPLACE FUNCTION evergreen.levenshtein_damerau_edistance ( a TEXT, b TEXT, INT ) RETURNS NUMERIC AS $F$
648 use Text::Levenshtein::Damerau::XS qw/xs_edistance/;
649 return xs_edistance(@_);
650 $F$ LANGUAGE PLPERLU STRICT IMMUTABLE;
652 CREATE TABLE search.symspell_dictionary (
653 keyword_count INT NOT NULL DEFAULT 0,
654 title_count INT NOT NULL DEFAULT 0,
655 author_count INT NOT NULL DEFAULT 0,
656 subject_count INT NOT NULL DEFAULT 0,
657 series_count INT NOT NULL DEFAULT 0,
658 identifier_count INT NOT NULL DEFAULT 0,
660 prefix_key TEXT PRIMARY KEY,
662 keyword_suggestions TEXT[],
663 title_suggestions TEXT[],
664 author_suggestions TEXT[],
665 subject_suggestions TEXT[],
666 series_suggestions TEXT[],
667 identifier_suggestions TEXT[]
668 ) WITH (fillfactor = 80);
670 CREATE OR REPLACE FUNCTION search.symspell_parse_words ( phrase TEXT )
671 RETURNS SETOF TEXT AS $F$
672 SELECT UNNEST(x) FROM regexp_matches($1, '([[:alnum:]]+''*[[:alnum:]]*)', 'g') x;
673 $F$ LANGUAGE SQL STRICT IMMUTABLE;
675 -- This version does not preserve input word order!
676 CREATE OR REPLACE FUNCTION search.symspell_parse_words_distinct ( phrase TEXT )
677 RETURNS SETOF TEXT AS $F$
678 SELECT DISTINCT UNNEST(x) FROM regexp_matches($1, '([[:alnum:]]+''*[[:alnum:]]*)', 'g') x;
679 $F$ LANGUAGE SQL STRICT IMMUTABLE;
681 CREATE OR REPLACE FUNCTION search.symspell_transfer_casing ( withCase TEXT, withoutCase TEXT )
688 woChars := regexp_split_to_array(withoutCase,'');
689 FOR curr IN SELECT x FROM regexp_split_to_table(withCase, '') x LOOP
690 IF curr = evergreen.uppercase(curr) THEN
691 woChars[ind] := evergreen.uppercase(woChars[ind]);
695 RETURN ARRAY_TO_STRING(woChars,'');
697 $F$ LANGUAGE PLPGSQL STRICT IMMUTABLE;
699 CREATE OR REPLACE FUNCTION search.symspell_generate_edits (
703 ) RETURNS TEXT[] AS $F$
707 sublist TEXT[] := '{}';
709 FOR I IN 1 .. CHARACTER_LENGTH(raw_word) LOOP
710 item := SUBSTRING(raw_word FROM 1 FOR I - 1) || SUBSTRING(raw_word FROM I + 1);
711 IF NOT list @> ARRAY[item] THEN
712 list := item || list;
713 IF dist < maxED AND CHARACTER_LENGTH(raw_word) > dist + 1 THEN
714 sublist := search.symspell_generate_edits(item, dist + 1, maxED) || sublist;
720 RETURN evergreen.text_array_merge_unique(list, sublist);
722 RETURN list || sublist;
725 $F$ LANGUAGE PLPGSQL STRICT IMMUTABLE;
727 -- DROP TYPE search.symspell_lookup_output CASCADE;
728 CREATE TYPE search.symspell_lookup_output AS (
730 suggestion_count INT,
733 qwerty_kb_match NUMERIC,
738 prefix_key_count INT,
742 CREATE OR REPLACE FUNCTION search.symspell_lookup (
745 verbosity INT DEFAULT 2,
746 xfer_case BOOL DEFAULT FALSE,
747 count_threshold INT DEFAULT 1,
748 soundex_weight INT DEFAULT 0,
749 pg_trgm_weight INT DEFAULT 0,
750 kbdist_weight INT DEFAULT 0
751 ) RETURNS SETOF search.symspell_lookup_output AS $F$
756 edit_list TEXT[] := '{}';
757 seen_list TEXT[] := '{}';
758 output search.symspell_lookup_output;
759 output_list search.symspell_lookup_output[];
767 smallest_ed INT := -1;
770 SELECT value::INT INTO prefix_length FROM config.internal_flag WHERE name = 'symspell.prefix_length' AND enabled;
771 prefix_length := COALESCE(prefix_length, 6);
773 SELECT value::INT INTO maxED FROM config.internal_flag WHERE name = 'symspell.max_edit_distance' AND enabled;
774 maxED := COALESCE(maxED, 3);
776 word_list := ARRAY_AGG(x) FROM search.symspell_parse_words(raw_input) x;
778 -- Common case exact match test for preformance
779 IF verbosity = 0 AND CARDINALITY(word_list) = 1 AND CHARACTER_LENGTH(word_list[1]) <= prefix_length THEN
781 'SELECT '||search_class||'_suggestions AS suggestions,
782 '||search_class||'_count AS count,
784 FROM search.symspell_dictionary
785 WHERE prefix_key = $1
786 AND '||search_class||'_count >= $2
787 AND '||search_class||'_suggestions @> ARRAY[$1]'
788 INTO entry USING evergreen.lowercase(word_list[1]), COALESCE(count_threshold,1);
789 IF entry.prefix_key IS NOT NULL THEN
790 output.lev_distance := 0; -- definitionally
791 output.prefix_key := entry.prefix_key;
792 output.prefix_key_count := entry.count;
793 output.suggestion_count := entry.count;
794 output.input := word_list[1];
796 output.suggestion := search.symspell_transfer_casing(output.input, entry.prefix_key);
798 output.suggestion := entry.prefix_key;
800 output.norm_input := entry.prefix_key;
801 output.qwerty_kb_match := 1;
802 output.pg_trgm_sim := 1;
803 output.soundex_sim := 1;
810 FOREACH word IN ARRAY word_list LOOP
812 input := evergreen.lowercase(word);
814 IF CHARACTER_LENGTH(input) > prefix_length THEN
815 prefix_key := SUBSTRING(input FROM 1 FOR prefix_length);
816 edit_list := ARRAY[input,prefix_key] || search.symspell_generate_edits(prefix_key, 1, maxED);
818 edit_list := input || search.symspell_generate_edits(input, 1, maxED);
821 SELECT ARRAY_AGG(x ORDER BY CHARACTER_LENGTH(x) DESC) INTO edit_list FROM UNNEST(edit_list) x;
828 FOREACH entry_key IN ARRAY edit_list LOOP
830 IF global_ed IS NOT NULL THEN
831 smallest_ed := global_ed;
834 'SELECT '||search_class||'_suggestions AS suggestions,
835 '||search_class||'_count AS count,
837 FROM search.symspell_dictionary
838 WHERE prefix_key = $1
839 AND '||search_class||'_suggestions IS NOT NULL'
842 FOREACH sugg IN ARRAY entry.suggestions LOOP
843 IF NOT seen_list @> ARRAY[sugg] THEN
844 seen_list := seen_list || sugg;
845 IF input = sugg THEN -- exact match, no need to spend time on a call
846 output.lev_distance := 0;
847 output.suggestion_count = entry.count;
848 ELSIF ABS(CHARACTER_LENGTH(input) - CHARACTER_LENGTH(sugg)) > maxED THEN
849 -- They are definitionally too different to consider, just move on.
852 --output.lev_distance := levenshtein_less_equal(
853 output.lev_distance := evergreen.levenshtein_damerau_edistance(
858 IF output.lev_distance < 0 THEN
859 -- The Perl module returns -1 for "more distant than max".
860 output.lev_distance := maxED + 1;
861 -- This short-circuit's the count test below for speed, bypassing
862 -- a couple useless tests.
863 output.suggestion_count := -1;
865 EXECUTE 'SELECT '||search_class||'_count FROM search.symspell_dictionary WHERE prefix_key = $1'
866 INTO output.suggestion_count USING sugg;
870 -- The caller passes a minimum suggestion count threshold (or uses
871 -- the default of 0) and if the suggestion has that many or less uses
872 -- then we move on to the next suggestion, since this one is too rare.
873 CONTINUE WHEN output.suggestion_count < COALESCE(count_threshold,1);
875 -- Track the smallest edit distance among suggestions from this prefix key.
876 IF smallest_ed = -1 OR output.lev_distance < smallest_ed THEN
877 smallest_ed := output.lev_distance;
880 -- Track the smallest edit distance for all prefix keys for this word.
881 IF global_ed IS NULL OR smallest_ed < global_ed THEN
882 global_ed = smallest_ed;
885 -- Only proceed if the edit distance is <= the max for the dictionary.
886 IF output.lev_distance <= maxED THEN
887 IF output.lev_distance > global_ed AND verbosity <= 1 THEN
888 -- Lev distance is our main similarity measure. While
889 -- trgm or soundex similarity could be the main filter,
890 -- Lev is both language agnostic and faster.
892 -- Here we will skip suggestions that have a longer edit distance
893 -- than the shortest we've already found. This is simply an
894 -- optimization that allows us to avoid further processing
895 -- of this entry. It would be filtered out later.
900 -- If we have an exact match on the suggestion key we can also avoid
901 -- some function calls.
902 IF output.lev_distance = 0 THEN
903 output.qwerty_kb_match := 1;
904 output.pg_trgm_sim := 1;
905 output.soundex_sim := 1;
907 output.qwerty_kb_match := evergreen.qwerty_keyboard_distance_match(input, sugg);
908 output.pg_trgm_sim := similarity(input, sugg);
909 output.soundex_sim := difference(input, sugg) / 4.0;
912 -- Fill in some fields
914 output.suggestion := search.symspell_transfer_casing(word, sugg);
916 output.suggestion := sugg;
918 output.prefix_key := entry.prefix_key;
919 output.prefix_key_count := entry.count;
920 output.input := word;
921 output.norm_input := input;
922 output.word_pos := w_pos;
924 -- We can't "cache" a set of generated records directly, so
925 -- here we build up an array of search.symspell_lookup_output
926 -- records that we can revivicate later as a table using UNNEST().
927 output_list := output_list || output;
929 EXIT entry_key_loop WHEN smallest_ed = 0 AND verbosity = 0; -- exact match early exit
930 CONTINUE entry_key_loop WHEN smallest_ed = 0 AND verbosity = 1; -- exact match early jump to the next key
931 END IF; -- maxED test
932 END IF; -- suggestion not seen test
933 END LOOP; -- loop over suggestions
934 END LOOP; -- loop over entries
935 END LOOP; -- loop over entry_keys
937 -- Now we're done examining this word
938 IF verbosity = 0 THEN
939 -- Return the "best" suggestion from the smallest edit
940 -- distance group. We define best based on the weighting
941 -- of the non-lev similarity measures and use the suggestion
942 -- use count to break ties.
944 SELECT * FROM UNNEST(output_list)
945 ORDER BY lev_distance,
946 (soundex_sim * COALESCE(soundex_weight,0))
947 + (pg_trgm_sim * COALESCE(pg_trgm_weight,0))
948 + (qwerty_kb_match * COALESCE(kbdist_weight,0)) DESC,
949 suggestion_count DESC
951 ELSIF verbosity = 1 THEN
952 -- Return all suggestions from the smallest
953 -- edit distance group.
955 SELECT * FROM UNNEST(output_list) WHERE lev_distance = smallest_ed
956 ORDER BY (soundex_sim * COALESCE(soundex_weight,0))
957 + (pg_trgm_sim * COALESCE(pg_trgm_weight,0))
958 + (qwerty_kb_match * COALESCE(kbdist_weight,0)) DESC,
959 suggestion_count DESC;
960 ELSIF verbosity = 2 THEN
961 -- Return everything we find, along with relevant stats
963 SELECT * FROM UNNEST(output_list)
964 ORDER BY lev_distance,
965 (soundex_sim * COALESCE(soundex_weight,0))
966 + (pg_trgm_sim * COALESCE(pg_trgm_weight,0))
967 + (qwerty_kb_match * COALESCE(kbdist_weight,0)) DESC,
968 suggestion_count DESC;
969 ELSIF verbosity = 3 THEN
970 -- Return everything we find from the two smallest edit distance groups
972 SELECT * FROM UNNEST(output_list)
973 WHERE lev_distance IN (SELECT DISTINCT lev_distance FROM UNNEST(output_list) ORDER BY 1 LIMIT 2)
974 ORDER BY lev_distance,
975 (soundex_sim * COALESCE(soundex_weight,0))
976 + (pg_trgm_sim * COALESCE(pg_trgm_weight,0))
977 + (qwerty_kb_match * COALESCE(kbdist_weight,0)) DESC,
978 suggestion_count DESC;
979 ELSIF verbosity = 4 THEN
980 -- Return everything we find from the two smallest edit distance groups that are NOT 0 distance
982 SELECT * FROM UNNEST(output_list)
983 WHERE lev_distance IN (SELECT DISTINCT lev_distance FROM UNNEST(output_list) WHERE lev_distance > 0 ORDER BY 1 LIMIT 2)
984 ORDER BY lev_distance,
985 (soundex_sim * COALESCE(soundex_weight,0))
986 + (pg_trgm_sim * COALESCE(pg_trgm_weight,0))
987 + (qwerty_kb_match * COALESCE(kbdist_weight,0)) DESC,
988 suggestion_count DESC;
990 END LOOP; -- loop over words
992 $F$ LANGUAGE PLPGSQL;
994 CREATE OR REPLACE FUNCTION search.symspell_build_raw_entry (
997 no_limit BOOL DEFAULT FALSE,
998 prefix_length INT DEFAULT 6,
1000 ) RETURNS SETOF search.symspell_dictionary AS $F$
1005 entry search.symspell_dictionary%ROWTYPE;
1009 IF NOT no_limit AND CHARACTER_LENGTH(raw_input) > prefix_length THEN
1010 key := SUBSTRING(key FROM 1 FOR prefix_length);
1011 key_list := ARRAY[raw_input, key];
1013 key_list := ARRAY[key];
1016 FOREACH del_key IN ARRAY key_list LOOP
1017 entry.prefix_key := del_key;
1019 entry.keyword_count := 0;
1020 entry.title_count := 0;
1021 entry.author_count := 0;
1022 entry.subject_count := 0;
1023 entry.series_count := 0;
1024 entry.identifier_count := 0;
1026 entry.keyword_suggestions := '{}';
1027 entry.title_suggestions := '{}';
1028 entry.author_suggestions := '{}';
1029 entry.subject_suggestions := '{}';
1030 entry.series_suggestions := '{}';
1031 entry.identifier_suggestions := '{}';
1033 IF source_class = 'keyword' THEN entry.keyword_suggestions := ARRAY[raw_input]; END IF;
1034 IF source_class = 'title' THEN entry.title_suggestions := ARRAY[raw_input]; END IF;
1035 IF source_class = 'author' THEN entry.author_suggestions := ARRAY[raw_input]; END IF;
1036 IF source_class = 'subject' THEN entry.subject_suggestions := ARRAY[raw_input]; END IF;
1037 IF source_class = 'series' THEN entry.series_suggestions := ARRAY[raw_input]; END IF;
1038 IF source_class = 'identifier' THEN entry.identifier_suggestions := ARRAY[raw_input]; END IF;
1039 IF source_class = 'keyword' THEN entry.keyword_suggestions := ARRAY[raw_input]; END IF;
1041 IF del_key = raw_input THEN
1042 IF source_class = 'keyword' THEN entry.keyword_count := 1; END IF;
1043 IF source_class = 'title' THEN entry.title_count := 1; END IF;
1044 IF source_class = 'author' THEN entry.author_count := 1; END IF;
1045 IF source_class = 'subject' THEN entry.subject_count := 1; END IF;
1046 IF source_class = 'series' THEN entry.series_count := 1; END IF;
1047 IF source_class = 'identifier' THEN entry.identifier_count := 1; END IF;
1053 FOR del_key IN SELECT x FROM UNNEST(search.symspell_generate_edits(key, 1, maxED)) x LOOP
1055 entry.keyword_suggestions := '{}';
1056 entry.title_suggestions := '{}';
1057 entry.author_suggestions := '{}';
1058 entry.subject_suggestions := '{}';
1059 entry.series_suggestions := '{}';
1060 entry.identifier_suggestions := '{}';
1062 IF source_class = 'keyword' THEN entry.keyword_count := 0; END IF;
1063 IF source_class = 'title' THEN entry.title_count := 0; END IF;
1064 IF source_class = 'author' THEN entry.author_count := 0; END IF;
1065 IF source_class = 'subject' THEN entry.subject_count := 0; END IF;
1066 IF source_class = 'series' THEN entry.series_count := 0; END IF;
1067 IF source_class = 'identifier' THEN entry.identifier_count := 0; END IF;
1069 entry.prefix_key := del_key;
1071 IF source_class = 'keyword' THEN entry.keyword_suggestions := ARRAY[raw_input]; END IF;
1072 IF source_class = 'title' THEN entry.title_suggestions := ARRAY[raw_input]; END IF;
1073 IF source_class = 'author' THEN entry.author_suggestions := ARRAY[raw_input]; END IF;
1074 IF source_class = 'subject' THEN entry.subject_suggestions := ARRAY[raw_input]; END IF;
1075 IF source_class = 'series' THEN entry.series_suggestions := ARRAY[raw_input]; END IF;
1076 IF source_class = 'identifier' THEN entry.identifier_suggestions := ARRAY[raw_input]; END IF;
1077 IF source_class = 'keyword' THEN entry.keyword_suggestions := ARRAY[raw_input]; END IF;
1083 $F$ LANGUAGE PLPGSQL STRICT IMMUTABLE;
1085 CREATE OR REPLACE FUNCTION search.symspell_build_entries (
1088 old_input TEXT DEFAULT NULL,
1089 include_phrases BOOL DEFAULT FALSE
1090 ) RETURNS SETOF search.symspell_dictionary AS $F$
1097 entry search.symspell_dictionary;
1099 IF full_input IS NOT NULL THEN
1100 SELECT value::INT INTO prefix_length FROM config.internal_flag WHERE name = 'symspell.prefix_length' AND enabled;
1101 prefix_length := COALESCE(prefix_length, 6);
1103 SELECT value::INT INTO maxED FROM config.internal_flag WHERE name = 'symspell.max_edit_distance' AND enabled;
1104 maxED := COALESCE(maxED, 3);
1106 input := evergreen.lowercase(full_input);
1107 word_list := ARRAY_AGG(x) FROM search.symspell_parse_words_distinct(input) x;
1109 IF CARDINALITY(word_list) > 1 AND include_phrases THEN
1110 RETURN QUERY SELECT * FROM search.symspell_build_raw_entry(input, source_class, TRUE, prefix_length, maxED);
1113 FOREACH word IN ARRAY word_list LOOP
1114 RETURN QUERY SELECT * FROM search.symspell_build_raw_entry(word, source_class, FALSE, prefix_length, maxED);
1118 IF old_input IS NOT NULL THEN
1119 input := evergreen.lowercase(old_input);
1121 FOR word IN SELECT x FROM search.symspell_parse_words_distinct(input) x LOOP
1122 entry.prefix_key := word;
1124 entry.keyword_count := 0;
1125 entry.title_count := 0;
1126 entry.author_count := 0;
1127 entry.subject_count := 0;
1128 entry.series_count := 0;
1129 entry.identifier_count := 0;
1131 entry.keyword_suggestions := '{}';
1132 entry.title_suggestions := '{}';
1133 entry.author_suggestions := '{}';
1134 entry.subject_suggestions := '{}';
1135 entry.series_suggestions := '{}';
1136 entry.identifier_suggestions := '{}';
1138 IF source_class = 'keyword' THEN entry.keyword_count := -1; END IF;
1139 IF source_class = 'title' THEN entry.title_count := -1; END IF;
1140 IF source_class = 'author' THEN entry.author_count := -1; END IF;
1141 IF source_class = 'subject' THEN entry.subject_count := -1; END IF;
1142 IF source_class = 'series' THEN entry.series_count := -1; END IF;
1143 IF source_class = 'identifier' THEN entry.identifier_count := -1; END IF;
1149 $F$ LANGUAGE PLPGSQL;
1151 CREATE OR REPLACE FUNCTION search.symspell_build_and_merge_entries (
1154 old_input TEXT DEFAULT NULL,
1155 include_phrases BOOL DEFAULT FALSE
1156 ) RETURNS SETOF search.symspell_dictionary AS $F$
1159 conflict_entry RECORD;
1162 IF full_input = old_input THEN -- neither NULL, and are the same
1166 FOR new_entry IN EXECUTE $q$
1169 evergreen.text_array_merge_unique(s,'{}') suggestions
1170 FROM (SELECT prefix_key,
1171 ARRAY_AGG($q$ || source_class || $q$_suggestions[1]) s,
1172 SUM($q$ || source_class || $q$_count) count
1173 FROM search.symspell_build_entries($1, $2, $3, $4)
1175 $q$ USING full_input, source_class, old_input, include_phrases
1179 $q$ || source_class || $q$_suggestions suggestions,
1180 $q$ || source_class || $q$_count count
1181 FROM search.symspell_dictionary
1182 WHERE prefix_key = $1 $q$
1184 USING new_entry.prefix_key;
1186 IF new_entry.count <> 0 THEN -- Real word, and count changed
1187 IF conflict_entry.prefix_key IS NOT NULL THEN -- we'll be updating
1188 IF conflict_entry.count > 0 THEN -- it's a real word
1189 RETURN QUERY EXECUTE $q$
1190 UPDATE search.symspell_dictionary
1191 SET $q$ || source_class || $q$_count = $2
1192 WHERE prefix_key = $1
1194 USING new_entry.prefix_key, GREATEST(0, new_entry.count + conflict_entry.count);
1195 ELSE -- it was a prefix key or delete-emptied word before
1196 IF conflict_entry.suggestions @> new_entry.suggestions THEN -- already have all suggestions here...
1197 RETURN QUERY EXECUTE $q$
1198 UPDATE search.symspell_dictionary
1199 SET $q$ || source_class || $q$_count = $2
1200 WHERE prefix_key = $1
1202 USING new_entry.prefix_key, GREATEST(0, new_entry.count);
1203 ELSE -- new suggestion!
1204 RETURN QUERY EXECUTE $q$
1205 UPDATE search.symspell_dictionary
1206 SET $q$ || source_class || $q$_count = $2,
1207 $q$ || source_class || $q$_suggestions = $3
1208 WHERE prefix_key = $1
1210 USING new_entry.prefix_key, GREATEST(0, new_entry.count), evergreen.text_array_merge_unique(conflict_entry.suggestions,new_entry.suggestions);
1214 -- We keep the on-conflict clause just in case...
1215 RETURN QUERY EXECUTE $q$
1216 INSERT INTO search.symspell_dictionary AS d (
1217 $q$ || source_class || $q$_count,
1219 $q$ || source_class || $q$_suggestions
1220 ) VALUES ( $1, $2, $3 ) ON CONFLICT (prefix_key) DO
1221 UPDATE SET $q$ || source_class || $q$_count = d.$q$ || source_class || $q$_count + EXCLUDED.$q$ || source_class || $q$_count,
1222 $q$ || source_class || $q$_suggestions = evergreen.text_array_merge_unique(d.$q$ || source_class || $q$_suggestions, EXCLUDED.$q$ || source_class || $q$_suggestions)
1224 USING new_entry.count, new_entry.prefix_key, new_entry.suggestions;
1226 ELSE -- key only, or no change
1227 IF conflict_entry.prefix_key IS NOT NULL THEN -- we'll be updating
1228 IF NOT conflict_entry.suggestions @> new_entry.suggestions THEN -- There are new suggestions
1229 RETURN QUERY EXECUTE $q$
1230 UPDATE search.symspell_dictionary
1231 SET $q$ || source_class || $q$_suggestions = $2
1232 WHERE prefix_key = $1
1234 USING new_entry.prefix_key, evergreen.text_array_merge_unique(conflict_entry.suggestions,new_entry.suggestions);
1237 RETURN QUERY EXECUTE $q$
1238 INSERT INTO search.symspell_dictionary AS d (
1239 $q$ || source_class || $q$_count,
1241 $q$ || source_class || $q$_suggestions
1242 ) VALUES ( $1, $2, $3 ) ON CONFLICT (prefix_key) DO -- key exists, suggestions may be added due to this entry
1243 UPDATE SET $q$ || source_class || $q$_suggestions = evergreen.text_array_merge_unique(d.$q$ || source_class || $q$_suggestions, EXCLUDED.$q$ || source_class || $q$_suggestions)
1245 USING new_entry.count, new_entry.prefix_key, new_entry.suggestions;
1250 $F$ LANGUAGE PLPGSQL;
1252 CREATE OR REPLACE FUNCTION search.symspell_maintain_entries () RETURNS TRIGGER AS $f$
1255 new_value TEXT := NULL;
1256 old_value TEXT := NULL;
1258 search_class := COALESCE(TG_ARGV[0], SPLIT_PART(TG_TABLE_NAME,'_',1));
1260 IF TG_OP IN ('INSERT', 'UPDATE') THEN
1261 new_value := NEW.value;
1264 IF TG_OP IN ('DELETE', 'UPDATE') THEN
1265 old_value := OLD.value;
1268 PERFORM * FROM search.symspell_build_and_merge_entries(new_value, search_class, old_value);
1270 RETURN NULL; -- always fired AFTER
1272 $f$ LANGUAGE PLPGSQL;
1274 CREATE TRIGGER maintain_symspell_entries_tgr
1275 AFTER INSERT OR UPDATE OR DELETE ON metabib.title_field_entry
1276 FOR EACH ROW EXECUTE PROCEDURE search.symspell_maintain_entries();
1278 CREATE TRIGGER maintain_symspell_entries_tgr
1279 AFTER INSERT OR UPDATE OR DELETE ON metabib.author_field_entry
1280 FOR EACH ROW EXECUTE PROCEDURE search.symspell_maintain_entries();
1282 CREATE TRIGGER maintain_symspell_entries_tgr
1283 AFTER INSERT OR UPDATE OR DELETE ON metabib.subject_field_entry
1284 FOR EACH ROW EXECUTE PROCEDURE search.symspell_maintain_entries();
1286 CREATE TRIGGER maintain_symspell_entries_tgr
1287 AFTER INSERT OR UPDATE OR DELETE ON metabib.series_field_entry
1288 FOR EACH ROW EXECUTE PROCEDURE search.symspell_maintain_entries();
1290 CREATE TRIGGER maintain_symspell_entries_tgr
1291 AFTER INSERT OR UPDATE OR DELETE ON metabib.keyword_field_entry
1292 FOR EACH ROW EXECUTE PROCEDURE search.symspell_maintain_entries();
1294 CREATE TRIGGER maintain_symspell_entries_tgr
1295 AFTER INSERT OR UPDATE OR DELETE ON metabib.identifier_field_entry
1296 FOR EACH ROW EXECUTE PROCEDURE search.symspell_maintain_entries();
1299 /* This will generate the queries needed to generate the /file/ that can
1300 * be used to populate the dictionary table.
1302 select $z$select $y$select $y$||x.id||$y$, '$z$||x.x||$z$', count(*) from search.symspell_build_and_merge_entries($x$$y$ || x.value||$y$$x$, '$z$||x||$z$');$y$ from metabib.$z$||x||$z$_field_entry x;$z$ from (select 'keyword'::text x union select 'title' union select 'author' union select 'subject' union select 'series' union select 'identifier') x;
1307 \qecho 'The following should be run at the end of the upgrade before any'
1308 \qecho 'reingest occurs. Because new triggers are installed already,'
1309 \qecho 'updates to indexed strings will cause zero-count dictionary entries'
1310 \qecho 'to be recorded which will require updating every row again (or'
1311 \qecho 'starting from scratch) so best to do this before other batch'
1312 \qecho 'changes. A later reingest that does not significantly change'
1313 \qecho 'indexed strings will /not/ cause table bloat here, and will be'
1314 \qecho 'as fast as normal. A copy of the SQL in a ready-to-use, non-escaped'
1315 \qecho 'form is available inside a comment at the end of this upgrade sub-'
1316 \qecho 'script so you do not need to copy this comment from the psql ouptut.'
1322 \qecho 'select value from metabib.title_field_entry;'
1324 \qecho 'select value from metabib.author_field_entry;'
1325 \qecho '\\o subject'
1326 \qecho 'select value from metabib.subject_field_entry;'
1328 \qecho 'select value from metabib.series_field_entry;'
1329 \qecho '\\o identifier'
1330 \qecho 'select value from metabib.identifier_field_entry;'
1331 \qecho '\\o keyword'
1332 \qecho 'select value from metabib.keyword_field_entry;'
1338 \qecho '// Then, at the command line:'
1340 \qecho '$ ~/EG-src-path/Open-ILS/src/support-scripts/symspell-sideload.pl title > title.sql'
1341 \qecho '$ ~/EG-src-path/Open-ILS/src/support-scripts/symspell-sideload.pl author > author.sql'
1342 \qecho '$ ~/EG-src-path/Open-ILS/src/support-scripts/symspell-sideload.pl subject > subject.sql'
1343 \qecho '$ ~/EG-src-path/Open-ILS/src/support-scripts/symspell-sideload.pl series > series.sql'
1344 \qecho '$ ~/EG-src-path/Open-ILS/src/support-scripts/symspell-sideload.pl identifier > identifier.sql'
1345 \qecho '$ ~/EG-src-path/Open-ILS/src/support-scripts/symspell-sideload.pl keyword > keyword.sql'
1347 \qecho '// And, back in psql'
1349 \qecho 'ALTER TABLE search.symspell_dictionary SET UNLOGGED;'
1350 \qecho 'TRUNCATE search.symspell_dictionary;'
1352 \qecho '\\i identifier.sql'
1353 \qecho '\\i author.sql'
1354 \qecho '\\i title.sql'
1355 \qecho '\\i subject.sql'
1356 \qecho '\\i series.sql'
1357 \qecho '\\i keyword.sql'
1359 \qecho 'CLUSTER search.symspell_dictionary USING symspell_dictionary_pkey;'
1360 \qecho 'REINDEX TABLE search.symspell_dictionary;'
1361 \qecho 'ALTER TABLE search.symspell_dictionary SET LOGGED;'
1362 \qecho 'VACUUM ANALYZE search.symspell_dictionary;'
1364 \qecho 'DROP TABLE search.symspell_dictionary_partial_title;'
1365 \qecho 'DROP TABLE search.symspell_dictionary_partial_author;'
1366 \qecho 'DROP TABLE search.symspell_dictionary_partial_subject;'
1367 \qecho 'DROP TABLE search.symspell_dictionary_partial_series;'
1368 \qecho 'DROP TABLE search.symspell_dictionary_partial_identifier;'
1369 \qecho 'DROP TABLE search.symspell_dictionary_partial_keyword;'
1371 SELECT evergreen.upgrade_deps_block_check('1258', :eg_version);
1373 UPDATE config.metabib_field
1374 SET xpath = '//*[@tag=''260'' or @tag=''264''][1]'
1375 WHERE id = 52 AND xpath = '//*[@tag=''260'']';
1379 -- Update auditor tables to catch changes to source tables.
1380 -- Can be removed/skipped if there were no schema changes.
1381 SELECT auditor.update_auditors();
1384 \qecho This is a partial record attribute reingest of your bib records.
1385 \qecho It may take a while.
1386 \qecho You may cancel now without losing the effect of the rest of the
1387 \qecho upgrade script, and arrange the reingest later.
1388 SELECT COUNT(metabib.reingest_record_attributes(bre.id))
1389 FROM biblio.record_entry bre
1390 JOIN metabib.record_attr_flat mraf ON (bre.id = mraf.id)
1391 WHERE deleted IS FALSE
1392 AND attr = 'item_type'