__PACKAGE__->add_search_filter( 'locations' );
__PACKAGE__->add_search_filter( 'location_groups' );
__PACKAGE__->add_search_filter( 'bib_source' );
+__PACKAGE__->add_search_filter( 'badge_orgs' );
+__PACKAGE__->add_search_filter( 'badges' );
__PACKAGE__->add_search_filter( 'site' );
__PACKAGE__->add_search_filter( 'pref_ou' );
__PACKAGE__->add_search_filter( 'lasso' );
$rel = "($rel * COALESCE( NULLIF( FIRST(mrv.vlist \@> ARRAY[lang_with.id]), FALSE )::INT * $plw, 1))";
$$flat_plan{uses_mrv} = 1;
}
- $rel = "1.0/($rel)::NUMERIC";
my $mrv_join = '';
if ($$flat_plan{uses_mrv}) {
$bre_join = 'INNER JOIN biblio.record_entry bre ON m.source = bre.id';
}
- my $rank = $rel;
-
my $desc = 'ASC';
$desc = 'DESC' if ($self->find_modifier('descending'));
my $nullpos = 'NULLS LAST';
$nullpos = 'NULLS FIRST' if ($self->find_modifier('nullsfirst'));
+ # Do we have a badges() filter?
+ my $badges = '';
+ my ($badge_filter) = $self->find_filter('badges');
+ if ($badge_filter && @{$badge_filter->args}) {
+ $badges = join (',', grep /^\d+$/, @{$badge_filter->args});
+ }
+
+ # Do we have a badge_orgs() filter? (used for calculating popularity)
+ my $borgs = '';
+ my ($bo_filter) = $self->find_filter('badge_orgs');
+ if ($bo_filter && @{$bo_filter->args}) {
+ $borgs = join (',', grep /^\d+$/, @{$bo_filter->args});
+ }
+
+ # Build the badge-ish WITH query
+ my $pop_with = <<' WITH';
+ pop_with AS (
+ SELECT record,
+ ARRAY_AGG(badge) AS badges,
+ SUM(s.score::NUMERIC*b.weight::NUMERIC)/SUM(b.weight::NUMERIC) AS total_score
+ FROM rating.record_badge_score s
+ JOIN rating.badge b ON (
+ b.id = s.badge
+ WITH
+
+ $pop_with .= " AND b.id = ANY ('{$badges}')" if ($badges);
+ $pop_with .= " AND b.scope = ANY ('{$borgs}')" if ($borgs);
+ $pop_with .= ') GROUP BY 1)';
+
+ my $pop_join = $badges ? # inner join if we are restricting via badges()
+ 'INNER JOIN pop_with ON ( m.source = pop_with.record )' :
+ 'LEFT JOIN pop_with ON ( m.source = pop_with.record )';
+
+ $$flat_plan{with} .= ',' if $$flat_plan{with};
+ $$flat_plan{with} .= $pop_with;
+
+
+ my $rank;
+ my $pop_extra_sort = '';
if (grep {$_ eq $sort_filter} @{$self->QueryParser->dynamic_sorters}) {
$rank = "FIRST((SELECT value FROM metabib.record_sorter rbr WHERE rbr.source = m.source and attr = '$sort_filter'))"
} elsif ($sort_filter eq 'create_date') {
$rank = "FIRST((SELECT create_date FROM biblio.record_entry rbr WHERE rbr.id = m.source))";
} elsif ($sort_filter eq 'edit_date') {
$rank = "FIRST((SELECT edit_date FROM biblio.record_entry rbr WHERE rbr.id = m.source))";
+ } elsif ($sort_filter eq 'poprel') {
+ $rank = '1.0/((' . $rel . ') * (1.0 + AVG(COALESCE(pop_with.total_score::NUMERIC,0.0)) / 5.0))::NUMERIC';
+ } elsif ($sort_filter =~ /^pop/) {
+ $rank = '1.0/(AVG(COALESCE(pop_with.total_score::NUMERIC,0.0)) + 5.0)::NUMERIC';
+ my $pop_desc = $desc eq 'ASC' ? 'DESC' : 'ASC';
+ $pop_extra_sort = "3 $pop_desc $nullpos,";
} else {
# default to rel ranking
- $rank = $rel;
+ $rank = "1.0/($rel)::NUMERIC";
}
my $key = 'm.source';
$agg_records,
$rel AS rel,
$rank AS rank,
- FIRST(pubdate_t.value) AS tie_break
+ FIRST(pubdate_t.value) AS tie_break,
+ STRING_AGG(ARRAY_TO_STRING(pop_with.badges,','),',') AS badges,
+ AVG(COALESCE(pop_with.total_score::NUMERIC,0.0))::NUMERIC(2,1) AS popularity
FROM metabib.metarecord_source_map m
$$flat_plan{from}
- $pubdate_join
$mra_join
$mrv_join
$bre_join
+ $pop_join
+ $pubdate_join
$lang_join
WHERE 1=1
$flat_where
GROUP BY 1
- ORDER BY 4 $desc $nullpos, 5 DESC $nullpos, 3 DESC
+ ORDER BY 4 $desc $nullpos, $pop_extra_sort 5 DESC $nullpos, 3 DESC
LIMIT $core_limit
SQL
delete $$summary_row{id};
delete $$summary_row{rel};
delete $$summary_row{record};
+ delete $$summary_row{badges};
+ delete $$summary_row{popularity};
if (defined($simple_plan)) {
$$summary_row{complex_query} = $simple_plan ? 0 : 1;
cachable => 1,
);
+my $top_org;
+
sub query_parser_fts_wrapper {
my $self = shift;
my $client = shift;
die "No search arguments were passed to ".$self->api_name;
}
+ $top_org ||= actor::org_unit->search( { parent_ou => undef } )->next;
+
$log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
my $base_query = '';
for my $sclass ( keys %{$args{searches}} ) {
if ($args{preferred_language_weight} and !$base_plan->parse_tree->find_filter('preferred_language_weight') and !$base_plan->parse_tree->find_filter('preferred_language_multiplier'));
+ my $borgs = undef;
+ if (!$base_plan->parse_tree->find_filter('badge_orgs')) {
+ # supply a suitable badge_orgs filter unless user has
+ # explicitly supplied one
+ my $site = undef;
+
+ my @lg_id_list = @{$args{location_groups}} if (ref $args{location_groups});
+
+ my ($lg_filter) = $base_plan->parse_tree->find_filter('location_groups');
+ @lg_id_list = @{$lg_filter->args} if ($lg_filter && @{$lg_filter->args});
+
+ if (@lg_id_list) {
+ my @borg_list;
+ for my $lg ( grep { /^\d+$/ } @lg_id_list ) {
+ my $lg_obj = asset::copy_location_group->retrieve($lg);
+ next unless $lg_obj;
+
+ push(@borg_list, ''.$lg_obj->owner);
+ }
+ $borgs = join(',', @borg_list) if @borg_list;
+ }
+
+ if (!$borgs) {
+ my ($site_filter) = $base_plan->parse_tree->find_filter('site');
+ if ($site_filter && @{$site_filter->args}) {
+ $site = $top_org if ($site_filter->args->[0] eq '-');
+ $site = $top_org if ($site_filter->args->[0] eq $top_org->shortname);
+ $site = actor::org_unit->search( { shortname => $site_filter->args->[0] })->next unless ($site);
+ } elsif ($args{org_unit}) {
+ $site = $top_org if ($args{org_unit} eq '-');
+ $site = $top_org if ($args{org_unit} eq $top_org->shortname);
+ $site = actor::org_unit->search( { shortname => $args{org_unit} })->next unless ($site);
+ } else {
+ $site = $top_org;
+ }
+
+ if ($site) {
+ $borgs = OpenSRF::AppSession->create( 'open-ils.cstore' )->request(
+ 'open-ils.cstore.json_query.atomic',
+ { from => [ 'actor.org_unit_ancestors', $site->id ] }
+ )->gather(1);
+
+ if (ref $borgs && @$borgs) {
+ $borgs = join(',', map { $_->{'id'} } @$borgs);
+ } else {
+ $borgs = undef;
+ }
+ }
+ }
+ }
+
$query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
+ $query = "badge_orgs($borgs) $query" if ($borgs);
$query = "site($args{org_unit}) $query" if ($args{org_unit});
$query = "depth($args{depth}) $query" if (defined($args{depth}));
$query = "sort($args{sort}) $query" if ($args{sort});
$args{item_form} = [ split '', $f ];
}
- for my $filter ( qw/locations location_groups statuses between audience language lit_form item_form item_type bib_level vr_format/ ) {
+ for my $filter ( qw/locations location_groups statuses between audience language lit_form item_form item_type bib_level vr_format badges/ ) {
if (my $s = $args{$filter}) {
$s = [$s] if (!ref($s));
--- /dev/null
+/*
+ * Copyright (C) 2016 Equinox Software, Inc.
+ * Mike Rylander <miker@esilibrary.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ */
+
+
+BEGIN;
+
+ALTER TYPE search.search_result ADD ATTRIBUTE badges TEXT, ADD ATTRIBUTE popularity NUMERIC;
+
+CREATE OR REPLACE FUNCTION search.query_parser_fts (
+
+ param_search_ou INT,
+ param_depth INT,
+ param_query TEXT,
+ param_statuses INT[],
+ param_locations INT[],
+ param_offset INT,
+ param_check INT,
+ param_limit INT,
+ metarecord BOOL,
+ staff BOOL,
+ deleted_search BOOL,
+ param_pref_ou INT DEFAULT NULL
+) RETURNS SETOF search.search_result AS $func$
+DECLARE
+
+ current_res search.search_result%ROWTYPE;
+ search_org_list INT[];
+ luri_org_list INT[];
+ tmp_int_list INT[];
+
+ check_limit INT;
+ core_limit INT;
+ core_offset INT;
+ tmp_int INT;
+
+ core_result RECORD;
+ core_cursor REFCURSOR;
+ core_rel_query TEXT;
+
+ total_count INT := 0;
+ check_count INT := 0;
+ deleted_count INT := 0;
+ visible_count INT := 0;
+ excluded_count INT := 0;
+
+ luri_as_copy BOOL;
+BEGIN
+
+ check_limit := COALESCE( param_check, 1000 );
+ core_limit := COALESCE( param_limit, 25000 );
+ core_offset := COALESCE( param_offset, 0 );
+
+ SELECT COALESCE( enabled, FALSE ) INTO luri_as_copy FROM config.global_flag WHERE name = 'opac.located_uri.act_as_copy';
+
+ -- core_skip_chk := COALESCE( param_skip_chk, 1 );
+
+ IF param_search_ou > 0 THEN
+ IF param_depth IS NOT NULL THEN
+ SELECT ARRAY_AGG(distinct id) INTO search_org_list FROM actor.org_unit_descendants( param_search_ou, param_depth );
+ ELSE
+ SELECT ARRAY_AGG(distinct id) INTO search_org_list FROM actor.org_unit_descendants( param_search_ou );
+ END IF;
+
+ IF luri_as_copy THEN
+ SELECT ARRAY_AGG(distinct id) INTO luri_org_list FROM actor.org_unit_full_path( param_search_ou );
+ ELSE
+ SELECT ARRAY_AGG(distinct id) INTO luri_org_list FROM actor.org_unit_ancestors( param_search_ou );
+ END IF;
+
+ ELSIF param_search_ou < 0 THEN
+ SELECT ARRAY_AGG(distinct org_unit) INTO search_org_list FROM actor.org_lasso_map WHERE lasso = -param_search_ou;
+
+ FOR tmp_int IN SELECT * FROM UNNEST(search_org_list) LOOP
+
+ IF luri_as_copy THEN
+ SELECT ARRAY_AGG(distinct id) INTO tmp_int_list FROM actor.org_unit_full_path( tmp_int );
+ ELSE
+ SELECT ARRAY_AGG(distinct id) INTO tmp_int_list FROM actor.org_unit_ancestors( tmp_int );
+ END IF;
+
+ luri_org_list := luri_org_list || tmp_int_list;
+ END LOOP;
+
+ SELECT ARRAY_AGG(DISTINCT x.id) INTO luri_org_list FROM UNNEST(luri_org_list) x(id);
+
+ ELSIF param_search_ou = 0 THEN
+ -- reserved for user lassos (ou_buckets/type='lasso') with ID passed in depth ... hack? sure.
+ END IF;
+
+ IF param_pref_ou IS NOT NULL THEN
+ IF luri_as_copy THEN
+ SELECT ARRAY_AGG(distinct id) INTO tmp_int_list FROM actor.org_unit_full_path( param_pref_ou );
+ ELSE
+ SELECT ARRAY_AGG(distinct id) INTO tmp_int_list FROM actor.org_unit_ancestors( param_pref_ou );
+ END IF;
+
+ luri_org_list := luri_org_list || tmp_int_list;
+ END IF;
+
+ OPEN core_cursor FOR EXECUTE param_query;
+
+ LOOP
+
+ FETCH core_cursor INTO core_result;
+ EXIT WHEN NOT FOUND;
+ EXIT WHEN total_count >= core_limit;
+
+ total_count := total_count + 1;
+
+ CONTINUE WHEN total_count NOT BETWEEN core_offset + 1 AND check_limit + core_offset;
+
+ check_count := check_count + 1;
+
+ IF NOT deleted_search THEN
+
+ PERFORM 1 FROM biblio.record_entry b WHERE NOT b.deleted AND b.id IN ( SELECT * FROM unnest( core_result.records ) );
+ IF NOT FOUND THEN
+ -- RAISE NOTICE ' % were all deleted ... ', core_result.records;
+ deleted_count := deleted_count + 1;
+ CONTINUE;
+ END IF;
+
+ PERFORM 1
+ FROM biblio.record_entry b
+ JOIN config.bib_source s ON (b.source = s.id)
+ WHERE s.transcendant
+ AND b.id IN ( SELECT * FROM unnest( core_result.records ) );
+
+ IF FOUND THEN
+ -- RAISE NOTICE ' % were all transcendant ... ', core_result.records;
+ visible_count := visible_count + 1;
+
+ current_res.id = core_result.id;
+ current_res.rel = core_result.rel;
+ current_res.badges = core_result.badges;
+ current_res.popularity = core_result.popularity;
+
+ tmp_int := 1;
+ IF metarecord THEN
+ SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
+ END IF;
+
+ IF tmp_int = 1 THEN
+ current_res.record = core_result.records[1];
+ ELSE
+ current_res.record = NULL;
+ END IF;
+
+ RETURN NEXT current_res;
+
+ CONTINUE;
+ END IF;
+
+ PERFORM 1
+ FROM asset.call_number cn
+ JOIN asset.uri_call_number_map map ON (map.call_number = cn.id)
+ JOIN asset.uri uri ON (map.uri = uri.id)
+ WHERE NOT cn.deleted
+ AND cn.label = '##URI##'
+ AND uri.active
+ AND ( param_locations IS NULL OR array_upper(param_locations, 1) IS NULL )
+ AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
+ AND cn.owning_lib IN ( SELECT * FROM unnest( luri_org_list ) )
+ LIMIT 1;
+
+ IF FOUND THEN
+ -- RAISE NOTICE ' % have at least one URI ... ', core_result.records;
+ visible_count := visible_count + 1;
+
+ current_res.id = core_result.id;
+ current_res.rel = core_result.rel;
+ current_res.badges = core_result.badges;
+ current_res.popularity = core_result.popularity;
+
+ tmp_int := 1;
+ IF metarecord THEN
+ SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
+ END IF;
+
+ IF tmp_int = 1 THEN
+ current_res.record = core_result.records[1];
+ ELSE
+ current_res.record = NULL;
+ END IF;
+
+ RETURN NEXT current_res;
+
+ CONTINUE;
+ END IF;
+
+ IF param_statuses IS NOT NULL AND array_upper(param_statuses, 1) > 0 THEN
+
+ PERFORM 1
+ FROM asset.call_number cn
+ JOIN asset.copy cp ON (cp.call_number = cn.id)
+ WHERE NOT cn.deleted
+ AND NOT cp.deleted
+ AND cp.status IN ( SELECT * FROM unnest( param_statuses ) )
+ AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
+ AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
+ LIMIT 1;
+
+ IF NOT FOUND THEN
+ PERFORM 1
+ FROM biblio.peer_bib_copy_map pr
+ JOIN asset.copy cp ON (cp.id = pr.target_copy)
+ WHERE NOT cp.deleted
+ AND cp.status IN ( SELECT * FROM unnest( param_statuses ) )
+ AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
+ AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
+ LIMIT 1;
+
+ IF NOT FOUND THEN
+ -- RAISE NOTICE ' % and multi-home linked records were all status-excluded ... ', core_result.records;
+ excluded_count := excluded_count + 1;
+ CONTINUE;
+ END IF;
+ END IF;
+
+ END IF;
+
+ IF param_locations IS NOT NULL AND array_upper(param_locations, 1) > 0 THEN
+
+ PERFORM 1
+ FROM asset.call_number cn
+ JOIN asset.copy cp ON (cp.call_number = cn.id)
+ WHERE NOT cn.deleted
+ AND NOT cp.deleted
+ AND cp.location IN ( SELECT * FROM unnest( param_locations ) )
+ AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
+ AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
+ LIMIT 1;
+
+ IF NOT FOUND THEN
+ PERFORM 1
+ FROM biblio.peer_bib_copy_map pr
+ JOIN asset.copy cp ON (cp.id = pr.target_copy)
+ WHERE NOT cp.deleted
+ AND cp.location IN ( SELECT * FROM unnest( param_locations ) )
+ AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
+ AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
+ LIMIT 1;
+
+ IF NOT FOUND THEN
+ -- RAISE NOTICE ' % and multi-home linked records were all copy_location-excluded ... ', core_result.records;
+ excluded_count := excluded_count + 1;
+ CONTINUE;
+ END IF;
+ END IF;
+
+ END IF;
+
+ IF staff IS NULL OR NOT staff THEN
+
+ PERFORM 1
+ FROM asset.opac_visible_copies
+ WHERE circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
+ AND record IN ( SELECT * FROM unnest( core_result.records ) )
+ LIMIT 1;
+
+ IF NOT FOUND THEN
+ PERFORM 1
+ FROM biblio.peer_bib_copy_map pr
+ JOIN asset.opac_visible_copies cp ON (cp.copy_id = pr.target_copy)
+ WHERE cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
+ AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
+ LIMIT 1;
+
+ IF NOT FOUND THEN
+
+ -- RAISE NOTICE ' % and multi-home linked records were all visibility-excluded ... ', core_result.records;
+ excluded_count := excluded_count + 1;
+ CONTINUE;
+ END IF;
+ END IF;
+
+ ELSE
+
+ PERFORM 1
+ FROM asset.call_number cn
+ JOIN asset.copy cp ON (cp.call_number = cn.id)
+ WHERE NOT cn.deleted
+ AND NOT cp.deleted
+ AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
+ AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
+ LIMIT 1;
+
+ IF NOT FOUND THEN
+
+ PERFORM 1
+ FROM biblio.peer_bib_copy_map pr
+ JOIN asset.copy cp ON (cp.id = pr.target_copy)
+ WHERE NOT cp.deleted
+ AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
+ AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
+ LIMIT 1;
+
+ IF NOT FOUND THEN
+
+ PERFORM 1
+ FROM asset.call_number cn
+ JOIN asset.copy cp ON (cp.call_number = cn.id)
+ WHERE cn.record IN ( SELECT * FROM unnest( core_result.records ) )
+ AND NOT cp.deleted
+ LIMIT 1;
+
+ IF NOT FOUND THEN
+ -- Recheck Located URI visibility in the case of no "foreign" copies
+ PERFORM 1
+ FROM asset.call_number cn
+ JOIN asset.uri_call_number_map map ON (map.call_number = cn.id)
+ JOIN asset.uri uri ON (map.uri = uri.id)
+ WHERE NOT cn.deleted
+ AND cn.label = '##URI##'
+ AND uri.active
+ AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
+ AND cn.owning_lib NOT IN ( SELECT * FROM unnest( luri_org_list ) )
+ LIMIT 1;
+
+ IF FOUND THEN
+ -- RAISE NOTICE ' % were excluded for foreign located URIs... ', core_result.records;
+ excluded_count := excluded_count + 1;
+ CONTINUE;
+ END IF;
+ ELSE
+ -- RAISE NOTICE ' % and multi-home linked records were all visibility-excluded ... ', core_result.records;
+ excluded_count := excluded_count + 1;
+ CONTINUE;
+ END IF;
+ END IF;
+
+ END IF;
+
+ END IF;
+
+ END IF;
+
+ visible_count := visible_count + 1;
+
+ current_res.id = core_result.id;
+ current_res.rel = core_result.rel;
+ current_res.badges = core_result.badges;
+ current_res.popularity = core_result.popularity;
+
+ tmp_int := 1;
+ IF metarecord THEN
+ SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
+ END IF;
+
+ IF tmp_int = 1 THEN
+ current_res.record = core_result.records[1];
+ ELSE
+ current_res.record = NULL;
+ END IF;
+
+ RETURN NEXT current_res;
+
+ IF visible_count % 1000 = 0 THEN
+ -- RAISE NOTICE ' % visible so far ... ', visible_count;
+ END IF;
+
+ END LOOP;
+
+ current_res.id = NULL;
+ current_res.rel = NULL;
+ current_res.record = NULL;
+ current_res.badges = NULL;
+ current_res.popularity = NULL;
+ current_res.total = total_count;
+ current_res.checked = check_count;
+ current_res.deleted = deleted_count;
+ current_res.visible = visible_count;
+ current_res.excluded = excluded_count;
+
+ CLOSE core_cursor;
+
+ RETURN NEXT current_res;
+
+END;
+$func$ LANGUAGE PLPGSQL;
+
+COMMIT;
+