From d40353718d7fc67b666fc5eac35436f4720ecdff Mon Sep 17 00:00:00 2001 From: Julian Maurice Date: Fri, 27 Jul 2012 12:30:41 +0200 Subject: [PATCH] Bug 13706: New script dedup_authorities.pl This script allow to deduplicate authorities automatically. Script is in misc/maintenance/ It works this way: 1) authorities are fetched from the database. You can limit fetched results by authtypecode, or directly by specifying WHERE clause 2) for each authority: 2.1) build a Zebra query using the 'search_form' for the heading 2.2) run the query, retrieve the results 2.3) among duplicates, choose the one we want to keep (use --choose-method option). 2.5) use C4::Authorities::merge to merge authorities 3) delete the merged authorities Use --help for more informations on options To be done: 1 - Move to module and cover with tests 2 - Add option to only merge unused authorities 3 - Expand 'ppn' option to be 'control-number' option and allow specifying field 4 - More? 1 & 2 I will attempt - 3 & 4 may be future enhancements --- misc/maintenance/dedup_authorities.pl | 325 ++++++++++++++++++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100755 misc/maintenance/dedup_authorities.pl diff --git a/misc/maintenance/dedup_authorities.pl b/misc/maintenance/dedup_authorities.pl new file mode 100755 index 0000000000..d8c9b8efd6 --- /dev/null +++ b/misc/maintenance/dedup_authorities.pl @@ -0,0 +1,325 @@ +#!/usr/bin/perl +use Modern::Perl; +use C4::Context; +use C4::AuthoritiesMarc; +use C4::Biblio; +use C4::Search; +use C4::Charset; +use C4::Debug; +use C4::Heading; +use Koha::SearchEngine; +use Koha::SearchEngine::QueryBuilder; + +use Koha::Authorities; + +use Getopt::Long; +use YAML; +use List::MoreUtils qw/uniq/; + +=head1 NAME + +misc/migration_tools/dedup_authorities.pl - Deduping authorities script + +=head1 SYNOPSIS + +dedup_authorities.pl [ -h ] [ -where="authid < 5000" ] -c [ -v ] [ -m d ] [ -a PERSO_NAME ] + + Options: + -h --help display usage statement + -v --verbose increase verbosity, can be repeated for greater verbosity + -m --method method for choosing the reference authority, can be: date, used, or ppn (UNIMARC) + can be repeated + -w --where a SQL WHERE statement to limit the authority records checked + -c --confirm without this parameter no changes will be made, script will run in test mode + -a --authtypecode check only specified auth type, repeatable + +=head1 OPTIONS + +=over + +=item B<--method> + +Method(s) used to choose which authority to keep in case we found +duplicates. + is a string composed of letters describing what methods to use +and in which order. +Letters can be: + date: keep the most recent authority (based on 005 field) + used: keep the most used authority + ppn: PPN (UNIMARC only), keep the authority with a ppn (when some + authorities don't have one, based on 009 field) + +Example: +-m ppn -m date -m used +Among the authorities that have a PPN, keep the most recent, +and if two (or more) have the same date in 005, keep the +most used. + +Default is 'used' + +=item B<--where> + +limit the deduplication to SOME authorities only + +Example: +-where="authid < 5000" +will only auths with a low auth_id (old records) + +=item B<--verbose> + +display verbose loggin, can be repeated twice for more info + + +=item B<--help> + +show usage information. + +=back + +=cut + +my @methods; +my @authtypecodes; +my $help = 0; +my $confirm = 0; +my $verbose = 0; +my $wherestring = ""; + + +my $result = GetOptions ( + "v|verbose+" => \$verbose, + "c|confirm" => \$confirm, + "h|help" => \$help, + "w|where=s" => \$wherestring, + "m|method=s" => \@methods, + "a|authtypecode=s" => \@authtypecodes +); + +pod2usage( -verbose => 2) if ($help); + +print "RUNNING IN TEST MODE, NO CHANGES WILL BE MADE" unless $confirm; +$verbose = 1 unless ( $confirm || $verbose ); + + + +my @choose_subs; +@methods = ('used') unless @methods; +foreach my $method ( @methods ) { + if ($method eq 'date') { + push @choose_subs, \&_get_date; + } + elsif( $method eq 'ppn') { + die 'PPN method is only valid for UNIMARC' + unless( C4::Context->preference('marcflavour') eq 'UNIMARC' ); + push @choose_subs, \&_has_ppn; + } + elsif( $method eq 'used') { + push @choose_subs, \&_get_usage; + } + else { + warn "Choose method '$method' is not supported"; + } +} + +my $dbh = C4::Context->dbh; + +$verbose and logger("Fetching authtypecodes..."); +my $params = undef; +if( @authtypecodes ){ +$params = { authtypecode => { -in => \@authtypecodes } }; +} +my @auth_types = Koha::Authority::Types->search($params); +my %auth_match_headings = map { $_->authtypecode => $_->auth_tag_to_report } @auth_types; +$verbose and logger("Fetching authtypecodes done."); + +my %biblios; + +for my $authtype (@auth_types) { + my $authtypecode = $authtype->authtypecode; + my %duplicated; + my $deleted = 0; + my $updated_bibs = 0; + my $i = 0; + $verbose and logger("Deduping authtype '$authtypecode'"); + + $verbose and logger("Fetching authorities for '$authtypecode'... "); + my $authorities = Koha::Authorities->search({ authtypecode => $authtypecode }); + $authorities = $authorities->search( \$wherestring ) if $wherestring; + my $size = $authorities->count; + $verbose and logger("$size authorities found"); + + while ( my $authority = $authorities->next ) { + $i++; + if ($verbose >= 2) { + my $percentage = sprintf("%.2f", $i * 100 / $size); + logger("Processing authority $authority->authid ($i/$size $percentage%)"); + } elsif ($verbose and ($i % 100) == 0) { + my $percentage = sprintf("%.2f", $i * 100 / $size); + logger("Progression for authtype '$authtypecode': $i/$size ($percentage%)"); + } + + #authority was marked as duplicate + next if defined $duplicated{$authority->authid}; + my $authrecord = GetAuthority($authority->authid); + + next unless $authrecord; + SetUTF8Flag($authrecord); + + ($verbose >= 2) and logger("Building query..."); + my $field = $authrecord->field( $auth_match_headings{ $authtypecode } ); + my $heading = C4::Heading->new_from_field( $field, undef, 1 ); #new auth heading + my $search_term = $heading->search_form; + ($verbose >= 2) and logger("Building query done."); + ($verbose >= 2) and logger ("$search_term"); + + ($verbose >= 2) and logger("Searching..."); +#my ($error, $results) = SimpleSearch("match-heading:\"$search_term\"", undef, undef, ["authorityserver"]); + my $builder = Koha::SearchEngine::QueryBuilder->new({ index => $Koha::SearchEngine::AUTHORITIES_INDEX } ); + my $searcher = Koha::SearchEngine::Search->new({ index => $Koha::SearchEngine::AUTHORITIES_INDEX }); + my $query = $builder->build_authorities_query_compat(['match-heading'],[''],[''],['exact'],[$search_term],$authtypecode,''); + my ( $results,$total ) = $searcher->search_auth_compat( $query, 0, 50, undef ); + if ( !$results ) { + $debug and warn $@; + $debug and warn YAML::Dump($search_term); + $debug and warn $field->as_string; + next; + } + + $debug and warn YAML::Dump($results); + + if ( !$results or scalar( @$results ) < 1 ){ + ($verbose >= 2) and logger('No duplicates found for ' . $heading->display_form); + next; + } + my @recordids = map { $_->{authid} if $_->{authid} != $authority->authid } @$results; + ($verbose >= 2) and logger("Searching done."); + + ($verbose >= 2) and logger("Choosing records..."); + my ( $recordid_to_keep, @recordids_to_merge ) + = _choose_records( $authority->authid, @recordids ); + ($verbose >= 2) and logger("Choosing records done."); + unless (!$confirm or @recordids_to_merge == 0) { + ($verbose >= 2) and logger("Merging ". join(',',@recordids_to_merge) ." into $recordid_to_keep."); + for my $localauthid (@recordids_to_merge) { + next if $recordid_to_keep == $localauthid; + my @editedbiblios = eval { + merge({ + merge_from => $localauthid, + MARCfrom => undef, + mergeto => $recordid_to_keep, + MARCto => undef + }); + }; + if ($@) { + warn "merging $localauthid into $recordid_to_keep failed :", $@; + } else { + logger("Updated " . scalar @editedbiblios . " biblios"); + $updated_bibs += scalar @editedbiblios; + $duplicated{$localauthid} = 2; + logger("Deleting $localauthid"); + DelAuthority({ authid => $localauthid, skip_merge => 1 }); + $deleted++; + } + } + ($verbose >= 2) and logger("Merge done."); + $duplicated{$recordid_to_keep} = 1; + } elsif ($verbose >= 2) { + if (@recordids_to_merge > 0) { + logger('Would merge ' + . join(',', @recordids_to_merge) + . " into $recordid_to_keep."); + } else { + logger("No duplicates found for $recordid_to_keep"); + } + } + } + $verbose and logger("End of deduping for authtype '$authtypecode'"); + $verbose and logger("Updated $updated_bibs biblios"); + $verbose and logger("Deleted $deleted authorities"); +} + +# Update biblios +my @biblios_to_update = grep {defined $biblios{$_} and $biblios{$_} == 1} + keys %biblios; +if( @biblios_to_update > 0 ) { +} else { + logger("No biblios to update"); +} + + +exit 0; + +sub _get_id { + my $record = shift; + + if($record and (my $field = $record->field('001'))) { + return $field->data(); + } + return 0; +} + +sub _has_ppn { + my $record = shift; + + if($record and (my $field = $record->field('009'))) { + return $field->data() ? 1 : 0; + } + return 0; +} + +sub _get_date { + my $record = shift; + + if($record and (my $field = $record->field('005'))) { + return $field->data(); + } + return 0; +} + +sub _get_usage { + my $record = shift; + + if($record and (my $field = $record->field('001'))) { + return Koha::Authorities->get_usage_count({ authid => $field->data() }); + } + return 0; +} + +=head2 _choose_records + this function takes input of candidate record ids to merging + and returns + first the record to merge to + and list of records to merge from +=cut +sub _choose_records { + my @recordids = @_; + + my @records = map { GetAuthority($_) } @recordids; + my @candidate_auths = @records; + + # See http://www.sysarch.com/Perl/sort_paper.html Schwartzian transform + my @candidate_authids = + map $_->[0] => + sort { + $b->[1] <=> $a->[1] || + $b->[2] <=> $a->[2] || + $b->[3] <=> $a->[3] + } + map [ + _get_id($_), + $choose_subs[0] ? $choose_subs[0]->($_) : 0, + $choose_subs[1] ? $choose_subs[1]->($_) : 0, + $choose_subs[2] ? $choose_subs[2]->($_) : 0 + ] => + ( $records[0], @candidate_auths ); + + return @candidate_authids; +} + + +sub logger { + my ($sec, $min, $hour, $mday, $mon, $year) = localtime; + my $time_msg = sprintf "[%04d-%02d-%02d %02d:%02d:%02d]", + 1900+$year, 1+$mon, $mday, $hour, $min, $sec; + say $time_msg, ' ', @_; +} -- 2.11.0