]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/sql/Pg/upgrade/XXXX.schema.statisitcal-ratings.sql
LP#1549505: add flag to tweak popularity-adjusted relevance
[Evergreen.git] / Open-ILS / src / sql / Pg / upgrade / XXXX.schema.statisitcal-ratings.sql
1
2 BEGIN;
3
4 -- Create these so that the queries in the UDFs will validate
5 CREATE TEMP TABLE precalc_filter_bib_list (
6     id  BIGINT
7 ) ON COMMIT DROP;
8
9 CREATE TEMP TABLE precalc_bib_filter_bib_list (
10     id  BIGINT
11 ) ON COMMIT DROP;
12
13 CREATE TEMP TABLE precalc_src_filter_bib_list (
14     id  BIGINT
15 ) ON COMMIT DROP;
16
17 CREATE TEMP TABLE precalc_copy_filter_bib_list (
18     id  BIGINT,
19     copy  BIGINT
20 ) ON COMMIT DROP;
21
22 CREATE TEMP TABLE precalc_circ_mod_filter_bib_list (
23     id  BIGINT,
24     copy  BIGINT
25 ) ON COMMIT DROP;
26
27 CREATE TEMP TABLE precalc_location_filter_bib_list (
28     id  BIGINT,
29     copy  BIGINT
30 ) ON COMMIT DROP;
31
32 CREATE TEMP TABLE precalc_attr_filter_bib_list (
33     id  BIGINT
34 ) ON COMMIT DROP;
35
36 CREATE TEMP TABLE precalc_bibs_by_copy_list (
37     id  BIGINT
38 ) ON COMMIT DROP;
39
40 CREATE TEMP TABLE precalc_bibs_by_uri_list (
41     id  BIGINT
42 ) ON COMMIT DROP;
43
44 CREATE TEMP TABLE precalc_bibs_by_copy_or_uri_list (
45     id  BIGINT
46 ) ON COMMIT DROP;
47
48 CREATE TEMP TABLE precalc_bib_list (
49     id  BIGINT
50 ) ON COMMIT DROP;
51
52 -- rhrr needs to be a real table, so it can be fast. To that end, we use
53 -- a materialized view updated via a trigger.
54
55 DROP VIEW reporter.hold_request_record;
56
57 CREATE TABLE reporter.hold_request_record  AS
58 SELECT  id,
59         target,
60         hold_type,
61         CASE
62                 WHEN hold_type = 'T'
63                         THEN target
64                 WHEN hold_type = 'I'
65                         THEN (SELECT ssub.record_entry FROM serial.subscription ssub JOIN serial.issuance si ON (si.subscription = ssub.id) WHERE si.id = ahr.target)
66                 WHEN hold_type = 'V'
67                         THEN (SELECT cn.record FROM asset.call_number cn WHERE cn.id = ahr.target)
68                 WHEN hold_type IN ('C','R','F')
69                         THEN (SELECT cn.record FROM asset.call_number cn JOIN asset.copy cp ON (cn.id = cp.call_number) WHERE cp.id = ahr.target)
70                 WHEN hold_type = 'M'
71                         THEN (SELECT mr.master_record FROM metabib.metarecord mr WHERE mr.id = ahr.target)
72                 WHEN hold_type = 'P'
73                         THEN (SELECT bmp.record FROM biblio.monograph_part bmp WHERE bmp.id = ahr.target)
74         END AS bib_record
75   FROM  action.hold_request ahr;
76
77 CREATE UNIQUE INDEX reporter_hold_request_record_pkey_idx ON reporter.hold_request_record (id);
78 CREATE INDEX reporter_hold_request_record_bib_record_idx ON reporter.hold_request_record (bib_record);
79
80 ALTER TABLE reporter.hold_request_record ADD PRIMARY KEY USING INDEX reporter_hold_request_record_pkey_idx;
81
82 CREATE FUNCTION reporter.hold_request_record_mapper () RETURNS TRIGGER AS $$
83 BEGIN
84     IF TG_OP = 'INSERT' THEN
85         INSERT INTO reporter.hold_request_record (id, target, hold_type, bib_record)
86         SELECT  NEW.id,
87                 NEW.target,
88                 NEW.hold_type,
89                 CASE
90                     WHEN NEW.hold_type = 'T'
91                         THEN NEW.target
92                     WHEN NEW.hold_type = 'I'
93                         THEN (SELECT ssub.record_entry FROM serial.subscription ssub JOIN serial.issuance si ON (si.subscription = ssub.id) WHERE si.id = NEW.target)
94                     WHEN NEW.hold_type = 'V'
95                         THEN (SELECT cn.record FROM asset.call_number cn WHERE cn.id = NEW.target)
96                     WHEN NEW.hold_type IN ('C','R','F')
97                         THEN (SELECT cn.record FROM asset.call_number cn JOIN asset.copy cp ON (cn.id = cp.call_number) WHERE cp.id = NEW.target)
98                     WHEN NEW.hold_type = 'M'
99                         THEN (SELECT mr.master_record FROM metabib.metarecord mr WHERE mr.id = NEW.target)
100                     WHEN NEW.hold_type = 'P'
101                         THEN (SELECT bmp.record FROM biblio.monograph_part bmp WHERE bmp.id = NEW.target)
102                 END AS bib_record;
103     ELSIF TG_OP = 'UPDATE' AND (OLD.target <> NEW.target OR OLD.hold_type <> NEW.hold_type) THEN
104         UPDATE  reporter.hold_request_record
105           SET   target = NEW.target,
106                 hold_type = NEW.hold_type,
107                 bib_record = CASE
108                     WHEN NEW.hold_type = 'T'
109                         THEN NEW.target
110                     WHEN NEW.hold_type = 'I'
111                         THEN (SELECT ssub.record_entry FROM serial.subscription ssub JOIN serial.issuance si ON (si.subscription = ssub.id) WHERE si.id = NEW.target)
112                     WHEN NEW.hold_type = 'V'
113                         THEN (SELECT cn.record FROM asset.call_number cn WHERE cn.id = NEW.target)
114                     WHEN NEW.hold_type IN ('C','R','F')
115                         THEN (SELECT cn.record FROM asset.call_number cn JOIN asset.copy cp ON (cn.id = cp.call_number) WHERE cp.id = NEW.target)
116                     WHEN NEW.hold_type = 'M'
117                         THEN (SELECT mr.master_record FROM metabib.metarecord mr WHERE mr.id = NEW.target)
118                     WHEN NEW.hold_type = 'P'
119                         THEN (SELECT bmp.record FROM biblio.monograph_part bmp WHERE bmp.id = NEW.target)
120                 END;
121     END IF;
122     RETURN NEW;
123 END;
124 $$ LANGUAGE PLPGSQL;
125
126 CREATE TRIGGER reporter_hold_request_record_trigger AFTER INSERT OR UPDATE ON action.hold_request
127     FOR EACH ROW EXECUTE PROCEDURE reporter.hold_request_record_mapper();
128
129 CREATE SCHEMA rating;
130
131 INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
132     'opac.default_sort',
133     'OPAC Default Sort (titlesort, authorsort, pubdate, popularity, poprel, or empty for relevance)',
134     '',
135     TRUE
136 );
137
138 INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
139     'search.max_popularity_importance_multiplier',
140     oils_i18n_gettext(
141         'search.max_popularity_importance_multiplier',
142         'Maximum popularity importance multiplier for popularity-adjusted relevance searches (decimal value between 1.0 and 2.0)',
143         'cgf',
144         'label'
145     ),
146     '2.0',
147     TRUE
148 );
149
150 CREATE TABLE rating.popularity_parameter (
151     id          INT     PRIMARY KEY,
152     name        TEXT    NOT NULL UNIQUE, -- i18n
153     description TEXT,
154     func        TEXT,
155     require_horizon     BOOL    NOT NULL DEFAULT FALSE,
156     require_importance  BOOL    NOT NULL DEFAULT FALSE,
157     require_percentile  BOOL    NOT NULL DEFAULT FALSE
158 );
159
160 INSERT INTO rating.popularity_parameter (id,name,func,require_horizon,require_importance,require_percentile) VALUES
161     (1,'Holds Filled Over Time','rating.holds_filled_over_time',TRUE,FALSE,TRUE),
162     (2,'Holds Requested Over Time','rating.holds_placed_over_time',TRUE,FALSE,TRUE),
163     (3,'Current Hold Count','rating.current_hold_count',FALSE,FALSE,TRUE),
164     (4,'Circulations Over Time','rating.circs_over_time',TRUE,FALSE,TRUE),
165     (5,'Current Circulation Count','rating.current_circ_count',FALSE,FALSE,TRUE),
166     (6,'Out/Total Ratio','rating.checked_out_total_ratio',FALSE,FALSE,TRUE),
167     (7,'Holds/Total Ratio','rating.holds_total_ratio',FALSE,FALSE,TRUE),
168     (8,'Holds/Holdable Ratio','rating.holds_holdable_ratio',FALSE,FALSE,TRUE),
169     (9,'Percent of Time Circulating','rating.percent_time_circulating',FALSE,FALSE,TRUE),
170     (10,'Bibliographic Record Age (days, newer is better)','rating.bib_record_age',FALSE,FALSE,TRUE),
171     (11,'Publication Age (days, newer is better)','rating.bib_pub_age',FALSE,FALSE,TRUE),
172     (12,'On-line Bib has attributes','rating.generic_fixed_rating_by_uri',FALSE,FALSE,FALSE),
173     (13,'Bib has attributes and copies','rating.generic_fixed_rating_by_copy',FALSE,FALSE,FALSE),
174     (14,'Bib has attributes and copies or URIs','rating.generic_fixed_rating_by_copy_or_uri',FALSE,FALSE,FALSE),
175     (15,'Bib has attributes','rating.generic_fixed_rating_global',FALSE,FALSE,FALSE);
176
177 CREATE TABLE rating.badge (
178     id                      SERIAL      PRIMARY KEY,
179     name                    TEXT        NOT NULL,
180     description             TEXT,
181     scope                   INT         NOT NULL REFERENCES actor.org_unit (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
182     weight                  INT         NOT NULL DEFAULT 1,
183     horizon_age             INTERVAL,
184     importance_age          INTERVAL,
185     importance_interval     INTERVAL    NOT NULL DEFAULT '1 day',
186     importance_scale        NUMERIC     CHECK (importance_scale IS NULL OR importance_scale > 0.0),
187     recalc_interval         INTERVAL    NOT NULL DEFAULT '1 month',
188     attr_filter             TEXT,
189     src_filter              INT         REFERENCES config.bib_source (id) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
190     circ_mod_filter         TEXT        REFERENCES config.circ_modifier (code) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
191     loc_grp_filter          INT         REFERENCES asset.copy_location_group (id) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
192     popularity_parameter    INT         NOT NULL REFERENCES rating.popularity_parameter (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
193     fixed_rating            INT         CHECK (fixed_rating IS NULL OR fixed_rating BETWEEN -5 AND 5),
194     percentile              NUMERIC     CHECK (percentile IS NULL OR (percentile >= 50.0 AND percentile < 100.0)),
195     discard                 INT         NOT NULL DEFAULT 0, 
196     last_calc               TIMESTAMPTZ,
197     CONSTRAINT unique_name_scope UNIQUE (name,scope)
198 );
199
200 CREATE TABLE rating.record_badge_score (
201     id          BIGSERIAL   PRIMARY KEY,
202     record      BIGINT      NOT NULL REFERENCES biblio.record_entry (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
203     badge       INT         NOT NULL REFERENCES rating.badge (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
204     score       INT         NOT NULL CHECK (score BETWEEN -5 AND 5),
205     CONSTRAINT unique_record_badge UNIQUE (record,badge)
206 );
207 CREATE INDEX record_badge_score_badge_idx ON rating.record_badge_score (badge);
208 CREATE INDEX record_badge_score_record_idx ON rating.record_badge_score (record);
209
210 CREATE OR REPLACE VIEW rating.badge_with_orgs AS
211     WITH    org_scope AS (
212                 SELECT  id,
213                         array_agg(tree) AS orgs
214                   FROM  (SELECT id,
215                                 (actor.org_unit_descendants(id)).id AS tree
216                           FROM  actor.org_unit
217                         ) x
218                   GROUP BY 1
219             )
220     SELECT  b.*,
221             s.orgs
222       FROM  rating.badge b
223             JOIN org_scope s ON (b.scope = s.id);
224
225 CREATE OR REPLACE FUNCTION rating.precalc_src_filter(src INT)
226     RETURNS INT AS $f$
227 DECLARE
228     cnt     INT     := 0;
229 BEGIN
230
231     SET LOCAL client_min_messages = error;
232     DROP TABLE IF EXISTS precalc_src_filter_bib_list;
233     IF src IS NOT NULL THEN
234         CREATE TEMP TABLE precalc_src_filter_bib_list ON COMMIT DROP AS
235             SELECT id FROM biblio.record_entry
236             WHERE source = src AND NOT deleted;
237     ELSE
238         CREATE TEMP TABLE precalc_src_filter_bib_list ON COMMIT DROP AS
239             SELECT id FROM biblio.record_entry
240             WHERE id > 0 AND NOT deleted;
241     END IF;
242
243     SELECT count(*) INTO cnt FROM precalc_src_filter_bib_list;
244     RETURN cnt;
245 END;
246 $f$ LANGUAGE PLPGSQL;
247
248 CREATE OR REPLACE FUNCTION rating.precalc_circ_mod_filter(cm TEXT)
249     RETURNS INT AS $f$
250 DECLARE
251     cnt     INT     := 0;
252 BEGIN
253
254     SET LOCAL client_min_messages = error;
255     DROP TABLE IF EXISTS precalc_circ_mod_filter_bib_list;
256     IF cm IS NOT NULL THEN
257         CREATE TEMP TABLE precalc_circ_mod_filter_bib_list ON COMMIT DROP AS
258             SELECT  cn.record AS id,
259                     cp.id AS copy
260               FROM  asset.call_number cn
261                     JOIN asset.copy cp ON (cn.id = cp.call_number)
262               WHERE cp.circ_modifier = cm
263                     AND NOT cp.deleted;
264     ELSE
265         CREATE TEMP TABLE precalc_circ_mod_filter_bib_list ON COMMIT DROP AS
266             SELECT  cn.record AS id,
267                     cp.id AS copy
268               FROM  asset.call_number cn
269                     JOIN asset.copy cp ON (cn.id = cp.call_number)
270               WHERE NOT cp.deleted;
271     END IF;
272
273     SELECT count(*) INTO cnt FROM precalc_circ_mod_filter_bib_list;
274     RETURN cnt;
275 END;
276 $f$ LANGUAGE PLPGSQL;
277
278 CREATE OR REPLACE FUNCTION rating.precalc_location_filter(loc INT)
279     RETURNS INT AS $f$
280 DECLARE
281     cnt     INT     := 0;
282 BEGIN
283
284     SET LOCAL client_min_messages = error;
285     DROP TABLE IF EXISTS precalc_location_filter_bib_list;
286     IF loc IS NOT NULL THEN
287         CREATE TEMP TABLE precalc_location_filter_bib_list ON COMMIT DROP AS
288             SELECT  cn.record AS id,
289                     cp.id AS copy
290               FROM  asset.call_number cn
291                     JOIN asset.copy cp ON (cn.id = cp.call_number)
292                     JOIN asset.copy_location_group_map lg ON (cp.location = lg.location)
293               WHERE lg.lgroup = loc
294                     AND NOT cp.deleted;
295     ELSE
296         CREATE TEMP TABLE precalc_location_filter_bib_list ON COMMIT DROP AS
297             SELECT  cn.record AS id,
298                     cp.id AS copy
299               FROM  asset.call_number cn
300                     JOIN asset.copy cp ON (cn.id = cp.call_number)
301               WHERE NOT cp.deleted;
302     END IF;
303
304     SELECT count(*) INTO cnt FROM precalc_location_filter_bib_list;
305     RETURN cnt;
306 END;
307 $f$ LANGUAGE PLPGSQL;
308
309 -- all or limited...
310 CREATE OR REPLACE FUNCTION rating.precalc_attr_filter(attr_filter TEXT)
311     RETURNS INT AS $f$
312 DECLARE
313     cnt     INT := 0;
314     afilter TEXT;
315 BEGIN
316
317     SET LOCAL client_min_messages = error;
318     DROP TABLE IF EXISTS precalc_attr_filter_bib_list;
319     IF attr_filter IS NOT NULL THEN
320         afilter := metabib.compile_composite_attr(attr_filter);
321         CREATE TEMP TABLE precalc_attr_filter_bib_list ON COMMIT DROP AS
322             SELECT source AS id FROM metabib.record_attr_vector_list
323             WHERE vlist @@ metabib.compile_composite_attr(attr_filter);
324     ELSE
325         CREATE TEMP TABLE precalc_attr_filter_bib_list ON COMMIT DROP AS
326             SELECT source AS id FROM metabib.record_attr_vector_list;
327     END IF;
328
329     SELECT count(*) INTO cnt FROM precalc_attr_filter_bib_list;
330     RETURN cnt;
331 END;
332 $f$ LANGUAGE PLPGSQL;
333
334 CREATE OR REPLACE FUNCTION rating.precalc_bibs_by_copy(badge_id INT)
335     RETURNS INT AS $f$
336 DECLARE
337     cnt         INT     := 0;
338     badge_row   rating.badge_with_orgs%ROWTYPE;
339     base        TEXT;
340     whr         TEXT;
341 BEGIN
342
343     SELECT * INTO badge_row FROM rating.badge_with_orgs WHERE id = badge_id;
344
345     SET LOCAL client_min_messages = error;
346     DROP TABLE IF EXISTS precalc_bibs_by_copy_list;
347     CREATE TEMP TABLE precalc_bibs_by_copy_list ON COMMIT DROP AS
348         SELECT  DISTINCT cn.record AS id
349           FROM  asset.call_number cn
350                 JOIN asset.copy cp ON (cp.call_number = cn.id AND NOT cp.deleted)
351                 JOIN precalc_copy_filter_bib_list f ON (cp.id = f.copy)
352           WHERE cn.owning_lib = ANY (badge_row.orgs)
353                 AND NOT cn.deleted;
354
355     SELECT count(*) INTO cnt FROM precalc_bibs_by_copy_list;
356     RETURN cnt;
357 END;
358 $f$ LANGUAGE PLPGSQL;
359
360 CREATE OR REPLACE FUNCTION rating.precalc_bibs_by_uri(badge_id INT)
361     RETURNS INT AS $f$
362 DECLARE
363     cnt         INT     := 0;
364     badge_row   rating.badge_with_orgs%ROWTYPE;
365 BEGIN
366
367     SELECT * INTO badge_row FROM rating.badge_with_orgs WHERE id = badge_id;
368
369     SET LOCAL client_min_messages = error;
370     DROP TABLE IF EXISTS precalc_bibs_by_uri_list;
371     CREATE TEMP TABLE precalc_bibs_by_uri_list ON COMMIT DROP AS
372         SELECT  DISTINCT record AS id
373           FROM  asset.call_number cn
374                 JOIN asset.uri_call_number_map urim ON (urim.call_number = cn.id)
375                 JOIN asset.uri uri ON (urim.uri = uri.id AND uri.active)
376           WHERE cn.owning_lib = ANY (badge_row.orgs)
377                 AND cn.label = '##URI##'
378                 AND NOT cn.deleted;
379
380     SELECT count(*) INTO cnt FROM precalc_bibs_by_uri_list;
381     RETURN cnt;
382 END;
383 $f$ LANGUAGE PLPGSQL;
384
385 CREATE OR REPLACE FUNCTION rating.precalc_bibs_by_copy_or_uri(badge_id INT)
386     RETURNS INT AS $f$
387 DECLARE
388     cnt         INT     := 0;
389 BEGIN
390
391     PERFORM rating.precalc_bibs_by_copy(badge_id);
392     PERFORM rating.precalc_bibs_by_uri(badge_id);
393
394     SET LOCAL client_min_messages = error;
395     DROP TABLE IF EXISTS precalc_bibs_by_copy_or_uri_list;
396     CREATE TEMP TABLE precalc_bibs_by_copy_or_uri_list ON COMMIT DROP AS
397         SELECT id FROM precalc_bibs_by_copy_list
398             UNION
399         SELECT id FROM precalc_bibs_by_uri_list;
400
401     SELECT count(*) INTO cnt FROM precalc_bibs_by_copy_or_uri_list;
402     RETURN cnt;
403 END;
404 $f$ LANGUAGE PLPGSQL;
405
406
407 CREATE OR REPLACE FUNCTION rating.recalculate_badge_score ( badge_id INT, setup_only BOOL DEFAULT FALSE ) RETURNS VOID AS $f$
408 DECLARE
409     badge_row           rating.badge%ROWTYPE;
410     param           rating.popularity_parameter%ROWTYPE;
411 BEGIN
412     SET LOCAL client_min_messages = error;
413
414     -- Find what we're doing    
415     SELECT * INTO badge_row FROM rating.badge WHERE id = badge_id;
416     SELECT * INTO param FROM rating.popularity_parameter WHERE id = badge_row.popularity_parameter;
417
418     -- Calculate the filtered bib set, or all bibs if none
419     PERFORM rating.precalc_attr_filter(badge_row.attr_filter);
420     PERFORM rating.precalc_src_filter(badge_row.src_filter);
421     PERFORM rating.precalc_circ_mod_filter(badge_row.circ_mod_filter);
422     PERFORM rating.precalc_location_filter(badge_row.loc_grp_filter);
423
424     -- Bring the bib-level filter lists together
425     DROP TABLE IF EXISTS precalc_bib_filter_bib_list;
426     CREATE TEMP TABLE precalc_bib_filter_bib_list ON COMMIT DROP AS
427         SELECT id FROM precalc_attr_filter_bib_list
428             INTERSECT
429         SELECT id FROM precalc_src_filter_bib_list;
430
431     -- Bring the copy-level filter lists together. We're keeping this for bib_by_copy filtering later.
432     DROP TABLE IF EXISTS precalc_copy_filter_bib_list;
433     CREATE TEMP TABLE precalc_copy_filter_bib_list ON COMMIT DROP AS
434         SELECT id, copy FROM precalc_circ_mod_filter_bib_list
435             INTERSECT
436         SELECT id, copy FROM precalc_location_filter_bib_list;
437
438     -- Bring the collapsed filter lists together
439     DROP TABLE IF EXISTS precalc_filter_bib_list;
440     CREATE TEMP TABLE precalc_filter_bib_list ON COMMIT DROP AS
441         SELECT id FROM precalc_bib_filter_bib_list
442             INTERSECT
443         SELECT id FROM precalc_copy_filter_bib_list;
444
445     CREATE INDEX precalc_filter_bib_list_idx
446         ON precalc_filter_bib_list (id);
447
448     IF setup_only THEN
449         RETURN;
450     END IF;
451
452     -- If it's a fixed-rating badge, just do it ...
453     IF badge_row.fixed_rating IS NOT NULL THEN
454         DELETE FROM rating.record_badge_score WHERE badge = badge_id;
455         EXECUTE $e$
456             INSERT INTO rating.record_badge_score (record, badge, score)
457                 SELECT record, $1, $2 FROM $e$ || param.func || $e$($1)$e$
458         USING badge_id, badge_row.fixed_rating;
459
460         UPDATE rating.badge SET last_calc = NOW() WHERE id = badge_id;
461
462         RETURN;
463     END IF;
464     -- else, calculate!
465
466     -- Make a session-local scratchpad for calculating scores
467     CREATE TEMP TABLE record_score_scratchpad (
468         bib     BIGINT,
469         value   NUMERIC
470     ) ON COMMIT DROP;
471
472     -- Gather raw values
473     EXECUTE $e$
474         INSERT INTO record_score_scratchpad (bib, value)
475             SELECT * FROM $e$ || param.func || $e$($1)$e$
476     USING badge_id;
477
478     IF badge_row.discard > 0 OR badge_row.percentile IS NOT NULL THEN
479         -- To speed up discard-common
480         CREATE INDEX record_score_scratchpad_score_idx ON record_score_scratchpad (value);
481         ANALYZE record_score_scratchpad;
482     END IF;
483
484     IF badge_row.discard > 0 THEN -- Remove common low values (trim the long tail)
485         DELETE FROM record_score_scratchpad WHERE value IN (
486             SELECT DISTINCT value FROM record_score_scratchpad ORDER BY value LIMIT badge_row.discard
487         );
488     END IF;
489
490     IF badge_row.percentile IS NOT NULL THEN -- Cut population down to exceptional records
491         DELETE FROM record_score_scratchpad WHERE value <= (
492             SELECT value FROM (
493                 SELECT  value,
494                         CUME_DIST() OVER (ORDER BY value) AS p
495                   FROM  record_score_scratchpad
496             ) x WHERE p < badge_row.percentile / 100.0 ORDER BY p DESC LIMIT 1
497         );
498     END IF;
499
500
501     -- And, finally, push new data in
502     DELETE FROM rating.record_badge_score WHERE badge = badge_id;
503     INSERT INTO rating.record_badge_score (badge, record, score)
504         SELECT  badge_id,
505                 bib,
506                 GREATEST(ROUND((CUME_DIST() OVER (ORDER BY value)) * 5), 1) AS value
507           FROM  record_score_scratchpad;
508
509     DROP TABLE record_score_scratchpad;
510
511     -- Now, finally-finally, mark the badge as recalculated
512     UPDATE rating.badge SET last_calc = NOW() WHERE id = badge_id;
513
514     RETURN;
515 END;
516 $f$ LANGUAGE PLPGSQL;
517
518 CREATE OR REPLACE FUNCTION rating.holds_filled_over_time(badge_id INT)
519     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
520 DECLARE
521     badge   rating.badge_with_orgs%ROWTYPE;
522     iage    INT     := 1;
523     iint    INT     := NULL;
524     iscale  NUMERIC := NULL;
525 BEGIN
526
527     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
528
529     IF badge.horizon_age IS NULL THEN
530         RAISE EXCEPTION 'Badge "%" with id % requires a horizon age but has none.',
531             badge.name,
532             badge.id;
533     END IF;
534
535     PERFORM rating.precalc_bibs_by_copy(badge_id);
536
537     SET LOCAL client_min_messages = error;
538     DROP TABLE IF EXISTS precalc_bib_list;
539     CREATE TEMP TABLE precalc_bib_list ON COMMIT DROP AS
540         SELECT id FROM precalc_filter_bib_list
541             INTERSECT
542         SELECT id FROM precalc_bibs_by_copy_list;
543
544     iint := EXTRACT(EPOCH FROM badge.importance_interval);
545     IF badge.importance_age IS NOT NULL THEN
546         iage := (EXTRACT(EPOCH FROM badge.importance_age) / iint)::INT;
547     END IF;
548
549     -- if iscale is smaller than 1, scaling slope will be shallow ... BEWARE!
550     iscale := COALESCE(badge.importance_scale, 1.0);
551
552     RETURN QUERY
553      SELECT bib,
554             SUM( holds * GREATEST( iscale * (iage - hage), 1.0 ))
555       FROM (
556          SELECT f.id AS bib,
557                 (1 + EXTRACT(EPOCH FROM AGE(h.fulfillment_time)) / iint)::INT AS hage,
558                 COUNT(h.id)::INT AS holds
559           FROM  action.hold_request h
560                 JOIN reporter.hold_request_record rhrr ON (rhrr.id = h.id)
561                 JOIN precalc_bib_list f ON (f.id = rhrr.bib_record)
562           WHERE h.fulfillment_time >= NOW() - badge.horizon_age
563                 AND h.request_lib = ANY (badge.orgs)
564           GROUP BY 1, 2
565       ) x
566       GROUP BY 1;
567 END;
568 $f$ LANGUAGE PLPGSQL STRICT;
569
570 CREATE OR REPLACE FUNCTION rating.holds_placed_over_time(badge_id INT)
571     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
572 DECLARE
573     badge   rating.badge_with_orgs%ROWTYPE;
574     iage    INT     := 1;
575     iint    INT     := NULL;
576     iscale  NUMERIC := NULL;
577 BEGIN
578
579     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
580
581     IF badge.horizon_age IS NULL THEN
582         RAISE EXCEPTION 'Badge "%" with id % requires a horizon age but has none.',
583             badge.name,
584             badge.id;
585     END IF;
586
587     PERFORM rating.precalc_bibs_by_copy(badge_id);
588
589     SET LOCAL client_min_messages = error;
590     DROP TABLE IF EXISTS precalc_bib_list;
591     CREATE TEMP TABLE precalc_bib_list ON COMMIT DROP AS
592         SELECT id FROM precalc_filter_bib_list
593             INTERSECT
594         SELECT id FROM precalc_bibs_by_copy_list;
595
596     iint := EXTRACT(EPOCH FROM badge.importance_interval);
597     IF badge.importance_age IS NOT NULL THEN
598         iage := (EXTRACT(EPOCH FROM badge.importance_age) / iint)::INT;
599     END IF;
600
601     -- if iscale is smaller than 1, scaling slope will be shallow ... BEWARE!
602     iscale := COALESCE(badge.importance_scale, 1.0);
603
604     RETURN QUERY
605      SELECT bib,
606             SUM( holds * GREATEST( iscale * (iage - hage), 1.0 ))
607       FROM (
608          SELECT f.id AS bib,
609                 (1 + EXTRACT(EPOCH FROM AGE(h.request_time)) / iint)::INT AS hage,
610                 COUNT(h.id)::INT AS holds
611           FROM  action.hold_request h
612                 JOIN reporter.hold_request_record rhrr ON (rhrr.id = h.id)
613                 JOIN precalc_bib_list f ON (f.id = rhrr.bib_record)
614           WHERE h.request_time >= NOW() - badge.horizon_age
615                 AND h.request_lib = ANY (badge.orgs)
616           GROUP BY 1, 2
617       ) x
618       GROUP BY 1;
619 END;
620 $f$ LANGUAGE PLPGSQL STRICT;
621
622 CREATE OR REPLACE FUNCTION rating.current_hold_count(badge_id INT)
623     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
624 DECLARE
625     badge   rating.badge_with_orgs%ROWTYPE;
626 BEGIN
627
628     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
629
630     PERFORM rating.precalc_bibs_by_copy(badge_id);
631
632     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
633         SELECT id FROM precalc_filter_bib_list
634             INTERSECT
635         SELECT id FROM precalc_bibs_by_copy_list
636     );
637
638     ANALYZE precalc_copy_filter_bib_list;
639
640     RETURN QUERY
641      SELECT rhrr.bib_record AS bib,
642             COUNT(DISTINCT h.id)::NUMERIC AS holds
643       FROM  action.hold_request h
644             JOIN reporter.hold_request_record rhrr ON (rhrr.id = h.id)
645             JOIN action.hold_copy_map m ON (m.hold = h.id)
646             JOIN precalc_copy_filter_bib_list cf ON (rhrr.bib_record = cf.id AND m.target_copy = cf.copy)
647       WHERE h.fulfillment_time IS NULL
648             AND h.request_lib = ANY (badge.orgs)
649       GROUP BY 1;
650 END;
651 $f$ LANGUAGE PLPGSQL STRICT;
652
653 CREATE OR REPLACE FUNCTION rating.circs_over_time(badge_id INT)
654     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
655 DECLARE
656     badge   rating.badge_with_orgs%ROWTYPE;
657     iage    INT     := 1;
658     iint    INT     := NULL;
659     iscale  NUMERIC := NULL;
660 BEGIN
661
662     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
663
664     IF badge.horizon_age IS NULL THEN
665         RAISE EXCEPTION 'Badge "%" with id % requires a horizon age but has none.',
666             badge.name,
667             badge.id;
668     END IF;
669
670     PERFORM rating.precalc_bibs_by_copy(badge_id);
671
672     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
673         SELECT id FROM precalc_filter_bib_list
674             INTERSECT
675         SELECT id FROM precalc_bibs_by_copy_list
676     );
677
678     ANALYZE precalc_copy_filter_bib_list;
679
680     iint := EXTRACT(EPOCH FROM badge.importance_interval);
681     IF badge.importance_age IS NOT NULL THEN
682         iage := (EXTRACT(EPOCH FROM badge.importance_age) / iint)::INT;
683     END IF;
684
685     -- if iscale is smaller than 1, scaling slope will be shallow ... BEWARE!
686     iscale := COALESCE(badge.importance_scale, 1.0);
687
688     RETURN QUERY
689      SELECT bib,
690             SUM( circs * GREATEST( iscale * (iage - cage), 1.0 ))
691       FROM (
692          SELECT cn.record AS bib,
693                 (1 + EXTRACT(EPOCH FROM AGE(c.xact_start)) / iint)::INT AS cage,
694                 COUNT(c.id)::INT AS circs
695           FROM  action.circulation c
696                 JOIN precalc_copy_filter_bib_list cf ON (c.target_copy = cf.copy)
697                 JOIN asset.copy cp ON (cp.id = c.target_copy)
698                 JOIN asset.call_number cn ON (cn.id = cp.call_number)
699           WHERE c.xact_start >= NOW() - badge.horizon_age
700                 AND cn.owning_lib = ANY (badge.orgs)
701                 AND c.phone_renewal IS FALSE  -- we don't count renewals
702                 AND c.desk_renewal IS FALSE
703                 AND c.opac_renewal IS FALSE
704           GROUP BY 1, 2
705       ) x
706       GROUP BY 1;
707 END;
708 $f$ LANGUAGE PLPGSQL STRICT;
709
710 CREATE OR REPLACE FUNCTION rating.current_circ_count(badge_id INT)
711     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
712 DECLARE
713     badge   rating.badge_with_orgs%ROWTYPE;
714 BEGIN
715
716     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
717
718     PERFORM rating.precalc_bibs_by_copy(badge_id);
719
720     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
721         SELECT id FROM precalc_filter_bib_list
722             INTERSECT
723         SELECT id FROM precalc_bibs_by_copy_list
724     );
725
726     ANALYZE precalc_copy_filter_bib_list;
727
728     RETURN QUERY
729      SELECT cn.record AS bib,
730             COUNT(c.id)::NUMERIC AS circs
731       FROM  action.circulation c
732             JOIN precalc_copy_filter_bib_list cf ON (c.target_copy = cf.copy)
733             JOIN asset.copy cp ON (cp.id = c.target_copy)
734             JOIN asset.call_number cn ON (cn.id = cp.call_number)
735       WHERE c.checkin_time IS NULL
736             AND cn.owning_lib = ANY (badge.orgs)
737       GROUP BY 1;
738
739 END;
740 $f$ LANGUAGE PLPGSQL STRICT;
741
742 CREATE OR REPLACE FUNCTION rating.checked_out_total_ratio(badge_id INT)
743     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
744 DECLARE
745     badge   rating.badge_with_orgs%ROWTYPE;
746 BEGIN
747
748     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
749
750     PERFORM rating.precalc_bibs_by_copy(badge_id);
751
752     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
753         SELECT id FROM precalc_filter_bib_list
754             INTERSECT
755         SELECT id FROM precalc_bibs_by_copy_list
756     );
757
758     ANALYZE precalc_copy_filter_bib_list;
759
760     RETURN QUERY
761      SELECT bib,
762             SUM(checked_out)::NUMERIC / SUM(total)::NUMERIC
763       FROM  (SELECT cn.record AS bib,
764                     (cp.status = 1)::INT AS checked_out,
765                     1 AS total
766               FROM  asset.copy cp
767                     JOIN precalc_copy_filter_bib_list c ON (cp.id = c.copy)
768                     JOIN asset.call_number cn ON (cn.id = cp.call_number)
769               WHERE cn.owning_lib = ANY (badge.orgs)
770             ) x
771       GROUP BY 1;
772 END;
773 $f$ LANGUAGE PLPGSQL STRICT;
774
775 CREATE OR REPLACE FUNCTION rating.holds_total_ratio(badge_id INT)
776     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
777 DECLARE
778     badge   rating.badge_with_orgs%ROWTYPE;
779 BEGIN
780
781     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
782
783     PERFORM rating.precalc_bibs_by_copy(badge_id);
784
785     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
786         SELECT id FROM precalc_filter_bib_list
787             INTERSECT
788         SELECT id FROM precalc_bibs_by_copy_list
789     );
790
791     ANALYZE precalc_copy_filter_bib_list;
792
793     RETURN QUERY
794      SELECT cn.record AS bib,
795             COUNT(DISTINCT m.hold)::NUMERIC / COUNT(DISTINCT cp.id)::NUMERIC
796       FROM  asset.copy cp
797             JOIN precalc_copy_filter_bib_list c ON (cp.id = c.copy)
798             JOIN asset.call_number cn ON (cn.id = cp.call_number)
799             JOIN action.hold_copy_map m ON (m.target_copy = cp.id)
800       WHERE cn.owning_lib = ANY (badge.orgs)
801       GROUP BY 1;
802 END;
803 $f$ LANGUAGE PLPGSQL STRICT;
804
805 CREATE OR REPLACE FUNCTION rating.holds_holdable_ratio(badge_id INT)
806     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
807 DECLARE
808     badge   rating.badge_with_orgs%ROWTYPE;
809 BEGIN
810
811     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
812
813     PERFORM rating.precalc_bibs_by_copy(badge_id);
814
815     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
816         SELECT id FROM precalc_filter_bib_list
817             INTERSECT
818         SELECT id FROM precalc_bibs_by_copy_list
819     );
820
821     ANALYZE precalc_copy_filter_bib_list;
822
823     RETURN QUERY
824      SELECT cn.record AS bib,
825             COUNT(DISTINCT m.hold)::NUMERIC / COUNT(DISTINCT cp.id)::NUMERIC
826       FROM  asset.copy cp
827             JOIN precalc_copy_filter_bib_list c ON (cp.id = c.copy)
828             JOIN asset.copy_location cl ON (cl.id = cp.location)
829             JOIN config.copy_status cs ON (cs.id = cp.status)
830             JOIN asset.call_number cn ON (cn.id = cp.call_number)
831             JOIN action.hold_copy_map m ON (m.target_copy = cp.id)
832       WHERE cn.owning_lib = ANY (badge.orgs)
833             AND cp.holdable IS TRUE
834             AND cl.holdable IS TRUE
835             AND cs.holdable IS TRUE
836       GROUP BY 1;
837 END;
838 $f$ LANGUAGE PLPGSQL STRICT;
839
840 CREATE OR REPLACE FUNCTION rating.bib_record_age(badge_id INT)
841     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
842 DECLARE
843     badge   rating.badge_with_orgs%ROWTYPE;
844 BEGIN
845
846     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
847
848     PERFORM rating.precalc_bibs_by_copy_or_uri(badge_id);
849
850     SET LOCAL client_min_messages = error;
851     DROP TABLE IF EXISTS precalc_bib_list;
852     CREATE TEMP TABLE precalc_bib_list ON COMMIT DROP AS
853         SELECT id FROM precalc_filter_bib_list
854             INTERSECT
855         SELECT id FROM precalc_bibs_by_copy_or_uri_list;
856
857     RETURN QUERY
858      SELECT b.id,
859             1.0 / EXTRACT(EPOCH FROM AGE(b.create_date))::NUMERIC + 1.0
860       FROM  precalc_bib_list pop
861             JOIN biblio.record_entry b ON (b.id = pop.id);
862 END;
863 $f$ LANGUAGE PLPGSQL STRICT;
864
865 CREATE OR REPLACE FUNCTION rating.bib_pub_age(badge_id INT)
866     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
867 DECLARE
868     badge   rating.badge_with_orgs%ROWTYPE;
869 BEGIN
870
871     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
872
873     PERFORM rating.precalc_bibs_by_copy_or_uri(badge_id);
874
875     SET LOCAL client_min_messages = error;
876     DROP TABLE IF EXISTS precalc_bib_list;
877     CREATE TEMP TABLE precalc_bib_list ON COMMIT DROP AS
878         SELECT id FROM precalc_filter_bib_list
879             INTERSECT
880         SELECT id FROM precalc_bibs_by_copy_or_uri_list;
881
882     RETURN QUERY
883      SELECT pop.id AS bib,
884             s.value::NUMERIC
885       FROM  precalc_bib_list pop
886             JOIN metabib.record_sorter s ON (
887                 s.source = pop.id
888                 AND s.attr = 'pubdate'
889                 AND s.value ~ '^\d+$'
890             )
891       WHERE s.value::INT <= EXTRACT(YEAR FROM NOW())::INT;
892 END;
893 $f$ LANGUAGE PLPGSQL STRICT;
894
895 CREATE OR REPLACE FUNCTION rating.percent_time_circulating(badge_id INT)
896     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
897 DECLARE
898     badge   rating.badge_with_orgs%ROWTYPE;
899 BEGIN
900
901     SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
902
903     PERFORM rating.precalc_bibs_by_copy(badge_id);
904
905     DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
906         SELECT id FROM precalc_filter_bib_list
907             INTERSECT
908         SELECT id FROM precalc_bibs_by_copy_list
909     );
910
911     ANALYZE precalc_copy_filter_bib_list;
912
913     RETURN QUERY
914      SELECT bib,
915             SUM(COALESCE(circ_time,0))::NUMERIC / SUM(age)::NUMERIC
916       FROM  (SELECT cn.record AS bib,
917                     cp.id,
918                     EXTRACT( EPOCH FROM AGE(cp.active_date) ) + 1 AS age,
919                     SUM(  -- time copy spent circulating
920                         EXTRACT(
921                             EPOCH FROM
922                             AGE(
923                                 COALESCE(circ.checkin_time, circ.stop_fines_time, NOW()),
924                                 circ.xact_start
925                             )
926                         )
927                     )::NUMERIC AS circ_time
928               FROM  asset.copy cp
929                     JOIN precalc_copy_filter_bib_list c ON (cp.id = c.copy)
930                     JOIN asset.call_number cn ON (cn.id = cp.call_number)
931                     LEFT JOIN action.all_circulation circ ON (
932                         circ.target_copy = cp.id
933                         AND stop_fines NOT IN (
934                             'LOST',
935                             'LONGOVERDUE',
936                             'CLAIMSRETURNED',
937                             'LONGOVERDUE'
938                         )
939                         AND NOT (
940                             checkin_time IS NULL AND
941                             stop_fines = 'MAXFINES'
942                         )
943                     )
944               WHERE cn.owning_lib = ANY (badge.orgs)
945                     AND cp.active_date IS NOT NULL
946                     -- Next line requires that copies with no circs (circ.id IS NULL) also not be deleted
947                     AND ((circ.id IS NULL AND NOT cp.deleted) OR circ.id IS NOT NULL)
948               GROUP BY 1,2,3
949             ) x
950       GROUP BY 1;
951 END;
952 $f$ LANGUAGE PLPGSQL STRICT;
953
954 CREATE OR REPLACE FUNCTION rating.generic_fixed_rating_by_copy(badge_id INT)
955     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
956 BEGIN
957     PERFORM rating.precalc_bibs_by_copy(badge_id);
958     RETURN QUERY
959         SELECT id, 1.0 FROM precalc_filter_bib_list
960             INTERSECT
961         SELECT id, 1.0 FROM precalc_bibs_by_copy_list;
962 END;
963 $f$ LANGUAGE PLPGSQL STRICT;
964
965 CREATE OR REPLACE FUNCTION rating.generic_fixed_rating_by_uri(badge_id INT)
966     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
967 BEGIN
968     PERFORM rating.precalc_bibs_by_uri(badge_id);
969     RETURN QUERY
970         SELECT id, 1.0 FROM precalc_bib_filter_bib_list
971             INTERSECT
972         SELECT id, 1.0 FROM precalc_bibs_by_uri_list;
973 END;
974 $f$ LANGUAGE PLPGSQL STRICT;
975
976 CREATE OR REPLACE FUNCTION rating.generic_fixed_rating_by_copy_or_uri(badge_id INT)
977     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
978 BEGIN
979     PERFORM rating.precalc_bibs_by_copy_or_uri(badge_id);
980     RETURN QUERY
981         (SELECT id, 1.0 FROM precalc_filter_bib_list
982             INTERSECT
983         SELECT id, 1.0 FROM precalc_bibs_by_copy_list)
984             UNION
985         (SELECT id, 1.0 FROM precalc_bib_filter_bib_list
986             INTERSECT
987         SELECT id, 1.0 FROM precalc_bibs_by_uri_list);
988 END;
989 $f$ LANGUAGE PLPGSQL STRICT;
990
991 CREATE OR REPLACE FUNCTION rating.generic_fixed_rating_global(badge_id INT)
992     RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
993 BEGIN
994     RETURN QUERY
995         SELECT id, 1.0 FROM precalc_bib_filter_bib_list;
996 END;
997 $f$ LANGUAGE PLPGSQL STRICT;
998
999 CREATE INDEX hold_fulfillment_time_idx ON action.hold_request (fulfillment_time) WHERE fulfillment_time IS NOT NULL;
1000 CREATE INDEX hold_request_time_idx ON action.hold_request (request_time);
1001
1002 COMMIT;
1003