1 package OpenILS::Application::Search::Biblio;
2 use base qw/OpenSRF::Application/;
3 use strict; use warnings;
8 use OpenILS::Utils::Fieldmapper;
9 use OpenILS::Utils::ModsParser;
10 use OpenSRF::Utils::SettingsClient;
12 use OpenILS::Application::AppUtils;
16 use Time::HiRes qw(time);
17 use OpenSRF::EX qw(:try);
18 use Digest::MD5 qw(md5_hex);
23 my $apputils = "OpenILS::Application::AppUtils";
25 # Houses biblio search utilites
27 __PACKAGE__->register_method(
28 method => "biblio_search_marc",
29 api_name => "open-ils.search.biblio.marc",
31 note => "Searches biblio information by marc tag",
34 sub biblio_search_marc {
36 my( $self, $client, $search_hash, $string ) = @_;
38 warn "Building biblio marc session\n";
39 my $session = OpenSRF::AppSession->create("open-ils.storage");
42 warn "Sending biblio marc request. String $string\nSearch hash: " . Dumper($search_hash);
43 my $request = $session->request(
44 "open-ils.storage.direct.metabib.full_rec.search_fts.index_vector.atomic",
45 restrict => $search_hash,
47 my $data = $request->gather(1);
52 $session->disconnect();
60 # ---------------------------------------------------------------------------
61 # takes a list of record id's and turns the docs into friendly
62 # mods structures. Creates one MODS structure for each doc id.
63 # ---------------------------------------------------------------------------
64 sub _records_to_mods {
70 my $session = OpenSRF::AppSession->create("open-ils.storage");
71 my $request = $session->request(
72 "open-ils.storage.direct.biblio.record_entry.batch.retrieve", @ids );
74 my $last_content = undef;
76 while( my $response = $request->recv() ) {
79 my $u = OpenILS::Utils::ModsParser->new();
80 $u->start_mods_batch( $last_content->marc );
81 my $mods = $u->finish_mods_batch();
82 $mods->doc_id($last_content->id());
83 warn "Turning doc " . $mods->doc_id() . " into MODS\n";
84 $last_content = undef;
88 next unless $response;
90 if($response->isa("OpenSRF::EX")) {
91 throw $response ($response->stringify);
94 $last_content = $response->content;
99 my $u = OpenILS::Utils::ModsParser->new();
100 $u->start_mods_batch( $last_content->marc );
101 my $mods = $u->finish_mods_batch();
102 $mods->doc_id($last_content->id());
103 push @results, $mods;
108 $session->disconnect();
114 __PACKAGE__->register_method(
115 method => "record_id_to_mods",
116 api_name => "open-ils.search.biblio.record.mods.retrieve",
118 note => "Provide ID, we provide the mods"
121 # converts a record into a mods object with copy counts attached
122 sub record_id_to_mods {
124 my( $self, $client, $org_id, $id ) = @_;
126 my $mods_list = _records_to_mods( $id );
127 my $mods_obj = $mods_list->[0];
128 my $cmethod = $self->method_lookup(
129 "open-ils.search.biblio.record.copy_count");
130 my ($count) = $cmethod->run($org_id, $id);
131 $mods_obj->copy_count($count);
137 __PACKAGE__->register_method(
138 method => "record_id_to_mods_slim",
139 api_name => "open-ils.search.biblio.record.mods_slim.retrieve",
141 note => "Provide ID, we provide the mods"
144 # converts a record into a mods object with NO copy counts attached
145 sub record_id_to_mods_slim {
147 my( $self, $client, $id ) = @_;
148 warn "Retrieving MODS object for record $id\n";
149 return undef unless(defined $id);
151 my $mods_list = _records_to_mods( $id );
152 my $mods_obj = $mods_list->[0];
157 # Returns the number of copies attached to a record based on org location
158 __PACKAGE__->register_method(
159 method => "record_id_to_copy_count",
160 api_name => "open-ils.search.biblio.record.copy_count",
163 __PACKAGE__->register_method(
164 method => "record_id_to_copy_count",
165 api_name => "open-ils.search.biblio.metarecord.copy_count",
168 __PACKAGE__->register_method(
169 method => "record_id_to_copy_count",
170 api_name => "open-ils.search.biblio.metarecord.copy_count.staff",
172 sub record_id_to_copy_count {
173 my( $self, $client, $org_id, $record_id ) = @_;
175 my $method = "open-ils.storage.biblio.record_entry.copy_count.atomic";
177 if($self->api_name =~ /metarecord/) {
178 $method = "open-ils.storage.metabib.metarecord.copy_count.atomic";
182 if($self->api_name =~ /staff/ ) {
183 $method =~ s/atomic/staff\.atomic/og;
184 warn "Doing staff search $method\n";
188 my $session = OpenSRF::AppSession->create("open-ils.storage");
189 warn "copy_count retrieve $record_id\n";
190 return undef unless(defined $record_id);
192 my $request = $session->request(
193 $method, org_unit => $org_id => $key => $record_id );
196 my $count = $request->gather(1);
197 $session->disconnect();
198 return [ sort { $a->{depth} <=> $b->{depth} } @$count ];
203 # used for cat search classes
204 my $cat_search_hash = {
207 { tag => "100", subfield => "a"} ,
208 { tag => "700", subfield => "a"},
212 { tag => "245", subfield => "a"},
213 { tag => "242", subfield => "a"},
214 { tag => "240", subfield => "a"},
215 { tag => "210", subfield => "a"},
219 { tag => "650", subfield => "_" },
223 { tag => "035", subfield => "_" },
227 { tag => "020", subfield => "a" },
233 __PACKAGE__->register_method(
234 method => "biblio_search_tcn",
235 api_name => "open-ils.search.biblio.tcn",
237 note => "Retrieve a record by TCN",
240 sub biblio_search_tcn {
242 my( $self, $client, $tcn ) = @_;
244 $tcn =~ s/.*?(\w+)\s*$/$1/o;
245 warn "Searching TCN $tcn\n";
247 my $session = OpenSRF::AppSession->create( "open-ils.storage" );
248 my $request = $session->request(
249 "open-ils.storage.direct.biblio.record_entry.search.tcn_value.atomic", $tcn );
250 my $record_entry = $request->gather(1);
253 for my $record (@$record_entry) {
254 push @ids, $record->id;
257 $session->disconnect();
259 warn "received ID's for tcn search @ids\n";
262 return { count => $size, ids => \@ids };
267 # --------------------------------------------------------------------------------
270 __PACKAGE__->register_method(
271 method => "biblio_search_isbn",
272 api_name => "open-ils.search.biblio.isbn",
275 sub biblio_search_isbn {
276 my( $self, $client, $isbn ) = @_;
277 throw OpenSRF::EX::InvalidArg
279 ("biblio_search_isbn needs an ISBN to search")
280 unless defined $isbn;
282 warn "biblio search for ISBN $isbn\n";
283 my $method = $self->method_lookup("open-ils.search.biblio.marc");
284 my ($records) = $method->run( $cat_search_hash->{isbn}, $isbn );
286 return { count => 0 } unless($records and @$records);
287 my $size = @$records;
288 return { count => $size, ids => $records };
293 # --------------------------------------------------------------------------------
295 __PACKAGE__->register_method(
296 method => "biblio_barcode_to_copy",
297 api_name => "open-ils.search.asset.copy.find_by_barcode",
300 # turns a barcode into a copy object
301 sub biblio_barcode_to_copy {
302 my( $self, $client, $barcode ) = @_;
304 throw OpenSRF::EX::InvalidArg
305 ("search.biblio.barcode needs a barcode to search")
306 unless defined $barcode;
308 warn "copy search for barcode $barcode\n";
309 my $record = OpenILS::Application::AppUtils->simple_scalar_request(
311 "open-ils.storage.direct.asset.copy.search.barcode.atomic",
314 return undef unless($record);
319 __PACKAGE__->register_method(
320 method => "biblio_id_to_copy",
321 api_name => "open-ils.search.asset.copy.batch.retrieve",
324 # turns a barcode into a copy object
325 sub biblio_id_to_copy {
326 my( $self, $client, $ids ) = @_;
328 throw OpenSRF::EX::InvalidArg
329 ("search.biblio.batch.retrieve needs a id to search")
332 warn "copy search for ids @$ids\n";
333 my $record = OpenILS::Application::AppUtils->simple_scalar_request(
335 "open-ils.storage.direct.asset.copy.batch.retrieve.atomic",
343 __PACKAGE__->register_method(
344 method => "fleshed_copy_retrieve_batch",
345 api_name => "open-ils.search.asset.copy.fleshed.batch.retrieve",
348 sub fleshed_copy_retrieve_batch {
349 my( $self, $client, $ids ) = @_;
351 throw OpenSRF::EX::InvalidArg
352 ("search.biblio.batch.retrieve needs a id to search")
355 warn "fleshed copy search for id @$ids\n";
356 my $copy = OpenILS::Application::AppUtils->simple_scalar_request(
358 "open-ils.storage.fleshed.asset.copy.batch.retrieve.atomic",
364 __PACKAGE__->register_method(
365 method => "fleshed_copy_retrieve",
366 api_name => "open-ils.search.asset.copy.fleshed.retrieve",
369 sub fleshed_copy_retrieve {
370 my( $self, $client, $id ) = @_;
372 return undef unless defined $id;
373 warn "copy retrieve for id $id\n";
374 return OpenILS::Application::AppUtils->simple_scalar_request(
376 "open-ils.storage.fleshed.asset.copy.retrieve.atomic",
382 __PACKAGE__->register_method(
383 method => "biblio_barcode_to_title",
384 api_name => "open-ils.search.biblio.find_by_barcode",
387 sub biblio_barcode_to_title {
388 my( $self, $client, $barcode ) = @_;
391 throw OpenSRF::EX::ERROR
392 ("Not enough args to find_by_barcode");
395 my $title = $apputils->simple_scalar_request(
397 "open-ils.storage.biblio.record_entry.retrieve_by_barcode",
401 return { ids => [ $title->id ], count => 1 };
403 return { count => 0 };
409 __PACKAGE__->register_method(
410 method => "biblio_copy_to_mods",
411 api_name => "open-ils.search.biblio.copy.mods.retrieve",
414 # takes a copy object and returns it fleshed mods object
415 sub biblio_copy_to_mods {
416 my( $self, $client, $copy ) = @_;
418 throw OpenSRF::EX::InvalidArgs
419 ("copy.mods.retrieve needs a copy") unless( $copy );
421 new Fieldmapper::asset::copy($copy);
423 my $volume = OpenILS::Application::AppUtils->simple_scalar_request(
425 "open-ils.storage.direct.asset.call_number.retrieve",
426 $copy->call_number() );
428 my $mods = _records_to_mods($volume->record());
429 $mods = shift @$mods;
430 $volume->copies([$copy]);
431 push @{$mods->call_numbers()}, $volume;
437 sub barcode_to_mods {
442 # --------------------------------------------------------------------------------
446 __PACKAGE__->register_method(
447 method => "cat_biblio_search_class",
448 api_name => "open-ils.search.cat.biblio.class",
452 sub cat_biblio_search_class {
454 my( $self, $client, $org_id, $class, $sort, $string ) = @_;
456 throw OpenSRF::EX::InvalidArg
457 ("Not enough args to open-ils.search.cat.biblio.class")
458 unless( defined($org_id) and $class and $sort and $string );
463 my $method = $self->method_lookup("open-ils.search.biblio.marc");
465 throw OpenSRF::EX::PANIC
466 ("Can't lookup method 'open-ils.search.biblio.marc'");
469 my ($records) = $method->run( $cat_search_hash->{$class}, $string );
472 for my $i (@$records) { push @ids, $i->[0]; }
474 my $mods_list = _records_to_mods( @ids );
475 return undef unless (ref($mods_list) eq "ARRAY");
477 # ---------------------------------------------------------------
478 # append copy count information to the mods objects
479 my $session = OpenSRF::AppSession->create("open-ils.storage");
481 my $request = $session->request(
482 "open-ils.storage.direct.biblio.record_copy_count.batch", $org_id, @ids );
486 warn "receiving copy counts for doc $id\n";
488 my $response = $request->recv();
489 next unless $response;
491 if( $response and UNIVERSAL::isa($response, "Error")) {
492 throw $response ($response->stringify);
495 my $count = $response->content;
496 my $mods_obj = undef;
497 for my $m (@$mods_list) {
498 $mods_obj = $m if ($m->doc_id() == $id)
501 $mods_obj->copy_count($count);
504 $client->respond( $mods_obj );
510 $session->disconnect();
512 # ---------------------------------------------------------------
520 __PACKAGE__->register_method(
521 method => "cat_biblio_search_class_id",
522 api_name => "open-ils.search.cat.biblio.class.id",
524 note => "Searches biblio information by search class and returns the IDs",
527 sub cat_biblio_search_class_id {
529 my( $self, $client, $org_id, $class, $string, $limit, $offset ) = @_;
536 my $bool = ($class eq "subject" || $class eq "keyword");
537 $string = OpenILS::Application::Search->filter_search($string, $bool);
540 return OpenILS::EX->new("SEARCH_TOO_LARGE")->ex();
543 warn "Searching cat.biblio.class.id string: $string offset: $offset limit: $limit\n";
545 throw OpenSRF::EX::InvalidArg
546 ("Not enough args to open-ils.search.cat.biblio.class")
547 unless( defined($org_id) and $class and $string );
552 my $cache_key = md5_hex( $org_id . $class . $string );
553 my $id_array = OpenILS::Application::SearchCache->get_cache($cache_key);
556 warn "Returning class search from cache\n";
557 my $size = @$id_array;
558 my @ids = @$id_array[ $offset..($offset+$limit) ];
559 return { count => $size, ids => \@ids };
562 my $method = $self->method_lookup("open-ils.search.biblio.marc");
564 throw OpenSRF::EX::PANIC
565 ("Can't lookup method 'open-ils.search.biblio.marc'");
568 my ($records) = $method->run( $cat_search_hash->{$class}, $string );
572 for my $i (@$records) {
573 if(defined($i->[0])) {
574 push @cache_ids, $i->[0];
578 my @ids = @cache_ids[ $offset..($offset+$limit) ];
579 my $size = @$records;
581 OpenILS::Application::SearchCache->put_cache(
582 $cache_key, \@cache_ids, $size );
584 return { count =>$size, ids => \@ids };
589 __PACKAGE__->register_method(
590 method => "biblio_search_class_count",
591 api_name => "open-ils.search.biblio.class.count",
594 __PACKAGE__->register_method(
595 method => "biblio_search_class_count",
596 api_name => "open-ils.search.biblio.class.count.staff",
599 sub biblio_search_class_count {
601 my( $self, $client, $class, $string, $org_id, $org_type ) = @_;
603 warn "org: $org_id : depth: $org_type\n";
605 $org_id = "1" unless defined($org_id); # xxx
606 $org_type = 0 unless defined($org_type);
608 warn "Searching biblio.class.id\n" .
610 "org_id: $org_id\n" .
611 "depth: $org_type\n" ;
613 my $bool = ($class eq "subject" || $class eq "keyword");
614 $string = OpenILS::Application::Search->filter_search($string, $bool);
617 return OpenILS::EX->new("SEARCH_TOO_LARGE")->ex;
621 if( !defined($org_id) or !$class or !$string ) {
622 warn "not enbough args to metarecord search\n";
623 throw OpenSRF::EX::InvalidArg
624 ("Not enough args to open-ils.search.cat.biblio.class")
629 if( ($class ne "title") and ($class ne "author") and
630 ($class ne "subject") and ($class ne "keyword")
631 and ($class ne "series" )) {
632 warn "Invalid search class: $class\n";
633 throw OpenSRF::EX::InvalidArg ("Not a valid search class: $class")
636 # grab the mr id's from storage
638 my $method = "open-ils.storage.cachable.metabib.$class.search_fts.metarecord_count";
639 if($self->api_name =~ /staff/) {
640 $method = "$method.staff";
641 $method =~ s/\.cachable//o;
643 warn "Performing count method $method\n";
644 warn "API name " . $self->api_name() . "\n";
646 my $session = OpenSRF::AppSession->create('open-ils.storage');
648 my $request = $session->request( $method,
651 cache_page_size => 1,
654 my $count = $request->gather(1);
655 warn "Received count $count\n";
661 __PACKAGE__->register_method(
662 method => "biblio_search_class",
663 api_name => "open-ils.search.biblio.class",
666 __PACKAGE__->register_method(
667 method => "biblio_search_class",
668 api_name => "open-ils.search.biblio.class.unordered",
671 __PACKAGE__->register_method(
672 method => "biblio_search_class",
673 api_name => "open-ils.search.biblio.class.staff",
676 __PACKAGE__->register_method(
677 method => "biblio_search_class",
678 api_name => "open-ils.search.biblio.class.unordered.staff",
681 sub biblio_search_class {
683 my( $self, $client, $class, $string,
684 $org_id, $org_type, $limit, $offset ) = @_;
686 warn "org: $org_id : depth: $org_type : limit: $limit : offset: $offset\n";
688 $offset = 0 unless (defined($offset) and $offset > 0);
689 $limit = 100 unless (defined($limit) and $limit > 0);
690 $org_id = "1" unless (defined($org_id)); # xxx
691 $org_type = 0 unless (defined($org_type));
693 warn "Searching biblio.class.id\n" .
695 "\noffset: $offset\n" .
697 "org_id: $org_id\n" .
698 "depth: $org_type\n" ;
700 warn "Search filtering string " . time() . "\n";
701 $string = OpenILS::Application::Search->filter_search($string);
702 if(!$string) { return undef; }
704 if( !defined($org_id) or !$class or !$string ) {
705 warn "not enbough args to metarecord search\n";
706 throw OpenSRF::EX::InvalidArg
707 ("Not enough args to open-ils.search.cat.biblio.class")
712 if( ($class ne "title") and ($class ne "author") and
713 ($class ne "subject") and ($class ne "keyword")
714 and ($class ne "series") ) {
715 warn "Invalid search class: $class\n";
716 throw OpenSRF::EX::InvalidArg ("Not a valid search class: $class")
719 #my $method = "open-ils.storage.metabib.$class.search_fts.metarecord.atomic";
720 my $method = "open-ils.storage.cachable.metabib.$class.search_fts.metarecord.atomic";
722 if($self->api_name =~ /order/) {
723 $method = "open-ils.storage.cachable.metabib.$class.search_fts.metarecord.unordered.atomic",
724 #$method = "open-ils.storage.metabib.$class.search_fts.metarecord.unordered.atomic";
727 if($self->api_name =~ /staff/) {
728 $method =~ s/atomic/staff\.atomic/og;
729 $method =~ s/\.cachable//o;
732 warn "Performing search method $method\n";
733 warn "MR search method is $method\n";
735 my $session = OpenSRF::AppSession->create('open-ils.storage');
737 warn "Search making request " . time() . "\n";
738 my $request = $session->request(
745 cache_page_size => 200,
748 my $records = $request->gather(1);
749 warn "Search request complete " . time() . "\n";
754 warn "Received " . scalar(@$records) . " id's from class search\n";
756 # if we just get one, it won't be wrapped in an array
757 if(!ref($records->[0])) {
758 $records = [$records]; }
759 for my $i (@$records) {
766 @ids = grep { defined($_->[0]) } @ids;
769 $session->disconnect();
771 return { ids => \@ids };
778 __PACKAGE__->register_method(
779 method => "biblio_mrid_to_modsbatch",
780 api_name => "open-ils.search.biblio.metarecord.mods_slim.retrieve",
783 sub biblio_mrid_to_modsbatch {
784 my( $self, $client, $mrid ) = @_;
786 throw OpenSRF::EX::InvalidArg
787 ("search.biblio.metarecord_to_mods requires mr id")
788 unless defined( $mrid );
791 my $metarecord = OpenILS::Application::AppUtils->simple_scalar_request( "open-ils.storage",
792 "open-ils.storage.direct.metabib.metarecord.retrieve", $mrid );
795 throw OpenSRF::EX::ERROR ("No metarecord exists with the given id: $mrid");
798 my $master_id = $metarecord->master_record();
801 # check for existing mods
802 if($metarecord->mods()){
803 warn "We already have mods for " . $metarecord->id . "\n";
804 my $perl = JSON->JSON2perl($metarecord->mods());
805 return Fieldmapper::metabib::virtual_record->new($perl);
810 warn "Creating mods batch for metarecord $mrid\n";
811 my $id_hash = biblio_mrid_to_record_ids( undef, undef, $mrid );
812 my @ids = @{$id_hash->{ids}};
814 if(@ids < 1) { return undef; }
816 warn "Master ID is $master_id\n";
817 # grab the master record to start the mods batch
819 my $record = OpenILS::Application::AppUtils->simple_scalar_request( "open-ils.storage",
820 "open-ils.storage.direct.biblio.record_entry.retrieve", $master_id );
823 throw OpenSRF::EX::ERROR
824 ("No record returned with id $master_id");
827 my $u = OpenILS::Utils::ModsParser->new();
829 $u->start_mods_batch( $record->marc );
830 my $main_doc_id = $record->id();
832 @ids = grep { $_ ne $master_id } @ids;
834 # now we have to collect all of the marc objects and push them into a mods batch
835 my $session = OpenSRF::AppSession->create("open-ils.storage");
836 my $request = $session->request(
837 "open-ils.storage.direct.biblio.record_entry.batch.retrieve", @ids );
839 while( my $response = $request->recv() ) {
841 next unless $response;
842 if(UNIVERSAL::isa( $response,"OpenSRF::EX")) {
843 throw $response ($response->stringify);
846 my $content = $response->content;
849 $u->push_mods_batch( $content->marc );
853 my $mods = $u->finish_mods_batch();
854 $mods->doc_id($mrid);
857 $client->respond_complete($mods);
859 my $mods_string = JSON->perl2JSON($mods->decast);
861 $metarecord->mods($mods_string);
863 my $req = $session->request(
864 "open-ils.storage.direct.metabib.metarecord.update",
871 $session->disconnect();
879 # converts a mr id into a list of record ids
881 __PACKAGE__->register_method(
882 method => "biblio_mrid_to_record_ids",
883 api_name => "open-ils.search.biblio.metarecord_to_records",
886 sub biblio_mrid_to_record_ids {
887 my( $self, $client, $mrid ) = @_;
889 throw OpenSRF::EX::InvalidArg
890 ("search.biblio.metarecord_to_record_ids requires mr id")
891 unless defined( $mrid );
893 warn "Searching for record for MR $mrid\n";
895 my $mrmaps = OpenILS::Application::AppUtils->simple_scalar_request(
897 #"open-ils.storage.direct.metabib.metarecord_source_map.search.metarecord", $mrid );
898 "open-ils.storage.ordered.metabib.metarecord.records.atomic",
903 #for my $map (@$mrmaps) { push @ids, $map->source(); }
904 #warn "Recovered id's [@ids] for mr $mrid\n";
909 return { count => $size, ids => $mrmaps };
915 __PACKAGE__->register_method(
916 method => "biblio_record_to_marc_html",
917 api_name => "open-ils.search.biblio.record.html" );
919 my $parser = XML::LibXML->new();
920 my $xslt = XML::LibXSLT->new();
923 my $settings_client = OpenSRF::Utils::SettingsClient->new();
924 sub biblio_record_to_marc_html {
925 my( $self, $client, $recordid ) = @_;
928 my $dir = $settings_client->config_value( "dirs", "xsl" );
929 my $xsl = $settings_client->config_value(
930 "apps", "open-ils.search", "app_settings", "marc_html_xsl" );
932 $xsl = $parser->parse_file("$dir/$xsl");
933 $marc_sheet = $xslt->parse_stylesheet( $xsl );
937 my $record = $apputils->simple_scalar_request(
939 "open-ils.storage.direct.biblio.record_entry.retrieve",
942 my $xmldoc = $parser->parse_string($record->marc);
943 my $html = $marc_sheet->transform($xmldoc);
944 $html = $html->toString();
951 __PACKAGE__->register_method(
952 method => "retrieve_all_copy_locations",
953 api_name => "open-ils.search.config.copy_location.retrieve.all" );
955 my $shelving_locations;
956 sub retrieve_all_copy_locations {
957 my( $self, $client ) = @_;
958 if(!$shelving_locations) {
959 $shelving_locations = $apputils->simple_scalar_request(
961 "open-ils.storage.direct.asset.copy_location.retrieve.all.atomic");
963 return $shelving_locations;
968 __PACKAGE__->register_method(
969 method => "retrieve_all_copy_statuses",
970 api_name => "open-ils.search.config.copy_status.retrieve.all" );
973 sub retrieve_all_copy_statuses {
974 my( $self, $client ) = @_;
975 if(!$copy_statuses) {
976 $copy_statuses = $apputils->simple_scalar_request(
978 "open-ils.storage.direct.config.copy_status.retrieve.all.atomic" );
980 return $copy_statuses;
984 __PACKAGE__->register_method(
985 method => "copy_counts_per_org",
986 api_name => "open-ils.search.biblio.copy_counts.retrieve");
988 sub copy_counts_per_org {
989 my( $self, $client, $record_id ) = @_;
990 my $counts = $apputils->simple_scalar_request(
992 "open-ils.storage.biblio.record_entry.global_copy_count.atomic",
994 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];