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
1339 # count(distinct mmrsm.appropriate-id-field )
1340 # from metabib.facet_entry mfae
1341 # join config.metabib_field cmf on (mfae.field = cmf.id)
1342 # join metabib.metarecord_sourc_map mmrsm on (mfae.source = mmrsm.source)
1343 # where cmf.facet_field
1344 # and mmrsm.appropriate-id-field in IDLIST
1347 my $count_field = $metabib ? 'metarecord' : 'source';
1348 my $facets = $U->cstorereq( "open-ils.cstore.json_query.atomic",
1351 mfae => [ 'value' ],
1353 transform => 'count',
1355 column => $count_field,
1362 cmf => { field => 'id', fkey => 'field' },
1363 mmrsm => { field => 'source', fkey => 'source' }
1367 '+cmf' => 'facet_field',
1368 '+mmrsm' => { $count_field => $results }
1373 for my $facet (@$facets) {
1374 next unless ($facet->{value});
1375 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1378 $logger->info("facet compilation: cached with key=$key");
1380 $cache->put_cache($key, $data, $cache_timeout);
1383 sub cache_staged_search_page {
1384 # puts this set of results into the cache
1385 my($key, $page, $summary, $results) = @_;
1386 my $data = $cache->get_cache($key);
1389 summary => $summary,
1393 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1394 $summary->{estimated_hit_count}.", visible=".$summary->{visible});
1396 $cache->put_cache($key, $data, $cache_timeout);
1404 my $start = $offset;
1405 my $end = $offset + $limit - 1;
1407 $logger->debug("searching cache for $key : $start..$end\n");
1409 return undef unless $cache;
1410 my $data = $cache->get_cache($key);
1412 return undef unless $data;
1414 my $count = $data->[0];
1417 return undef unless $offset < $count;
1420 for( my $i = $offset; $i <= $end; $i++ ) {
1421 last unless my $d = $$data[$i];
1422 push( @result, $d );
1425 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1432 my( $key, $count, $data ) = @_;
1433 return undef unless $cache;
1434 $logger->debug("search_cache putting ".
1435 scalar(@$data)." items at key $key with timeout $cache_timeout");
1436 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1440 __PACKAGE__->register_method(
1441 method => "biblio_mrid_to_modsbatch_batch",
1442 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1445 sub biblio_mrid_to_modsbatch_batch {
1446 my( $self, $client, $mrids) = @_;
1447 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1449 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1450 for my $id (@$mrids) {
1451 next unless defined $id;
1452 my ($m) = $method->run($id);
1459 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1460 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1462 __PACKAGE__->register_method(
1463 method => "biblio_mrid_to_modsbatch",
1466 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1467 . "As usual, the .staff version of this method will include otherwise hidden records.",
1469 { desc => 'Metarecord ID', type => 'number' },
1470 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1473 desc => 'MVR Object, event on error',
1479 sub biblio_mrid_to_modsbatch {
1480 my( $self, $client, $mrid, $args) = @_;
1482 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1484 my ($mr, $evt) = _grab_metarecord($mrid);
1485 return $evt unless $mr;
1487 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1488 biblio_mrid_make_modsbatch($self, $client, $mr);
1490 return $mvr unless ref($args);
1492 # Here we find the lead record appropriate for the given filters
1493 # and use that for the title and author of the metarecord
1494 my $format = $$args{format};
1495 my $org = $$args{org};
1496 my $depth = $$args{depth};
1498 return $mvr unless $format or $org or $depth;
1500 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1501 $method = "$method.staff" if $self->api_name =~ /staff/o;
1503 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1505 if( my $mods = $U->record_to_mvr($rec) ) {
1507 $mvr->title( $mods->title );
1508 $mvr->author($mods->author);
1509 $logger->debug("mods_slim updating title and ".
1510 "author in mvr with ".$mods->title." : ".$mods->author);
1516 # converts a metarecord to an mvr
1519 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1520 return Fieldmapper::metabib::virtual_record->new($perl);
1523 # checks to see if a metarecord has mods, if so returns true;
1525 __PACKAGE__->register_method(
1526 method => "biblio_mrid_check_mvr",
1527 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1528 notes => "Takes a metarecord ID or a metarecord object and returns true "
1529 . "if the metarecord already has an mvr associated with it."
1532 sub biblio_mrid_check_mvr {
1533 my( $self, $client, $mrid ) = @_;
1537 if(ref($mrid)) { $mr = $mrid; }
1538 else { ($mr, $evt) = _grab_metarecord($mrid); }
1539 return $evt if $evt;
1541 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1543 return _mr_to_mvr($mr) if $mr->mods();
1547 sub _grab_metarecord {
1549 #my $e = OpenILS::Utils::Editor->new;
1550 my $e = new_editor();
1551 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1556 __PACKAGE__->register_method(
1557 method => "biblio_mrid_make_modsbatch",
1558 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1559 notes => "Takes either a metarecord ID or a metarecord object. "
1560 . "Forces the creations of an mvr for the given metarecord. "
1561 . "The created mvr is returned."
1564 sub biblio_mrid_make_modsbatch {
1565 my( $self, $client, $mrid ) = @_;
1567 #my $e = OpenILS::Utils::Editor->new;
1568 my $e = new_editor();
1575 $mr = $e->retrieve_metabib_metarecord($mrid)
1576 or return $e->event;
1579 my $masterid = $mr->master_record;
1580 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1582 my $ids = $U->storagereq(
1583 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1584 return undef unless @$ids;
1586 my $master = $e->retrieve_biblio_record_entry($masterid)
1587 or return $e->event;
1589 # start the mods batch
1590 my $u = OpenILS::Utils::ModsParser->new();
1591 $u->start_mods_batch( $master->marc );
1593 # grab all of the sub-records and shove them into the batch
1594 my @ids = grep { $_ ne $masterid } @$ids;
1595 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1600 my $r = $e->retrieve_biblio_record_entry($i);
1601 push( @$subrecs, $r ) if $r;
1606 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1607 $u->push_mods_batch( $_->marc ) if $_->marc;
1611 # finish up and send to the client
1612 my $mods = $u->finish_mods_batch();
1613 $mods->doc_id($mrid);
1614 $client->respond_complete($mods);
1617 # now update the mods string in the db
1618 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1621 #$e = OpenILS::Utils::Editor->new(xact => 1);
1622 $e = new_editor(xact => 1);
1623 $e->update_metabib_metarecord($mr)
1624 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1631 # converts a mr id into a list of record ids
1633 foreach (qw/open-ils.search.biblio.metarecord_to_records
1634 open-ils.search.biblio.metarecord_to_records.staff/)
1636 __PACKAGE__->register_method(
1637 method => "biblio_mrid_to_record_ids",
1640 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1641 . "As usual, the .staff version of this method will include otherwise hidden records.",
1643 { desc => 'Metarecord ID', type => 'number' },
1644 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1647 desc => 'Results object like {count => $i, ids =>[...]}',
1655 sub biblio_mrid_to_record_ids {
1656 my( $self, $client, $mrid, $args ) = @_;
1658 my $format = $$args{format};
1659 my $org = $$args{org};
1660 my $depth = $$args{depth};
1662 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1663 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1664 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1666 return { count => scalar(@$recs), ids => $recs };
1670 __PACKAGE__->register_method(
1671 method => "biblio_record_to_marc_html",
1672 api_name => "open-ils.search.biblio.record.html"
1675 __PACKAGE__->register_method(
1676 method => "biblio_record_to_marc_html",
1677 api_name => "open-ils.search.authority.to_html"
1680 # Persistent parsers and setting objects
1681 my $parser = XML::LibXML->new();
1682 my $xslt = XML::LibXSLT->new();
1684 my $slim_marc_sheet;
1685 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1687 sub biblio_record_to_marc_html {
1688 my($self, $client, $recordid, $slim, $marcxml) = @_;
1691 my $dir = $settings_client->config_value("dirs", "xsl");
1694 unless($slim_marc_sheet) {
1695 my $xsl = $settings_client->config_value(
1696 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1698 $xsl = $parser->parse_file("$dir/$xsl");
1699 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1702 $sheet = $slim_marc_sheet;
1706 unless($marc_sheet) {
1707 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1708 my $xsl = $settings_client->config_value(
1709 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1710 $xsl = $parser->parse_file("$dir/$xsl");
1711 $marc_sheet = $xslt->parse_stylesheet($xsl);
1713 $sheet = $marc_sheet;
1718 my $e = new_editor();
1719 if($self->api_name =~ /authority/) {
1720 $record = $e->retrieve_authority_record_entry($recordid)
1721 or return $e->event;
1723 $record = $e->retrieve_biblio_record_entry($recordid)
1724 or return $e->event;
1726 $marcxml = $record->marc;
1729 my $xmldoc = $parser->parse_string($marcxml);
1730 my $html = $sheet->transform($xmldoc);
1731 return $html->documentElement->toString();
1734 __PACKAGE__->register_method(
1735 method => "format_biblio_record_entry",
1736 api_name => "open-ils.search.biblio.record.print",
1738 desc => 'Returns a printable version of the specified bib record',
1740 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1743 desc => q/An action_trigger.event object or error event./,
1748 __PACKAGE__->register_method(
1749 method => "format_biblio_record_entry",
1750 api_name => "open-ils.search.biblio.record.email",
1752 desc => 'Emails an A/T templated version of the specified bib records to the authorized user',
1754 { desc => 'Authentication token', type => 'string'},
1755 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1758 desc => q/Undefined on success, otherwise an error event./,
1764 sub format_biblio_record_entry {
1765 my($self, $conn, $arg1, $arg2) = @_;
1767 my $for_print = ($self->api_name =~ /print/);
1768 my $for_email = ($self->api_name =~ /email/);
1770 my $e; my $auth; my $bib_id; my $context_org;
1774 $context_org = $arg2 || $U->fetch_org_tree->id;
1775 $e = new_editor(xact => 1);
1776 } elsif ($for_email) {
1779 $e = new_editor(authtoken => $auth, xact => 1);
1780 return $e->die_event unless $e->checkauth;
1781 $context_org = $e->requestor->home_ou;
1785 if (ref $bib_id ne 'ARRAY') {
1786 $bib_ids = [ $bib_id ];
1791 my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1792 $bucket->btype('temp');
1793 $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1795 $bucket->owner($e->requestor)
1799 my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1801 for my $id (@$bib_ids) {
1803 my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1805 my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1806 $bucket_entry->target_biblio_record_entry($bib);
1807 $bucket_entry->bucket($bucket_obj->id);
1808 $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
1815 return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
1817 } elsif ($for_email) {
1819 $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
1826 __PACKAGE__->register_method(
1827 method => "retrieve_all_copy_statuses",
1828 api_name => "open-ils.search.config.copy_status.retrieve.all"
1831 sub retrieve_all_copy_statuses {
1832 my( $self, $client ) = @_;
1833 return new_editor()->retrieve_all_config_copy_status();
1837 __PACKAGE__->register_method(
1838 method => "copy_counts_per_org",
1839 api_name => "open-ils.search.biblio.copy_counts.retrieve"
1842 __PACKAGE__->register_method(
1843 method => "copy_counts_per_org",
1844 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1847 sub copy_counts_per_org {
1848 my( $self, $client, $record_id ) = @_;
1850 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1852 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1853 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1855 my $counts = $apputils->simple_scalar_request(
1856 "open-ils.storage", $method, $record_id );
1858 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1863 __PACKAGE__->register_method(
1864 method => "copy_count_summary",
1865 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
1866 notes => "returns an array of these: "
1867 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1868 . "where statusx is a copy status name. The statuses are sorted by ID.",
1872 sub copy_count_summary {
1873 my( $self, $client, $rid, $org, $depth ) = @_;
1876 my $data = $U->storagereq(
1877 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
1879 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1882 __PACKAGE__->register_method(
1883 method => "copy_location_count_summary",
1884 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
1885 notes => "returns an array of these: "
1886 . "[ org_id, callnumber_label, copy_location, <status1_count>, <status2_count>,...] "
1887 . "where statusx is a copy status name. The statuses are sorted by ID.",
1890 sub copy_location_count_summary {
1891 my( $self, $client, $rid, $org, $depth ) = @_;
1894 my $data = $U->storagereq(
1895 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1897 return [ sort { $a->[1] cmp $b->[1] || $a->[2] cmp $b->[2] } @$data ];
1900 __PACKAGE__->register_method(
1901 method => "copy_count_location_summary",
1902 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
1903 notes => "returns an array of these: "
1904 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1905 . "where statusx is a copy status name. The statuses are sorted by ID."
1908 sub copy_count_location_summary {
1909 my( $self, $client, $rid, $org, $depth ) = @_;
1912 my $data = $U->storagereq(
1913 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1914 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1918 foreach (qw/open-ils.search.biblio.marc
1919 open-ils.search.biblio.marc.staff/)
1921 __PACKAGE__->register_method(
1922 method => "marc_search",
1925 desc => 'Fetch biblio IDs based on MARC record criteria. '
1926 . 'As usual, the .staff version of the search includes otherwise hidden records',
1929 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
1930 'See perldoc ' . __PACKAGE__ . ' for more detail.',
1933 {desc => 'limit (optional)', type => 'number'},
1934 {desc => 'offset (optional)', type => 'number'}
1937 desc => 'Results object like: { "count": $i, "ids": [...] }',
1944 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
1946 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
1948 searches: complex query object (required)
1949 org_unit: The org ID to focus the search at
1950 depth : The org depth
1951 limit : integer search limit default: 10
1952 offset : integer search offset default: 0
1953 sort : What field to sort the results on? [ author | title | pubdate ]
1954 sort_dir: In what direction do we sort? [ asc | desc ]
1956 Additional keys to refine search criteria:
1959 language : Language (code)
1960 lit_form : Literary form
1961 item_form: Item form
1962 item_type: Item type
1963 format : The MARC format
1965 Please note that the specific strings to be used in the "addtional keys" will be entirely
1966 dependent on your loaded data.
1968 All keys except "searches" are optional.
1969 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
1971 For example, an arg hash might look like:
1993 The arghash is eventually passed to the SRF call:
1994 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
1996 Presently, search uses the cache unconditionally.
2000 # FIXME: that example above isn't actually tested.
2001 # TODO: docache option?
2003 my( $self, $conn, $args, $limit, $offset ) = @_;
2005 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2006 $method .= ".staff" if $self->api_name =~ /staff/;
2007 $method .= ".atomic";
2009 $limit ||= 10; # FIXME: what about $args->{limit} ?
2010 $offset ||= 0; # FIXME: what about $args->{offset} ?
2013 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2014 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2016 my $recs = search_cache($ckey, $offset, $limit);
2019 $recs = $U->storagereq($method, %$args) || [];
2021 put_cache($ckey, scalar(@$recs), $recs);
2022 $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
2029 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2030 my @recs = map { $_->[0] } @$recs;
2032 return { ids => \@recs, count => $count };
2036 __PACKAGE__->register_method(
2037 method => "biblio_search_isbn",
2038 api_name => "open-ils.search.biblio.isbn",
2040 desc => 'Retrieve biblio IDs for a given ISBN',
2042 {desc => 'ISBN', type => 'string'} # or number maybe? How normalized is our storage data?
2045 desc => 'Results object like: { "count": $i, "ids": [...] }',
2051 sub biblio_search_isbn {
2052 my( $self, $client, $isbn ) = @_;
2053 $logger->debug("Searching ISBN $isbn");
2054 my $recs = $U->storagereq('open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn);
2055 return { ids => $recs, count => scalar(@$recs) };
2058 __PACKAGE__->register_method(
2059 method => "biblio_search_isbn_batch",
2060 api_name => "open-ils.search.biblio.isbn_list",
2063 sub biblio_search_isbn_batch {
2064 my( $self, $client, $isbn_list ) = @_;
2065 $logger->debug("Searching ISBNs @$isbn_list");
2066 my @recs = (); my %rec_set = ();
2067 foreach my $isbn ( @$isbn_list ) {
2068 foreach my $rec ( @{ $U->storagereq(
2069 'open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn )
2071 if (! $rec_set{ $rec }) {
2072 $rec_set{ $rec } = 1;
2077 return { ids => \@recs, count => scalar(@recs) };
2080 __PACKAGE__->register_method(
2081 method => "biblio_search_issn",
2082 api_name => "open-ils.search.biblio.issn",
2084 desc => 'Retrieve biblio IDs for a given ISSN',
2086 {desc => 'ISBN', type => 'string'}
2089 desc => 'Results object like: { "count": $i, "ids": [...] }',
2095 sub biblio_search_issn {
2096 my( $self, $client, $issn ) = @_;
2097 $logger->debug("Searching ISSN $issn");
2098 my $e = new_editor();
2100 my $recs = $U->storagereq(
2101 'open-ils.storage.id_list.biblio.record_entry.search.issn.atomic', $issn );
2102 return { ids => $recs, count => scalar(@$recs) };
2106 __PACKAGE__->register_method(
2107 method => "fetch_mods_by_copy",
2108 api_name => "open-ils.search.biblio.mods_from_copy",
2111 desc => 'Retrieve MODS record given an attached copy ID',
2113 { desc => 'Copy ID', type => 'number' }
2116 desc => 'MODS record, event on error or uncataloged item'
2121 sub fetch_mods_by_copy {
2122 my( $self, $client, $copyid ) = @_;
2123 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2124 return $evt if $evt;
2125 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2126 return $apputils->record_to_mvr($record);
2130 # -------------------------------------------------------------------------------------
2132 __PACKAGE__->register_method(
2133 method => "cn_browse",
2134 api_name => "open-ils.search.callnumber.browse.target",
2135 notes => "Starts a callnumber browse"
2138 __PACKAGE__->register_method(
2139 method => "cn_browse",
2140 api_name => "open-ils.search.callnumber.browse.page_up",
2141 notes => "Returns the previous page of callnumbers",
2144 __PACKAGE__->register_method(
2145 method => "cn_browse",
2146 api_name => "open-ils.search.callnumber.browse.page_down",
2147 notes => "Returns the next page of callnumbers",
2151 # RETURNS array of arrays like so: label, owning_lib, record, id
2153 my( $self, $client, @params ) = @_;
2156 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
2157 if( $self->api_name =~ /target/ );
2158 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2159 if( $self->api_name =~ /page_up/ );
2160 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2161 if( $self->api_name =~ /page_down/ );
2163 return $apputils->simplereq( 'open-ils.storage', $method, @params );
2165 # -------------------------------------------------------------------------------------
2167 __PACKAGE__->register_method(
2168 method => "fetch_cn",
2169 api_name => "open-ils.search.callnumber.retrieve",
2171 notes => "retrieves a callnumber based on ID",
2175 my( $self, $client, $id ) = @_;
2176 my( $cn, $evt ) = $apputils->fetch_callnumber( $id );
2177 return $evt if $evt;
2181 __PACKAGE__->register_method(
2182 method => "fetch_copy_by_cn",
2183 api_name => 'open-ils.search.copies_by_call_number.retrieve',
2185 Returns an array of copy ID's by callnumber ID
2186 @param cnid The callnumber ID
2187 @return An array of copy IDs
2191 sub fetch_copy_by_cn {
2192 my( $self, $conn, $cnid ) = @_;
2193 return $U->cstorereq(
2194 'open-ils.cstore.direct.asset.copy.id_list.atomic',
2195 { call_number => $cnid, deleted => 'f' } );
2198 __PACKAGE__->register_method(
2199 method => 'fetch_cn_by_info',
2200 api_name => 'open-ils.search.call_number.retrieve_by_info',
2202 @param label The callnumber label
2203 @param record The record the cn is attached to
2204 @param org The owning library of the cn
2205 @return The callnumber object
2210 sub fetch_cn_by_info {
2211 my( $self, $conn, $label, $record, $org ) = @_;
2212 return $U->cstorereq(
2213 'open-ils.cstore.direct.asset.call_number.search',
2214 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2219 __PACKAGE__->register_method(
2220 method => 'bib_extras',
2221 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all'
2223 __PACKAGE__->register_method(
2224 method => 'bib_extras',
2225 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all'
2227 __PACKAGE__->register_method(
2228 method => 'bib_extras',
2229 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all'
2231 __PACKAGE__->register_method(
2232 method => 'bib_extras',
2233 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all'
2235 __PACKAGE__->register_method(
2236 method => 'bib_extras',
2237 api_name => 'open-ils.search.biblio.audience_map.retrieve.all'
2243 my $e = new_editor();
2245 return $e->retrieve_all_config_lit_form_map()
2246 if( $self->api_name =~ /lit_form/ );
2248 return $e->retrieve_all_config_item_form_map()
2249 if( $self->api_name =~ /item_form_map/ );
2251 return $e->retrieve_all_config_item_type_map()
2252 if( $self->api_name =~ /item_type_map/ );
2254 return $e->retrieve_all_config_bib_level_map()
2255 if( $self->api_name =~ /bib_level_map/ );
2257 return $e->retrieve_all_config_audience_map()
2258 if( $self->api_name =~ /audience_map/ );
2265 __PACKAGE__->register_method(
2266 method => 'fetch_slim_record',
2267 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2269 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2271 { desc => 'Array of Record IDs', type => 'array' }
2274 desc => 'Array of biblio records, event on error'
2279 sub fetch_slim_record {
2280 my( $self, $conn, $ids ) = @_;
2282 #my $editor = OpenILS::Utils::Editor->new;
2283 my $editor = new_editor();
2286 return $editor->event unless
2287 my $r = $editor->retrieve_biblio_record_entry($_);
2296 __PACKAGE__->register_method(
2297 method => 'rec_to_mr_rec_descriptors',
2298 api_name => 'open-ils.search.metabib.record_to_descriptors',
2300 specialized method...
2301 Given a biblio record id or a metarecord id,
2302 this returns a list of metabib.record_descriptor
2303 objects that live within the same metarecord
2304 @param args Object of args including:
2308 sub rec_to_mr_rec_descriptors {
2309 my( $self, $conn, $args ) = @_;
2311 my $rec = $$args{record};
2312 my $mrec = $$args{metarecord};
2313 my $item_forms = $$args{item_forms};
2314 my $item_types = $$args{item_types};
2315 my $item_lang = $$args{item_lang};
2317 my $e = new_editor();
2321 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2322 return $e->event unless @$map;
2323 $mrec = $$map[0]->metarecord;
2326 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2327 return $e->event unless @$recs;
2329 my @recs = map { $_->source } @$recs;
2330 my $search = { record => \@recs };
2331 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2332 $search->{item_type} = $item_types if $item_types and @$item_types;
2333 $search->{item_lang} = $item_lang if $item_lang;
2335 my $desc = $e->search_metabib_record_descriptor($search);
2337 return { metarecord => $mrec, descriptors => $desc };
2341 __PACKAGE__->register_method(
2342 method => 'fetch_age_protect',
2343 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2346 sub fetch_age_protect {
2347 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2351 __PACKAGE__->register_method(
2352 method => 'copies_by_cn_label',
2353 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2356 __PACKAGE__->register_method(
2357 method => 'copies_by_cn_label',
2358 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2361 sub copies_by_cn_label {
2362 my( $self, $conn, $record, $label, $circ_lib ) = @_;
2363 my $e = new_editor();
2364 my $cns = $e->search_asset_call_number({record => $record, label => $label, deleted => 'f'}, {idlist=>1});
2365 return [] unless @$cns;
2367 # show all non-deleted copies in the staff client ...
2368 if ($self->api_name =~ /staff$/o) {
2369 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2372 # ... otherwise, grab the copies ...
2373 my $copies = $e->search_asset_copy(
2374 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2375 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2379 # ... and test for location and status visibility
2380 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];