Skip to content

Instantly share code, notes, and snippets.

@a-roussos
Last active August 19, 2023 11:09
Show Gist options
  • Save a-roussos/cdafddf9554b4b9f9a5a1d353e027be7 to your computer and use it in GitHub Desktop.
Save a-roussos/cdafddf9554b4b9f9a5a1d353e027be7 to your computer and use it in GitHub Desktop.
Add (partial) facet support to Koha's Z39.50/SRU responder

Inspired by this mailing list post, the patch file included in this Gist adds partial facet support to Koha's Z39.50/SRU responder.

At the moment, only the facetLimit and facetCount request parameters are supported.

Support for the facetStart and facetSort parameters may be added in due course -- watch this space 😉

The order of the facets returned is determined by the FacetOrder System Preference (alphabetically or by usage count).

Also, I have chosen not to implement facet truncation based on the value of the FacetLabelTruncationLength System Preference; if you need this, it should be trivial to add.

The patch was developed and tested in a UNIMARC installation. However, the code added is MARC-agnostic and will most likely work in MARC21 setups as well.

It is meant to be applied to a Koha package installation (i.e. not a Git-based installation).

To apply it, first create a backup of lib/Koha/Z3950Responder.pm and lib/Koha/Z3950Responder/ZebraSession.pm.

Then, upload the patch to your home directory and issue the following commands as root:

cd /usr/share/koha
patch -p1 < /home/username/Z3950Responder.patch

Finally, re-start the Z39.50 responder with (replace instancename with your Koha instance name):

koha-z3950-responder --restart instancename

For more information on facets in a Z39.50/SRU context, see these links:

https://docs.oasis-open.org/search-ws/searchRetrieve/v1.0/os/part3-sru2.0/searchRetrieve-v1.0-os-part3-sru2.0.html#_Toc324162452

https://metacpan.org/dist/Net-Z3950-SimpleServer/view/SimpleServer.pm#Facets

https://software.indexdata.com/yaz/doc/facets.html

