return cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
}
-
-my $elastic_fields;
-sub elastic_search {
- my ($query, $offset, $limit) = @_;
-
- my ($available) = ($query =~ s/(\#available)//g);
- my ($descending) = ($query =~ s/(\#descending)//g);
-
- my @funcs = qw/site depth sort item_lang/; # todo add others
- my %calls;
-
- for my $func (@funcs) {
- my ($val) = ($query =~ /$func\(([^\)]+)\)/);
- if (defined $val) {
- # scrub from query string
- $query =~ s/$func\(([^\)]+)\)//g;
- $calls{$func} = $val;
- }
- }
-
- my $elastic_query = {
- # Fetch only the bib ID field from each source document
- _source => ['id'],
- size => $limit,
- from => $offset,
- query => {
- bool => {
- must => {
- query_string => {
- default_field => 'keyword',
- query => $query
- }
- }
- }
- }
- };
-
- if (my $sn = $calls{site}) {
- elastic_add_holdings_filter(
- $elastic_query, $sn, $calls{depth}, $available);
- }
-
- if (my $key = $calls{sort}) {
-
- # These sort fields match the default display field entries.
- # TODO: index fields specific to sorting
-
- my $dir = $descending ? 'desc' : 'asc';
- if ($key =~ /title/) {
- $elastic_query->{sort} = [
- {'titlesort' => $dir},
- ];
-
- } elsif ($key =~ /author/) {
- $elastic_query->{sort} = [
- {'authorsort' => $dir},
- ];
-
- } elsif ($key =~ /pubdate/) {
- $elastic_query->{sort} = [
- {'pubdate' => $dir}
- ];
- }
- }
-
- my $es = OpenILS::Elastic::BibSearch->new('main');
- $es->connect;
- my $results = $es->search($elastic_query);
-
- return {count => 0} unless $results;
-
- return {
- count => $results->{hits}->{total},
- ids => [
- map { [$_->{_id}, undef, $_->{_score}] }
- grep {defined $_} @{$results->{hits}->{hits}}
- ]
- };
-}
-
-# avoid repetitive calls to DB for org info.
-my %org_data_cache = (by_shortname => {}, ancestors_at => {});
-
-sub elastic_add_holdings_filter {
- my ($elastic_query, $shortname, $depth, $available) = @_;
-
- if (!$org_data_cache{by_shortname}{$shortname}) {
- $org_data_cache{by_shortname}{$shortname} =
- $U->find_org_by_shortname($U->get_org_tree, $shortname);
- }
-
- my $org = $org_data_cache{by_shortname}{$shortname};
-
- my $types = $U->get_org_types; # pulls from cache
- my ($type) = grep {$_->id == $org->ou_type} @$types;
-
- $depth = defined $depth ? min($depth, $type->depth) : $type->depth;
-
- if ($depth > 0) {
-
- if (!$org_data_cache{ancestors_at}{$shortname}) {
- $org_data_cache{ancestors_at}{$shortname} = {};
- }
-
- if (!$org_data_cache{ancestors_at}{$shortname}{$depth}) {
- $org_data_cache{ancestors_at}{$shortname}{$depth} =
- $U->get_org_descendants($org->id, $depth);
- }
-
- my $org_ids = $org_data_cache{ancestors_at}{$shortname}{$depth};
-
- # Add a boolean OR-filter on holdings circ lib and optionally
- # add a boolean AND-filter on copy status for availability
- # checking.
- $elastic_query->{query}->{bool}->{filter} = {
- nested => {
- path => 'holdings',
- query => {bool => {should => []}}
- }
- };
-
- my $should =
- $elastic_query->{query}{bool}{filter}{nested}{query}{bool}{should};
-
- for my $org_id (@$org_ids) {
-
- # Ensure at least one copy exists at the selected org unit
- my $and = {
- bool => {
- must => [
- {term => {'holdings.circ_lib' => $org_id}}
- ]
- }
- };
-
- # When limiting to available, ensure at least one of the
- # above copies is in status 0 or 7.
- # TODO: consult config.copy_status.is_available
- push(
- @{$and->{bool}{must}},
- {terms => {'holdings.status' => [0, 7]}}
- ) if $available;
-
- push(@$should, $and);
- }
-
- } elsif ($available) {
- # Limit to results that have an available copy, but don't worry
- # about where the copy lives, since we're searching globally.
-
- $elastic_query->{query}->{bool}->{filter} = {
- nested => {
- path => 'holdings',
- query => {bool => {must => [
- # TODO: consult config.copy_status.is_available
- {terms => {'holdings.status' => [0, 7]}}
- ]}}
- }
- };
- }
-}
-
sub fetch_display_fields {
my $self = shift;
my $conn = shift;
# Use the QueryParser module to make sense of the inbound search query.
use OpenILS::Application::Storage::Driver::Pg::QueryParser;
-# bib fields defined in the elastic bib-search index
-my $bib_search_fields;
# avoid repetitive calls to DB for org info.
my %org_data_cache = (by_shortname => {}, ancestors_at => {});
+# bib fields defined in the elastic bib-search index
+my $bib_fields;
+my $hidden_copy_statuses;
+my $hidden_copy_locations;
+my $avail_copy_statuses;
+
+sub init_data {
+ return if $bib_fields;
+
+ my $e = new_editor();
+
+ $bib_fields = $e->retrieve_all_elastic_bib_field;
+
+ my $stats = $e->json_query({
+ select => {ccs => ['id', 'opac_visible', 'is_available']},
+ from => 'ccs',
+ where => {'-or' => [
+ {opac_visible => 'f'},
+ {is_available => 't'}
+ ]}
+ });
+
+ $hidden_copy_statuses =
+ [map {$_->{id}} grep {$_->{opac_visible} eq 'f'} @$stats];
+
+ $avail_copy_statuses =
+ [map {$_->{id}} grep {$_->{is_available} eq 't'} @$stats];
+
+ # Include deleted copy locations since this is an exclusion set.
+ my $locs = $e->json_query({
+ select => {acpl => ['id']},
+ from => 'acpl',
+ where => {opac_visible => 'f'}
+ });
+
+ $hidden_copy_locations = [map {$_->{id}} @$locs];
+}
+
# Translate a bib search API call into something consumable by Elasticsearch
# Translate search results into a structure consistent with a bib search
# API response.
sub bib_search {
my ($class, $query, $staff, $offset, $limit) = @_;
- $logger->info("ES parsing API query $query");
+ $logger->info("ES parsing API query $query staff=$staff");
- $bib_search_fields =
- new_editor()->retrieve_all_elastic_bib_field()
- unless $bib_search_fields;
+ init_data();
my ($elastic_query, $cache_key) =
compile_elastic_query($query, $staff, $offset, $limit);
$elastic->{query}->{bool}->{must} =
[translate_query_node($elastic, $query_struct)];
- add_elastic_holdings_filter($elastic, $elastic->{__site},
- $elastic->{__depth}, $elastic->{__available}) if $elastic->{__site};
+ add_elastic_holdings_filter($elastic, $staff,
+ $elastic->{__site}, $elastic->{__depth}, $elastic->{__available});
add_elastic_facet_aggregations($elastic);
# class-level searches are OR/should searches across all
# fields in the selected class.
@fields = map {$_->name}
- grep {$_->search_group eq $field_class} @$bib_search_fields
+ grep {$_->search_group eq $field_class} @$bib_fields
unless @fields;
# note: $joiner is always '&' for type=node
my ($bib_field) = grep {
$_->name eq $name && $_->search_group eq $field_class
- } @$bib_search_fields;
+ } @$bib_fields;
my $hash = $facets->{$bib_field->metabib_field} = {};
sub add_elastic_facet_aggregations {
my ($elastic_query) = @_;
- my @facet_fields = grep {$_->facet_field eq 't'} @$bib_search_fields;
+ my @facet_fields = grep {$_->facet_field eq 't'} @$bib_fields;
return unless @facet_fields;
$elastic_query->{aggs} = {};
}
sub add_elastic_holdings_filter {
- my ($elastic_query, $shortname, $depth, $available) = @_;
+ my ($elastic_query, $staff, $shortname, $depth, $available) = @_;
- if (!$org_data_cache{by_shortname}{$shortname}) {
- $org_data_cache{by_shortname}{$shortname} =
- $U->find_org_by_shortname($U->get_org_tree, $shortname);
- }
+ # in non-staff mode, ensure at least on copy in scope is visible
+ my $visible = !$staff;
- my $org = $org_data_cache{by_shortname}{$shortname};
+ my $org;
+ if ($shortname) {
- my $types = $U->get_org_types; # pulls from cache
- my ($type) = grep {$_->id == $org->ou_type} @$types;
+ if (!$org_data_cache{by_shortname}{$shortname}) {
+ $org_data_cache{by_shortname}{$shortname} =
+ $U->find_org_by_shortname($U->get_org_tree, $shortname);
+ }
- $depth = defined $depth ? min($depth, $type->depth) : $type->depth;
+ $org = $org_data_cache{by_shortname}{$shortname};
- # TODO: if $staff is false, add a holdings filter for
- # opac_visble / location.opac_visible / status.opac_visible
+ my $types = $U->get_org_types; # pulls from cache
+ my ($type) = grep {$_->id == $org->ou_type} @$types;
+ $depth = defined $depth ? min($depth, $type->depth) : $type->depth;
+ }
+
+ my $visible_filters = [
+ query => {
+ bool => {
+ must_not => {
+ terms => {'holdings.status' => $hidden_copy_statuses}
+ }
+ }
+ },
+ query => {
+ bool => {
+ must_not => {
+ terms => {'holdings.location' => $hidden_copy_locations}
+ }
+ }
+ }
+ ];
# array of filters in progress
my $filters = $elastic_query->{query}->{bool}->{filter};
}
};
- # When limiting to available, ensure at least one of the
- # above copies is in status 0 or 7.
- # TODO: consult config.copy_status.is_available
- push(
- @{$and->{bool}{must}},
- {terms => {'holdings.status' => [0, 7]}}
- ) if $available;
+ # When limiting to visible/available, ensure at least one of the
+ # copies from the above org-limited set is visible/available.
+ if ($available) {
+ push(
+ @{$and->{bool}{must}},
+ {terms => {'holdings.status' => $avail_copy_statuses}}
+ );
+
+ } elsif ($visible) {
+ push(@{$and->{bool}{must}}, @$visible_filters);
+ }
push(@$should, $and);
}
nested => {
path => 'holdings',
query => {bool => {must => [
- # TODO: consult config.copy_status.is_available
- {terms => {'holdings.status' => [0, 7]}}
+ {terms => {'holdings.status' => $avail_copy_statuses}}
]}}
}
};
push(@$filters, $filter);
+
+ } elsif ($visible) {
+ my $filter = {
+ nested => {
+ path => 'holdings',
+ query => {bool => {must => $visible_filters}}
+ }
+ };
+
+ push(@$filters, $filter);
}
+
+ $logger->info("ES holdings filter is " .
+ OpenSRF::Utils::JSON->perl2JSON(@$filters));
}
1;