1 package OpenILS::Application::Search::Biblio;
2 use base qw/OpenILS::Application/;
3 use strict; use warnings;
6 use OpenSRF::Utils::JSON;
7 use OpenILS::Utils::Fieldmapper;
8 use OpenILS::Utils::ModsParser;
9 use OpenSRF::Utils::SettingsClient;
10 use OpenILS::Utils::CStoreEditor q/:funcs/;
11 use OpenSRF::Utils::Cache;
14 use OpenSRF::Utils::Logger qw/:logger/;
17 use OpenSRF::Utils::JSON;
19 use Time::HiRes qw(time);
20 use OpenSRF::EX qw(:try);
21 use Digest::MD5 qw(md5_hex);
27 $Data::Dumper::Indent = 0;
29 use OpenILS::Const qw/:const/;
31 use OpenILS::Application::AppUtils;
32 my $apputils = "OpenILS::Application::AppUtils";
35 my $pfx = "open-ils.search_";
43 $cache = OpenSRF::Utils::Cache->new('global');
44 my $sclient = OpenSRF::Utils::SettingsClient->new();
45 $cache_timeout = $sclient->config_value(
46 "apps", "open-ils.search", "app_settings", "cache_timeout" ) || 300;
48 $superpage_size = $sclient->config_value(
49 "apps", "open-ils.search", "app_settings", "superpage_size" ) || 500;
51 $max_superpages = $sclient->config_value(
52 "apps", "open-ils.search", "app_settings", "max_superpages" ) || 20;
54 $logger->info("Search cache timeout is $cache_timeout, ".
55 " superpage_size is $superpage_size, max_superpages is $max_superpages");
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.cstore");
71 my $request = $session->request(
72 "open-ils.cstore.direct.biblio.record_entry.search", { id => \@ids } );
74 while( my $resp = $request->recv ) {
75 my $content = $resp->content;
76 next if $content->id == OILS_PRECAT_RECORD;
77 my $u = OpenILS::Utils::ModsParser->new(); # FIXME: we really need a new parser for each object?
78 $u->start_mods_batch( $content->marc );
79 my $mods = $u->finish_mods_batch();
80 $mods->doc_id($content->id());
81 $mods->tcn($content->tcn_value);
85 $session->disconnect();
89 __PACKAGE__->register_method(
90 method => "record_id_to_mods",
91 api_name => "open-ils.search.biblio.record.mods.retrieve",
94 desc => "Provide ID, we provide the MODS object with copy count. "
95 . "Note: this method does NOT take an array of IDs like mods_slim.retrieve", # FIXME: do it here too
97 { desc => 'Record ID', type => 'number' }
100 desc => 'MODS object', type => 'object'
105 # converts a record into a mods object with copy counts attached
106 sub record_id_to_mods {
108 my( $self, $client, $org_id, $id ) = @_;
110 my $mods_list = _records_to_mods( $id );
111 my $mods_obj = $mods_list->[0];
112 my $cmethod = $self->method_lookup("open-ils.search.biblio.record.copy_count");
113 my ($count) = $cmethod->run($org_id, $id);
114 $mods_obj->copy_count($count);
121 __PACKAGE__->register_method(
122 method => "record_id_to_mods_slim",
123 api_name => "open-ils.search.biblio.record.mods_slim.retrieve",
127 desc => "Provide ID(s), we provide the MODS",
129 { desc => 'Record ID or array of IDs' }
132 desc => 'MODS object(s), event on error'
137 # converts a record into a mods object with NO copy counts attached
138 sub record_id_to_mods_slim {
139 my( $self, $client, $id ) = @_;
140 return undef unless defined $id;
142 if(ref($id) and ref($id) == 'ARRAY') {
143 return _records_to_mods( @$id );
145 my $mods_list = _records_to_mods( $id );
146 my $mods_obj = $mods_list->[0];
147 return OpenILS::Event->new('BIBLIO_RECORD_ENTRY_NOT_FOUND') unless $mods_obj;
153 __PACKAGE__->register_method(
154 method => "record_id_to_mods_slim_batch",
155 api_name => "open-ils.search.biblio.record.mods_slim.batch.retrieve",
158 sub record_id_to_mods_slim_batch {
159 my($self, $conn, $id_list) = @_;
160 $conn->respond(_records_to_mods($_)->[0]) for @$id_list;
165 # Returns the number of copies attached to a record based on org location
166 __PACKAGE__->register_method(
167 method => "record_id_to_copy_count",
168 api_name => "open-ils.search.biblio.record.copy_count",
170 desc => q/Returns a copy summary for the given record for the context org
171 unit and all ancestor org units/,
173 {desc => 'Context org unit id', type => 'number'},
174 {desc => 'Record ID', type => 'number'}
177 desc => q/summary object per org unit in the set, where the set
178 includes the context org unit and all parent org units.
179 Object includes the keys "transcendant", "count", "org_unit", "depth",
180 "unshadow", "available". Each is a count, except "org_unit" which is
181 the context org unit and "depth" which is the depth of the context org unit
188 __PACKAGE__->register_method(
189 method => "record_id_to_copy_count",
190 api_name => "open-ils.search.biblio.record.copy_count.staff",
193 desc => q/Returns a copy summary for the given record for the context org
194 unit and all ancestor org units/,
196 {desc => 'Context org unit id', type => 'number'},
197 {desc => 'Record ID', type => 'number'}
200 desc => q/summary object per org unit in the set, where the set
201 includes the context org unit and all parent org units.
202 Object includes the keys "transcendant", "count", "org_unit", "depth",
203 "unshadow", "available". Each is a count, except "org_unit" which is
204 the context org unit and "depth" which is the depth of the context org unit
211 __PACKAGE__->register_method(
212 method => "record_id_to_copy_count",
213 api_name => "open-ils.search.biblio.metarecord.copy_count",
215 desc => q/Returns a copy summary for the given record for the context org
216 unit and all ancestor org units/,
218 {desc => 'Context org unit id', type => 'number'},
219 {desc => 'Record ID', type => 'number'}
222 desc => q/summary object per org unit in the set, where the set
223 includes the context org unit and all parent org units.
224 Object includes the keys "transcendant", "count", "org_unit", "depth",
225 "unshadow", "available". Each is a count, except "org_unit" which is
226 the context org unit and "depth" which is the depth of the context org unit
233 __PACKAGE__->register_method(
234 method => "record_id_to_copy_count",
235 api_name => "open-ils.search.biblio.metarecord.copy_count.staff",
237 desc => q/Returns a copy summary for the given record for the context org
238 unit and all ancestor org units/,
240 {desc => 'Context org unit id', type => 'number'},
241 {desc => 'Record ID', type => 'number'}
244 desc => q/summary object per org unit in the set, where the set
245 includes the context org unit and all parent org units.
246 Object includes the keys "transcendant", "count", "org_unit", "depth",
247 "unshadow", "available". Each is a count, except "org_unit" which is
248 the context org unit and "depth" which is the depth of the context org unit
255 sub record_id_to_copy_count {
256 my( $self, $client, $org_id, $record_id ) = @_;
258 return [] unless $record_id;
260 my $method = "open-ils.storage.biblio.record_entry.copy_count.atomic";
263 if($self->api_name =~ /metarecord/) {
264 $method = "open-ils.storage.metabib.metarecord.copy_count.atomic";
268 $method =~ s/atomic/staff\.atomic/og if($self->api_name =~ /staff/ );
270 my $count = $U->storagereq($method, org_unit => $org_id, $key => $record_id);
272 return [ sort { $a->{depth} <=> $b->{depth} } @$count ];
276 __PACKAGE__->register_method(
277 method => "biblio_search_tcn",
278 api_name => "open-ils.search.biblio.tcn",
281 desc => "Retrieve related record ID(s) given a TCN",
283 { desc => 'TCN', type => 'string' },
284 { desc => 'Flag indicating to include deleted records', type => 'string' }
287 desc => 'Results object like: { "count": $i, "ids": [...] }',
294 sub biblio_search_tcn {
296 my( $self, $client, $tcn, $include_deleted ) = @_;
298 $tcn =~ s/^\s+|\s+$//og;
300 my $e = new_editor();
301 my $search = {tcn_value => $tcn};
302 $search->{deleted} = 'f' unless $include_deleted;
303 my $recs = $e->search_biblio_record_entry( $search, {idlist =>1} );
305 return { count => scalar(@$recs), ids => $recs };
309 # --------------------------------------------------------------------------------
311 __PACKAGE__->register_method(
312 method => "biblio_barcode_to_copy",
313 api_name => "open-ils.search.asset.copy.find_by_barcode",
315 sub biblio_barcode_to_copy {
316 my( $self, $client, $barcode ) = @_;
317 my( $copy, $evt ) = $U->fetch_copy_by_barcode($barcode);
322 __PACKAGE__->register_method(
323 method => "biblio_id_to_copy",
324 api_name => "open-ils.search.asset.copy.batch.retrieve",
326 sub biblio_id_to_copy {
327 my( $self, $client, $ids ) = @_;
328 $logger->info("Fetching copies @$ids");
329 return $U->cstorereq(
330 "open-ils.cstore.direct.asset.copy.search.atomic", { id => $ids } );
334 __PACKAGE__->register_method(
335 method => "biblio_id_to_uris",
336 api_name=> "open-ils.search.asset.uri.retrieve_by_bib",
340 @param BibID Which bib record contains the URIs
341 @param OrgID Where to look for URIs
342 @param OrgDepth Range adjustment for OrgID
343 @return A stream or list of 'auri' objects
347 sub biblio_id_to_uris {
348 my( $self, $client, $bib, $org, $depth ) = @_;
349 die "Org ID required" unless defined($org);
350 die "Bib ID required" unless defined($bib);
353 push @params, $depth if (defined $depth);
355 my $ids = $U->cstorereq( "open-ils.cstore.json_query.atomic",
356 { select => { auri => [ 'id' ] },
360 field => 'call_number',
366 filter => { active => 't' }
377 select => { aou => [ { column => 'id', transform => 'actor.org_unit_descendants', params => \@params, result_field => 'id' } ] },
379 where => { id => $org },
389 my $uris = $U->cstorereq(
390 "open-ils.cstore.direct.asset.uri.search.atomic",
391 { id => [ map { (values %$_) } @$ids ] }
394 $client->respond($_) for (@$uris);
400 __PACKAGE__->register_method(
401 method => "copy_retrieve",
402 api_name => "open-ils.search.asset.copy.retrieve",
405 desc => 'Retrieve a copy object based on the Copy ID',
407 { desc => 'Copy ID', type => 'number'}
410 desc => 'Copy object, event on error'
416 my( $self, $client, $cid ) = @_;
417 my( $copy, $evt ) = $U->fetch_copy($cid);
418 return $evt || $copy;
421 __PACKAGE__->register_method(
422 method => "volume_retrieve",
423 api_name => "open-ils.search.asset.call_number.retrieve"
425 sub volume_retrieve {
426 my( $self, $client, $vid ) = @_;
427 my $e = new_editor();
428 my $vol = $e->retrieve_asset_call_number($vid) or return $e->event;
432 __PACKAGE__->register_method(
433 method => "fleshed_copy_retrieve_batch",
434 api_name => "open-ils.search.asset.copy.fleshed.batch.retrieve",
438 sub fleshed_copy_retrieve_batch {
439 my( $self, $client, $ids ) = @_;
440 $logger->info("Fetching fleshed copies @$ids");
441 return $U->cstorereq(
442 "open-ils.cstore.direct.asset.copy.search.atomic",
445 flesh_fields => { acp => [ qw/ circ_lib location status stat_cat_entries / ] }
450 __PACKAGE__->register_method(
451 method => "fleshed_copy_retrieve",
452 api_name => "open-ils.search.asset.copy.fleshed.retrieve",
455 sub fleshed_copy_retrieve {
456 my( $self, $client, $id ) = @_;
457 my( $c, $e) = $U->fetch_fleshed_copy($id);
462 __PACKAGE__->register_method(
463 method => 'fleshed_by_barcode',
464 api_name => "open-ils.search.asset.copy.fleshed2.find_by_barcode",
467 sub fleshed_by_barcode {
468 my( $self, $conn, $barcode ) = @_;
469 my $e = new_editor();
470 my $copyid = $e->search_asset_copy(
471 {barcode => $barcode, deleted => 'f'}, {idlist=>1})->[0]
473 return fleshed_copy_retrieve2( $self, $conn, $copyid);
477 __PACKAGE__->register_method(
478 method => "fleshed_copy_retrieve2",
479 api_name => "open-ils.search.asset.copy.fleshed2.retrieve",
483 sub fleshed_copy_retrieve2 {
484 my( $self, $client, $id ) = @_;
485 my $e = new_editor();
486 my $copy = $e->retrieve_asset_copy(
493 qw/ location status stat_cat_entry_copy_maps notes age_protect /
495 ascecm => [qw/ stat_cat stat_cat_entry /],
499 ) or return $e->event;
501 # For backwards compatibility
502 #$copy->stat_cat_entries($copy->stat_cat_entry_copy_maps);
504 if( $copy->status->id == OILS_COPY_STATUS_CHECKED_OUT ) {
506 $e->search_action_circulation(
508 { target_copy => $copy->id },
510 order_by => { circ => 'xact_start desc' },
522 __PACKAGE__->register_method(
523 method => 'flesh_copy_custom',
524 api_name => 'open-ils.search.asset.copy.fleshed.custom',
528 sub flesh_copy_custom {
529 my( $self, $conn, $copyid, $fields ) = @_;
530 my $e = new_editor();
531 my $copy = $e->retrieve_asset_copy(
541 ) or return $e->event;
546 __PACKAGE__->register_method(
547 method => "biblio_barcode_to_title",
548 api_name => "open-ils.search.biblio.find_by_barcode",
551 sub biblio_barcode_to_title {
552 my( $self, $client, $barcode ) = @_;
554 my $title = $apputils->simple_scalar_request(
556 "open-ils.storage.biblio.record_entry.retrieve_by_barcode", $barcode );
558 return { ids => [ $title->id ], count => 1 } if $title;
559 return { count => 0 };
562 __PACKAGE__->register_method(
563 method => 'title_id_by_item_barcode',
564 api_name => 'open-ils.search.bib_id.by_barcode',
567 desc => 'Retrieve copy object with fleshed record, given the barcode',
569 { desc => 'Item barcode', type => 'string' }
572 desc => 'Asset copy object with fleshed record and callnumber, or event on error or null set'
577 sub title_id_by_item_barcode {
578 my( $self, $conn, $barcode ) = @_;
579 my $e = new_editor();
580 my $copies = $e->search_asset_copy(
582 { deleted => 'f', barcode => $barcode },
586 acp => [ 'call_number' ],
593 return $e->event unless @$copies;
594 return $$copies[0]->call_number->record->id;
598 __PACKAGE__->register_method(
599 method => "biblio_copy_to_mods",
600 api_name => "open-ils.search.biblio.copy.mods.retrieve",
603 # takes a copy object and returns it fleshed mods object
604 sub biblio_copy_to_mods {
605 my( $self, $client, $copy ) = @_;
607 my $volume = $U->cstorereq(
608 "open-ils.cstore.direct.asset.call_number.retrieve",
609 $copy->call_number() );
611 my $mods = _records_to_mods($volume->record());
612 $mods = shift @$mods;
613 $volume->copies([$copy]);
614 push @{$mods->call_numbers()}, $volume;
622 OpenILS::Application::Search::Biblio
628 =head3 open-ils.search.biblio.multiclass.query (arghash, query, docache)
630 For arghash and docache, see B<open-ils.search.biblio.multiclass>.
632 The query argument is a string, but built like a hash with key: value pairs.
633 Recognized search keys include:
635 keyword (kw) - search keyword(s) *
636 author (au) - search author(s) *
637 name (au) - same as author *
638 title (ti) - search title *
639 subject (su) - search subject *
640 series (se) - search series *
641 lang - limit by language (specifiy multiple langs with lang:l1 lang:l2 ...)
642 site - search at specified org unit, corresponds to actor.org_unit.shortname
643 sort - sort type (title, author, pubdate)
644 dir - sort direction (asc, desc)
645 available - if set to anything other than "false" or "0", limits to available items
647 * Searching keyword, author, title, subject, and series supports additional search
648 subclasses, specified with a "|". For example, C<title|proper:gone with the wind>.
650 For more, see B<config.metabib_field>.
654 foreach (qw/open-ils.search.biblio.multiclass.query
655 open-ils.search.biblio.multiclass.query.staff
656 open-ils.search.metabib.multiclass.query
657 open-ils.search.metabib.multiclass.query.staff/)
659 __PACKAGE__->register_method(
661 method => 'multiclass_query',
663 desc => 'Perform a search query. The .staff version of the call includes otherwise hidden hits.',
665 {name => 'arghash', desc => 'Arg hash (see open-ils.search.biblio.multiclass)', type => 'object'},
666 {name => 'query', desc => 'Raw human-readable query (see perldoc '. __PACKAGE__ .')', type => 'string'},
667 {name => 'docache', desc => 'Flag for caching (see open-ils.search.biblio.multiclass)', type => 'object'},
670 desc => 'Search results from query, like: { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
671 type => 'object', # TODO: update as miker's new elements are included
677 sub multiclass_query {
678 my($self, $conn, $arghash, $query, $docache) = @_;
680 $logger->debug("initial search query => $query");
681 my $orig_query = $query;
685 $query =~ s/^\s+//go;
687 # convert convenience classes (e.g. kw for keyword) to the full class name
688 $query =~ s/kw(:|\|)/keyword$1/go;
689 $query =~ s/ti(:|\|)/title$1/go;
690 $query =~ s/au(:|\|)/author$1/go;
691 $query =~ s/su(:|\|)/subject$1/go;
692 $query =~ s/se(:|\|)/series$1/go;
693 $query =~ s/name(:|\|)/author$1/og;
695 $logger->debug("cleansed query string => $query");
698 my $simple_class_re = qr/((?:\w+(?:\|\w+)?):[^:]+?)$/;
699 my $class_list_re = qr/(?:keyword|title|author|subject|series)/;
700 my $modifier_list_re = qr/(?:site|dir|sort|lang|available)/;
703 while ($query =~ s/$simple_class_re//so) {
706 my $where = index($qpart,':');
707 my $type = substr($qpart, 0, $where++);
708 my $value = substr($qpart, $where);
710 if ($type !~ /^(?:$class_list_re|$modifier_list_re)/o) {
711 $tmp_value = "$qpart $tmp_value";
715 if ($type =~ /$class_list_re/o ) {
716 $value .= $tmp_value;
720 next unless $type and $value;
722 $value =~ s/^\s*//og;
723 $value =~ s/\s*$//og;
724 $type = 'sort_dir' if $type eq 'dir';
726 if($type eq 'site') {
727 # 'site' is the org shortname. when using this, we also want
728 # to search at the requested org's depth
729 my $e = new_editor();
730 if(my $org = $e->search_actor_org_unit({shortname => $value})->[0]) {
731 $arghash->{org_unit} = $org->id if $org;
732 $arghash->{depth} = $e->retrieve_actor_org_unit_type($org->ou_type)->depth;
734 $logger->warn("'site:' query used on invalid org shortname: $value ... ignoring");
737 } elsif($type eq 'available') {
739 $arghash->{available} = 1 unless $value eq 'false' or $value eq '0';
741 } elsif($type eq 'lang') {
742 # collect languages into an array of languages
743 $arghash->{language} = [] unless $arghash->{language};
744 push(@{$arghash->{language}}, $value);
746 } elsif($type =~ /^sort/o) {
747 # sort and sort_dir modifiers
748 $arghash->{$type} = $value;
751 # append the search term to the term under construction
752 $search->{$type} = {} unless $search->{$type};
753 $search->{$type}->{term} =
754 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $value" : $value;
758 $query .= " $tmp_value";
759 $query =~ s/\s+/ /go;
760 $query =~ s/^\s+//go;
761 $query =~ s/\s+$//go;
763 my $type = $arghash->{default_class} || 'keyword';
764 $type = ($type eq '-') ? 'keyword' : $type;
765 $type = ($type !~ /^(title|author|keyword|subject|series)(?:\|\w+)?$/o) ? 'keyword' : $type;
768 # This is the front part of the string before any special tokens were
769 # parsed OR colon-separated strings that do not denote a class.
770 # Add this data to the default search class
771 $search->{$type} = {} unless $search->{$type};
772 $search->{$type}->{term} =
773 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $query" : $query;
775 my $real_search = $arghash->{searches} = { $type => { term => $orig_query } };
777 # capture the original limit because the search method alters the limit internally
778 my $ol = $arghash->{limit};
780 my $sclient = OpenSRF::Utils::SettingsClient->new;
782 (my $method = $self->api_name) =~ s/\.query//o;
784 $method =~ s/multiclass/multiclass.staged/
785 if $sclient->config_value(apps => 'open-ils.search',
786 app_settings => 'use_staged_search') =~ /true/i;
788 $arghash->{preferred_language} = $U->get_org_locale($arghash->{org_unit})
789 unless $arghash->{preferred_language};
791 $method = $self->method_lookup($method);
792 my ($data) = $method->run($arghash, $docache);
794 $arghash->{searches} = $search if (!$data->{complex_query});
796 $arghash->{limit} = $ol if $ol;
797 $data->{compiled_search} = $arghash;
798 $data->{query} = $orig_query;
800 $logger->info("compiled search is " . OpenSRF::Utils::JSON->perl2JSON($arghash));
805 __PACKAGE__->register_method(
806 method => 'cat_search_z_style_wrapper',
807 api_name => 'open-ils.search.biblio.zstyle',
809 signature => q/@see open-ils.search.biblio.multiclass/
812 __PACKAGE__->register_method(
813 method => 'cat_search_z_style_wrapper',
814 api_name => 'open-ils.search.biblio.zstyle.staff',
816 signature => q/@see open-ils.search.biblio.multiclass/
819 sub cat_search_z_style_wrapper {
822 my $authtoken = shift;
825 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
827 my $ou = $cstore->request(
828 'open-ils.cstore.direct.actor.org_unit.search',
829 { parent_ou => undef }
832 my $result = { service => 'native-evergreen-catalog', records => [] };
833 my $searchhash = { limit => $$args{limit}, offset => $$args{offset}, org_unit => $ou->id };
835 $$searchhash{searches}{title}{term} = $$args{search}{title} if $$args{search}{title};
836 $$searchhash{searches}{author}{term} = $$args{search}{author} if $$args{search}{author};
837 $$searchhash{searches}{subject}{term} = $$args{search}{subject} if $$args{search}{subject};
838 $$searchhash{searches}{keyword}{term} = $$args{search}{keyword} if $$args{search}{keyword};
840 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{tcn} if $$args{search}{tcn};
841 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{isbn} if $$args{search}{isbn};
842 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{issn} if $$args{search}{issn};
843 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{publisher} if $$args{search}{publisher};
844 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{pubdate} if $$args{search}{pubdate};
845 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{item_type} if $$args{search}{item_type};
847 my $list = the_quest_for_knowledge( $self, $client, $searchhash );
849 if ($list->{count} > 0) {
850 $result->{count} = $list->{count};
852 my $records = $cstore->request(
853 'open-ils.cstore.direct.biblio.record_entry.search.atomic',
854 { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
857 for my $rec ( @$records ) {
859 my $u = OpenILS::Utils::ModsParser->new();
860 $u->start_mods_batch( $rec->marc );
861 my $mods = $u->finish_mods_batch();
863 push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
869 $cstore->disconnect();
873 # ----------------------------------------------------------------------------
874 # These are the main OPAC search methods
875 # ----------------------------------------------------------------------------
877 __PACKAGE__->register_method(
878 method => 'the_quest_for_knowledge',
879 api_name => 'open-ils.search.biblio.multiclass',
881 desc => "Performs a multi class biblio or metabib search",
884 desc => "A search hash with keys: "
885 . "searches, org_unit, depth, limit, offset, format, sort, sort_dir. "
886 . "See perldoc " . __PACKAGE__ . " for more detail",
890 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
895 desc => 'An object of the form: '
896 . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
901 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
903 The search-hash argument can have the following elements:
905 searches: { "$class" : "$value", ...} [REQUIRED]
906 org_unit: The org id to focus the search at
907 depth : The org depth
908 limit : The search limit default: 10
909 offset : The search offset default: 0
910 format : The MARC format
911 sort : What field to sort the results on? [ author | title | pubdate ]
912 sort_dir: What direction do we sort? [ asc | desc ]
914 The searches element is required, must have a hashref value, and the hashref must contain at least one
915 of the following classes as a key:
923 The value paired with a key is the associated search string.
925 The docache argument enables/disables searching and saving results in cache (default OFF).
927 The return object, if successful, will look like:
929 { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
933 __PACKAGE__->register_method(
934 method => 'the_quest_for_knowledge',
935 api_name => 'open-ils.search.biblio.multiclass.staff',
936 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
938 __PACKAGE__->register_method(
939 method => 'the_quest_for_knowledge',
940 api_name => 'open-ils.search.metabib.multiclass',
941 signature => q/@see open-ils.search.biblio.multiclass/
943 __PACKAGE__->register_method(
944 method => 'the_quest_for_knowledge',
945 api_name => 'open-ils.search.metabib.multiclass.staff',
946 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
949 sub the_quest_for_knowledge {
950 my( $self, $conn, $searchhash, $docache ) = @_;
952 return { count => 0 } unless $searchhash and
953 ref $searchhash->{searches} eq 'HASH';
955 my $method = 'open-ils.storage.biblio.multiclass.search_fts';
959 if($self->api_name =~ /metabib/) {
961 $method =~ s/biblio/metabib/o;
964 # do some simple sanity checking
965 if(!$searchhash->{searches} or
966 ( !grep { /^(?:title|author|subject|series|keyword)/ } keys %{$searchhash->{searches}} ) ) {
967 return { count => 0 };
970 my $offset = $searchhash->{offset} || 0; # user value or default in local var now
971 my $limit = $searchhash->{limit} || 10; # user value or default in local var now
972 my $end = $offset + $limit - 1;
975 $searchhash->{offset} = 0; # possible user value overwritten in hash
976 $searchhash->{limit} = $maxlimit; # possible user value overwritten in hash
978 return { count => 0 } if $offset > $maxlimit;
981 push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
982 my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
983 my $ckey = $pfx . md5_hex($method . $s);
985 $logger->info("bib search for: $s");
987 $searchhash->{limit} -= $offset;
991 my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
995 $method .= ".staff" if($self->api_name =~ /staff/);
996 $method .= ".atomic";
998 for (keys %$searchhash) {
999 delete $$searchhash{$_}
1000 unless defined $$searchhash{$_};
1003 $result = $U->storagereq( $method, %$searchhash );
1007 $docache = 0; # results came FROM cache, so we don't write back
1010 return {count => 0} unless ($result && $$result[0]);
1014 my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
1017 # If we didn't get this data from the cache, put it into the cache
1018 # then return the correct offset of records
1019 $logger->debug("putting search cache $ckey\n");
1020 put_cache($ckey, $count, \@recs);
1024 # if we have the full set of data, trim out
1025 # the requested chunk based on limit and offset
1027 for ($offset..$end) {
1028 last unless $recs[$_];
1029 push(@t, $recs[$_]);
1034 return { ids => \@recs, count => $count };
1038 __PACKAGE__->register_method(
1039 method => 'staged_search',
1040 api_name => 'open-ils.search.biblio.multiclass.staged',
1042 desc => 'Staged search filters out unavailable items. This means that it relies on an estimation strategy for determining ' .
1043 'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering. See "estimation_strategy" in your SRF config.',
1046 desc => "A search hash with keys: "
1047 . "searches, limit, offset. The others are optional, but the 'searches' key/value pair is required, with the value being a hashref. "
1048 . "See perldoc " . __PACKAGE__ . " for more detail",
1052 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
1057 desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids. '
1058 . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
1063 __PACKAGE__->register_method(
1064 method => 'staged_search',
1065 api_name => 'open-ils.search.biblio.multiclass.staged.staff',
1066 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1068 __PACKAGE__->register_method(
1069 method => 'staged_search',
1070 api_name => 'open-ils.search.metabib.multiclass.staged',
1071 signature => q/@see open-ils.search.biblio.multiclass.staged/
1073 __PACKAGE__->register_method(
1074 method => 'staged_search',
1075 api_name => 'open-ils.search.metabib.multiclass.staged.staff',
1076 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1080 my($self, $conn, $search_hash, $docache) = @_;
1082 my $method = ($self->api_name =~ /metabib/) ?
1083 'open-ils.storage.metabib.multiclass.staged.search_fts':
1084 'open-ils.storage.biblio.multiclass.staged.search_fts';
1086 $method .= '.staff' if $self->api_name =~ /staff$/;
1087 $method .= '.atomic';
1089 return {count => 0} unless (
1091 $search_hash->{searches} and
1092 scalar( keys %{$search_hash->{searches}} ));
1094 my $search_duration;
1095 my $user_offset = $search_hash->{offset} || 0; # user-specified offset
1096 my $user_limit = $search_hash->{limit} || 10;
1097 $user_offset = ($user_offset >= 0) ? $user_offset : 0;
1098 $user_limit = ($user_limit >= 0) ? $user_limit : 10;
1101 # we're grabbing results on a per-superpage basis, which means the
1102 # limit and offset should coincide with superpage boundaries
1103 $search_hash->{offset} = 0;
1104 $search_hash->{limit} = $superpage_size;
1106 # force a well-known check_limit
1107 $search_hash->{check_limit} = $superpage_size;
1108 # restrict total tested to superpage size * number of superpages
1109 $search_hash->{core_limit} = $superpage_size * $max_superpages;
1111 # Set the configured estimation strategy, defaults to 'inclusion'.
1112 my $estimation_strategy = OpenSRF::Utils::SettingsClient
1115 apps => 'open-ils.search', app_settings => 'estimation_strategy'
1117 $search_hash->{estimation_strategy} = $estimation_strategy;
1119 # pull any existing results from the cache
1120 my $key = search_cache_key($method, $search_hash);
1121 my $facet_key = $key.'_facets';
1122 my $cache_data = $cache->get_cache($key) || {};
1124 # keep retrieving results until we find enough to
1125 # fulfill the user-specified limit and offset
1126 my $all_results = [];
1127 my $page; # current superpage
1128 my $est_hit_count = 0;
1129 my $current_page_summary = {};
1130 my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1131 my $is_real_hit_count = 0;
1134 for($page = 0; $page < $max_superpages; $page++) {
1136 my $data = $cache_data->{$page};
1140 $logger->debug("staged search: analyzing superpage $page");
1143 # this window of results is already cached
1144 $logger->debug("staged search: found cached results");
1145 $summary = $data->{summary};
1146 $results = $data->{results};
1149 # retrieve the window of results from the database
1150 $logger->debug("staged search: fetching results from the database");
1151 $search_hash->{skip_check} = $page * $superpage_size;
1153 $results = $U->storagereq($method, %$search_hash);
1154 $search_duration = time - $start;
1155 $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1156 $summary = shift(@$results) if $results;
1159 $logger->info("search timed out: duration=$search_duration: params=".
1160 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1161 return {count => 0};
1164 my $hc = $summary->{estimated_hit_count} || $summary->{visible};
1166 $logger->info("search returned 0 results: duration=$search_duration: params=".
1167 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1170 # Create backwards-compatible result structures
1171 if($self->api_name =~ /biblio/) {
1172 $results = [map {[$_->{id}]} @$results];
1174 $results = [map {[$_->{id}, $_->{rel}, $_->{record}]} @$results];
1177 push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1178 $results = [grep {defined $_->[0]} @$results];
1179 cache_staged_search_page($key, $page, $summary, $results) if $docache;
1182 $current_page_summary = $summary;
1184 # add the new set of results to the set under construction
1185 push(@$all_results, @$results);
1187 my $current_count = scalar(@$all_results);
1189 $est_hit_count = $summary->{estimated_hit_count} || $summary->{visible}
1192 $logger->debug("staged search: located $current_count, with estimated hits=".
1193 $summary->{estimated_hit_count}." : visible=".$summary->{visible}.", checked=".$summary->{checked});
1195 if (defined($summary->{estimated_hit_count})) {
1196 foreach (qw/ checked visible excluded deleted /) {
1197 $global_summary->{$_} += $summary->{$_};
1199 $global_summary->{total} = $summary->{total};
1202 # we've found all the possible hits
1203 last if $current_count == $summary->{visible}
1204 and not defined $summary->{estimated_hit_count};
1206 # we've found enough results to satisfy the requested limit/offset
1207 last if $current_count >= ($user_limit + $user_offset);
1209 # we've scanned all possible hits
1210 if($summary->{checked} < $superpage_size) {
1211 $est_hit_count = scalar(@$all_results);
1212 # we have all possible results in hand, so we know the final hit count
1213 $is_real_hit_count = 1;
1218 my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1220 # refine the estimate if we have more than one superpage
1221 if ($page > 0 and not $is_real_hit_count) {
1222 if ($global_summary->{checked} >= $global_summary->{total}) {
1223 $est_hit_count = $global_summary->{visible};
1225 my $updated_hit_count = $U->storagereq(
1226 'open-ils.storage.fts_paging_estimate',
1227 $global_summary->{checked},
1228 $global_summary->{visible},
1229 $global_summary->{excluded},
1230 $global_summary->{deleted},
1231 $global_summary->{total}
1233 $est_hit_count = $updated_hit_count->{$estimation_strategy};
1237 $conn->respond_complete(
1239 count => $est_hit_count,
1240 core_limit => $search_hash->{core_limit},
1241 superpage_size => $search_hash->{check_limit},
1242 superpage_summary => $current_page_summary,
1243 facet_key => $facet_key,
1248 cache_facets($facet_key, $new_ids, ($self->api_name =~ /metabib/) ? 1 : 0) if $docache;
1253 # creates a unique token to represent the query in the cache
1254 sub search_cache_key {
1256 my $search_hash = shift;
1258 for my $key (sort keys %$search_hash) {
1259 push(@sorted, ($key => $$search_hash{$key}))
1260 unless $key eq 'limit' or
1262 $key eq 'skip_check';
1264 my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1265 return $pfx . md5_hex($method . $s);
1268 sub retrieve_cached_facets {
1273 return undef unless ($key and $key =~ /_facets$/);
1275 return $cache->get_cache($key) || {};
1278 __PACKAGE__->register_method(
1279 method => "retrieve_cached_facets",
1280 api_name => "open-ils.search.facet_cache.retrieve"
1285 # add facets for this search to the facet cache
1286 my($key, $results, $metabib) = @_;
1287 my $data = $cache->get_cache($key);
1290 return undef unless (@$results);
1292 # The query we're constructing
1296 # count(distinct mmrsm.appropriate-id-field )
1297 # from metabib.facet_entry mfae
1298 # join config.metabib_field cmf on (mfae.field = cmf.id)
1299 # join metabib.metarecord_sourc_map mmrsm on (mfae.source = mmrsm.source)
1300 # where cmf.facet_field
1301 # and mmrsm.appropriate-id-field in IDLIST
1304 my $count_field = $metabib ? 'metarecord' : 'source';
1305 my $facets = $U->cstorereq( "open-ils.cstore.json_query.atomic",
1308 mfae => [ 'value' ],
1310 transform => 'count',
1312 column => $count_field,
1319 cmf => { field => 'id', fkey => 'field' },
1320 mmrsm => { field => 'source', fkey => 'source' }
1324 '+cmf' => 'facet_field',
1325 '+mmrsm' => { $count_field => $results }
1330 for my $facet (@$facets) {
1331 next unless ($facet->{value});
1332 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1335 $logger->info("facet compilation: cached with key=$key");
1337 $cache->put_cache($key, $data, $cache_timeout);
1340 sub cache_staged_search_page {
1341 # puts this set of results into the cache
1342 my($key, $page, $summary, $results) = @_;
1343 my $data = $cache->get_cache($key);
1346 summary => $summary,
1350 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1351 $summary->{estimated_hit_count}.", visible=".$summary->{visible});
1353 $cache->put_cache($key, $data, $cache_timeout);
1361 my $start = $offset;
1362 my $end = $offset + $limit - 1;
1364 $logger->debug("searching cache for $key : $start..$end\n");
1366 return undef unless $cache;
1367 my $data = $cache->get_cache($key);
1369 return undef unless $data;
1371 my $count = $data->[0];
1374 return undef unless $offset < $count;
1377 for( my $i = $offset; $i <= $end; $i++ ) {
1378 last unless my $d = $$data[$i];
1379 push( @result, $d );
1382 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1389 my( $key, $count, $data ) = @_;
1390 return undef unless $cache;
1391 $logger->debug("search_cache putting ".
1392 scalar(@$data)." items at key $key with timeout $cache_timeout");
1393 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1397 __PACKAGE__->register_method(
1398 method => "biblio_mrid_to_modsbatch_batch",
1399 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1402 sub biblio_mrid_to_modsbatch_batch {
1403 my( $self, $client, $mrids) = @_;
1404 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1406 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1407 for my $id (@$mrids) {
1408 next unless defined $id;
1409 my ($m) = $method->run($id);
1416 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1417 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1419 __PACKAGE__->register_method(
1420 method => "biblio_mrid_to_modsbatch",
1423 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1424 . "As usual, the .staff version of this method will include otherwise hidden records.",
1426 { desc => 'Metarecord ID', type => 'number' },
1427 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1430 desc => 'MVR Object, event on error',
1436 sub biblio_mrid_to_modsbatch {
1437 my( $self, $client, $mrid, $args) = @_;
1439 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1441 my ($mr, $evt) = _grab_metarecord($mrid);
1442 return $evt unless $mr;
1444 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1445 biblio_mrid_make_modsbatch($self, $client, $mr);
1447 return $mvr unless ref($args);
1449 # Here we find the lead record appropriate for the given filters
1450 # and use that for the title and author of the metarecord
1451 my $format = $$args{format};
1452 my $org = $$args{org};
1453 my $depth = $$args{depth};
1455 return $mvr unless $format or $org or $depth;
1457 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1458 $method = "$method.staff" if $self->api_name =~ /staff/o;
1460 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1462 if( my $mods = $U->record_to_mvr($rec) ) {
1464 $mvr->title( $mods->title );
1465 $mvr->author($mods->author);
1466 $logger->debug("mods_slim updating title and ".
1467 "author in mvr with ".$mods->title." : ".$mods->author);
1473 # converts a metarecord to an mvr
1476 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1477 return Fieldmapper::metabib::virtual_record->new($perl);
1480 # checks to see if a metarecord has mods, if so returns true;
1482 __PACKAGE__->register_method(
1483 method => "biblio_mrid_check_mvr",
1484 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1485 notes => "Takes a metarecord ID or a metarecord object and returns true "
1486 . "if the metarecord already has an mvr associated with it."
1489 sub biblio_mrid_check_mvr {
1490 my( $self, $client, $mrid ) = @_;
1494 if(ref($mrid)) { $mr = $mrid; }
1495 else { ($mr, $evt) = _grab_metarecord($mrid); }
1496 return $evt if $evt;
1498 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1500 return _mr_to_mvr($mr) if $mr->mods();
1504 sub _grab_metarecord {
1506 #my $e = OpenILS::Utils::Editor->new;
1507 my $e = new_editor();
1508 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1513 __PACKAGE__->register_method(
1514 method => "biblio_mrid_make_modsbatch",
1515 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1516 notes => "Takes either a metarecord ID or a metarecord object. "
1517 . "Forces the creations of an mvr for the given metarecord. "
1518 . "The created mvr is returned."
1521 sub biblio_mrid_make_modsbatch {
1522 my( $self, $client, $mrid ) = @_;
1524 #my $e = OpenILS::Utils::Editor->new;
1525 my $e = new_editor();
1532 $mr = $e->retrieve_metabib_metarecord($mrid)
1533 or return $e->event;
1536 my $masterid = $mr->master_record;
1537 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1539 my $ids = $U->storagereq(
1540 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1541 return undef unless @$ids;
1543 my $master = $e->retrieve_biblio_record_entry($masterid)
1544 or return $e->event;
1546 # start the mods batch
1547 my $u = OpenILS::Utils::ModsParser->new();
1548 $u->start_mods_batch( $master->marc );
1550 # grab all of the sub-records and shove them into the batch
1551 my @ids = grep { $_ ne $masterid } @$ids;
1552 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1557 my $r = $e->retrieve_biblio_record_entry($i);
1558 push( @$subrecs, $r ) if $r;
1563 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1564 $u->push_mods_batch( $_->marc ) if $_->marc;
1568 # finish up and send to the client
1569 my $mods = $u->finish_mods_batch();
1570 $mods->doc_id($mrid);
1571 $client->respond_complete($mods);
1574 # now update the mods string in the db
1575 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1578 #$e = OpenILS::Utils::Editor->new(xact => 1);
1579 $e = new_editor(xact => 1);
1580 $e->update_metabib_metarecord($mr)
1581 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1588 # converts a mr id into a list of record ids
1590 foreach (qw/open-ils.search.biblio.metarecord_to_records
1591 open-ils.search.biblio.metarecord_to_records.staff/)
1593 __PACKAGE__->register_method(
1594 method => "biblio_mrid_to_record_ids",
1597 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1598 . "As usual, the .staff version of this method will include otherwise hidden records.",
1600 { desc => 'Metarecord ID', type => 'number' },
1601 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1604 desc => 'Results object like {count => $i, ids =>[...]}',
1612 sub biblio_mrid_to_record_ids {
1613 my( $self, $client, $mrid, $args ) = @_;
1615 my $format = $$args{format};
1616 my $org = $$args{org};
1617 my $depth = $$args{depth};
1619 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1620 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1621 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1623 return { count => scalar(@$recs), ids => $recs };
1627 __PACKAGE__->register_method(
1628 method => "biblio_record_to_marc_html",
1629 api_name => "open-ils.search.biblio.record.html"
1632 __PACKAGE__->register_method(
1633 method => "biblio_record_to_marc_html",
1634 api_name => "open-ils.search.authority.to_html"
1637 # Persistent parsers and setting objects
1638 my $parser = XML::LibXML->new();
1639 my $xslt = XML::LibXSLT->new();
1641 my $slim_marc_sheet;
1642 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1644 sub biblio_record_to_marc_html {
1645 my($self, $client, $recordid, $slim, $marcxml) = @_;
1648 my $dir = $settings_client->config_value("dirs", "xsl");
1651 unless($slim_marc_sheet) {
1652 my $xsl = $settings_client->config_value(
1653 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1655 $xsl = $parser->parse_file("$dir/$xsl");
1656 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1659 $sheet = $slim_marc_sheet;
1663 unless($marc_sheet) {
1664 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1665 my $xsl = $settings_client->config_value(
1666 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1667 $xsl = $parser->parse_file("$dir/$xsl");
1668 $marc_sheet = $xslt->parse_stylesheet($xsl);
1670 $sheet = $marc_sheet;
1675 my $e = new_editor();
1676 if($self->api_name =~ /authority/) {
1677 $record = $e->retrieve_authority_record_entry($recordid)
1678 or return $e->event;
1680 $record = $e->retrieve_biblio_record_entry($recordid)
1681 or return $e->event;
1683 $marcxml = $record->marc;
1686 my $xmldoc = $parser->parse_string($marcxml);
1687 my $html = $sheet->transform($xmldoc);
1688 return $html->documentElement->toString();
1691 __PACKAGE__->register_method(
1692 method => "format_biblio_record_entry",
1693 api_name => "open-ils.search.biblio.record.print",
1695 desc => 'Returns a printable version of the specified bib record',
1697 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1700 desc => q/An action_trigger.event object or error event./,
1705 __PACKAGE__->register_method(
1706 method => "format_biblio_record_entry",
1707 api_name => "open-ils.search.biblio.record.email",
1709 desc => 'Emails an A/T templated version of the specified bib records to the authorized user',
1711 { desc => 'Authentication token', type => 'string'},
1712 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1715 desc => q/Undefined on success, otherwise an error event./,
1721 sub format_biblio_record_entry {
1722 my($self, $conn, $arg1, $arg2) = @_;
1724 my $for_print = ($self->api_name =~ /print/);
1725 my $for_email = ($self->api_name =~ /email/);
1727 my $e; my $auth; my $bib_id; my $context_org;
1731 $context_org = $arg2 || $U->fetch_org_tree->id;
1732 $e = new_editor(xact => 1);
1733 } elsif ($for_email) {
1736 $e = new_editor(authtoken => $auth, xact => 1);
1737 return $e->die_event unless $e->checkauth;
1738 $context_org = $e->requestor->home_ou;
1742 if (ref $bib_id ne 'ARRAY') {
1743 $bib_ids = [ $bib_id ];
1748 my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1749 $bucket->btype('temp');
1750 $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1752 $bucket->owner($e->requestor)
1756 my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1758 for my $id (@$bib_ids) {
1760 my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1762 my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1763 $bucket_entry->target_biblio_record_entry($bib);
1764 $bucket_entry->bucket($bucket_obj->id);
1765 $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
1772 return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
1774 } elsif ($for_email) {
1776 $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
1783 __PACKAGE__->register_method(
1784 method => "retrieve_all_copy_statuses",
1785 api_name => "open-ils.search.config.copy_status.retrieve.all"
1788 sub retrieve_all_copy_statuses {
1789 my( $self, $client ) = @_;
1790 return new_editor()->retrieve_all_config_copy_status();
1794 __PACKAGE__->register_method(
1795 method => "copy_counts_per_org",
1796 api_name => "open-ils.search.biblio.copy_counts.retrieve"
1799 __PACKAGE__->register_method(
1800 method => "copy_counts_per_org",
1801 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1804 sub copy_counts_per_org {
1805 my( $self, $client, $record_id ) = @_;
1807 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1809 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1810 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1812 my $counts = $apputils->simple_scalar_request(
1813 "open-ils.storage", $method, $record_id );
1815 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1820 __PACKAGE__->register_method(
1821 method => "copy_count_summary",
1822 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
1823 notes => "returns an array of these: "
1824 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1825 . "where statusx is a copy status name. The statuses are sorted by ID.",
1829 sub copy_count_summary {
1830 my( $self, $client, $rid, $org, $depth ) = @_;
1833 my $data = $U->storagereq(
1834 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
1836 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1839 __PACKAGE__->register_method(
1840 method => "copy_location_count_summary",
1841 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
1842 notes => "returns an array of these: "
1843 . "[ org_id, callnumber_label, copy_location, <status1_count>, <status2_count>,...] "
1844 . "where statusx is a copy status name. The statuses are sorted by ID.",
1847 sub copy_location_count_summary {
1848 my( $self, $client, $rid, $org, $depth ) = @_;
1851 my $data = $U->storagereq(
1852 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1854 return [ sort { $a->[1] cmp $b->[1] || $a->[2] cmp $b->[2] } @$data ];
1857 __PACKAGE__->register_method(
1858 method => "copy_count_location_summary",
1859 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
1860 notes => "returns an array of these: "
1861 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1862 . "where statusx is a copy status name. The statuses are sorted by ID."
1865 sub copy_count_location_summary {
1866 my( $self, $client, $rid, $org, $depth ) = @_;
1869 my $data = $U->storagereq(
1870 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1871 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1875 foreach (qw/open-ils.search.biblio.marc
1876 open-ils.search.biblio.marc.staff/)
1878 __PACKAGE__->register_method(
1879 method => "marc_search",
1882 desc => 'Fetch biblio IDs based on MARC record criteria. '
1883 . 'As usual, the .staff version of the search includes otherwise hidden records',
1886 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
1887 'See perldoc ' . __PACKAGE__ . ' for more detail.',
1890 {desc => 'limit (optional)', type => 'number'},
1891 {desc => 'offset (optional)', type => 'number'}
1894 desc => 'Results object like: { "count": $i, "ids": [...] }',
1901 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
1903 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
1905 searches: complex query object (required)
1906 org_unit: The org ID to focus the search at
1907 depth : The org depth
1908 limit : integer search limit default: 10
1909 offset : integer search offset default: 0
1910 sort : What field to sort the results on? [ author | title | pubdate ]
1911 sort_dir: In what direction do we sort? [ asc | desc ]
1913 Additional keys to refine search criteria:
1916 language : Language (code)
1917 lit_form : Literary form
1918 item_form: Item form
1919 item_type: Item type
1920 format : The MARC format
1922 Please note that the specific strings to be used in the "addtional keys" will be entirely
1923 dependent on your loaded data.
1925 All keys except "searches" are optional.
1926 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
1928 For example, an arg hash might look like:
1950 The arghash is eventually passed to the SRF call:
1951 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
1953 Presently, search uses the cache unconditionally.
1957 # FIXME: that example above isn't actually tested.
1958 # TODO: docache option?
1960 my( $self, $conn, $args, $limit, $offset ) = @_;
1962 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
1963 $method .= ".staff" if $self->api_name =~ /staff/;
1964 $method .= ".atomic";
1966 $limit ||= 10; # FIXME: what about $args->{limit} ?
1967 $offset ||= 0; # FIXME: what about $args->{offset} ?
1970 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
1971 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
1973 my $recs = search_cache($ckey, $offset, $limit);
1976 $recs = $U->storagereq($method, %$args) || [];
1978 put_cache($ckey, scalar(@$recs), $recs);
1979 $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
1986 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
1987 my @recs = map { $_->[0] } @$recs;
1989 return { ids => \@recs, count => $count };
1993 __PACKAGE__->register_method(
1994 method => "biblio_search_isbn",
1995 api_name => "open-ils.search.biblio.isbn",
1997 desc => 'Retrieve biblio IDs for a given ISBN',
1999 {desc => 'ISBN', type => 'string'} # or number maybe? How normalized is our storage data?
2002 desc => 'Results object like: { "count": $i, "ids": [...] }',
2008 sub biblio_search_isbn {
2009 my( $self, $client, $isbn ) = @_;
2010 $logger->debug("Searching ISBN $isbn");
2011 my $recs = $U->storagereq('open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn);
2012 return { ids => $recs, count => scalar(@$recs) };
2015 __PACKAGE__->register_method(
2016 method => "biblio_search_isbn_batch",
2017 api_name => "open-ils.search.biblio.isbn_list",
2020 sub biblio_search_isbn_batch {
2021 my( $self, $client, $isbn_list ) = @_;
2022 $logger->debug("Searching ISBNs @$isbn_list");
2023 my @recs = (); my %rec_set = ();
2024 foreach my $isbn ( @$isbn_list ) {
2025 foreach my $rec ( @{ $U->storagereq(
2026 'open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn )
2028 if (! $rec_set{ $rec }) {
2029 $rec_set{ $rec } = 1;
2034 return { ids => \@recs, count => scalar(@recs) };
2037 __PACKAGE__->register_method(
2038 method => "biblio_search_issn",
2039 api_name => "open-ils.search.biblio.issn",
2041 desc => 'Retrieve biblio IDs for a given ISSN',
2043 {desc => 'ISBN', type => 'string'}
2046 desc => 'Results object like: { "count": $i, "ids": [...] }',
2052 sub biblio_search_issn {
2053 my( $self, $client, $issn ) = @_;
2054 $logger->debug("Searching ISSN $issn");
2055 my $e = new_editor();
2057 my $recs = $U->storagereq(
2058 'open-ils.storage.id_list.biblio.record_entry.search.issn.atomic', $issn );
2059 return { ids => $recs, count => scalar(@$recs) };
2063 __PACKAGE__->register_method(
2064 method => "fetch_mods_by_copy",
2065 api_name => "open-ils.search.biblio.mods_from_copy",
2068 desc => 'Retrieve MODS record given an attached copy ID',
2070 { desc => 'Copy ID', type => 'number' }
2073 desc => 'MODS record, event on error or uncataloged item'
2078 sub fetch_mods_by_copy {
2079 my( $self, $client, $copyid ) = @_;
2080 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2081 return $evt if $evt;
2082 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2083 return $apputils->record_to_mvr($record);
2087 # -------------------------------------------------------------------------------------
2089 __PACKAGE__->register_method(
2090 method => "cn_browse",
2091 api_name => "open-ils.search.callnumber.browse.target",
2092 notes => "Starts a callnumber browse"
2095 __PACKAGE__->register_method(
2096 method => "cn_browse",
2097 api_name => "open-ils.search.callnumber.browse.page_up",
2098 notes => "Returns the previous page of callnumbers",
2101 __PACKAGE__->register_method(
2102 method => "cn_browse",
2103 api_name => "open-ils.search.callnumber.browse.page_down",
2104 notes => "Returns the next page of callnumbers",
2108 # RETURNS array of arrays like so: label, owning_lib, record, id
2110 my( $self, $client, @params ) = @_;
2113 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
2114 if( $self->api_name =~ /target/ );
2115 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2116 if( $self->api_name =~ /page_up/ );
2117 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2118 if( $self->api_name =~ /page_down/ );
2120 return $apputils->simplereq( 'open-ils.storage', $method, @params );
2122 # -------------------------------------------------------------------------------------
2124 __PACKAGE__->register_method(
2125 method => "fetch_cn",
2126 api_name => "open-ils.search.callnumber.retrieve",
2128 notes => "retrieves a callnumber based on ID",
2132 my( $self, $client, $id ) = @_;
2133 my( $cn, $evt ) = $apputils->fetch_callnumber( $id );
2134 return $evt if $evt;
2138 __PACKAGE__->register_method(
2139 method => "fetch_copy_by_cn",
2140 api_name => 'open-ils.search.copies_by_call_number.retrieve',
2142 Returns an array of copy ID's by callnumber ID
2143 @param cnid The callnumber ID
2144 @return An array of copy IDs
2148 sub fetch_copy_by_cn {
2149 my( $self, $conn, $cnid ) = @_;
2150 return $U->cstorereq(
2151 'open-ils.cstore.direct.asset.copy.id_list.atomic',
2152 { call_number => $cnid, deleted => 'f' } );
2155 __PACKAGE__->register_method(
2156 method => 'fetch_cn_by_info',
2157 api_name => 'open-ils.search.call_number.retrieve_by_info',
2159 @param label The callnumber label
2160 @param record The record the cn is attached to
2161 @param org The owning library of the cn
2162 @return The callnumber object
2167 sub fetch_cn_by_info {
2168 my( $self, $conn, $label, $record, $org ) = @_;
2169 return $U->cstorereq(
2170 'open-ils.cstore.direct.asset.call_number.search',
2171 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2176 __PACKAGE__->register_method(
2177 method => 'bib_extras',
2178 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all'
2180 __PACKAGE__->register_method(
2181 method => 'bib_extras',
2182 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all'
2184 __PACKAGE__->register_method(
2185 method => 'bib_extras',
2186 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all'
2188 __PACKAGE__->register_method(
2189 method => 'bib_extras',
2190 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all'
2192 __PACKAGE__->register_method(
2193 method => 'bib_extras',
2194 api_name => 'open-ils.search.biblio.audience_map.retrieve.all'
2200 my $e = new_editor();
2202 return $e->retrieve_all_config_lit_form_map()
2203 if( $self->api_name =~ /lit_form/ );
2205 return $e->retrieve_all_config_item_form_map()
2206 if( $self->api_name =~ /item_form_map/ );
2208 return $e->retrieve_all_config_item_type_map()
2209 if( $self->api_name =~ /item_type_map/ );
2211 return $e->retrieve_all_config_bib_level_map()
2212 if( $self->api_name =~ /bib_level_map/ );
2214 return $e->retrieve_all_config_audience_map()
2215 if( $self->api_name =~ /audience_map/ );
2222 __PACKAGE__->register_method(
2223 method => 'fetch_slim_record',
2224 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2226 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2228 { desc => 'Array of Record IDs', type => 'array' }
2231 desc => 'Array of biblio records, event on error'
2236 sub fetch_slim_record {
2237 my( $self, $conn, $ids ) = @_;
2239 #my $editor = OpenILS::Utils::Editor->new;
2240 my $editor = new_editor();
2243 return $editor->event unless
2244 my $r = $editor->retrieve_biblio_record_entry($_);
2253 __PACKAGE__->register_method(
2254 method => 'rec_to_mr_rec_descriptors',
2255 api_name => 'open-ils.search.metabib.record_to_descriptors',
2257 specialized method...
2258 Given a biblio record id or a metarecord id,
2259 this returns a list of metabib.record_descriptor
2260 objects that live within the same metarecord
2261 @param args Object of args including:
2265 sub rec_to_mr_rec_descriptors {
2266 my( $self, $conn, $args ) = @_;
2268 my $rec = $$args{record};
2269 my $mrec = $$args{metarecord};
2270 my $item_forms = $$args{item_forms};
2271 my $item_types = $$args{item_types};
2272 my $item_lang = $$args{item_lang};
2274 my $e = new_editor();
2278 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2279 return $e->event unless @$map;
2280 $mrec = $$map[0]->metarecord;
2283 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2284 return $e->event unless @$recs;
2286 my @recs = map { $_->source } @$recs;
2287 my $search = { record => \@recs };
2288 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2289 $search->{item_type} = $item_types if $item_types and @$item_types;
2290 $search->{item_lang} = $item_lang if $item_lang;
2292 my $desc = $e->search_metabib_record_descriptor($search);
2294 return { metarecord => $mrec, descriptors => $desc };
2298 __PACKAGE__->register_method(
2299 method => 'fetch_age_protect',
2300 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2303 sub fetch_age_protect {
2304 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2308 __PACKAGE__->register_method(
2309 method => 'copies_by_cn_label',
2310 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2313 __PACKAGE__->register_method(
2314 method => 'copies_by_cn_label',
2315 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2318 sub copies_by_cn_label {
2319 my( $self, $conn, $record, $label, $circ_lib ) = @_;
2320 my $e = new_editor();
2321 my $cns = $e->search_asset_call_number({record => $record, label => $label, deleted => 'f'}, {idlist=>1});
2322 return [] unless @$cns;
2324 # show all non-deleted copies in the staff client ...
2325 if ($self->api_name =~ /staff$/o) {
2326 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2329 # ... otherwise, grab the copies ...
2330 my $copies = $e->search_asset_copy(
2331 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2332 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2336 # ... and test for location and status visibility
2337 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];