copy loc/stat filters wip
authorBill Erickson <berickxx@gmail.com>
Mon, 5 Nov 2018 22:21:09 +0000 (17:21 -0500)
committerBill Erickson <berickxx@gmail.com>
Wed, 28 Aug 2019 21:41:55 +0000 (17:41 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Elastic.pm

index 446135b..ce75bed 100644 (file)
@@ -1317,168 +1317,6 @@ sub staged_search {
     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;
index 902d7cf..eae4683 100644 (file)
@@ -30,23 +30,57 @@ my $U = "OpenILS::Application::AppUtils";
 # 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);
@@ -100,8 +134,8 @@ sub compile_elastic_query {
     $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);
 
@@ -204,7 +238,7 @@ sub translate_query_node {
         # 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
@@ -359,7 +393,7 @@ sub format_facets {
 
         my ($bib_field) = grep {
             $_->name eq $name && $_->search_group eq $field_class
-        } @$bib_search_fields;
+        } @$bib_fields;
 
         my $hash = $facets->{$bib_field->metabib_field} = {};
 
@@ -375,7 +409,7 @@ sub format_facets {
 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} = {};
@@ -390,22 +424,42 @@ sub add_elastic_facet_aggregations {
 }
 
 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};
@@ -449,13 +503,17 @@ sub add_elastic_holdings_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);
         }
@@ -468,14 +526,26 @@ sub add_elastic_holdings_filter {
             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;