diff --git a/lib/C4/Search.pm b/lib/C4/Search.pm
index e2de365..34012ff 100644
--- a/lib/C4/Search.pm
+++ b/lib/C4/Search.pm
@@ -55,6 +55,7 @@ BEGIN {
new_record_from_zebra
z3950_search_args
getIndexes
+ GetFacets
);
}
diff --git a/lib/Koha/Z3950Responder.pm b/lib/Koha/Z3950Responder.pm
index 3e1f1d2..c560d84 100644
--- a/lib/Koha/Z3950Responder.pm
+++ b/lib/Koha/Z3950Responder.pm
@@ -187,6 +187,10 @@ sub search_handler {
if $SearchEngine ne C4::Context->preference('SearchEngine');
$args->{HANDLE}->search_handler($args);
+
+ my $facets_counter = $args->{HANDLE}->{resultsets}->{default}->{facets_counter};
+ my $facets_response = my_facets_response($args->{INPUTFACETS}, $facets_counter);
+ $args->{OUTPUTFACETS} = $facets_response;
}
=head3 fetch_handler
@@ -213,4 +217,119 @@ sub close_handler {
$args->{HANDLE}->close_handler( $args );
}
+=head3 my_facets_response
+
+This subroutine is called from the search handler. It takes a
+Net::Z3950::FacetList array as input, and returns an array of
+the same type.
+
+For more information on facets in a Z39.50/SRU context, see these links:
+https://docs.oasis-open.org/search-ws/searchRetrieve/v1.0/os/part3-sru2.0/searchRetrieve-v1.0-os-part3-sru2.0.html#_Toc324162452
+https://metacpan.org/dist/Net-Z3950-SimpleServer/view/SimpleServer.pm#Facets
+https://software.indexdata.com/yaz/doc/facets.html
+
+The code for this function was inspired by the sample `ztest.pl`
+program that ships with the Net::Z3950::SimpleServer distribution,
+and was adapted to suit Koha's facet configuration.
+
+=cut
+
+sub my_facets_response {
+ my $facets_request = shift;
+ my $facets_counter = shift;
+
+ # Generate facets response. We use $facets_request as basis.
+ my $zfacetlist = [];
+ bless $zfacetlist, 'Net::Z3950::FacetList';
+ my $i = 0;
+ foreach my $x (@$facets_request) {
+ my $facetname = "unknown";
+ my $sortorder = 0;
+ my $limit = 5;
+ my $offset = 1;
+ foreach my $attr (@{$x->{'attributes'}}) {
+ my $type = $attr->{'attributeType'};
+ my $value = $attr->{'attributeValue'};
+ if ($type == 1) { $facetname = $value; }
+ if ($type == 2) { $sortorder = $value; } # FIXME not implemented yet
+ if ($type == 3) { $limit = $value; }
+ if ($type == 4) { $offset = $value; } # FIXME not implemented yet
+ }
+ if ( $facetname ne 'unknown' ) {
+ my $zfacetfield = {};
+ bless $zfacetfield, 'Net::Z3950::FacetField';
+ if ($limit > 0) {
+ $zfacetlist->[$i++] = $zfacetfield;
+ }
+ my $zattributes = [];
+ bless $zattributes, 'Net::Z3950::RPN::Attributes';
+ $zfacetfield->{'attributes'} = $zattributes;
+ my $zattribute = {};
+ bless $zattribute, 'Net::Z3950::RPN::Attribute';
+ $zattribute->{'attributeType'} = 1;
+ $zattribute->{'attributeValue'} = $facetname;
+ $zattributes->[0] = $zattribute;
+ my $zfacetterms = [];
+ bless $zfacetterms, 'Net::Z3950::FacetTerms';
+ $zfacetfield->{'terms'} = $zfacetterms;
+ }
+ foreach my $key_idx ( @$facets_counter ) {
+ if ( $key_idx->{type_link_value} eq $facetname ) {
+ my $facets_count = scalar @{ $key_idx->{facets} };
+ my $j = 0;
+ foreach my $data ( @{ $key_idx->{facets} } ) {
+ if ( $j < $limit ) {
+ my $term_value = $data->{facet_label_value};
+ my $term_count = $data->{facet_count};
+ my $zfacetfield = {};
+ bless $zfacetfield, 'Net::Z3950::FacetField';
+ if ($limit > 0) {
+ $zfacetlist->[$i++] = $zfacetfield;
+ }
+ my $zfacetterms = [];
+ bless $zfacetterms, 'Net::Z3950::FacetTerms';
+ $zfacetfield->{'terms'} = $zfacetterms;
+ my $zfacetterm = {};
+ bless $zfacetterm, 'Net::Z3950::FacetTerm';
+ $zfacetterm->{'term'} = $term_value;
+ $zfacetterm->{'count'} = $term_count;
+ $zfacetterms->[$j++] = $zfacetterm;
+ }
+ }
+ }
+ if ( $facetname eq 'unknown' ) {
+ my $zfacetfield = {};
+ bless $zfacetfield, 'Net::Z3950::FacetField';
+ if ($limit > 0) {
+ $zfacetlist->[$i++] = $zfacetfield;
+ }
+ my $zattributes = [];
+ bless $zattributes, 'Net::Z3950::RPN::Attributes';
+ $zfacetfield->{'attributes'} = $zattributes;
+ my $zattribute = {};
+ bless $zattribute, 'Net::Z3950::RPN::Attribute';
+ $zattribute->{'attributeType'} = 1;
+ $zattribute->{'attributeValue'} = $key_idx->{type_link_value};
+ $zattributes->[0] = $zattribute;
+ my $zfacetterms = [];
+ bless $zfacetterms, 'Net::Z3950::FacetTerms';
+ $zfacetfield->{'terms'} = $zfacetterms;
+ my $k = 0;
+ foreach my $data ( @{ $key_idx->{facets} } ) {
+ if ( $k < $limit ) {
+ my $term_value = $data->{facet_label_value};
+ my $term_count = $data->{facet_count};
+ my $zfacetterm = {};
+ bless $zfacetterm, 'Net::Z3950::FacetTerm';
+ $zfacetterm->{'term'} = $term_value;
+ $zfacetterm->{'count'} = $term_count;
+ $zfacetterms->[$k++] = $zfacetterm;
+ }
+ }
+ }
+ }
+ }
+ return $zfacetlist;
+}
+
1;
diff --git a/lib/Koha/Z3950Responder/ZebraSession.pm b/lib/Koha/Z3950Responder/ZebraSession.pm
index 302472b..2459eca 100644
--- a/lib/Koha/Z3950Responder/ZebraSession.pm
+++ b/lib/Koha/Z3950Responder/ZebraSession.pm
@@ -25,6 +25,9 @@ use Koha::Logger;
use ZOOM;
+use C4::Search qw( GetFacets );
+use C4::Koha qw( getFacets );
+
=head1 NAME
Koha::Z3950Responder::ZebraSession
@@ -60,6 +63,9 @@ sub start_search {
$results = $connection->search_pqf( $args->{QUERY} );
+ my $elementSetName = $results->option( 'elementSetName' );
+ $results->option( elementSetName => $elementSetName );
+
$self->log_debug(' retry successful') if ($in_retry);
};
if ($@) {
@@ -75,12 +81,97 @@ sub start_search {
}
my $hits = $results ? $results->size() : -1;
+
+ my $facets_counter = {};
+ my $facets_info = {};
+ my $facets = getFacets();
+ my @facets_loop;
+
+ $facets_counter = GetFacets( $results );
+
+ for my $facet ( @$facets ) {
+ $facets_info->{ $facet->{ idx } }->{ label_value } = $facet->{ label };
+ }
+
+ my $itemtypes = { map { $_->{itemtype} => $_ } @{ Koha::ItemTypes->search_with_localization->unblessed } };
+ my $branches = Koha::Libraries->search( {}, { order_by => ['branchname'] } )->unblessed;
+
+ for my $link_value ( sort { $a cmp $b } keys %$facets_counter ) {
+ my @this_facets_array;
+ for my $one_facet ( sort { $facets_counter->{$link_value}->{$b} <=>
+ $facets_counter->{$link_value}->{$a}
+ } keys %{ $facets_counter->{$link_value} } ) {
+ my $facet_label_value = $one_facet;
+
+ # if it's a branch, label by the name, not the code
+ if ( $link_value =~ /branch/ ) {
+ if ( defined $branches
+ && ref($branches) eq "HASH"
+ && defined $branches->{$one_facet}
+ && ref( $branches->{$one_facet} ) eq "HASH" ) {
+ $facet_label_value = $branches->{$one_facet}->{'branchname'};
+ } else {
+ $facet_label_value = "*";
+ }
+ }
+
+ # if it's an itemtype, label by the name, not the code
+ if ( $link_value =~ /itype/ ) {
+ if ( defined $itemtypes
+ && ref($itemtypes) eq "HASH"
+ && defined $itemtypes->{$one_facet}
+ && ref( $itemtypes->{$one_facet} ) eq "HASH" ) {
+ $facet_label_value = $itemtypes->{$one_facet}->{translated_description};
+ }
+ }
+
+ # if it's a location code, use the name instead of the code
+ if ( $link_value =~ /location/ ) {
+ my $av = Koha::AuthorisedValues->search({ category => 'LOC', authorised_value => $one_facet });
+ $facet_label_value = $av->count ? $av->next->opac_description : '';
+ }
+
+ # if it's a collection code, use the name instead of the code
+ if ( $link_value =~ /ccode/ ) {
+ my $av = Koha::AuthorisedValues->search({ category => 'CCODE', authorised_value => $one_facet });
+ $facet_label_value = $av->count ? $av->next->opac_description : '';
+ }
+
+ push @this_facets_array, {
+ facet_count => $facets_counter->{$link_value}->{$one_facet},
+ facet_label_value => $facet_label_value,
+ type_link_value => $link_value,
+ } if ($facet_label_value);
+ }
+
+ push @facets_loop, {
+ type_link_value => $link_value,
+ type_id => $link_value . "_id",
+ facets => \@this_facets_array,
+ } unless ( ( $facets_info->{$link_value}->{'label_value'} =~ /Libraries/ )
+ and ( Koha::Libraries->search->count == 1 ) );
+ }
+
+ # This sorts the facets into alphabetical order
+ if (@facets_loop) {
+ foreach my $f (@facets_loop) {
+ if( C4::Context->preference('FacetOrder') eq 'Alphabetical' ) {
+ $f->{facets} =
+ [ sort { uc($a->{facet_label_value}) cmp
+ uc($b->{facet_label_value})
+ } @{ $f->{facets} }
+ ];
+ }
+ }
+ }
+
my $resultset = {
database => $database,
connection => $connection,
results => $results,
query => $args->{QUERY},
- hits => $hits
+ hits => $hits,
+ facets_counter => \@facets_loop,
};
return ( $resultset, $hits );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment