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 ]
913 tag_circulated_records : Boolean, if true, records that are in the user's visible checkout history
914 will be tagged with an additional value ("1") as the last value in the record ID array for
915 each record. Requires the 'authtoken'
916 authtoken : Authentication token string; When actions are performed that require a user login
917 (e.g. tagging circulated records), the authentication token is required
919 The searches element is required, must have a hashref value, and the hashref must contain at least one
920 of the following classes as a key:
928 The value paired with a key is the associated search string.
930 The docache argument enables/disables searching and saving results in cache (default OFF).
932 The return object, if successful, will look like:
934 { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
938 __PACKAGE__->register_method(
939 method => 'the_quest_for_knowledge',
940 api_name => 'open-ils.search.biblio.multiclass.staff',
941 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
943 __PACKAGE__->register_method(
944 method => 'the_quest_for_knowledge',
945 api_name => 'open-ils.search.metabib.multiclass',
946 signature => q/@see open-ils.search.biblio.multiclass/
948 __PACKAGE__->register_method(
949 method => 'the_quest_for_knowledge',
950 api_name => 'open-ils.search.metabib.multiclass.staff',
951 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
954 sub the_quest_for_knowledge {
955 my( $self, $conn, $searchhash, $docache ) = @_;
957 return { count => 0 } unless $searchhash and
958 ref $searchhash->{searches} eq 'HASH';
960 my $method = 'open-ils.storage.biblio.multiclass.search_fts';
964 if($self->api_name =~ /metabib/) {
966 $method =~ s/biblio/metabib/o;
969 # do some simple sanity checking
970 if(!$searchhash->{searches} or
971 ( !grep { /^(?:title|author|subject|series|keyword)/ } keys %{$searchhash->{searches}} ) ) {
972 return { count => 0 };
975 my $offset = $searchhash->{offset} || 0; # user value or default in local var now
976 my $limit = $searchhash->{limit} || 10; # user value or default in local var now
977 my $end = $offset + $limit - 1;
980 $searchhash->{offset} = 0; # possible user value overwritten in hash
981 $searchhash->{limit} = $maxlimit; # possible user value overwritten in hash
983 return { count => 0 } if $offset > $maxlimit;
986 push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
987 my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
988 my $ckey = $pfx . md5_hex($method . $s);
990 $logger->info("bib search for: $s");
992 $searchhash->{limit} -= $offset;
996 my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
1000 $method .= ".staff" if($self->api_name =~ /staff/);
1001 $method .= ".atomic";
1003 for (keys %$searchhash) {
1004 delete $$searchhash{$_}
1005 unless defined $$searchhash{$_};
1008 $result = $U->storagereq( $method, %$searchhash );
1012 $docache = 0; # results came FROM cache, so we don't write back
1015 return {count => 0} unless ($result && $$result[0]);
1019 my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
1022 # If we didn't get this data from the cache, put it into the cache
1023 # then return the correct offset of records
1024 $logger->debug("putting search cache $ckey\n");
1025 put_cache($ckey, $count, \@recs);
1029 # if we have the full set of data, trim out
1030 # the requested chunk based on limit and offset
1032 for ($offset..$end) {
1033 last unless $recs[$_];
1034 push(@t, $recs[$_]);
1039 return { ids => \@recs, count => $count };
1043 __PACKAGE__->register_method(
1044 method => 'staged_search',
1045 api_name => 'open-ils.search.biblio.multiclass.staged',
1047 desc => 'Staged search filters out unavailable items. This means that it relies on an estimation strategy for determining ' .
1048 'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering. See "estimation_strategy" in your SRF config.',
1051 desc => "A search hash with keys: "
1052 . "searches, limit, offset. The others are optional, but the 'searches' key/value pair is required, with the value being a hashref. "
1053 . "See perldoc " . __PACKAGE__ . " for more detail",
1057 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
1062 desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids. '
1063 . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
1068 __PACKAGE__->register_method(
1069 method => 'staged_search',
1070 api_name => 'open-ils.search.biblio.multiclass.staged.staff',
1071 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1073 __PACKAGE__->register_method(
1074 method => 'staged_search',
1075 api_name => 'open-ils.search.metabib.multiclass.staged',
1076 signature => q/@see open-ils.search.biblio.multiclass.staged/
1078 __PACKAGE__->register_method(
1079 method => 'staged_search',
1080 api_name => 'open-ils.search.metabib.multiclass.staged.staff',
1081 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1085 my($self, $conn, $search_hash, $docache) = @_;
1087 my $IAmMetabib = ($self->api_name =~ /metabib/) ? 1 : 0;
1089 my $method = $IAmMetabib?
1090 'open-ils.storage.metabib.multiclass.staged.search_fts':
1091 'open-ils.storage.biblio.multiclass.staged.search_fts';
1093 $method .= '.staff' if $self->api_name =~ /staff$/;
1094 $method .= '.atomic';
1096 return {count => 0} unless (
1098 $search_hash->{searches} and
1099 scalar( keys %{$search_hash->{searches}} ));
1101 my $search_duration;
1102 my $user_offset = $search_hash->{offset} || 0; # user-specified offset
1103 my $user_limit = $search_hash->{limit} || 10;
1104 $user_offset = ($user_offset >= 0) ? $user_offset : 0;
1105 $user_limit = ($user_limit >= 0) ? $user_limit : 10;
1108 # we're grabbing results on a per-superpage basis, which means the
1109 # limit and offset should coincide with superpage boundaries
1110 $search_hash->{offset} = 0;
1111 $search_hash->{limit} = $superpage_size;
1113 # force a well-known check_limit
1114 $search_hash->{check_limit} = $superpage_size;
1115 # restrict total tested to superpage size * number of superpages
1116 $search_hash->{core_limit} = $superpage_size * $max_superpages;
1118 # Set the configured estimation strategy, defaults to 'inclusion'.
1119 my $estimation_strategy = OpenSRF::Utils::SettingsClient
1122 apps => 'open-ils.search', app_settings => 'estimation_strategy'
1124 $search_hash->{estimation_strategy} = $estimation_strategy;
1126 # pull any existing results from the cache
1127 my $key = search_cache_key($method, $search_hash);
1128 my $facet_key = $key.'_facets';
1129 my $cache_data = $cache->get_cache($key) || {};
1131 # keep retrieving results until we find enough to
1132 # fulfill the user-specified limit and offset
1133 my $all_results = [];
1134 my $page; # current superpage
1135 my $est_hit_count = 0;
1136 my $current_page_summary = {};
1137 my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1138 my $is_real_hit_count = 0;
1141 for($page = 0; $page < $max_superpages; $page++) {
1143 my $data = $cache_data->{$page};
1147 $logger->debug("staged search: analyzing superpage $page");
1150 # this window of results is already cached
1151 $logger->debug("staged search: found cached results");
1152 $summary = $data->{summary};
1153 $results = $data->{results};
1156 # retrieve the window of results from the database
1157 $logger->debug("staged search: fetching results from the database");
1158 $search_hash->{skip_check} = $page * $superpage_size;
1160 $results = $U->storagereq($method, %$search_hash);
1161 $search_duration = time - $start;
1162 $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1163 $summary = shift(@$results) if $results;
1166 $logger->info("search timed out: duration=$search_duration: params=".
1167 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1168 return {count => 0};
1171 my $hc = $summary->{estimated_hit_count} || $summary->{visible};
1173 $logger->info("search returned 0 results: duration=$search_duration: params=".
1174 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1177 # Create backwards-compatible result structures
1179 $results = [map {[$_->{id}, $_->{rel}, $_->{record}]} @$results];
1181 $results = [map {[$_->{id}]} @$results];
1184 tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib)
1185 if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1187 push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1188 $results = [grep {defined $_->[0]} @$results];
1189 cache_staged_search_page($key, $page, $summary, $results) if $docache;
1192 $current_page_summary = $summary;
1194 # add the new set of results to the set under construction
1195 push(@$all_results, @$results);
1197 my $current_count = scalar(@$all_results);
1199 $est_hit_count = $summary->{estimated_hit_count} || $summary->{visible}
1202 $logger->debug("staged search: located $current_count, with estimated hits=".
1203 $summary->{estimated_hit_count}." : visible=".$summary->{visible}.", checked=".$summary->{checked});
1205 if (defined($summary->{estimated_hit_count})) {
1206 foreach (qw/ checked visible excluded deleted /) {
1207 $global_summary->{$_} += $summary->{$_};
1209 $global_summary->{total} = $summary->{total};
1212 # we've found all the possible hits
1213 last if $current_count == $summary->{visible}
1214 and not defined $summary->{estimated_hit_count};
1216 # we've found enough results to satisfy the requested limit/offset
1217 last if $current_count >= ($user_limit + $user_offset);
1219 # we've scanned all possible hits
1220 if($summary->{checked} < $superpage_size) {
1221 $est_hit_count = scalar(@$all_results);
1222 # we have all possible results in hand, so we know the final hit count
1223 $is_real_hit_count = 1;
1228 my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1230 # refine the estimate if we have more than one superpage
1231 if ($page > 0 and not $is_real_hit_count) {
1232 if ($global_summary->{checked} >= $global_summary->{total}) {
1233 $est_hit_count = $global_summary->{visible};
1235 my $updated_hit_count = $U->storagereq(
1236 'open-ils.storage.fts_paging_estimate',
1237 $global_summary->{checked},
1238 $global_summary->{visible},
1239 $global_summary->{excluded},
1240 $global_summary->{deleted},
1241 $global_summary->{total}
1243 $est_hit_count = $updated_hit_count->{$estimation_strategy};
1247 $conn->respond_complete(
1249 count => $est_hit_count,
1250 core_limit => $search_hash->{core_limit},
1251 superpage_size => $search_hash->{check_limit},
1252 superpage_summary => $current_page_summary,
1253 facet_key => $facet_key,
1258 cache_facets($facet_key, $new_ids, $IAmMetabib) if $docache;
1263 sub tag_circulated_records {
1264 my ($auth, $results, $metabib) = @_;
1265 my $e = new_editor(authtoken => $auth);
1266 return $results unless $e->checkauth;
1269 select => { acn => [{ column => 'record', alias => 'tagme' }] },
1270 from => { acp => 'acn' },
1271 where => { id => { in => { from => ['action.usr_visible_circ_copies', $e->requestor->id] } } },
1277 select => { mmsm => [{ column => 'metarecord', alias => 'tagme' }] },
1279 where => { source => { in => $query } },
1284 # Give me the distinct set of bib records that exist in the user's visible circulation history
1285 my $circ_recs = $e->json_query( $query );
1287 # if the record appears in the circ history, push a 1 onto
1288 # the rec array structure to indicate truthiness
1289 for my $rec (@$results) {
1290 push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1296 # creates a unique token to represent the query in the cache
1297 sub search_cache_key {
1299 my $search_hash = shift;
1301 for my $key (sort keys %$search_hash) {
1302 push(@sorted, ($key => $$search_hash{$key}))
1303 unless $key eq 'limit' or
1305 $key eq 'skip_check';
1307 my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1308 return $pfx . md5_hex($method . $s);
1311 sub retrieve_cached_facets {
1316 return undef unless ($key and $key =~ /_facets$/);
1318 return $cache->get_cache($key) || {};
1321 __PACKAGE__->register_method(
1322 method => "retrieve_cached_facets",
1323 api_name => "open-ils.search.facet_cache.retrieve"
1328 # add facets for this search to the facet cache
1329 my($key, $results, $metabib) = @_;
1330 my $data = $cache->get_cache($key);
1333 return undef unless (@$results);
1335 # The query we're constructing
1337 # select mfae.field as id,
1339 # count(distinct mmrsm.appropriate-id-field )
1340 # from metabib.facet_entry mfae
1341 # join metabib.metarecord_sourc_map mmrsm on (mfae.source = mmrsm.source)
1342 # where mmrsm.appropriate-id-field in IDLIST
1345 my $count_field = $metabib ? 'metarecord' : 'source';
1346 my $facets = $U->cstorereq( "open-ils.cstore.json_query.atomic",
1348 mfae => [ { column => 'field', alias => 'id'}, 'value' ],
1350 transform => 'count',
1352 column => $count_field,
1359 mmrsm => { field => 'source', fkey => 'source' }
1363 '+mmrsm' => { $count_field => $results }
1368 for my $facet (@$facets) {
1369 next unless ($facet->{value});
1370 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1373 $logger->info("facet compilation: cached with key=$key");
1375 $cache->put_cache($key, $data, $cache_timeout);
1378 sub cache_staged_search_page {
1379 # puts this set of results into the cache
1380 my($key, $page, $summary, $results) = @_;
1381 my $data = $cache->get_cache($key);
1384 summary => $summary,
1388 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1389 $summary->{estimated_hit_count}.", visible=".$summary->{visible});
1391 $cache->put_cache($key, $data, $cache_timeout);
1399 my $start = $offset;
1400 my $end = $offset + $limit - 1;
1402 $logger->debug("searching cache for $key : $start..$end\n");
1404 return undef unless $cache;
1405 my $data = $cache->get_cache($key);
1407 return undef unless $data;
1409 my $count = $data->[0];
1412 return undef unless $offset < $count;
1415 for( my $i = $offset; $i <= $end; $i++ ) {
1416 last unless my $d = $$data[$i];
1417 push( @result, $d );
1420 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1427 my( $key, $count, $data ) = @_;
1428 return undef unless $cache;
1429 $logger->debug("search_cache putting ".
1430 scalar(@$data)." items at key $key with timeout $cache_timeout");
1431 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1435 __PACKAGE__->register_method(
1436 method => "biblio_mrid_to_modsbatch_batch",
1437 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1440 sub biblio_mrid_to_modsbatch_batch {
1441 my( $self, $client, $mrids) = @_;
1442 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1444 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1445 for my $id (@$mrids) {
1446 next unless defined $id;
1447 my ($m) = $method->run($id);
1454 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1455 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1457 __PACKAGE__->register_method(
1458 method => "biblio_mrid_to_modsbatch",
1461 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1462 . "As usual, the .staff version of this method will include otherwise hidden records.",
1464 { desc => 'Metarecord ID', type => 'number' },
1465 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1468 desc => 'MVR Object, event on error',
1474 sub biblio_mrid_to_modsbatch {
1475 my( $self, $client, $mrid, $args) = @_;
1477 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1479 my ($mr, $evt) = _grab_metarecord($mrid);
1480 return $evt unless $mr;
1482 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1483 biblio_mrid_make_modsbatch($self, $client, $mr);
1485 return $mvr unless ref($args);
1487 # Here we find the lead record appropriate for the given filters
1488 # and use that for the title and author of the metarecord
1489 my $format = $$args{format};
1490 my $org = $$args{org};
1491 my $depth = $$args{depth};
1493 return $mvr unless $format or $org or $depth;
1495 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1496 $method = "$method.staff" if $self->api_name =~ /staff/o;
1498 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1500 if( my $mods = $U->record_to_mvr($rec) ) {
1502 $mvr->title( $mods->title );
1503 $mvr->author($mods->author);
1504 $logger->debug("mods_slim updating title and ".
1505 "author in mvr with ".$mods->title." : ".$mods->author);
1511 # converts a metarecord to an mvr
1514 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1515 return Fieldmapper::metabib::virtual_record->new($perl);
1518 # checks to see if a metarecord has mods, if so returns true;
1520 __PACKAGE__->register_method(
1521 method => "biblio_mrid_check_mvr",
1522 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1523 notes => "Takes a metarecord ID or a metarecord object and returns true "
1524 . "if the metarecord already has an mvr associated with it."
1527 sub biblio_mrid_check_mvr {
1528 my( $self, $client, $mrid ) = @_;
1532 if(ref($mrid)) { $mr = $mrid; }
1533 else { ($mr, $evt) = _grab_metarecord($mrid); }
1534 return $evt if $evt;
1536 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1538 return _mr_to_mvr($mr) if $mr->mods();
1542 sub _grab_metarecord {
1544 #my $e = OpenILS::Utils::Editor->new;
1545 my $e = new_editor();
1546 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1551 __PACKAGE__->register_method(
1552 method => "biblio_mrid_make_modsbatch",
1553 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1554 notes => "Takes either a metarecord ID or a metarecord object. "
1555 . "Forces the creations of an mvr for the given metarecord. "
1556 . "The created mvr is returned."
1559 sub biblio_mrid_make_modsbatch {
1560 my( $self, $client, $mrid ) = @_;
1562 #my $e = OpenILS::Utils::Editor->new;
1563 my $e = new_editor();
1570 $mr = $e->retrieve_metabib_metarecord($mrid)
1571 or return $e->event;
1574 my $masterid = $mr->master_record;
1575 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1577 my $ids = $U->storagereq(
1578 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1579 return undef unless @$ids;
1581 my $master = $e->retrieve_biblio_record_entry($masterid)
1582 or return $e->event;
1584 # start the mods batch
1585 my $u = OpenILS::Utils::ModsParser->new();
1586 $u->start_mods_batch( $master->marc );
1588 # grab all of the sub-records and shove them into the batch
1589 my @ids = grep { $_ ne $masterid } @$ids;
1590 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1595 my $r = $e->retrieve_biblio_record_entry($i);
1596 push( @$subrecs, $r ) if $r;
1601 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1602 $u->push_mods_batch( $_->marc ) if $_->marc;
1606 # finish up and send to the client
1607 my $mods = $u->finish_mods_batch();
1608 $mods->doc_id($mrid);
1609 $client->respond_complete($mods);
1612 # now update the mods string in the db
1613 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1616 #$e = OpenILS::Utils::Editor->new(xact => 1);
1617 $e = new_editor(xact => 1);
1618 $e->update_metabib_metarecord($mr)
1619 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1626 # converts a mr id into a list of record ids
1628 foreach (qw/open-ils.search.biblio.metarecord_to_records
1629 open-ils.search.biblio.metarecord_to_records.staff/)
1631 __PACKAGE__->register_method(
1632 method => "biblio_mrid_to_record_ids",
1635 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1636 . "As usual, the .staff version of this method will include otherwise hidden records.",
1638 { desc => 'Metarecord ID', type => 'number' },
1639 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1642 desc => 'Results object like {count => $i, ids =>[...]}',
1650 sub biblio_mrid_to_record_ids {
1651 my( $self, $client, $mrid, $args ) = @_;
1653 my $format = $$args{format};
1654 my $org = $$args{org};
1655 my $depth = $$args{depth};
1657 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1658 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1659 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1661 return { count => scalar(@$recs), ids => $recs };
1665 __PACKAGE__->register_method(
1666 method => "biblio_record_to_marc_html",
1667 api_name => "open-ils.search.biblio.record.html"
1670 __PACKAGE__->register_method(
1671 method => "biblio_record_to_marc_html",
1672 api_name => "open-ils.search.authority.to_html"
1675 # Persistent parsers and setting objects
1676 my $parser = XML::LibXML->new();
1677 my $xslt = XML::LibXSLT->new();
1679 my $slim_marc_sheet;
1680 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1682 sub biblio_record_to_marc_html {
1683 my($self, $client, $recordid, $slim, $marcxml) = @_;
1686 my $dir = $settings_client->config_value("dirs", "xsl");
1689 unless($slim_marc_sheet) {
1690 my $xsl = $settings_client->config_value(
1691 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1693 $xsl = $parser->parse_file("$dir/$xsl");
1694 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1697 $sheet = $slim_marc_sheet;
1701 unless($marc_sheet) {
1702 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1703 my $xsl = $settings_client->config_value(
1704 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1705 $xsl = $parser->parse_file("$dir/$xsl");
1706 $marc_sheet = $xslt->parse_stylesheet($xsl);
1708 $sheet = $marc_sheet;
1713 my $e = new_editor();
1714 if($self->api_name =~ /authority/) {
1715 $record = $e->retrieve_authority_record_entry($recordid)
1716 or return $e->event;
1718 $record = $e->retrieve_biblio_record_entry($recordid)
1719 or return $e->event;
1721 $marcxml = $record->marc;
1724 my $xmldoc = $parser->parse_string($marcxml);
1725 my $html = $sheet->transform($xmldoc);
1726 return $html->documentElement->toString();
1729 __PACKAGE__->register_method(
1730 method => "format_biblio_record_entry",
1731 api_name => "open-ils.search.biblio.record.print",
1733 desc => 'Returns a printable version of the specified bib record',
1735 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1738 desc => q/An action_trigger.event object or error event./,
1743 __PACKAGE__->register_method(
1744 method => "format_biblio_record_entry",
1745 api_name => "open-ils.search.biblio.record.email",
1747 desc => 'Emails an A/T templated version of the specified bib records to the authorized user',
1749 { desc => 'Authentication token', type => 'string'},
1750 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1753 desc => q/Undefined on success, otherwise an error event./,
1759 sub format_biblio_record_entry {
1760 my($self, $conn, $arg1, $arg2) = @_;
1762 my $for_print = ($self->api_name =~ /print/);
1763 my $for_email = ($self->api_name =~ /email/);
1765 my $e; my $auth; my $bib_id; my $context_org;
1769 $context_org = $arg2 || $U->fetch_org_tree->id;
1770 $e = new_editor(xact => 1);
1771 } elsif ($for_email) {
1774 $e = new_editor(authtoken => $auth, xact => 1);
1775 return $e->die_event unless $e->checkauth;
1776 $context_org = $e->requestor->home_ou;
1780 if (ref $bib_id ne 'ARRAY') {
1781 $bib_ids = [ $bib_id ];
1786 my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1787 $bucket->btype('temp');
1788 $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1790 $bucket->owner($e->requestor)
1794 my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1796 for my $id (@$bib_ids) {
1798 my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1800 my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1801 $bucket_entry->target_biblio_record_entry($bib);
1802 $bucket_entry->bucket($bucket_obj->id);
1803 $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
1810 return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
1812 } elsif ($for_email) {
1814 $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
1821 __PACKAGE__->register_method(
1822 method => "retrieve_all_copy_statuses",
1823 api_name => "open-ils.search.config.copy_status.retrieve.all"
1826 sub retrieve_all_copy_statuses {
1827 my( $self, $client ) = @_;
1828 return new_editor()->retrieve_all_config_copy_status();
1832 __PACKAGE__->register_method(
1833 method => "copy_counts_per_org",
1834 api_name => "open-ils.search.biblio.copy_counts.retrieve"
1837 __PACKAGE__->register_method(
1838 method => "copy_counts_per_org",
1839 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1842 sub copy_counts_per_org {
1843 my( $self, $client, $record_id ) = @_;
1845 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1847 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1848 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1850 my $counts = $apputils->simple_scalar_request(
1851 "open-ils.storage", $method, $record_id );
1853 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1858 __PACKAGE__->register_method(
1859 method => "copy_count_summary",
1860 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
1861 notes => "returns an array of these: "
1862 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1863 . "where statusx is a copy status name. The statuses are sorted by ID.",
1867 sub copy_count_summary {
1868 my( $self, $client, $rid, $org, $depth ) = @_;
1871 my $data = $U->storagereq(
1872 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
1874 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1877 __PACKAGE__->register_method(
1878 method => "copy_location_count_summary",
1879 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
1880 notes => "returns an array of these: "
1881 . "[ org_id, callnumber_label, copy_location, <status1_count>, <status2_count>,...] "
1882 . "where statusx is a copy status name. The statuses are sorted by ID.",
1885 sub copy_location_count_summary {
1886 my( $self, $client, $rid, $org, $depth ) = @_;
1889 my $data = $U->storagereq(
1890 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1892 return [ sort { $a->[1] cmp $b->[1] || $a->[2] cmp $b->[2] } @$data ];
1895 __PACKAGE__->register_method(
1896 method => "copy_count_location_summary",
1897 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
1898 notes => "returns an array of these: "
1899 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1900 . "where statusx is a copy status name. The statuses are sorted by ID."
1903 sub copy_count_location_summary {
1904 my( $self, $client, $rid, $org, $depth ) = @_;
1907 my $data = $U->storagereq(
1908 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1909 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1913 foreach (qw/open-ils.search.biblio.marc
1914 open-ils.search.biblio.marc.staff/)
1916 __PACKAGE__->register_method(
1917 method => "marc_search",
1920 desc => 'Fetch biblio IDs based on MARC record criteria. '
1921 . 'As usual, the .staff version of the search includes otherwise hidden records',
1924 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
1925 'See perldoc ' . __PACKAGE__ . ' for more detail.',
1928 {desc => 'limit (optional)', type => 'number'},
1929 {desc => 'offset (optional)', type => 'number'}
1932 desc => 'Results object like: { "count": $i, "ids": [...] }',
1939 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
1941 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
1943 searches: complex query object (required)
1944 org_unit: The org ID to focus the search at
1945 depth : The org depth
1946 limit : integer search limit default: 10
1947 offset : integer search offset default: 0
1948 sort : What field to sort the results on? [ author | title | pubdate ]
1949 sort_dir: In what direction do we sort? [ asc | desc ]
1951 Additional keys to refine search criteria:
1954 language : Language (code)
1955 lit_form : Literary form
1956 item_form: Item form
1957 item_type: Item type
1958 format : The MARC format
1960 Please note that the specific strings to be used in the "addtional keys" will be entirely
1961 dependent on your loaded data.
1963 All keys except "searches" are optional.
1964 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
1966 For example, an arg hash might look like:
1988 The arghash is eventually passed to the SRF call:
1989 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
1991 Presently, search uses the cache unconditionally.
1995 # FIXME: that example above isn't actually tested.
1996 # TODO: docache option?
1998 my( $self, $conn, $args, $limit, $offset ) = @_;
2000 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2001 $method .= ".staff" if $self->api_name =~ /staff/;
2002 $method .= ".atomic";
2004 $limit ||= 10; # FIXME: what about $args->{limit} ?
2005 $offset ||= 0; # FIXME: what about $args->{offset} ?
2008 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2009 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2011 my $recs = search_cache($ckey, $offset, $limit);
2014 $recs = $U->storagereq($method, %$args) || [];
2016 put_cache($ckey, scalar(@$recs), $recs);
2017 $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
2024 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2025 my @recs = map { $_->[0] } @$recs;
2027 return { ids => \@recs, count => $count };
2031 __PACKAGE__->register_method(
2032 method => "biblio_search_isbn",
2033 api_name => "open-ils.search.biblio.isbn",
2035 desc => 'Retrieve biblio IDs for a given ISBN',
2037 {desc => 'ISBN', type => 'string'} # or number maybe? How normalized is our storage data?
2040 desc => 'Results object like: { "count": $i, "ids": [...] }',
2046 sub biblio_search_isbn {
2047 my( $self, $client, $isbn ) = @_;
2048 $logger->debug("Searching ISBN $isbn");
2049 my $recs = $U->storagereq('open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn);
2050 return { ids => $recs, count => scalar(@$recs) };
2053 __PACKAGE__->register_method(
2054 method => "biblio_search_isbn_batch",
2055 api_name => "open-ils.search.biblio.isbn_list",
2058 sub biblio_search_isbn_batch {
2059 my( $self, $client, $isbn_list ) = @_;
2060 $logger->debug("Searching ISBNs @$isbn_list");
2061 my @recs = (); my %rec_set = ();
2062 foreach my $isbn ( @$isbn_list ) {
2063 foreach my $rec ( @{ $U->storagereq(
2064 'open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn )
2066 if (! $rec_set{ $rec }) {
2067 $rec_set{ $rec } = 1;
2072 return { ids => \@recs, count => scalar(@recs) };
2075 __PACKAGE__->register_method(
2076 method => "biblio_search_issn",
2077 api_name => "open-ils.search.biblio.issn",
2079 desc => 'Retrieve biblio IDs for a given ISSN',
2081 {desc => 'ISBN', type => 'string'}
2084 desc => 'Results object like: { "count": $i, "ids": [...] }',
2090 sub biblio_search_issn {
2091 my( $self, $client, $issn ) = @_;
2092 $logger->debug("Searching ISSN $issn");
2093 my $e = new_editor();
2095 my $recs = $U->storagereq(
2096 'open-ils.storage.id_list.biblio.record_entry.search.issn.atomic', $issn );
2097 return { ids => $recs, count => scalar(@$recs) };
2101 __PACKAGE__->register_method(
2102 method => "fetch_mods_by_copy",
2103 api_name => "open-ils.search.biblio.mods_from_copy",
2106 desc => 'Retrieve MODS record given an attached copy ID',
2108 { desc => 'Copy ID', type => 'number' }
2111 desc => 'MODS record, event on error or uncataloged item'
2116 sub fetch_mods_by_copy {
2117 my( $self, $client, $copyid ) = @_;
2118 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2119 return $evt if $evt;
2120 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2121 return $apputils->record_to_mvr($record);
2125 # -------------------------------------------------------------------------------------
2127 __PACKAGE__->register_method(
2128 method => "cn_browse",
2129 api_name => "open-ils.search.callnumber.browse.target",
2130 notes => "Starts a callnumber browse"
2133 __PACKAGE__->register_method(
2134 method => "cn_browse",
2135 api_name => "open-ils.search.callnumber.browse.page_up",
2136 notes => "Returns the previous page of callnumbers",
2139 __PACKAGE__->register_method(
2140 method => "cn_browse",
2141 api_name => "open-ils.search.callnumber.browse.page_down",
2142 notes => "Returns the next page of callnumbers",
2146 # RETURNS array of arrays like so: label, owning_lib, record, id
2148 my( $self, $client, @params ) = @_;
2151 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
2152 if( $self->api_name =~ /target/ );
2153 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2154 if( $self->api_name =~ /page_up/ );
2155 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2156 if( $self->api_name =~ /page_down/ );
2158 return $apputils->simplereq( 'open-ils.storage', $method, @params );
2160 # -------------------------------------------------------------------------------------
2162 __PACKAGE__->register_method(
2163 method => "fetch_cn",
2164 api_name => "open-ils.search.callnumber.retrieve",
2166 notes => "retrieves a callnumber based on ID",
2170 my( $self, $client, $id ) = @_;
2171 my( $cn, $evt ) = $apputils->fetch_callnumber( $id );
2172 return $evt if $evt;
2176 __PACKAGE__->register_method(
2177 method => "fetch_copy_by_cn",
2178 api_name => 'open-ils.search.copies_by_call_number.retrieve',
2180 Returns an array of copy ID's by callnumber ID
2181 @param cnid The callnumber ID
2182 @return An array of copy IDs
2186 sub fetch_copy_by_cn {
2187 my( $self, $conn, $cnid ) = @_;
2188 return $U->cstorereq(
2189 'open-ils.cstore.direct.asset.copy.id_list.atomic',
2190 { call_number => $cnid, deleted => 'f' } );
2193 __PACKAGE__->register_method(
2194 method => 'fetch_cn_by_info',
2195 api_name => 'open-ils.search.call_number.retrieve_by_info',
2197 @param label The callnumber label
2198 @param record The record the cn is attached to
2199 @param org The owning library of the cn
2200 @return The callnumber object
2205 sub fetch_cn_by_info {
2206 my( $self, $conn, $label, $record, $org ) = @_;
2207 return $U->cstorereq(
2208 'open-ils.cstore.direct.asset.call_number.search',
2209 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2214 __PACKAGE__->register_method(
2215 method => 'bib_extras',
2216 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all'
2218 __PACKAGE__->register_method(
2219 method => 'bib_extras',
2220 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all'
2222 __PACKAGE__->register_method(
2223 method => 'bib_extras',
2224 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all'
2226 __PACKAGE__->register_method(
2227 method => 'bib_extras',
2228 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all'
2230 __PACKAGE__->register_method(
2231 method => 'bib_extras',
2232 api_name => 'open-ils.search.biblio.audience_map.retrieve.all'
2238 my $e = new_editor();
2240 return $e->retrieve_all_config_lit_form_map()
2241 if( $self->api_name =~ /lit_form/ );
2243 return $e->retrieve_all_config_item_form_map()
2244 if( $self->api_name =~ /item_form_map/ );
2246 return $e->retrieve_all_config_item_type_map()
2247 if( $self->api_name =~ /item_type_map/ );
2249 return $e->retrieve_all_config_bib_level_map()
2250 if( $self->api_name =~ /bib_level_map/ );
2252 return $e->retrieve_all_config_audience_map()
2253 if( $self->api_name =~ /audience_map/ );
2260 __PACKAGE__->register_method(
2261 method => 'fetch_slim_record',
2262 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2264 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2266 { desc => 'Array of Record IDs', type => 'array' }
2269 desc => 'Array of biblio records, event on error'
2274 sub fetch_slim_record {
2275 my( $self, $conn, $ids ) = @_;
2277 #my $editor = OpenILS::Utils::Editor->new;
2278 my $editor = new_editor();
2281 return $editor->event unless
2282 my $r = $editor->retrieve_biblio_record_entry($_);
2291 __PACKAGE__->register_method(
2292 method => 'rec_to_mr_rec_descriptors',
2293 api_name => 'open-ils.search.metabib.record_to_descriptors',
2295 specialized method...
2296 Given a biblio record id or a metarecord id,
2297 this returns a list of metabib.record_descriptor
2298 objects that live within the same metarecord
2299 @param args Object of args including:
2303 sub rec_to_mr_rec_descriptors {
2304 my( $self, $conn, $args ) = @_;
2306 my $rec = $$args{record};
2307 my $mrec = $$args{metarecord};
2308 my $item_forms = $$args{item_forms};
2309 my $item_types = $$args{item_types};
2310 my $item_lang = $$args{item_lang};
2312 my $e = new_editor();
2316 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2317 return $e->event unless @$map;
2318 $mrec = $$map[0]->metarecord;
2321 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2322 return $e->event unless @$recs;
2324 my @recs = map { $_->source } @$recs;
2325 my $search = { record => \@recs };
2326 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2327 $search->{item_type} = $item_types if $item_types and @$item_types;
2328 $search->{item_lang} = $item_lang if $item_lang;
2330 my $desc = $e->search_metabib_record_descriptor($search);
2332 return { metarecord => $mrec, descriptors => $desc };
2336 __PACKAGE__->register_method(
2337 method => 'fetch_age_protect',
2338 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2341 sub fetch_age_protect {
2342 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2346 __PACKAGE__->register_method(
2347 method => 'copies_by_cn_label',
2348 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2351 __PACKAGE__->register_method(
2352 method => 'copies_by_cn_label',
2353 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2356 sub copies_by_cn_label {
2357 my( $self, $conn, $record, $label, $circ_lib ) = @_;
2358 my $e = new_editor();
2359 my $cns = $e->search_asset_call_number({record => $record, label => $label, deleted => 'f'}, {idlist=>1});
2360 return [] unless @$cns;
2362 # show all non-deleted copies in the staff client ...
2363 if ($self->api_name =~ /staff$/o) {
2364 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2367 # ... otherwise, grab the copies ...
2368 my $copies = $e->search_asset_copy(
2369 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2370 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2374 # ... and test for location and status visibility
2375 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];