Bug 24157: New permission - merge_invoices
[srvgit] / C4 / Matcher.pm
index aaead2f..6644ec6 100644 (file)
@@ -23,7 +23,8 @@ use MARC::Record;
 
 use Koha::SearchEngine;
 use Koha::SearchEngine::Search;
-use Koha::Util::Normalize qw/legacy_default remove_spaces upper_case lower_case/;
+use Koha::SearchEngine::QueryBuilder;
+use Koha::Util::Normalize qw/legacy_default remove_spaces upper_case lower_case ISBN/;
 
 =head1 NAME
 
@@ -43,7 +44,7 @@ C4::Matcher - find MARC records matching another one
   $matcher->add_matchpoint('isbn', 1000, [ { tag => '020', subfields => 'a', norms => [] } ]);
 
   $matcher->add_simple_required_check('245', 'a', -1, 0, '', '245', 'a', -1, 0, '');
-  $matcher->add_required_check([ { tag => '245', subfields => 'a', norms => [] } ], 
+  $matcher->add_required_check([ { tag => '245', subfields => 'a', norms => [] } ],
                                [ { tag => '245', subfields => 'a', norms => [] } ]);
 
   my @matches = $matcher->get_matches($marc_record, $max_matches);
@@ -164,7 +165,7 @@ sub fetch {
     $sth->execute($id);
     my $row = $sth->fetchrow_hashref;
     $sth->finish();
-    return undef unless defined $row;
+    return unless defined $row;
 
     my $self = {};
     $self->{'id'} = $row->{'matcher_id'};
@@ -330,7 +331,7 @@ sub _store_matchpoint {
     my $matcher_id = $self->{'id'};
     $sth = $dbh->prepare_cached("INSERT INTO matchpoints (matcher_id, search_index, score)
                                  VALUES (?, ?, ?)");
-    $sth->execute($matcher_id, $matchpoint->{'index'}, $matchpoint->{'score'});
+    $sth->execute($matcher_id, $matchpoint->{'index'}, $matchpoint->{'score'}||0);
     my $matchpoint_id = $dbh->{'mysql_insertid'};
     my $seqnum = 0;
     foreach my $component (@{ $matchpoint->{'components'} }) {
@@ -342,7 +343,7 @@ sub _store_matchpoint {
         $sth->bind_param(2, $seqnum);
         $sth->bind_param(3, $component->{'tag'});
         $sth->bind_param(4, join "", sort keys %{ $component->{'subfields'} });
-        $sth->bind_param(5, $component->{'offset'});
+        $sth->bind_param(5, $component->{'offset'}||0);
         $sth->bind_param(6, $component->{'length'});
         $sth->execute();
         my $matchpoint_component_id = $dbh->{'mysql_insertid'};
@@ -619,28 +620,20 @@ sub get_matches {
     my $self = shift;
     my ($source_record, $max_matches) = @_;
 
-    my %matches = ();
+    my $matches = {};
 
-    my $QParser;
-    $QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser'));
     foreach my $matchpoint ( @{ $self->{'matchpoints'} } ) {
         my @source_keys = _get_match_keys( $source_record, $matchpoint );
 
         next if scalar(@source_keys) == 0;
 
-        # FIXME - because of a bug in QueryParser, an expression ofthe
-        # format 'isbn:"isbn1" || isbn:"isbn2" || isbn"isbn3"...'
-        # does not get parsed correctly, so we will not
-        # do AggressiveMatchOnISBN if UseQueryParser is on
         @source_keys = C4::Koha::GetVariationsOfISBNs(@source_keys)
           if ( $matchpoint->{index} =~ /^isbn$/i
-            && C4::Context->preference('AggressiveMatchOnISBN') )
-            && !C4::Context->preference('UseQueryParser');
+            && C4::Context->preference('AggressiveMatchOnISBN') );
 
         @source_keys = C4::Koha::GetVariationsOfISSNs(@source_keys)
           if ( $matchpoint->{index} =~ /^issn$/i
-            && C4::Context->preference('AggressiveMatchOnISSN') )
-            && !C4::Context->preference('UseQueryParser');
+            && C4::Context->preference('AggressiveMatchOnISSN') );
 
         # build query
         my $query;
@@ -649,25 +642,35 @@ sub get_matches {
         my $total_hits;
         if ( $self->{'record_type'} eq 'biblio' ) {
 
-            #NOTE: The QueryParser can't handle the CCL syntax of 'qualifier','qualifier', so fallback to non-QueryParser.
-            #NOTE: You can see this in C4::Search::SimpleSearch() as well in a different way.
-            if ($QParser && $matchpoint->{'index'} !~ m/\w,\w/) {
-                $query = join( " || ",
-                    map { "$matchpoint->{'index'}:$_" } @source_keys );
+            my $phr = ( C4::Context->preference('AggressiveMatchOnISBN') || C4::Context->preference('AggressiveMatchOnISSN') )  ? ',phr' : q{};
+            $query = join( " OR ",
+                map { "$matchpoint->{'index'}$phr=\"$_\"" } @source_keys );
+                #NOTE: double-quote the values so you don't get a "Embedded truncation not supported" error when a term has a ? in it.
+
+            # Use state variables to avoid recreating the objects every time.
+            # With Elasticsearch this also avoids creating a massive amount of
+            # ES connectors that would eventually run out of file descriptors.
+            state $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
+            ( $error, $searchresults, $total_hits ) =
+              $searcher->simple_search_compat( $query, 0, $max_matches, undef, skip_normalize => 1 );
+
+            if ( defined $error ) {
+                warn "search failed ($query) $error";
             }
             else {
-                my $phr = ( C4::Context->preference('AggressiveMatchOnISBN') || C4::Context->preference('AggressiveMatchOnISSN') )  ? ',phr' : q{};
-                $query = join( " or ",
-                    map { "$matchpoint->{'index'}$phr=\"$_\"" } @source_keys );
-                    #NOTE: double-quote the values so you don't get a "Embedded truncation not supported" error when a term has a ? in it.
+                foreach my $matched ( @{$searchresults} ) {
+                    my $target_record = C4::Search::new_record_from_zebra( 'biblioserver', $matched );
+                    my ( $biblionumber_tag, $biblionumber_subfield ) = C4::Biblio::GetMarcFromKohaField( "biblio.biblionumber" );
+                    my $id = ( $biblionumber_tag > 10 ) ?
+                        $target_record->field($biblionumber_tag)->subfield($biblionumber_subfield) :
+                        $target_record->field($biblionumber_tag)->data();
+                    $matches->{$id}->{score} += $matchpoint->{score};
+                    $matches->{$id}->{record} = $target_record;
+                }
             }
 
-            my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
-            ( $error, $searchresults, $total_hits ) =
-              $searcher->simple_search_compat( $query, 0, $max_matches, undef, skip_normalize => 1 );
         }
         elsif ( $self->{'record_type'} eq 'authority' ) {
-            my $authresults;
             my @marclist;
             my @and_or;
             my @excluding = [];
@@ -679,49 +682,53 @@ sub get_matches {
                 push @operator, 'exact';
                 push @value,    $key;
             }
-            require C4::AuthoritiesMarc;
-            ( $authresults, $total_hits ) =
-              C4::AuthoritiesMarc::SearchAuthorities(
-                \@marclist,  \@and_or, \@excluding, \@operator,
-                \@value,     0,        20,          undef,
-                'AuthidAsc', 1
-              );
-            foreach my $result (@$authresults) {
-                push @$searchresults, $result->{'authid'};
-            }
-        }
+            # Use state variables to avoid recreating the objects every time.
+            # With Elasticsearch this also avoids creating a massive amount of
+            # ES connectors that would eventually run out of file descriptors.
+            state $builder  = Koha::SearchEngine::QueryBuilder->new({index => $Koha::SearchEngine::AUTHORITIES_INDEX});
+            state $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::AUTHORITIES_INDEX});
+            my $search_query = $builder->build_authorities_query_compat(
+                \@marclist, \@and_or, \@excluding, \@operator,
+                \@value, undef, 'AuthidAsc'
+            );
+            my ( $authresults, $total ) = $searcher->search_auth_compat( $search_query, 0, 20 );
 
-        if ( defined $error ) {
-            warn "search failed ($query) $error";
-        }
-        else {
-            foreach my $matched ( @{$searchresults} ) {
-                $matches{$matched} += $matchpoint->{'score'};
+            foreach my $result (@$authresults) {
+                my $id = $result->{authid};
+                $matches->{$id}->{score} += $matchpoint->{'score'};
+                $matches->{$id}->{record} = $id;
             }
         }
     }
 
     # get rid of any that don't meet the threshold
-    %matches = map { ($matches{$_} >= $self->{'threshold'}) ? ($_ => $matches{$_}) : () } keys %matches;
-
-    # get rid of any that don't meet the required checks
-    %matches = map { _passes_required_checks($source_record, $_, $self->{'required_checks'}) ?  ($_ => $matches{$_}) : () } 
-                keys %matches unless ($self->{'record_type'} eq 'auth');
+    $matches = { map { ($matches->{$_}->{score} >= $self->{'threshold'}) ? ($_ => $matches->{$_}) : () } keys %$matches };
 
     my @results = ();
     if ($self->{'record_type'} eq 'biblio') {
         require C4::Biblio;
-        foreach my $marcblob (keys %matches) {
-            my $target_record = C4::Search::new_record_from_zebra('biblioserver',$marcblob);
-            my $record_number;
-            my $result = C4::Biblio::TransformMarcToKoha($target_record, '');
-            $record_number = $result->{'biblionumber'};
-            push @results, { 'record_id' => $record_number, 'score' => $matches{$marcblob} };
+        # get rid of any that don't meet the required checks
+        $matches = {
+            map {
+                _passes_required_checks( $source_record, $matches->{$_}->{'record'}, $self->{'required_checks'} )
+                  ? ( $_ => $matches->{$_} )
+                  : ()
+            } keys %$matches
+        };
+
+        foreach my $id ( keys %$matches ) {
+            push @results, {
+                record_id => $id,
+                score     => $matches->{$id}->{score}
+            };
         }
     } elsif ($self->{'record_type'} eq 'authority') {
         require C4::AuthoritiesMarc;
-        foreach my $authid (keys %matches) {
-            push @results, { 'record_id' => $authid, 'score' => $matches{$authid} };
+        foreach my $id (keys %$matches) {
+            push @results, {
+                record_id => $id,
+                score     => $matches->{$id}->{score}
+            };
         }
     }
     @results = sort {
@@ -732,7 +739,6 @@ sub get_matches {
         @results = @results[0..$max_matches-1];
     }
     return @results;
-
 }
 
 =head2 dump
@@ -768,8 +774,7 @@ sub dump {
 }
 
 sub _passes_required_checks {
-    my ($source_record, $target_blob, $matchchecks) = @_;
-    my $target_record = MARC::Record->new_from_usmarc($target_blob); # FIXME -- need to avoid parsing record twice
+    my ($source_record, $target_record, $matchchecks) = @_;
 
     # no checks supplied == automatic pass
     return 1 if $#{ $matchchecks } == -1;
@@ -844,6 +849,9 @@ sub _get_match_keys {
                     elsif ( $norm eq 'legacy_default' ) {
                         $key = legacy_default($key);
                     }
+                    elsif ( $norm eq 'ISBN' ) {
+                        $key = ISBN($key);
+                    }
                 } else {
                     warn "Invalid normalization routine required ($norm)"
                         unless $norm eq 'none';
@@ -880,7 +888,8 @@ sub valid_normalization_routines {
         'remove_spaces',
         'upper_case',
         'lower_case',
-        'legacy_default'
+        'legacy_default',
+        'ISBN'
     );
 }