3 # Copyright (C) 2007 LibLime, 2012 C & P Bibliography Services
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 2 of the License, or (at your option) any later
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License along
17 # with Koha; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
26 use vars
qw($VERSION);
29 # set the version for version checking
30 $VERSION = 3.07.00.049;
35 C4::Matcher - find MARC records matching another one
39 my @matchers = C4::Matcher::GetMatcherList();
41 my $matcher = C4::Matcher->new($record_type);
42 $matcher->threshold($threshold);
43 $matcher->code($code);
44 $matcher->description($description);
46 $matcher->add_simple_matchpoint('isbn', 1000, '020', 'a', -1, 0, '');
47 $matcher->add_simple_matchpoint('Date', 1000, '008', '', 7, 4, '');
48 $matcher->add_matchpoint('isbn', 1000, [ { tag => '020', subfields => 'a', norms => [] } ]);
50 $matcher->add_simple_required_check('245', 'a', -1, 0, '', '245', 'a', -1, 0, '');
51 $matcher->add_required_check([ { tag => '245', subfields => 'a', norms => [] } ],
52 [ { tag => '245', subfields => 'a', norms => [] } ]);
54 my @matches = $matcher->get_matches($marc_record, $max_matches);
56 foreach $match (@matches) {
58 # matches already sorted in order of
60 print "record ID: $match->{'record_id'};
61 print "score: $match->{'score'};
65 my $matcher_description = $matcher->dump();
73 my @matchers = C4::Matcher::GetMatcherList();
75 Returns an array of hashrefs list all matchers
76 present in the database. Each hashref includes:
85 my $dbh = C4::Context->dbh;
87 my $sth = $dbh->prepare_cached("SELECT matcher_id, code, description FROM marc_matchers ORDER BY matcher_id");
90 while (my $row = $sth->fetchrow_hashref) {
98 my $matcher_id = C4::Matcher::GetMatcherId($code);
100 Returns the matcher_id of a code.
106 my $dbh = C4::Context->dbh;
108 my $matcher_id = $dbh->selectrow_array("SELECT matcher_id FROM marc_matchers WHERE code = ?", undef, $code);
116 my $matcher = C4::Matcher->new($record_type, $threshold);
118 Creates a new Matcher. C<$record_type> indicates which search
119 database to use, e.g., 'biblio' or 'authority' and defaults to
120 'biblio', while C<$threshold> is the minimum score required for a match
121 and defaults to 1000.
129 $self->{'id'} = undef;
132 $self->{'record_type'} = shift;
134 $self->{'record_type'} = 'biblio';
138 $self->{'threshold'} = shift;
140 $self->{'threshold'} = 1000;
143 $self->{'code'} = '';
144 $self->{'description'} = '';
146 $self->{'matchpoints'} = [];
147 $self->{'required_checks'} = [];
155 my $matcher = C4::Matcher->fetch($id);
157 Creates a matcher object from the version stored
158 in the database. If a matcher with the given
159 id does not exist, returns undef.
166 my $dbh = C4::Context->dbh();
168 my $sth = $dbh->prepare_cached("SELECT * FROM marc_matchers WHERE matcher_id = ?");
170 my $row = $sth->fetchrow_hashref;
172 return undef unless defined $row;
175 $self->{'id'} = $row->{'matcher_id'};
176 $self->{'record_type'} = $row->{'record_type'};
177 $self->{'code'} = $row->{'code'};
178 $self->{'description'} = $row->{'description'};
179 $self->{'threshold'} = int($row->{'threshold'});
183 $self->{'matchpoints'} = [];
184 $sth = $dbh->prepare_cached("SELECT * FROM matcher_matchpoints WHERE matcher_id = ? ORDER BY matchpoint_id");
185 $sth->execute($self->{'id'});
186 while (my $row = $sth->fetchrow_hashref) {
187 my $matchpoint = $self->_fetch_matchpoint($row->{'matchpoint_id'});
188 push @{ $self->{'matchpoints'} }, $matchpoint;
192 $self->{'required_checks'} = [];
193 $sth = $dbh->prepare_cached("SELECT * FROM matchchecks WHERE matcher_id = ? ORDER BY matchcheck_id");
194 $sth->execute($self->{'id'});
195 while (my $row = $sth->fetchrow_hashref) {
196 my $source_matchpoint = $self->_fetch_matchpoint($row->{'source_matchpoint_id'});
197 my $target_matchpoint = $self->_fetch_matchpoint($row->{'target_matchpoint_id'});
199 $matchcheck->{'source_matchpoint'} = $source_matchpoint;
200 $matchcheck->{'target_matchpoint'} = $target_matchpoint;
201 push @{ $self->{'required_checks'} }, $matchcheck;
207 sub _fetch_matchpoint {
209 my $matchpoint_id = shift;
211 my $dbh = C4::Context->dbh;
212 my $sth = $dbh->prepare_cached("SELECT * FROM matchpoints WHERE matchpoint_id = ?");
213 $sth->execute($matchpoint_id);
214 my $row = $sth->fetchrow_hashref;
216 $matchpoint->{'index'} = $row->{'search_index'};
217 $matchpoint->{'score'} = int($row->{'score'});
220 $matchpoint->{'components'} = [];
221 $sth = $dbh->prepare_cached("SELECT * FROM matchpoint_components WHERE matchpoint_id = ? ORDER BY sequence");
222 $sth->execute($matchpoint_id);
223 while ($row = $sth->fetchrow_hashref) {
225 $component->{'tag'} = $row->{'tag'};
226 $component->{'subfields'} = { map { $_ => 1 } split(//, $row->{'subfields'}) };
227 $component->{'offset'} = int($row->{'offset'});
228 $component->{'length'} = int($row->{'length'});
229 $component->{'norms'} = [];
230 my $sth2 = $dbh->prepare_cached("SELECT *
231 FROM matchpoint_component_norms
232 WHERE matchpoint_component_id = ? ORDER BY sequence");
233 $sth2->execute($row->{'matchpoint_component_id'});
234 while (my $row2 = $sth2->fetchrow_hashref) {
235 push @{ $component->{'norms'} }, $row2->{'norm_routine'};
237 push @{ $matchpoint->{'components'} }, $component;
244 my $id = $matcher->store();
246 Stores matcher in database. The return value is the ID
247 of the marc_matchers row. If the matcher was
248 previously retrieved from the database via the fetch()
249 method, the DB representation of the matcher
257 if (defined $self->{'id'}) {
259 $self->_del_matcher_components();
260 $self->_update_marc_matchers();
263 $self->_new_marc_matchers();
265 $self->_store_matcher_components();
266 return $self->{'id'};
269 sub _del_matcher_components {
272 my $dbh = C4::Context->dbh();
273 my $sth = $dbh->prepare_cached("DELETE FROM matchpoints WHERE matcher_id = ?");
274 $sth->execute($self->{'id'});
275 $sth = $dbh->prepare_cached("DELETE FROM matchchecks WHERE matcher_id = ?");
276 $sth->execute($self->{'id'});
277 # foreign key delete cascades take care of deleting relevant rows
278 # from matcher_matchpoints, matchpoint_components, and
279 # matchpoint_component_norms
282 sub _update_marc_matchers {
285 my $dbh = C4::Context->dbh();
286 my $sth = $dbh->prepare_cached("UPDATE marc_matchers
291 WHERE matcher_id = ?");
292 $sth->execute($self->{'code'}, $self->{'description'}, $self->{'record_type'}, $self->{'threshold'}, $self->{'id'});
295 sub _new_marc_matchers {
298 my $dbh = C4::Context->dbh();
299 my $sth = $dbh->prepare_cached("INSERT INTO marc_matchers
300 (code, description, record_type, threshold)
301 VALUES (?, ?, ?, ?)");
302 $sth->execute($self->{'code'}, $self->{'description'}, $self->{'record_type'}, $self->{'threshold'});
303 $self->{'id'} = $dbh->{'mysql_insertid'};
306 sub _store_matcher_components {
309 my $dbh = C4::Context->dbh();
311 my $matcher_id = $self->{'id'};
312 foreach my $matchpoint (@{ $self->{'matchpoints'}}) {
313 my $matchpoint_id = $self->_store_matchpoint($matchpoint);
314 $sth = $dbh->prepare_cached("INSERT INTO matcher_matchpoints (matcher_id, matchpoint_id)
316 $sth->execute($matcher_id, $matchpoint_id);
318 foreach my $matchcheck (@{ $self->{'required_checks'} }) {
319 my $source_matchpoint_id = $self->_store_matchpoint($matchcheck->{'source_matchpoint'});
320 my $target_matchpoint_id = $self->_store_matchpoint($matchcheck->{'target_matchpoint'});
321 $sth = $dbh->prepare_cached("INSERT INTO matchchecks
322 (matcher_id, source_matchpoint_id, target_matchpoint_id)
324 $sth->execute($matcher_id, $source_matchpoint_id, $target_matchpoint_id);
329 sub _store_matchpoint {
331 my $matchpoint = shift;
333 my $dbh = C4::Context->dbh();
335 my $matcher_id = $self->{'id'};
336 $sth = $dbh->prepare_cached("INSERT INTO matchpoints (matcher_id, search_index, score)
338 $sth->execute($matcher_id, $matchpoint->{'index'}, $matchpoint->{'score'});
339 my $matchpoint_id = $dbh->{'mysql_insertid'};
341 foreach my $component (@{ $matchpoint->{'components'} }) {
343 $sth = $dbh->prepare_cached("INSERT INTO matchpoint_components
344 (matchpoint_id, sequence, tag, subfields, offset, length)
345 VALUES (?, ?, ?, ?, ?, ?)");
346 $sth->bind_param(1, $matchpoint_id);
347 $sth->bind_param(2, $seqnum);
348 $sth->bind_param(3, $component->{'tag'});
349 $sth->bind_param(4, join "", sort keys %{ $component->{'subfields'} });
350 $sth->bind_param(5, $component->{'offset'});
351 $sth->bind_param(6, $component->{'length'});
353 my $matchpoint_component_id = $dbh->{'mysql_insertid'};
355 foreach my $norm (@{ $component->{'norms'} }) {
357 $sth = $dbh->prepare_cached("INSERT INTO matchpoint_component_norms
358 (matchpoint_component_id, sequence, norm_routine)
360 $sth->execute($matchpoint_component_id, $normseq, $norm);
363 return $matchpoint_id;
369 C4::Matcher->delete($id);
371 Deletes the matcher of the specified ID
378 my $matcher_id = shift;
380 my $dbh = C4::Context->dbh;
381 my $sth = $dbh->prepare("DELETE FROM marc_matchers WHERE matcher_id = ?");
382 $sth->execute($matcher_id); # relying on cascading deletes to clean up everything
387 $matcher->record_type('biblio');
388 my $record_type = $matcher->record_type();
396 @_ ? $self->{'record_type'} = shift : $self->{'record_type'};
401 $matcher->threshold(1000);
402 my $threshold = $matcher->threshold();
410 @_ ? $self->{'threshold'} = shift : $self->{'threshold'};
416 my $id = $matcher->_id();
418 Accessor method. Note that using this method
419 to set the DB ID of the matcher should not be
420 done outside of the editing CGI.
426 @_ ? $self->{'id'} = shift : $self->{'id'};
431 $matcher->code('ISBN');
432 my $code = $matcher->code();
440 @_ ? $self->{'code'} = shift : $self->{'code'};
445 $matcher->description('match on ISBN');
446 my $description = $matcher->description();
454 @_ ? $self->{'description'} = shift : $self->{'description'};
457 =head2 add_matchpoint
459 $matcher->add_matchpoint($index, $score, $matchcomponents);
461 Adds a matchpoint that may include multiple components. The $index
462 parameter identifies the index that will be searched, while $score
463 is the weight that will be added if a match is found.
465 $matchcomponents should be a reference to an array of matchpoint
466 compoents, each of which should be a hash containing the following
474 The normalization_rules value should in turn be a reference to an
475 array, each element of which should be a reference to a
476 normalization subroutine (under C4::Normalize) to be applied
477 to the source string.
483 my ($index, $score, $matchcomponents) = @_;
486 $matchpoint->{'index'} = $index;
487 $matchpoint->{'score'} = $score;
488 $matchpoint->{'components'} = [];
489 foreach my $input_component (@{ $matchcomponents }) {
490 push @{ $matchpoint->{'components'} }, _parse_match_component($input_component);
492 push @{ $self->{'matchpoints'} }, $matchpoint;
495 =head2 add_simple_matchpoint
497 $matcher->add_simple_matchpoint($index, $score, $source_tag,
498 $source_subfields, $source_offset,
499 $source_length, $source_normalizer);
502 Adds a simple matchpoint rule -- after composing a key based on the source tag and subfields,
503 normalized per the normalization fuction, search the index. All records retrieved
504 will receive the assigned score.
508 sub add_simple_matchpoint {
510 my ($index, $score, $source_tag, $source_subfields, $source_offset, $source_length, $source_normalizer) = @_;
512 $self->add_matchpoint($index, $score, [
513 { tag => $source_tag, subfields => $source_subfields,
514 offset => $source_offset, 'length' => $source_length,
515 norms => [ $source_normalizer ]
520 =head2 add_required_check
522 $match->add_required_check($source_matchpoint, $target_matchpoint);
524 Adds a required check definition. A required check means that in
525 order for a match to be considered valid, the key derived from the
526 source (incoming) record must match the key derived from the target
527 (already in DB) record.
529 Unlike a regular matchpoint, only the first repeat of each tag
530 in the source and target match criteria are considered.
532 A typical example of a required check would be verifying that the
533 titles and publication dates match.
535 $source_matchpoint and $target_matchpoint are each a reference to
536 an array of hashes, where each hash follows the same definition
537 as the matchpoint component specification in add_matchpoint, i.e.,
545 The normalization_rules value should in turn be a reference to an
546 array, each element of which should be a reference to a
547 normalization subroutine (under C4::Normalize) to be applied
548 to the source string.
552 sub add_required_check {
554 my ($source_matchpoint, $target_matchpoint) = @_;
557 $matchcheck->{'source_matchpoint'}->{'index'} = '';
558 $matchcheck->{'source_matchpoint'}->{'score'} = 0;
559 $matchcheck->{'source_matchpoint'}->{'components'} = [];
560 $matchcheck->{'target_matchpoint'}->{'index'} = '';
561 $matchcheck->{'target_matchpoint'}->{'score'} = 0;
562 $matchcheck->{'target_matchpoint'}->{'components'} = [];
563 foreach my $input_component (@{ $source_matchpoint }) {
564 push @{ $matchcheck->{'source_matchpoint'}->{'components'} }, _parse_match_component($input_component);
566 foreach my $input_component (@{ $target_matchpoint }) {
567 push @{ $matchcheck->{'target_matchpoint'}->{'components'} }, _parse_match_component($input_component);
569 push @{ $self->{'required_checks'} }, $matchcheck;
572 =head2 add_simple_required_check
574 $matcher->add_simple_required_check($source_tag, $source_subfields,
575 $source_offset, $source_length, $source_normalizer,
576 $target_tag, $target_subfields, $target_offset,
577 $target_length, $target_normalizer);
579 Adds a required check, which requires that the normalized keys made from the source and targets
580 must match for a match to be considered valid.
584 sub add_simple_required_check {
586 my ($source_tag, $source_subfields, $source_offset, $source_length, $source_normalizer,
587 $target_tag, $target_subfields, $target_offset, $target_length, $target_normalizer) = @_;
589 $self->add_required_check(
590 [ { tag => $source_tag, subfields => $source_subfields, offset => $source_offset, 'length' => $source_length,
591 norms => [ $source_normalizer ] } ],
592 [ { tag => $target_tag, subfields => $target_subfields, offset => $target_offset, 'length' => $target_length,
593 norms => [ $target_normalizer ] } ]
599 my @matches = $matcher->get_matches($marc_record, $max_matches);
600 foreach $match (@matches) {
601 # matches already sorted in order of
603 print "record ID: $match->{'record_id'};
604 print "score: $match->{'score'};
607 Identifies all of the records matching the given MARC record. For a record already
608 in the database to be considered a match, it must meet the following criteria:
612 =item 1. Total score from its matching field must exceed the supplied threshold.
614 =item 2. It must pass all required checks.
618 Only the top $max_matches matches are returned. The returned array is sorted
619 in order of decreasing score, i.e., the best match is first.
625 my ($source_record, $max_matches) = @_;
630 $QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser'));
631 foreach my $matchpoint ( @{ $self->{'matchpoints'} } ) {
632 my @source_keys = _get_match_keys( $source_record, $matchpoint );
634 next if scalar(@source_keys) == 0;
636 # FIXME - because of a bug in QueryParser, an expression ofthe
637 # format 'isbn:"isbn1" || isbn:"isbn2" || isbn"isbn3"...'
638 # does not get parsed correctly, so we will not
639 # do AggressiveMatchOnISBN if UseQueryParser is on
640 @source_keys = C4::Koha::GetVariationsOfISBNs(@source_keys)
641 if ( $matchpoint->{index} =~ /^isbn$/i
642 && C4::Context->preference('AggressiveMatchOnISBN') )
643 && !C4::Context->preference('UseQueryParser');
650 if ( $self->{'record_type'} eq 'biblio' ) {
653 $query = join( " || ",
654 map { "$matchpoint->{'index'}:$_" } @source_keys );
657 my $phr = C4::Context->preference('AggressiveMatchOnISBN') ? ',phr' : q{};
658 $query = join( " or ",
659 map { "$matchpoint->{'index'}$phr=$_" } @source_keys );
664 ( $error, $searchresults, $total_hits ) =
665 C4
::Search
::SimpleSearch
( $query, 0, $max_matches );
667 elsif ( $self->{'record_type'} eq 'authority' ) {
674 foreach my $key (@source_keys) {
675 push @marclist, $matchpoint->{'index'};
677 push @operator, 'exact';
680 require C4
::AuthoritiesMarc
;
681 ( $authresults, $total_hits ) =
682 C4
::AuthoritiesMarc
::SearchAuthorities
(
683 \
@marclist, \
@and_or, \
@excluding, \
@operator,
684 \
@value, 0, 20, undef,
687 foreach my $result (@
$authresults) {
688 push @
$searchresults, $result->{'authid'};
692 if ( defined $error ) {
693 warn "search failed ($query) $error";
696 foreach my $matched ( @
{$searchresults} ) {
697 $matches{$matched} += $matchpoint->{'score'};
702 # get rid of any that don't meet the threshold
703 %matches = map { ($matches{$_} >= $self->{'threshold'}) ?
($_ => $matches{$_}) : () } keys %matches;
705 # get rid of any that don't meet the required checks
706 %matches = map { _passes_required_checks
($source_record, $_, $self->{'required_checks'}) ?
($_ => $matches{$_}) : () }
707 keys %matches unless ($self->{'record_type'} eq 'auth');
710 if ($self->{'record_type'} eq 'biblio') {
712 foreach my $marcblob (keys %matches) {
713 my $target_record = C4
::Search
::new_record_from_zebra
('biblioserver',$marcblob);
715 my $result = C4
::Biblio
::TransformMarcToKoha
(C4
::Context
->dbh, $target_record, '');
716 $record_number = $result->{'biblionumber'};
717 push @results, { 'record_id' => $record_number, 'score' => $matches{$marcblob} };
719 } elsif ($self->{'record_type'} eq 'authority') {
720 require C4
::AuthoritiesMarc
;
721 foreach my $authid (keys %matches) {
722 push @results, { 'record_id' => $authid, 'score' => $matches{$authid} };
725 @results = sort { $b->{'score'} cmp $a->{'score'} } @results;
726 if (scalar(@results) > $max_matches) {
727 @results = @results[0..$max_matches-1];
735 $description = $matcher->dump();
737 Returns a reference to a structure containing all of the information
738 in the matcher object. This is mainly a convenience method to
739 aid setting up a HTML editing form.
748 $result->{'matcher_id'} = $self->{'id'};
749 $result->{'code'} = $self->{'code'};
750 $result->{'description'} = $self->{'description'};
751 $result->{'record_type'} = $self->{'record_type'};
753 $result->{'matchpoints'} = [];
754 foreach my $matchpoint (@
{ $self->{'matchpoints'} }) {
755 push @
{ $result->{'matchpoints'} }, $matchpoint;
757 $result->{'matchchecks'} = [];
758 foreach my $matchcheck (@
{ $self->{'required_checks'} }) {
759 push @
{ $result->{'matchchecks'} }, $matchcheck;
765 sub _passes_required_checks
{
766 my ($source_record, $target_blob, $matchchecks) = @_;
767 my $target_record = MARC
::Record
->new_from_usmarc($target_blob); # FIXME -- need to avoid parsing record twice
769 # no checks supplied == automatic pass
770 return 1 if $#{ $matchchecks } == -1;
772 foreach my $matchcheck (@
{ $matchchecks }) {
773 my $source_key = join "", _get_match_keys
($source_record, $matchcheck->{'source_matchpoint'});
774 my $target_key = join "", _get_match_keys
($target_record, $matchcheck->{'target_matchpoint'});
775 return 0 unless $source_key eq $target_key;
780 sub _get_match_keys
{
781 my $source_record = shift;
782 my $matchpoint = shift;
783 my $check_only_first_repeat = @_ ?
shift : 0;
785 # If there is more than one component to the matchpoint (e.g.,
786 # matchpoint includes both 003 and 001), any repeats
787 # of the first component's tag are identified; repeats
788 # of the subsequent components' tags are appended to
789 # each parallel key dervied from the first component,
790 # up to the number of repeats of the first component's tag.
792 # For example, if the record has one 003 and two 001s, only
793 # one key is retrieved because there is only one 003. The key
794 # will consist of the contents of the first 003 and first 001.
796 # If there are two 003s and two 001s, there will be two keys:
797 # first 003 + first 001
798 # second 003 + second 001
801 for (my $i = 0; $i <= $#{ $matchpoint->{'components'} }; $i++) {
802 my $component = $matchpoint->{'components'}->[$i];
804 FIELD
: foreach my $field ($source_record->field($component->{'tag'})) {
806 last FIELD
if $j > 0 and $check_only_first_repeat;
807 last FIELD
if $i > 0 and $j > $#keys;
810 if ($field->is_control_field()) {
811 $string=$field->data();
813 foreach my $subfield ($field->subfields()) {
814 if (exists $component->{'subfields'}->{$subfield->[0]}) {
815 $string .= " " . $subfield->[1];
819 if ($component->{'length'}>0) {
820 $string= substr($string, $component->{'offset'}, $component->{'length'});
821 # FIXME normalize, substr
822 } elsif ($component->{'offset'}) {
823 $string= substr($string, $component->{'offset'});
825 $key = _normalize
($string);
827 push @keys, $key if $key;
829 $keys[$j] .= " $key" if $key;
837 sub _parse_match_component
{
838 my $input_component = shift;
841 $component->{'tag'} = $input_component->{'tag'};
842 $component->{'subfields'} = { map { $_ => 1 } split(//, $input_component->{'subfields'}) };
843 $component->{'offset'} = exists($input_component->{'offset'}) ?
$input_component->{'offset'} : -1;
844 $component->{'length'} = $input_component->{'length'} ?
$input_component->{'length'} : 0;
845 $component->{'norms'} = $input_component->{'norms'} ?
$input_component->{'norms'} : [];
850 # FIXME - default normalizer
852 my $value = uc shift;
853 $value =~ s/[.;:,\]\[\)\(\/'"]//g
;
855 #$value =~ s/^\s+$//;
858 #$value =~ s/[.;,\]\[\)\(\/"']//g;
867 Koha Development Team <http://koha-community.org/>
869 Galen Charlton <galen.charlton@liblime.com>