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 $method = ($self->api_name =~ /metabib/) ?
1088 'open-ils.storage.metabib.multiclass.staged.search_fts':
1089 'open-ils.storage.biblio.multiclass.staged.search_fts';
1091 $method .= '.staff' if $self->api_name =~ /staff$/;
1092 $method .= '.atomic';
1094 return {count => 0} unless (
1096 $search_hash->{searches} and
1097 scalar( keys %{$search_hash->{searches}} ));
1099 my $search_duration;
1100 my $user_offset = $search_hash->{offset} || 0; # user-specified offset
1101 my $user_limit = $search_hash->{limit} || 10;
1102 $user_offset = ($user_offset >= 0) ? $user_offset : 0;
1103 $user_limit = ($user_limit >= 0) ? $user_limit : 10;
1106 # we're grabbing results on a per-superpage basis, which means the
1107 # limit and offset should coincide with superpage boundaries
1108 $search_hash->{offset} = 0;
1109 $search_hash->{limit} = $superpage_size;
1111 # force a well-known check_limit
1112 $search_hash->{check_limit} = $superpage_size;
1113 # restrict total tested to superpage size * number of superpages
1114 $search_hash->{core_limit} = $superpage_size * $max_superpages;
1116 # Set the configured estimation strategy, defaults to 'inclusion'.
1117 my $estimation_strategy = OpenSRF::Utils::SettingsClient
1120 apps => 'open-ils.search', app_settings => 'estimation_strategy'
1122 $search_hash->{estimation_strategy} = $estimation_strategy;
1124 # pull any existing results from the cache
1125 my $key = search_cache_key($method, $search_hash);
1126 my $facet_key = $key.'_facets';
1127 my $cache_data = $cache->get_cache($key) || {};
1129 # keep retrieving results until we find enough to
1130 # fulfill the user-specified limit and offset
1131 my $all_results = [];
1132 my $page; # current superpage
1133 my $est_hit_count = 0;
1134 my $current_page_summary = {};
1135 my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1136 my $is_real_hit_count = 0;
1139 for($page = 0; $page < $max_superpages; $page++) {
1141 my $data = $cache_data->{$page};
1145 $logger->debug("staged search: analyzing superpage $page");
1148 # this window of results is already cached
1149 $logger->debug("staged search: found cached results");
1150 $summary = $data->{summary};
1151 $results = $data->{results};
1154 # retrieve the window of results from the database
1155 $logger->debug("staged search: fetching results from the database");
1156 $search_hash->{skip_check} = $page * $superpage_size;
1158 $results = $U->storagereq($method, %$search_hash);
1159 $search_duration = time - $start;
1160 $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1161 $summary = shift(@$results) if $results;
1164 $logger->info("search timed out: duration=$search_duration: params=".
1165 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1166 return {count => 0};
1169 my $hc = $summary->{estimated_hit_count} || $summary->{visible};
1171 $logger->info("search returned 0 results: duration=$search_duration: params=".
1172 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1175 # Create backwards-compatible result structures
1176 if($self->api_name =~ /biblio/) {
1177 $results = [map {[$_->{id}]} @$results];
1179 tag_circulated_records($search_hash->{authtoken}, $results)
1180 if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1183 $results = [map {[$_->{id}, $_->{rel}, $_->{record}]} @$results];
1186 push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1187 $results = [grep {defined $_->[0]} @$results];
1188 cache_staged_search_page($key, $page, $summary, $results) if $docache;
1191 $current_page_summary = $summary;
1193 # add the new set of results to the set under construction
1194 push(@$all_results, @$results);
1196 my $current_count = scalar(@$all_results);
1198 $est_hit_count = $summary->{estimated_hit_count} || $summary->{visible}
1201 $logger->debug("staged search: located $current_count, with estimated hits=".
1202 $summary->{estimated_hit_count}." : visible=".$summary->{visible}.", checked=".$summary->{checked});
1204 if (defined($summary->{estimated_hit_count})) {
1205 foreach (qw/ checked visible excluded deleted /) {
1206 $global_summary->{$_} += $summary->{$_};
1208 $global_summary->{total} = $summary->{total};
1211 # we've found all the possible hits
1212 last if $current_count == $summary->{visible}
1213 and not defined $summary->{estimated_hit_count};
1215 # we've found enough results to satisfy the requested limit/offset
1216 last if $current_count >= ($user_limit + $user_offset);
1218 # we've scanned all possible hits
1219 if($summary->{checked} < $superpage_size) {
1220 $est_hit_count = scalar(@$all_results);
1221 # we have all possible results in hand, so we know the final hit count
1222 $is_real_hit_count = 1;
1227 my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1229 # refine the estimate if we have more than one superpage
1230 if ($page > 0 and not $is_real_hit_count) {
1231 if ($global_summary->{checked} >= $global_summary->{total}) {
1232 $est_hit_count = $global_summary->{visible};
1234 my $updated_hit_count = $U->storagereq(
1235 'open-ils.storage.fts_paging_estimate',
1236 $global_summary->{checked},
1237 $global_summary->{visible},
1238 $global_summary->{excluded},
1239 $global_summary->{deleted},
1240 $global_summary->{total}
1242 $est_hit_count = $updated_hit_count->{$estimation_strategy};
1246 $conn->respond_complete(
1248 count => $est_hit_count,
1249 core_limit => $search_hash->{core_limit},
1250 superpage_size => $search_hash->{check_limit},
1251 superpage_summary => $current_page_summary,
1252 facet_key => $facet_key,
1257 cache_facets($facet_key, $new_ids, ($self->api_name =~ /metabib/) ? 1 : 0) if $docache;
1262 sub tag_circulated_records {
1263 my ($auth, $results) = @_;
1264 my $e = new_editor(authtoken => $auth);
1265 return $results unless $e->checkauth;
1267 # Give me the distinct set of bib records that exist in the user's visible circulation history
1268 my $circ_recs = $e->json_query({
1269 select => {acn => [{column => 'record', transform => 'distinct'}]},
1270 from => {acp => 'acn'},
1275 from => ['action.usr_visible_circ_copies', $e->requestor->id]}
1281 # if the record appears in the circ history, push a 1 onto
1282 # the rec array structure to indicate truthiness
1283 for my $rec (@$results) {
1284 push(@$rec, 1) if grep { $_->{record} eq $$rec[0] } @$circ_recs;
1290 # creates a unique token to represent the query in the cache
1291 sub search_cache_key {
1293 my $search_hash = shift;
1295 for my $key (sort keys %$search_hash) {
1296 push(@sorted, ($key => $$search_hash{$key}))
1297 unless $key eq 'limit' or
1299 $key eq 'skip_check';
1301 my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1302 return $pfx . md5_hex($method . $s);
1305 sub retrieve_cached_facets {
1310 return undef unless ($key and $key =~ /_facets$/);
1312 return $cache->get_cache($key) || {};
1315 __PACKAGE__->register_method(
1316 method => "retrieve_cached_facets",
1317 api_name => "open-ils.search.facet_cache.retrieve"
1322 # add facets for this search to the facet cache
1323 my($key, $results, $metabib) = @_;
1324 my $data = $cache->get_cache($key);
1327 return undef unless (@$results);
1329 # The query we're constructing
1333 # count(distinct mmrsm.appropriate-id-field )
1334 # from metabib.facet_entry mfae
1335 # join config.metabib_field cmf on (mfae.field = cmf.id)
1336 # join metabib.metarecord_sourc_map mmrsm on (mfae.source = mmrsm.source)
1337 # where cmf.facet_field
1338 # and mmrsm.appropriate-id-field in IDLIST
1341 my $count_field = $metabib ? 'metarecord' : 'source';
1342 my $facets = $U->cstorereq( "open-ils.cstore.json_query.atomic",
1345 mfae => [ 'value' ],
1347 transform => 'count',
1349 column => $count_field,
1356 cmf => { field => 'id', fkey => 'field' },
1357 mmrsm => { field => 'source', fkey => 'source' }
1361 '+cmf' => 'facet_field',
1362 '+mmrsm' => { $count_field => $results }
1367 for my $facet (@$facets) {
1368 next unless ($facet->{value});
1369 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1372 $logger->info("facet compilation: cached with key=$key");
1374 $cache->put_cache($key, $data, $cache_timeout);
1377 sub cache_staged_search_page {
1378 # puts this set of results into the cache
1379 my($key, $page, $summary, $results) = @_;
1380 my $data = $cache->get_cache($key);
1383 summary => $summary,
1387 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1388 $summary->{estimated_hit_count}.", visible=".$summary->{visible});
1390 $cache->put_cache($key, $data, $cache_timeout);
1398 my $start = $offset;
1399 my $end = $offset + $limit - 1;
1401 $logger->debug("searching cache for $key : $start..$end\n");
1403 return undef unless $cache;
1404 my $data = $cache->get_cache($key);
1406 return undef unless $data;
1408 my $count = $data->[0];
1411 return undef unless $offset < $count;
1414 for( my $i = $offset; $i <= $end; $i++ ) {
1415 last unless my $d = $$data[$i];
1416 push( @result, $d );
1419 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1426 my( $key, $count, $data ) = @_;
1427 return undef unless $cache;
1428 $logger->debug("search_cache putting ".
1429 scalar(@$data)." items at key $key with timeout $cache_timeout");
1430 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1434 __PACKAGE__->register_method(
1435 method => "biblio_mrid_to_modsbatch_batch",
1436 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1439 sub biblio_mrid_to_modsbatch_batch {
1440 my( $self, $client, $mrids) = @_;
1441 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1443 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1444 for my $id (@$mrids) {
1445 next unless defined $id;
1446 my ($m) = $method->run($id);
1453 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1454 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1456 __PACKAGE__->register_method(
1457 method => "biblio_mrid_to_modsbatch",
1460 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1461 . "As usual, the .staff version of this method will include otherwise hidden records.",
1463 { desc => 'Metarecord ID', type => 'number' },
1464 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1467 desc => 'MVR Object, event on error',
1473 sub biblio_mrid_to_modsbatch {
1474 my( $self, $client, $mrid, $args) = @_;
1476 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1478 my ($mr, $evt) = _grab_metarecord($mrid);
1479 return $evt unless $mr;
1481 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1482 biblio_mrid_make_modsbatch($self, $client, $mr);
1484 return $mvr unless ref($args);
1486 # Here we find the lead record appropriate for the given filters
1487 # and use that for the title and author of the metarecord
1488 my $format = $$args{format};
1489 my $org = $$args{org};
1490 my $depth = $$args{depth};
1492 return $mvr unless $format or $org or $depth;
1494 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1495 $method = "$method.staff" if $self->api_name =~ /staff/o;
1497 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1499 if( my $mods = $U->record_to_mvr($rec) ) {
1501 $mvr->title( $mods->title );
1502 $mvr->author($mods->author);
1503 $logger->debug("mods_slim updating title and ".
1504 "author in mvr with ".$mods->title." : ".$mods->author);
1510 # converts a metarecord to an mvr
1513 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1514 return Fieldmapper::metabib::virtual_record->new($perl);
1517 # checks to see if a metarecord has mods, if so returns true;
1519 __PACKAGE__->register_method(
1520 method => "biblio_mrid_check_mvr",
1521 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1522 notes => "Takes a metarecord ID or a metarecord object and returns true "
1523 . "if the metarecord already has an mvr associated with it."
1526 sub biblio_mrid_check_mvr {
1527 my( $self, $client, $mrid ) = @_;
1531 if(ref($mrid)) { $mr = $mrid; }
1532 else { ($mr, $evt) = _grab_metarecord($mrid); }
1533 return $evt if $evt;
1535 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1537 return _mr_to_mvr($mr) if $mr->mods();
1541 sub _grab_metarecord {
1543 #my $e = OpenILS::Utils::Editor->new;
1544 my $e = new_editor();
1545 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1550 __PACKAGE__->register_method(
1551 method => "biblio_mrid_make_modsbatch",
1552 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1553 notes => "Takes either a metarecord ID or a metarecord object. "
1554 . "Forces the creations of an mvr for the given metarecord. "
1555 . "The created mvr is returned."
1558 sub biblio_mrid_make_modsbatch {
1559 my( $self, $client, $mrid ) = @_;
1561 #my $e = OpenILS::Utils::Editor->new;
1562 my $e = new_editor();
1569 $mr = $e->retrieve_metabib_metarecord($mrid)
1570 or return $e->event;
1573 my $masterid = $mr->master_record;
1574 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1576 my $ids = $U->storagereq(
1577 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1578 return undef unless @$ids;
1580 my $master = $e->retrieve_biblio_record_entry($masterid)
1581 or return $e->event;
1583 # start the mods batch
1584 my $u = OpenILS::Utils::ModsParser->new();
1585 $u->start_mods_batch( $master->marc );
1587 # grab all of the sub-records and shove them into the batch
1588 my @ids = grep { $_ ne $masterid } @$ids;
1589 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1594 my $r = $e->retrieve_biblio_record_entry($i);
1595 push( @$subrecs, $r ) if $r;
1600 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1601 $u->push_mods_batch( $_->marc ) if $_->marc;
1605 # finish up and send to the client
1606 my $mods = $u->finish_mods_batch();
1607 $mods->doc_id($mrid);
1608 $client->respond_complete($mods);
1611 # now update the mods string in the db
1612 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1615 #$e = OpenILS::Utils::Editor->new(xact => 1);
1616 $e = new_editor(xact => 1);
1617 $e->update_metabib_metarecord($mr)
1618 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1625 # converts a mr id into a list of record ids
1627 foreach (qw/open-ils.search.biblio.metarecord_to_records
1628 open-ils.search.biblio.metarecord_to_records.staff/)
1630 __PACKAGE__->register_method(
1631 method => "biblio_mrid_to_record_ids",
1634 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1635 . "As usual, the .staff version of this method will include otherwise hidden records.",
1637 { desc => 'Metarecord ID', type => 'number' },
1638 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1641 desc => 'Results object like {count => $i, ids =>[...]}',
1649 sub biblio_mrid_to_record_ids {
1650 my( $self, $client, $mrid, $args ) = @_;
1652 my $format = $$args{format};
1653 my $org = $$args{org};
1654 my $depth = $$args{depth};
1656 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1657 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1658 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1660 return { count => scalar(@$recs), ids => $recs };
1664 __PACKAGE__->register_method(
1665 method => "biblio_record_to_marc_html",
1666 api_name => "open-ils.search.biblio.record.html"
1669 __PACKAGE__->register_method(
1670 method => "biblio_record_to_marc_html",
1671 api_name => "open-ils.search.authority.to_html"
1674 # Persistent parsers and setting objects
1675 my $parser = XML::LibXML->new();
1676 my $xslt = XML::LibXSLT->new();
1678 my $slim_marc_sheet;
1679 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1681 sub biblio_record_to_marc_html {
1682 my($self, $client, $recordid, $slim, $marcxml) = @_;
1685 my $dir = $settings_client->config_value("dirs", "xsl");
1688 unless($slim_marc_sheet) {
1689 my $xsl = $settings_client->config_value(
1690 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1692 $xsl = $parser->parse_file("$dir/$xsl");
1693 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1696 $sheet = $slim_marc_sheet;
1700 unless($marc_sheet) {
1701 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1702 my $xsl = $settings_client->config_value(
1703 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1704 $xsl = $parser->parse_file("$dir/$xsl");
1705 $marc_sheet = $xslt->parse_stylesheet($xsl);
1707 $sheet = $marc_sheet;
1712 my $e = new_editor();
1713 if($self->api_name =~ /authority/) {
1714 $record = $e->retrieve_authority_record_entry($recordid)
1715 or return $e->event;
1717 $record = $e->retrieve_biblio_record_entry($recordid)
1718 or return $e->event;
1720 $marcxml = $record->marc;
1723 my $xmldoc = $parser->parse_string($marcxml);
1724 my $html = $sheet->transform($xmldoc);
1725 return $html->documentElement->toString();
1728 __PACKAGE__->register_method(
1729 method => "format_biblio_record_entry",
1730 api_name => "open-ils.search.biblio.record.print",
1732 desc => 'Returns a printable version of the specified bib record',
1734 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1737 desc => q/An action_trigger.event object or error event./,
1742 __PACKAGE__->register_method(
1743 method => "format_biblio_record_entry",
1744 api_name => "open-ils.search.biblio.record.email",
1746 desc => 'Emails an A/T templated version of the specified bib records to the authorized user',
1748 { desc => 'Authentication token', type => 'string'},
1749 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1752 desc => q/Undefined on success, otherwise an error event./,
1758 sub format_biblio_record_entry {
1759 my($self, $conn, $arg1, $arg2) = @_;
1761 my $for_print = ($self->api_name =~ /print/);
1762 my $for_email = ($self->api_name =~ /email/);
1764 my $e; my $auth; my $bib_id; my $context_org;
1768 $context_org = $arg2 || $U->fetch_org_tree->id;
1769 $e = new_editor(xact => 1);
1770 } elsif ($for_email) {
1773 $e = new_editor(authtoken => $auth, xact => 1);
1774 return $e->die_event unless $e->checkauth;
1775 $context_org = $e->requestor->home_ou;
1779 if (ref $bib_id ne 'ARRAY') {
1780 $bib_ids = [ $bib_id ];
1785 my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1786 $bucket->btype('temp');
1787 $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1789 $bucket->owner($e->requestor)
1793 my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1795 for my $id (@$bib_ids) {
1797 my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1799 my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1800 $bucket_entry->target_biblio_record_entry($bib);
1801 $bucket_entry->bucket($bucket_obj->id);
1802 $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
1809 return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
1811 } elsif ($for_email) {
1813 $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
1820 __PACKAGE__->register_method(
1821 method => "retrieve_all_copy_statuses",
1822 api_name => "open-ils.search.config.copy_status.retrieve.all"
1825 sub retrieve_all_copy_statuses {
1826 my( $self, $client ) = @_;
1827 return new_editor()->retrieve_all_config_copy_status();
1831 __PACKAGE__->register_method(
1832 method => "copy_counts_per_org",
1833 api_name => "open-ils.search.biblio.copy_counts.retrieve"
1836 __PACKAGE__->register_method(
1837 method => "copy_counts_per_org",
1838 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1841 sub copy_counts_per_org {
1842 my( $self, $client, $record_id ) = @_;
1844 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1846 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1847 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1849 my $counts = $apputils->simple_scalar_request(
1850 "open-ils.storage", $method, $record_id );
1852 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1857 __PACKAGE__->register_method(
1858 method => "copy_count_summary",
1859 api_name => "open-ils.search.biblio.copy_counts.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.",
1866 sub copy_count_summary {
1867 my( $self, $client, $rid, $org, $depth ) = @_;
1870 my $data = $U->storagereq(
1871 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
1873 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1876 __PACKAGE__->register_method(
1877 method => "copy_location_count_summary",
1878 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
1879 notes => "returns an array of these: "
1880 . "[ org_id, callnumber_label, copy_location, <status1_count>, <status2_count>,...] "
1881 . "where statusx is a copy status name. The statuses are sorted by ID.",
1884 sub copy_location_count_summary {
1885 my( $self, $client, $rid, $org, $depth ) = @_;
1888 my $data = $U->storagereq(
1889 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1891 return [ sort { $a->[1] cmp $b->[1] || $a->[2] cmp $b->[2] } @$data ];
1894 __PACKAGE__->register_method(
1895 method => "copy_count_location_summary",
1896 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
1897 notes => "returns an array of these: "
1898 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1899 . "where statusx is a copy status name. The statuses are sorted by ID."
1902 sub copy_count_location_summary {
1903 my( $self, $client, $rid, $org, $depth ) = @_;
1906 my $data = $U->storagereq(
1907 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1908 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1912 foreach (qw/open-ils.search.biblio.marc
1913 open-ils.search.biblio.marc.staff/)
1915 __PACKAGE__->register_method(
1916 method => "marc_search",
1919 desc => 'Fetch biblio IDs based on MARC record criteria. '
1920 . 'As usual, the .staff version of the search includes otherwise hidden records',
1923 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
1924 'See perldoc ' . __PACKAGE__ . ' for more detail.',
1927 {desc => 'limit (optional)', type => 'number'},
1928 {desc => 'offset (optional)', type => 'number'}
1931 desc => 'Results object like: { "count": $i, "ids": [...] }',
1938 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
1940 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
1942 searches: complex query object (required)
1943 org_unit: The org ID to focus the search at
1944 depth : The org depth
1945 limit : integer search limit default: 10
1946 offset : integer search offset default: 0
1947 sort : What field to sort the results on? [ author | title | pubdate ]
1948 sort_dir: In what direction do we sort? [ asc | desc ]
1950 Additional keys to refine search criteria:
1953 language : Language (code)
1954 lit_form : Literary form
1955 item_form: Item form
1956 item_type: Item type
1957 format : The MARC format
1959 Please note that the specific strings to be used in the "addtional keys" will be entirely
1960 dependent on your loaded data.
1962 All keys except "searches" are optional.
1963 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
1965 For example, an arg hash might look like:
1987 The arghash is eventually passed to the SRF call:
1988 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
1990 Presently, search uses the cache unconditionally.
1994 # FIXME: that example above isn't actually tested.
1995 # TODO: docache option?
1997 my( $self, $conn, $args, $limit, $offset ) = @_;
1999 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2000 $method .= ".staff" if $self->api_name =~ /staff/;
2001 $method .= ".atomic";
2003 $limit ||= 10; # FIXME: what about $args->{limit} ?
2004 $offset ||= 0; # FIXME: what about $args->{offset} ?
2007 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2008 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2010 my $recs = search_cache($ckey, $offset, $limit);
2013 $recs = $U->storagereq($method, %$args) || [];
2015 put_cache($ckey, scalar(@$recs), $recs);
2016 $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
2023 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2024 my @recs = map { $_->[0] } @$recs;
2026 return { ids => \@recs, count => $count };
2030 __PACKAGE__->register_method(
2031 method => "biblio_search_isbn",
2032 api_name => "open-ils.search.biblio.isbn",
2034 desc => 'Retrieve biblio IDs for a given ISBN',
2036 {desc => 'ISBN', type => 'string'} # or number maybe? How normalized is our storage data?
2039 desc => 'Results object like: { "count": $i, "ids": [...] }',
2045 sub biblio_search_isbn {
2046 my( $self, $client, $isbn ) = @_;
2047 $logger->debug("Searching ISBN $isbn");
2048 my $recs = $U->storagereq('open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn);
2049 return { ids => $recs, count => scalar(@$recs) };
2052 __PACKAGE__->register_method(
2053 method => "biblio_search_isbn_batch",
2054 api_name => "open-ils.search.biblio.isbn_list",
2057 sub biblio_search_isbn_batch {
2058 my( $self, $client, $isbn_list ) = @_;
2059 $logger->debug("Searching ISBNs @$isbn_list");
2060 my @recs = (); my %rec_set = ();
2061 foreach my $isbn ( @$isbn_list ) {
2062 foreach my $rec ( @{ $U->storagereq(
2063 'open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn )
2065 if (! $rec_set{ $rec }) {
2066 $rec_set{ $rec } = 1;
2071 return { ids => \@recs, count => scalar(@recs) };
2074 __PACKAGE__->register_method(
2075 method => "biblio_search_issn",
2076 api_name => "open-ils.search.biblio.issn",
2078 desc => 'Retrieve biblio IDs for a given ISSN',
2080 {desc => 'ISBN', type => 'string'}
2083 desc => 'Results object like: { "count": $i, "ids": [...] }',
2089 sub biblio_search_issn {
2090 my( $self, $client, $issn ) = @_;
2091 $logger->debug("Searching ISSN $issn");
2092 my $e = new_editor();
2094 my $recs = $U->storagereq(
2095 'open-ils.storage.id_list.biblio.record_entry.search.issn.atomic', $issn );
2096 return { ids => $recs, count => scalar(@$recs) };
2100 __PACKAGE__->register_method(
2101 method => "fetch_mods_by_copy",
2102 api_name => "open-ils.search.biblio.mods_from_copy",
2105 desc => 'Retrieve MODS record given an attached copy ID',
2107 { desc => 'Copy ID', type => 'number' }
2110 desc => 'MODS record, event on error or uncataloged item'
2115 sub fetch_mods_by_copy {
2116 my( $self, $client, $copyid ) = @_;
2117 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2118 return $evt if $evt;
2119 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2120 return $apputils->record_to_mvr($record);
2124 # -------------------------------------------------------------------------------------
2126 __PACKAGE__->register_method(
2127 method => "cn_browse",
2128 api_name => "open-ils.search.callnumber.browse.target",
2129 notes => "Starts a callnumber browse"
2132 __PACKAGE__->register_method(
2133 method => "cn_browse",
2134 api_name => "open-ils.search.callnumber.browse.page_up",
2135 notes => "Returns the previous page of callnumbers",
2138 __PACKAGE__->register_method(
2139 method => "cn_browse",
2140 api_name => "open-ils.search.callnumber.browse.page_down",
2141 notes => "Returns the next page of callnumbers",
2145 # RETURNS array of arrays like so: label, owning_lib, record, id
2147 my( $self, $client, @params ) = @_;
2150 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
2151 if( $self->api_name =~ /target/ );
2152 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2153 if( $self->api_name =~ /page_up/ );
2154 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2155 if( $self->api_name =~ /page_down/ );
2157 return $apputils->simplereq( 'open-ils.storage', $method, @params );
2159 # -------------------------------------------------------------------------------------
2161 __PACKAGE__->register_method(
2162 method => "fetch_cn",
2163 api_name => "open-ils.search.callnumber.retrieve",
2165 notes => "retrieves a callnumber based on ID",
2169 my( $self, $client, $id ) = @_;
2170 my( $cn, $evt ) = $apputils->fetch_callnumber( $id );
2171 return $evt if $evt;
2175 __PACKAGE__->register_method(
2176 method => "fetch_copy_by_cn",
2177 api_name => 'open-ils.search.copies_by_call_number.retrieve',
2179 Returns an array of copy ID's by callnumber ID
2180 @param cnid The callnumber ID
2181 @return An array of copy IDs
2185 sub fetch_copy_by_cn {
2186 my( $self, $conn, $cnid ) = @_;
2187 return $U->cstorereq(
2188 'open-ils.cstore.direct.asset.copy.id_list.atomic',
2189 { call_number => $cnid, deleted => 'f' } );
2192 __PACKAGE__->register_method(
2193 method => 'fetch_cn_by_info',
2194 api_name => 'open-ils.search.call_number.retrieve_by_info',
2196 @param label The callnumber label
2197 @param record The record the cn is attached to
2198 @param org The owning library of the cn
2199 @return The callnumber object
2204 sub fetch_cn_by_info {
2205 my( $self, $conn, $label, $record, $org ) = @_;
2206 return $U->cstorereq(
2207 'open-ils.cstore.direct.asset.call_number.search',
2208 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2213 __PACKAGE__->register_method(
2214 method => 'bib_extras',
2215 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all'
2217 __PACKAGE__->register_method(
2218 method => 'bib_extras',
2219 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all'
2221 __PACKAGE__->register_method(
2222 method => 'bib_extras',
2223 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all'
2225 __PACKAGE__->register_method(
2226 method => 'bib_extras',
2227 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all'
2229 __PACKAGE__->register_method(
2230 method => 'bib_extras',
2231 api_name => 'open-ils.search.biblio.audience_map.retrieve.all'
2237 my $e = new_editor();
2239 return $e->retrieve_all_config_lit_form_map()
2240 if( $self->api_name =~ /lit_form/ );
2242 return $e->retrieve_all_config_item_form_map()
2243 if( $self->api_name =~ /item_form_map/ );
2245 return $e->retrieve_all_config_item_type_map()
2246 if( $self->api_name =~ /item_type_map/ );
2248 return $e->retrieve_all_config_bib_level_map()
2249 if( $self->api_name =~ /bib_level_map/ );
2251 return $e->retrieve_all_config_audience_map()
2252 if( $self->api_name =~ /audience_map/ );
2259 __PACKAGE__->register_method(
2260 method => 'fetch_slim_record',
2261 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2263 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2265 { desc => 'Array of Record IDs', type => 'array' }
2268 desc => 'Array of biblio records, event on error'
2273 sub fetch_slim_record {
2274 my( $self, $conn, $ids ) = @_;
2276 #my $editor = OpenILS::Utils::Editor->new;
2277 my $editor = new_editor();
2280 return $editor->event unless
2281 my $r = $editor->retrieve_biblio_record_entry($_);
2290 __PACKAGE__->register_method(
2291 method => 'rec_to_mr_rec_descriptors',
2292 api_name => 'open-ils.search.metabib.record_to_descriptors',
2294 specialized method...
2295 Given a biblio record id or a metarecord id,
2296 this returns a list of metabib.record_descriptor
2297 objects that live within the same metarecord
2298 @param args Object of args including:
2302 sub rec_to_mr_rec_descriptors {
2303 my( $self, $conn, $args ) = @_;
2305 my $rec = $$args{record};
2306 my $mrec = $$args{metarecord};
2307 my $item_forms = $$args{item_forms};
2308 my $item_types = $$args{item_types};
2309 my $item_lang = $$args{item_lang};
2311 my $e = new_editor();
2315 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2316 return $e->event unless @$map;
2317 $mrec = $$map[0]->metarecord;
2320 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2321 return $e->event unless @$recs;
2323 my @recs = map { $_->source } @$recs;
2324 my $search = { record => \@recs };
2325 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2326 $search->{item_type} = $item_types if $item_types and @$item_types;
2327 $search->{item_lang} = $item_lang if $item_lang;
2329 my $desc = $e->search_metabib_record_descriptor($search);
2331 return { metarecord => $mrec, descriptors => $desc };
2335 __PACKAGE__->register_method(
2336 method => 'fetch_age_protect',
2337 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2340 sub fetch_age_protect {
2341 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2345 __PACKAGE__->register_method(
2346 method => 'copies_by_cn_label',
2347 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2350 __PACKAGE__->register_method(
2351 method => 'copies_by_cn_label',
2352 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2355 sub copies_by_cn_label {
2356 my( $self, $conn, $record, $label, $circ_lib ) = @_;
2357 my $e = new_editor();
2358 my $cns = $e->search_asset_call_number({record => $record, label => $label, deleted => 'f'}, {idlist=>1});
2359 return [] unless @$cns;
2361 # show all non-deleted copies in the staff client ...
2362 if ($self->api_name =~ /staff$/o) {
2363 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2366 # ... otherwise, grab the copies ...
2367 my $copies = $e->search_asset_copy(
2368 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2369 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2373 # ... and test for location and status visibility
2374 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];