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
249 unit. "depth" is always -1 when the count from a lasso search is
250 performed, since depth doesn't mean anything in a lasso context.
257 sub record_id_to_copy_count {
258 my( $self, $client, $org_id, $record_id ) = @_;
260 return [] unless $record_id;
262 my $key = $self->api_name =~ /metarecord/ ? 'metarecord' : 'record';
263 my $staff = $self->api_name =~ /staff/ ? 't' : 'f';
265 my $data = $U->cstorereq(
266 "open-ils.cstore.json_query.atomic",
267 { from => ['asset.' . $key . '_copy_count' => $org_id => $record_id => $staff] }
271 for my $d ( @$data ) { # fix up the key name change required by stored-proc version
272 $$d{count} = delete $$d{visible};
276 return [ sort { $a->{depth} <=> $b->{depth} } @count ];
280 __PACKAGE__->register_method(
281 method => "biblio_search_tcn",
282 api_name => "open-ils.search.biblio.tcn",
285 desc => "Retrieve related record ID(s) given a TCN",
287 { desc => 'TCN', type => 'string' },
288 { desc => 'Flag indicating to include deleted records', type => 'string' }
291 desc => 'Results object like: { "count": $i, "ids": [...] }',
298 sub biblio_search_tcn {
300 my( $self, $client, $tcn, $include_deleted ) = @_;
302 $tcn =~ s/^\s+|\s+$//og;
304 my $e = new_editor();
305 my $search = {tcn_value => $tcn};
306 $search->{deleted} = 'f' unless $include_deleted;
307 my $recs = $e->search_biblio_record_entry( $search, {idlist =>1} );
309 return { count => scalar(@$recs), ids => $recs };
313 # --------------------------------------------------------------------------------
315 __PACKAGE__->register_method(
316 method => "biblio_barcode_to_copy",
317 api_name => "open-ils.search.asset.copy.find_by_barcode",
319 sub biblio_barcode_to_copy {
320 my( $self, $client, $barcode ) = @_;
321 my( $copy, $evt ) = $U->fetch_copy_by_barcode($barcode);
326 __PACKAGE__->register_method(
327 method => "biblio_id_to_copy",
328 api_name => "open-ils.search.asset.copy.batch.retrieve",
330 sub biblio_id_to_copy {
331 my( $self, $client, $ids ) = @_;
332 $logger->info("Fetching copies @$ids");
333 return $U->cstorereq(
334 "open-ils.cstore.direct.asset.copy.search.atomic", { id => $ids } );
338 __PACKAGE__->register_method(
339 method => "biblio_id_to_uris",
340 api_name=> "open-ils.search.asset.uri.retrieve_by_bib",
344 @param BibID Which bib record contains the URIs
345 @param OrgID Where to look for URIs
346 @param OrgDepth Range adjustment for OrgID
347 @return A stream or list of 'auri' objects
351 sub biblio_id_to_uris {
352 my( $self, $client, $bib, $org, $depth ) = @_;
353 die "Org ID required" unless defined($org);
354 die "Bib ID required" unless defined($bib);
357 push @params, $depth if (defined $depth);
359 my $ids = $U->cstorereq( "open-ils.cstore.json_query.atomic",
360 { select => { auri => [ 'id' ] },
364 field => 'call_number',
370 filter => { active => 't' }
381 select => { aou => [ { column => 'id', transform => 'actor.org_unit_descendants', params => \@params, result_field => 'id' } ] },
383 where => { id => $org },
393 my $uris = $U->cstorereq(
394 "open-ils.cstore.direct.asset.uri.search.atomic",
395 { id => [ map { (values %$_) } @$ids ] }
398 $client->respond($_) for (@$uris);
404 __PACKAGE__->register_method(
405 method => "copy_retrieve",
406 api_name => "open-ils.search.asset.copy.retrieve",
409 desc => 'Retrieve a copy object based on the Copy ID',
411 { desc => 'Copy ID', type => 'number'}
414 desc => 'Copy object, event on error'
420 my( $self, $client, $cid ) = @_;
421 my( $copy, $evt ) = $U->fetch_copy($cid);
422 return $evt || $copy;
425 __PACKAGE__->register_method(
426 method => "volume_retrieve",
427 api_name => "open-ils.search.asset.call_number.retrieve"
429 sub volume_retrieve {
430 my( $self, $client, $vid ) = @_;
431 my $e = new_editor();
432 my $vol = $e->retrieve_asset_call_number($vid) or return $e->event;
436 __PACKAGE__->register_method(
437 method => "fleshed_copy_retrieve_batch",
438 api_name => "open-ils.search.asset.copy.fleshed.batch.retrieve",
442 sub fleshed_copy_retrieve_batch {
443 my( $self, $client, $ids ) = @_;
444 $logger->info("Fetching fleshed copies @$ids");
445 return $U->cstorereq(
446 "open-ils.cstore.direct.asset.copy.search.atomic",
449 flesh_fields => { acp => [ qw/ circ_lib location status stat_cat_entries / ] }
454 __PACKAGE__->register_method(
455 method => "fleshed_copy_retrieve",
456 api_name => "open-ils.search.asset.copy.fleshed.retrieve",
459 sub fleshed_copy_retrieve {
460 my( $self, $client, $id ) = @_;
461 my( $c, $e) = $U->fetch_fleshed_copy($id);
466 __PACKAGE__->register_method(
467 method => 'fleshed_by_barcode',
468 api_name => "open-ils.search.asset.copy.fleshed2.find_by_barcode",
471 sub fleshed_by_barcode {
472 my( $self, $conn, $barcode ) = @_;
473 my $e = new_editor();
474 my $copyid = $e->search_asset_copy(
475 {barcode => $barcode, deleted => 'f'}, {idlist=>1})->[0]
477 return fleshed_copy_retrieve2( $self, $conn, $copyid);
481 __PACKAGE__->register_method(
482 method => "fleshed_copy_retrieve2",
483 api_name => "open-ils.search.asset.copy.fleshed2.retrieve",
487 sub fleshed_copy_retrieve2 {
488 my( $self, $client, $id ) = @_;
489 my $e = new_editor();
490 my $copy = $e->retrieve_asset_copy(
497 qw/ location status stat_cat_entry_copy_maps notes age_protect /
499 ascecm => [qw/ stat_cat stat_cat_entry /],
503 ) or return $e->event;
505 # For backwards compatibility
506 #$copy->stat_cat_entries($copy->stat_cat_entry_copy_maps);
508 if( $copy->status->id == OILS_COPY_STATUS_CHECKED_OUT ) {
510 $e->search_action_circulation(
512 { target_copy => $copy->id },
514 order_by => { circ => 'xact_start desc' },
526 __PACKAGE__->register_method(
527 method => 'flesh_copy_custom',
528 api_name => 'open-ils.search.asset.copy.fleshed.custom',
532 sub flesh_copy_custom {
533 my( $self, $conn, $copyid, $fields ) = @_;
534 my $e = new_editor();
535 my $copy = $e->retrieve_asset_copy(
545 ) or return $e->event;
550 __PACKAGE__->register_method(
551 method => "biblio_barcode_to_title",
552 api_name => "open-ils.search.biblio.find_by_barcode",
555 sub biblio_barcode_to_title {
556 my( $self, $client, $barcode ) = @_;
558 my $title = $apputils->simple_scalar_request(
560 "open-ils.storage.biblio.record_entry.retrieve_by_barcode", $barcode );
562 return { ids => [ $title->id ], count => 1 } if $title;
563 return { count => 0 };
566 __PACKAGE__->register_method(
567 method => 'title_id_by_item_barcode',
568 api_name => 'open-ils.search.bib_id.by_barcode',
571 desc => 'Retrieve copy object with fleshed record, given the barcode',
573 { desc => 'Item barcode', type => 'string' }
576 desc => 'Asset copy object with fleshed record and callnumber, or event on error or null set'
581 sub title_id_by_item_barcode {
582 my( $self, $conn, $barcode ) = @_;
583 my $e = new_editor();
584 my $copies = $e->search_asset_copy(
586 { deleted => 'f', barcode => $barcode },
590 acp => [ 'call_number' ],
597 return $e->event unless @$copies;
598 return $$copies[0]->call_number->record->id;
602 __PACKAGE__->register_method(
603 method => "biblio_copy_to_mods",
604 api_name => "open-ils.search.biblio.copy.mods.retrieve",
607 # takes a copy object and returns it fleshed mods object
608 sub biblio_copy_to_mods {
609 my( $self, $client, $copy ) = @_;
611 my $volume = $U->cstorereq(
612 "open-ils.cstore.direct.asset.call_number.retrieve",
613 $copy->call_number() );
615 my $mods = _records_to_mods($volume->record());
616 $mods = shift @$mods;
617 $volume->copies([$copy]);
618 push @{$mods->call_numbers()}, $volume;
626 OpenILS::Application::Search::Biblio
632 =head3 open-ils.search.biblio.multiclass.query (arghash, query, docache)
634 For arghash and docache, see B<open-ils.search.biblio.multiclass>.
636 The query argument is a string, but built like a hash with key: value pairs.
637 Recognized search keys include:
639 keyword (kw) - search keyword(s) *
640 author (au) - search author(s) *
641 name (au) - same as author *
642 title (ti) - search title *
643 subject (su) - search subject *
644 series (se) - search series *
645 lang - limit by language (specifiy multiple langs with lang:l1 lang:l2 ...)
646 site - search at specified org unit, corresponds to actor.org_unit.shortname
647 sort - sort type (title, author, pubdate)
648 dir - sort direction (asc, desc)
649 available - if set to anything other than "false" or "0", limits to available items
651 * Searching keyword, author, title, subject, and series supports additional search
652 subclasses, specified with a "|". For example, C<title|proper:gone with the wind>.
654 For more, see B<config.metabib_field>.
658 foreach (qw/open-ils.search.biblio.multiclass.query
659 open-ils.search.biblio.multiclass.query.staff
660 open-ils.search.metabib.multiclass.query
661 open-ils.search.metabib.multiclass.query.staff/)
663 __PACKAGE__->register_method(
665 method => 'multiclass_query',
667 desc => 'Perform a search query. The .staff version of the call includes otherwise hidden hits.',
669 {name => 'arghash', desc => 'Arg hash (see open-ils.search.biblio.multiclass)', type => 'object'},
670 {name => 'query', desc => 'Raw human-readable query (see perldoc '. __PACKAGE__ .')', type => 'string'},
671 {name => 'docache', desc => 'Flag for caching (see open-ils.search.biblio.multiclass)', type => 'object'},
674 desc => 'Search results from query, like: { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
675 type => 'object', # TODO: update as miker's new elements are included
681 sub multiclass_query {
682 my($self, $conn, $arghash, $query, $docache) = @_;
684 $logger->debug("initial search query => $query");
685 my $orig_query = $query;
689 $query =~ s/^\s+//go;
691 # convert convenience classes (e.g. kw for keyword) to the full class name
692 $query =~ s/kw(:|\|)/keyword$1/go;
693 $query =~ s/ti(:|\|)/title$1/go;
694 $query =~ s/au(:|\|)/author$1/go;
695 $query =~ s/su(:|\|)/subject$1/go;
696 $query =~ s/se(:|\|)/series$1/go;
697 $query =~ s/name(:|\|)/author$1/og;
699 $logger->debug("cleansed query string => $query");
702 my $simple_class_re = qr/((?:\w+(?:\|\w+)?):[^:]+?)$/;
703 my $class_list_re = qr/(?:keyword|title|author|subject|series)/;
704 my $modifier_list_re = qr/(?:site|dir|sort|lang|available)/;
707 while ($query =~ s/$simple_class_re//so) {
710 my $where = index($qpart,':');
711 my $type = substr($qpart, 0, $where++);
712 my $value = substr($qpart, $where);
714 if ($type !~ /^(?:$class_list_re|$modifier_list_re)/o) {
715 $tmp_value = "$qpart $tmp_value";
719 if ($type =~ /$class_list_re/o ) {
720 $value .= $tmp_value;
724 next unless $type and $value;
726 $value =~ s/^\s*//og;
727 $value =~ s/\s*$//og;
728 $type = 'sort_dir' if $type eq 'dir';
730 if($type eq 'site') {
731 # 'site' is the org shortname. when using this, we also want
732 # to search at the requested org's depth
733 my $e = new_editor();
734 if(my $org = $e->search_actor_org_unit({shortname => $value})->[0]) {
735 $arghash->{org_unit} = $org->id if $org;
736 $arghash->{depth} = $e->retrieve_actor_org_unit_type($org->ou_type)->depth;
738 $logger->warn("'site:' query used on invalid org shortname: $value ... ignoring");
741 } elsif($type eq 'available') {
743 $arghash->{available} = 1 unless $value eq 'false' or $value eq '0';
745 } elsif($type eq 'lang') {
746 # collect languages into an array of languages
747 $arghash->{language} = [] unless $arghash->{language};
748 push(@{$arghash->{language}}, $value);
750 } elsif($type =~ /^sort/o) {
751 # sort and sort_dir modifiers
752 $arghash->{$type} = $value;
755 # append the search term to the term under construction
756 $search->{$type} = {} unless $search->{$type};
757 $search->{$type}->{term} =
758 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $value" : $value;
762 $query .= " $tmp_value";
763 $query =~ s/\s+/ /go;
764 $query =~ s/^\s+//go;
765 $query =~ s/\s+$//go;
767 my $type = $arghash->{default_class} || 'keyword';
768 $type = ($type eq '-') ? 'keyword' : $type;
769 $type = ($type !~ /^(title|author|keyword|subject|series)(?:\|\w+)?$/o) ? 'keyword' : $type;
772 # This is the front part of the string before any special tokens were
773 # parsed OR colon-separated strings that do not denote a class.
774 # Add this data to the default search class
775 $search->{$type} = {} unless $search->{$type};
776 $search->{$type}->{term} =
777 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $query" : $query;
779 my $real_search = $arghash->{searches} = { $type => { term => $orig_query } };
781 # capture the original limit because the search method alters the limit internally
782 my $ol = $arghash->{limit};
784 my $sclient = OpenSRF::Utils::SettingsClient->new;
786 (my $method = $self->api_name) =~ s/\.query//o;
788 $method =~ s/multiclass/multiclass.staged/
789 if $sclient->config_value(apps => 'open-ils.search',
790 app_settings => 'use_staged_search') =~ /true/i;
792 $arghash->{preferred_language} = $U->get_org_locale($arghash->{org_unit})
793 unless $arghash->{preferred_language};
795 $method = $self->method_lookup($method);
796 my ($data) = $method->run($arghash, $docache);
798 $arghash->{searches} = $search if (!$data->{complex_query});
800 $arghash->{limit} = $ol if $ol;
801 $data->{compiled_search} = $arghash;
802 $data->{query} = $orig_query;
804 $logger->info("compiled search is " . OpenSRF::Utils::JSON->perl2JSON($arghash));
809 __PACKAGE__->register_method(
810 method => 'cat_search_z_style_wrapper',
811 api_name => 'open-ils.search.biblio.zstyle',
813 signature => q/@see open-ils.search.biblio.multiclass/
816 __PACKAGE__->register_method(
817 method => 'cat_search_z_style_wrapper',
818 api_name => 'open-ils.search.biblio.zstyle.staff',
820 signature => q/@see open-ils.search.biblio.multiclass/
823 sub cat_search_z_style_wrapper {
826 my $authtoken = shift;
829 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
831 my $ou = $cstore->request(
832 'open-ils.cstore.direct.actor.org_unit.search',
833 { parent_ou => undef }
836 my $result = { service => 'native-evergreen-catalog', records => [] };
837 my $searchhash = { limit => $$args{limit}, offset => $$args{offset}, org_unit => $ou->id };
839 $$searchhash{searches}{title}{term} = $$args{search}{title} if $$args{search}{title};
840 $$searchhash{searches}{author}{term} = $$args{search}{author} if $$args{search}{author};
841 $$searchhash{searches}{subject}{term} = $$args{search}{subject} if $$args{search}{subject};
842 $$searchhash{searches}{keyword}{term} = $$args{search}{keyword} if $$args{search}{keyword};
844 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{tcn} if $$args{search}{tcn};
845 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{isbn} if $$args{search}{isbn};
846 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{issn} if $$args{search}{issn};
847 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{publisher} if $$args{search}{publisher};
848 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{pubdate} if $$args{search}{pubdate};
849 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{item_type} if $$args{search}{item_type};
851 my $list = the_quest_for_knowledge( $self, $client, $searchhash );
853 if ($list->{count} > 0) {
854 $result->{count} = $list->{count};
856 my $records = $cstore->request(
857 'open-ils.cstore.direct.biblio.record_entry.search.atomic',
858 { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
861 for my $rec ( @$records ) {
863 my $u = OpenILS::Utils::ModsParser->new();
864 $u->start_mods_batch( $rec->marc );
865 my $mods = $u->finish_mods_batch();
867 push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
873 $cstore->disconnect();
877 # ----------------------------------------------------------------------------
878 # These are the main OPAC search methods
879 # ----------------------------------------------------------------------------
881 __PACKAGE__->register_method(
882 method => 'the_quest_for_knowledge',
883 api_name => 'open-ils.search.biblio.multiclass',
885 desc => "Performs a multi class biblio or metabib search",
888 desc => "A search hash with keys: "
889 . "searches, org_unit, depth, limit, offset, format, sort, sort_dir. "
890 . "See perldoc " . __PACKAGE__ . " for more detail",
894 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
899 desc => 'An object of the form: '
900 . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
905 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
907 The search-hash argument can have the following elements:
909 searches: { "$class" : "$value", ...} [REQUIRED]
910 org_unit: The org id to focus the search at
911 depth : The org depth
912 limit : The search limit default: 10
913 offset : The search offset default: 0
914 format : The MARC format
915 sort : What field to sort the results on? [ author | title | pubdate ]
916 sort_dir: What direction do we sort? [ asc | desc ]
917 tag_circulated_records : Boolean, if true, records that are in the user's visible checkout history
918 will be tagged with an additional value ("1") as the last value in the record ID array for
919 each record. Requires the 'authtoken'
920 authtoken : Authentication token string; When actions are performed that require a user login
921 (e.g. tagging circulated records), the authentication token is required
923 The searches element is required, must have a hashref value, and the hashref must contain at least one
924 of the following classes as a key:
932 The value paired with a key is the associated search string.
934 The docache argument enables/disables searching and saving results in cache (default OFF).
936 The return object, if successful, will look like:
938 { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
942 __PACKAGE__->register_method(
943 method => 'the_quest_for_knowledge',
944 api_name => 'open-ils.search.biblio.multiclass.staff',
945 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
947 __PACKAGE__->register_method(
948 method => 'the_quest_for_knowledge',
949 api_name => 'open-ils.search.metabib.multiclass',
950 signature => q/@see open-ils.search.biblio.multiclass/
952 __PACKAGE__->register_method(
953 method => 'the_quest_for_knowledge',
954 api_name => 'open-ils.search.metabib.multiclass.staff',
955 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
958 sub the_quest_for_knowledge {
959 my( $self, $conn, $searchhash, $docache ) = @_;
961 return { count => 0 } unless $searchhash and
962 ref $searchhash->{searches} eq 'HASH';
964 my $method = 'open-ils.storage.biblio.multiclass.search_fts';
968 if($self->api_name =~ /metabib/) {
970 $method =~ s/biblio/metabib/o;
973 # do some simple sanity checking
974 if(!$searchhash->{searches} or
975 ( !grep { /^(?:title|author|subject|series|keyword)/ } keys %{$searchhash->{searches}} ) ) {
976 return { count => 0 };
979 my $offset = $searchhash->{offset} || 0; # user value or default in local var now
980 my $limit = $searchhash->{limit} || 10; # user value or default in local var now
981 my $end = $offset + $limit - 1;
984 $searchhash->{offset} = 0; # possible user value overwritten in hash
985 $searchhash->{limit} = $maxlimit; # possible user value overwritten in hash
987 return { count => 0 } if $offset > $maxlimit;
990 push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
991 my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
992 my $ckey = $pfx . md5_hex($method . $s);
994 $logger->info("bib search for: $s");
996 $searchhash->{limit} -= $offset;
1000 my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
1004 $method .= ".staff" if($self->api_name =~ /staff/);
1005 $method .= ".atomic";
1007 for (keys %$searchhash) {
1008 delete $$searchhash{$_}
1009 unless defined $$searchhash{$_};
1012 $result = $U->storagereq( $method, %$searchhash );
1016 $docache = 0; # results came FROM cache, so we don't write back
1019 return {count => 0} unless ($result && $$result[0]);
1023 my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
1026 # If we didn't get this data from the cache, put it into the cache
1027 # then return the correct offset of records
1028 $logger->debug("putting search cache $ckey\n");
1029 put_cache($ckey, $count, \@recs);
1033 # if we have the full set of data, trim out
1034 # the requested chunk based on limit and offset
1036 for ($offset..$end) {
1037 last unless $recs[$_];
1038 push(@t, $recs[$_]);
1043 return { ids => \@recs, count => $count };
1047 __PACKAGE__->register_method(
1048 method => 'staged_search',
1049 api_name => 'open-ils.search.biblio.multiclass.staged',
1051 desc => 'Staged search filters out unavailable items. This means that it relies on an estimation strategy for determining ' .
1052 'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering. See "estimation_strategy" in your SRF config.',
1055 desc => "A search hash with keys: "
1056 . "searches, limit, offset. The others are optional, but the 'searches' key/value pair is required, with the value being a hashref. "
1057 . "See perldoc " . __PACKAGE__ . " for more detail",
1061 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
1066 desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids. '
1067 . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
1072 __PACKAGE__->register_method(
1073 method => 'staged_search',
1074 api_name => 'open-ils.search.biblio.multiclass.staged.staff',
1075 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1077 __PACKAGE__->register_method(
1078 method => 'staged_search',
1079 api_name => 'open-ils.search.metabib.multiclass.staged',
1080 signature => q/@see open-ils.search.biblio.multiclass.staged/
1082 __PACKAGE__->register_method(
1083 method => 'staged_search',
1084 api_name => 'open-ils.search.metabib.multiclass.staged.staff',
1085 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1089 my($self, $conn, $search_hash, $docache) = @_;
1091 my $IAmMetabib = ($self->api_name =~ /metabib/) ? 1 : 0;
1093 my $method = $IAmMetabib?
1094 'open-ils.storage.metabib.multiclass.staged.search_fts':
1095 'open-ils.storage.biblio.multiclass.staged.search_fts';
1097 $method .= '.staff' if $self->api_name =~ /staff$/;
1098 $method .= '.atomic';
1100 return {count => 0} unless (
1102 $search_hash->{searches} and
1103 scalar( keys %{$search_hash->{searches}} ));
1105 my $search_duration;
1106 my $user_offset = $search_hash->{offset} || 0; # user-specified offset
1107 my $user_limit = $search_hash->{limit} || 10;
1108 $user_offset = ($user_offset >= 0) ? $user_offset : 0;
1109 $user_limit = ($user_limit >= 0) ? $user_limit : 10;
1112 # we're grabbing results on a per-superpage basis, which means the
1113 # limit and offset should coincide with superpage boundaries
1114 $search_hash->{offset} = 0;
1115 $search_hash->{limit} = $superpage_size;
1117 # force a well-known check_limit
1118 $search_hash->{check_limit} = $superpage_size;
1119 # restrict total tested to superpage size * number of superpages
1120 $search_hash->{core_limit} = $superpage_size * $max_superpages;
1122 # Set the configured estimation strategy, defaults to 'inclusion'.
1123 my $estimation_strategy = OpenSRF::Utils::SettingsClient
1126 apps => 'open-ils.search', app_settings => 'estimation_strategy'
1128 $search_hash->{estimation_strategy} = $estimation_strategy;
1130 # pull any existing results from the cache
1131 my $key = search_cache_key($method, $search_hash);
1132 my $facet_key = $key.'_facets';
1133 my $cache_data = $cache->get_cache($key) || {};
1135 # keep retrieving results until we find enough to
1136 # fulfill the user-specified limit and offset
1137 my $all_results = [];
1138 my $page; # current superpage
1139 my $est_hit_count = 0;
1140 my $current_page_summary = {};
1141 my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1142 my $is_real_hit_count = 0;
1145 for($page = 0; $page < $max_superpages; $page++) {
1147 my $data = $cache_data->{$page};
1151 $logger->debug("staged search: analyzing superpage $page");
1154 # this window of results is already cached
1155 $logger->debug("staged search: found cached results");
1156 $summary = $data->{summary};
1157 $results = $data->{results};
1160 # retrieve the window of results from the database
1161 $logger->debug("staged search: fetching results from the database");
1162 $search_hash->{skip_check} = $page * $superpage_size;
1164 $results = $U->storagereq($method, %$search_hash);
1165 $search_duration = time - $start;
1166 $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1167 $summary = shift(@$results) if $results;
1170 $logger->info("search timed out: duration=$search_duration: params=".
1171 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1172 return {count => 0};
1175 my $hc = $summary->{estimated_hit_count} || $summary->{visible};
1177 $logger->info("search returned 0 results: duration=$search_duration: params=".
1178 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1181 # Create backwards-compatible result structures
1183 $results = [map {[$_->{id}, $_->{rel}, $_->{record}]} @$results];
1185 $results = [map {[$_->{id}]} @$results];
1188 tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib)
1189 if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1191 push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1192 $results = [grep {defined $_->[0]} @$results];
1193 cache_staged_search_page($key, $page, $summary, $results) if $docache;
1196 $current_page_summary = $summary;
1198 # add the new set of results to the set under construction
1199 push(@$all_results, @$results);
1201 my $current_count = scalar(@$all_results);
1203 $est_hit_count = $summary->{estimated_hit_count} || $summary->{visible}
1206 $logger->debug("staged search: located $current_count, with estimated hits=".
1207 $summary->{estimated_hit_count}." : visible=".$summary->{visible}.", checked=".$summary->{checked});
1209 if (defined($summary->{estimated_hit_count})) {
1210 foreach (qw/ checked visible excluded deleted /) {
1211 $global_summary->{$_} += $summary->{$_};
1213 $global_summary->{total} = $summary->{total};
1216 # we've found all the possible hits
1217 last if $current_count == $summary->{visible}
1218 and not defined $summary->{estimated_hit_count};
1220 # we've found enough results to satisfy the requested limit/offset
1221 last if $current_count >= ($user_limit + $user_offset);
1223 # we've scanned all possible hits
1224 if($summary->{checked} < $superpage_size) {
1225 $est_hit_count = scalar(@$all_results);
1226 # we have all possible results in hand, so we know the final hit count
1227 $is_real_hit_count = 1;
1232 my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1234 # refine the estimate if we have more than one superpage
1235 if ($page > 0 and not $is_real_hit_count) {
1236 if ($global_summary->{checked} >= $global_summary->{total}) {
1237 $est_hit_count = $global_summary->{visible};
1239 my $updated_hit_count = $U->storagereq(
1240 'open-ils.storage.fts_paging_estimate',
1241 $global_summary->{checked},
1242 $global_summary->{visible},
1243 $global_summary->{excluded},
1244 $global_summary->{deleted},
1245 $global_summary->{total}
1247 $est_hit_count = $updated_hit_count->{$estimation_strategy};
1251 $conn->respond_complete(
1253 count => $est_hit_count,
1254 core_limit => $search_hash->{core_limit},
1255 superpage_size => $search_hash->{check_limit},
1256 superpage_summary => $current_page_summary,
1257 facet_key => $facet_key,
1262 cache_facets($facet_key, $new_ids, $IAmMetabib) if $docache;
1267 sub tag_circulated_records {
1268 my ($auth, $results, $metabib) = @_;
1269 my $e = new_editor(authtoken => $auth);
1270 return $results unless $e->checkauth;
1273 select => { acn => [{ column => 'record', alias => 'tagme' }] },
1274 from => { acp => 'acn' },
1275 where => { id => { in => { from => ['action.usr_visible_circ_copies', $e->requestor->id] } } },
1281 select => { mmsm => [{ column => 'metarecord', alias => 'tagme' }] },
1283 where => { source => { in => $query } },
1288 # Give me the distinct set of bib records that exist in the user's visible circulation history
1289 my $circ_recs = $e->json_query( $query );
1291 # if the record appears in the circ history, push a 1 onto
1292 # the rec array structure to indicate truthiness
1293 for my $rec (@$results) {
1294 push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1300 # creates a unique token to represent the query in the cache
1301 sub search_cache_key {
1303 my $search_hash = shift;
1305 for my $key (sort keys %$search_hash) {
1306 push(@sorted, ($key => $$search_hash{$key}))
1307 unless $key eq 'limit' or
1309 $key eq 'skip_check';
1311 my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1312 return $pfx . md5_hex($method . $s);
1315 sub retrieve_cached_facets {
1321 return undef unless ($key and $key =~ /_facets$/);
1323 my $blob = $cache->get_cache($key) || {};
1327 for my $f ( keys %$blob ) {
1328 my @sorted = map{ { $$_[1] => $$_[0] } } sort {$$b[0] <=> $$a[0] || $$a[1] cmp $$b[1]} map { [$$blob{$f}{$_}, $_] } keys %{ $$blob{$f} };
1329 @sorted = @sorted[0 .. $limit - 1] if (scalar(@sorted) > $limit);
1330 for my $s ( @sorted ) {
1331 my ($k) = keys(%$s);
1332 my ($v) = values(%$s);
1333 $$facets{$f}{$k} = $v;
1343 __PACKAGE__->register_method(
1344 method => "retrieve_cached_facets",
1345 api_name => "open-ils.search.facet_cache.retrieve",
1347 desc => 'Returns facet data derived from a specific search based on a key '.
1348 'generated by open-ils.search.biblio.multiclass.staged and friends.',
1351 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1356 desc => 'Two level hash of facet values. Top level key is the facet id defined on the config.metabib_field table. '.
1357 'Second level key is a string facet value. Datum attached to each facet value is the number of distinct records, '.
1358 'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1359 'facet retrieval. These counts are calculated for all superpages that have been checked for visibility.',
1367 # add facets for this search to the facet cache
1368 my($key, $results, $metabib) = @_;
1369 my $data = $cache->get_cache($key);
1372 return undef unless (@$results);
1374 # The query we're constructing
1376 # select mfae.field as id,
1378 # count(distinct mmrsm.appropriate-id-field )
1379 # from metabib.facet_entry mfae
1380 # join metabib.metarecord_sourc_map mmrsm on (mfae.source = mmrsm.source)
1381 # where mmrsm.appropriate-id-field in IDLIST
1384 my $count_field = $metabib ? 'metarecord' : 'source';
1385 my $facets = $U->cstorereq( "open-ils.cstore.json_query.atomic",
1387 mfae => [ { column => 'field', alias => 'id'}, 'value' ],
1389 transform => 'count',
1391 column => $count_field,
1398 mmrsm => { field => 'source', fkey => 'source' }
1402 '+mmrsm' => { $count_field => $results }
1407 for my $facet (@$facets) {
1408 next unless ($facet->{value});
1409 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1412 $logger->info("facet compilation: cached with key=$key");
1414 $cache->put_cache($key, $data, $cache_timeout);
1417 sub cache_staged_search_page {
1418 # puts this set of results into the cache
1419 my($key, $page, $summary, $results) = @_;
1420 my $data = $cache->get_cache($key);
1423 summary => $summary,
1427 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1428 $summary->{estimated_hit_count}.", visible=".$summary->{visible});
1430 $cache->put_cache($key, $data, $cache_timeout);
1438 my $start = $offset;
1439 my $end = $offset + $limit - 1;
1441 $logger->debug("searching cache for $key : $start..$end\n");
1443 return undef unless $cache;
1444 my $data = $cache->get_cache($key);
1446 return undef unless $data;
1448 my $count = $data->[0];
1451 return undef unless $offset < $count;
1454 for( my $i = $offset; $i <= $end; $i++ ) {
1455 last unless my $d = $$data[$i];
1456 push( @result, $d );
1459 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1466 my( $key, $count, $data ) = @_;
1467 return undef unless $cache;
1468 $logger->debug("search_cache putting ".
1469 scalar(@$data)." items at key $key with timeout $cache_timeout");
1470 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1474 __PACKAGE__->register_method(
1475 method => "biblio_mrid_to_modsbatch_batch",
1476 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1479 sub biblio_mrid_to_modsbatch_batch {
1480 my( $self, $client, $mrids) = @_;
1481 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1483 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1484 for my $id (@$mrids) {
1485 next unless defined $id;
1486 my ($m) = $method->run($id);
1493 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1494 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1496 __PACKAGE__->register_method(
1497 method => "biblio_mrid_to_modsbatch",
1500 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1501 . "As usual, the .staff version of this method will include otherwise hidden records.",
1503 { desc => 'Metarecord ID', type => 'number' },
1504 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1507 desc => 'MVR Object, event on error',
1513 sub biblio_mrid_to_modsbatch {
1514 my( $self, $client, $mrid, $args) = @_;
1516 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1518 my ($mr, $evt) = _grab_metarecord($mrid);
1519 return $evt unless $mr;
1521 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1522 biblio_mrid_make_modsbatch($self, $client, $mr);
1524 return $mvr unless ref($args);
1526 # Here we find the lead record appropriate for the given filters
1527 # and use that for the title and author of the metarecord
1528 my $format = $$args{format};
1529 my $org = $$args{org};
1530 my $depth = $$args{depth};
1532 return $mvr unless $format or $org or $depth;
1534 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1535 $method = "$method.staff" if $self->api_name =~ /staff/o;
1537 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1539 if( my $mods = $U->record_to_mvr($rec) ) {
1541 $mvr->title( $mods->title );
1542 $mvr->author($mods->author);
1543 $logger->debug("mods_slim updating title and ".
1544 "author in mvr with ".$mods->title." : ".$mods->author);
1550 # converts a metarecord to an mvr
1553 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1554 return Fieldmapper::metabib::virtual_record->new($perl);
1557 # checks to see if a metarecord has mods, if so returns true;
1559 __PACKAGE__->register_method(
1560 method => "biblio_mrid_check_mvr",
1561 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1562 notes => "Takes a metarecord ID or a metarecord object and returns true "
1563 . "if the metarecord already has an mvr associated with it."
1566 sub biblio_mrid_check_mvr {
1567 my( $self, $client, $mrid ) = @_;
1571 if(ref($mrid)) { $mr = $mrid; }
1572 else { ($mr, $evt) = _grab_metarecord($mrid); }
1573 return $evt if $evt;
1575 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1577 return _mr_to_mvr($mr) if $mr->mods();
1581 sub _grab_metarecord {
1583 #my $e = OpenILS::Utils::Editor->new;
1584 my $e = new_editor();
1585 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1590 __PACKAGE__->register_method(
1591 method => "biblio_mrid_make_modsbatch",
1592 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1593 notes => "Takes either a metarecord ID or a metarecord object. "
1594 . "Forces the creations of an mvr for the given metarecord. "
1595 . "The created mvr is returned."
1598 sub biblio_mrid_make_modsbatch {
1599 my( $self, $client, $mrid ) = @_;
1601 #my $e = OpenILS::Utils::Editor->new;
1602 my $e = new_editor();
1609 $mr = $e->retrieve_metabib_metarecord($mrid)
1610 or return $e->event;
1613 my $masterid = $mr->master_record;
1614 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1616 my $ids = $U->storagereq(
1617 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1618 return undef unless @$ids;
1620 my $master = $e->retrieve_biblio_record_entry($masterid)
1621 or return $e->event;
1623 # start the mods batch
1624 my $u = OpenILS::Utils::ModsParser->new();
1625 $u->start_mods_batch( $master->marc );
1627 # grab all of the sub-records and shove them into the batch
1628 my @ids = grep { $_ ne $masterid } @$ids;
1629 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1634 my $r = $e->retrieve_biblio_record_entry($i);
1635 push( @$subrecs, $r ) if $r;
1640 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1641 $u->push_mods_batch( $_->marc ) if $_->marc;
1645 # finish up and send to the client
1646 my $mods = $u->finish_mods_batch();
1647 $mods->doc_id($mrid);
1648 $client->respond_complete($mods);
1651 # now update the mods string in the db
1652 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1655 #$e = OpenILS::Utils::Editor->new(xact => 1);
1656 $e = new_editor(xact => 1);
1657 $e->update_metabib_metarecord($mr)
1658 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1665 # converts a mr id into a list of record ids
1667 foreach (qw/open-ils.search.biblio.metarecord_to_records
1668 open-ils.search.biblio.metarecord_to_records.staff/)
1670 __PACKAGE__->register_method(
1671 method => "biblio_mrid_to_record_ids",
1674 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1675 . "As usual, the .staff version of this method will include otherwise hidden records.",
1677 { desc => 'Metarecord ID', type => 'number' },
1678 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1681 desc => 'Results object like {count => $i, ids =>[...]}',
1689 sub biblio_mrid_to_record_ids {
1690 my( $self, $client, $mrid, $args ) = @_;
1692 my $format = $$args{format};
1693 my $org = $$args{org};
1694 my $depth = $$args{depth};
1696 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1697 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1698 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1700 return { count => scalar(@$recs), ids => $recs };
1704 __PACKAGE__->register_method(
1705 method => "biblio_record_to_marc_html",
1706 api_name => "open-ils.search.biblio.record.html"
1709 __PACKAGE__->register_method(
1710 method => "biblio_record_to_marc_html",
1711 api_name => "open-ils.search.authority.to_html"
1714 # Persistent parsers and setting objects
1715 my $parser = XML::LibXML->new();
1716 my $xslt = XML::LibXSLT->new();
1718 my $slim_marc_sheet;
1719 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1721 sub biblio_record_to_marc_html {
1722 my($self, $client, $recordid, $slim, $marcxml) = @_;
1725 my $dir = $settings_client->config_value("dirs", "xsl");
1728 unless($slim_marc_sheet) {
1729 my $xsl = $settings_client->config_value(
1730 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1732 $xsl = $parser->parse_file("$dir/$xsl");
1733 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1736 $sheet = $slim_marc_sheet;
1740 unless($marc_sheet) {
1741 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1742 my $xsl = $settings_client->config_value(
1743 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1744 $xsl = $parser->parse_file("$dir/$xsl");
1745 $marc_sheet = $xslt->parse_stylesheet($xsl);
1747 $sheet = $marc_sheet;
1752 my $e = new_editor();
1753 if($self->api_name =~ /authority/) {
1754 $record = $e->retrieve_authority_record_entry($recordid)
1755 or return $e->event;
1757 $record = $e->retrieve_biblio_record_entry($recordid)
1758 or return $e->event;
1760 $marcxml = $record->marc;
1763 my $xmldoc = $parser->parse_string($marcxml);
1764 my $html = $sheet->transform($xmldoc);
1765 return $html->documentElement->toString();
1768 __PACKAGE__->register_method(
1769 method => "format_biblio_record_entry",
1770 api_name => "open-ils.search.biblio.record.print",
1772 desc => 'Returns a printable version of the specified bib record',
1774 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1777 desc => q/An action_trigger.event object or error event./,
1782 __PACKAGE__->register_method(
1783 method => "format_biblio_record_entry",
1784 api_name => "open-ils.search.biblio.record.email",
1786 desc => 'Emails an A/T templated version of the specified bib records to the authorized user',
1788 { desc => 'Authentication token', type => 'string'},
1789 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1792 desc => q/Undefined on success, otherwise an error event./,
1798 sub format_biblio_record_entry {
1799 my($self, $conn, $arg1, $arg2) = @_;
1801 my $for_print = ($self->api_name =~ /print/);
1802 my $for_email = ($self->api_name =~ /email/);
1804 my $e; my $auth; my $bib_id; my $context_org;
1808 $context_org = $arg2 || $U->fetch_org_tree->id;
1809 $e = new_editor(xact => 1);
1810 } elsif ($for_email) {
1813 $e = new_editor(authtoken => $auth, xact => 1);
1814 return $e->die_event unless $e->checkauth;
1815 $context_org = $e->requestor->home_ou;
1819 if (ref $bib_id ne 'ARRAY') {
1820 $bib_ids = [ $bib_id ];
1825 my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1826 $bucket->btype('temp');
1827 $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1829 $bucket->owner($e->requestor)
1833 my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1835 for my $id (@$bib_ids) {
1837 my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1839 my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1840 $bucket_entry->target_biblio_record_entry($bib);
1841 $bucket_entry->bucket($bucket_obj->id);
1842 $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
1849 return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
1851 } elsif ($for_email) {
1853 $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
1860 __PACKAGE__->register_method(
1861 method => "retrieve_all_copy_statuses",
1862 api_name => "open-ils.search.config.copy_status.retrieve.all"
1865 sub retrieve_all_copy_statuses {
1866 my( $self, $client ) = @_;
1867 return new_editor()->retrieve_all_config_copy_status();
1871 __PACKAGE__->register_method(
1872 method => "copy_counts_per_org",
1873 api_name => "open-ils.search.biblio.copy_counts.retrieve"
1876 __PACKAGE__->register_method(
1877 method => "copy_counts_per_org",
1878 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1881 sub copy_counts_per_org {
1882 my( $self, $client, $record_id ) = @_;
1884 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1886 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1887 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1889 my $counts = $apputils->simple_scalar_request(
1890 "open-ils.storage", $method, $record_id );
1892 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1897 __PACKAGE__->register_method(
1898 method => "copy_count_summary",
1899 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
1900 notes => "returns an array of these: "
1901 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1902 . "where statusx is a copy status name. The statuses are sorted by ID.",
1906 sub copy_count_summary {
1907 my( $self, $client, $rid, $org, $depth ) = @_;
1910 my $data = $U->storagereq(
1911 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
1913 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1916 __PACKAGE__->register_method(
1917 method => "copy_location_count_summary",
1918 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
1919 notes => "returns an array of these: "
1920 . "[ org_id, callnumber_label, copy_location, <status1_count>, <status2_count>,...] "
1921 . "where statusx is a copy status name. The statuses are sorted by ID.",
1924 sub copy_location_count_summary {
1925 my( $self, $client, $rid, $org, $depth ) = @_;
1928 my $data = $U->storagereq(
1929 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1931 return [ sort { $a->[1] cmp $b->[1] || $a->[2] cmp $b->[2] } @$data ];
1934 __PACKAGE__->register_method(
1935 method => "copy_count_location_summary",
1936 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
1937 notes => "returns an array of these: "
1938 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1939 . "where statusx is a copy status name. The statuses are sorted by ID."
1942 sub copy_count_location_summary {
1943 my( $self, $client, $rid, $org, $depth ) = @_;
1946 my $data = $U->storagereq(
1947 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1948 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1952 foreach (qw/open-ils.search.biblio.marc
1953 open-ils.search.biblio.marc.staff/)
1955 __PACKAGE__->register_method(
1956 method => "marc_search",
1959 desc => 'Fetch biblio IDs based on MARC record criteria. '
1960 . 'As usual, the .staff version of the search includes otherwise hidden records',
1963 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
1964 'See perldoc ' . __PACKAGE__ . ' for more detail.',
1967 {desc => 'limit (optional)', type => 'number'},
1968 {desc => 'offset (optional)', type => 'number'}
1971 desc => 'Results object like: { "count": $i, "ids": [...] }',
1978 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
1980 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
1982 searches: complex query object (required)
1983 org_unit: The org ID to focus the search at
1984 depth : The org depth
1985 limit : integer search limit default: 10
1986 offset : integer search offset default: 0
1987 sort : What field to sort the results on? [ author | title | pubdate ]
1988 sort_dir: In what direction do we sort? [ asc | desc ]
1990 Additional keys to refine search criteria:
1993 language : Language (code)
1994 lit_form : Literary form
1995 item_form: Item form
1996 item_type: Item type
1997 format : The MARC format
1999 Please note that the specific strings to be used in the "addtional keys" will be entirely
2000 dependent on your loaded data.
2002 All keys except "searches" are optional.
2003 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
2005 For example, an arg hash might look like:
2027 The arghash is eventually passed to the SRF call:
2028 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2030 Presently, search uses the cache unconditionally.
2034 # FIXME: that example above isn't actually tested.
2035 # TODO: docache option?
2037 my( $self, $conn, $args, $limit, $offset ) = @_;
2039 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2040 $method .= ".staff" if $self->api_name =~ /staff/;
2041 $method .= ".atomic";
2043 $limit ||= 10; # FIXME: what about $args->{limit} ?
2044 $offset ||= 0; # FIXME: what about $args->{offset} ?
2047 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2048 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2050 my $recs = search_cache($ckey, $offset, $limit);
2053 $recs = $U->storagereq($method, %$args) || [];
2055 put_cache($ckey, scalar(@$recs), $recs);
2056 $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
2063 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2064 my @recs = map { $_->[0] } @$recs;
2066 return { ids => \@recs, count => $count };
2070 __PACKAGE__->register_method(
2071 method => "biblio_search_isbn",
2072 api_name => "open-ils.search.biblio.isbn",
2074 desc => 'Retrieve biblio IDs for a given ISBN',
2076 {desc => 'ISBN', type => 'string'} # or number maybe? How normalized is our storage data?
2079 desc => 'Results object like: { "count": $i, "ids": [...] }',
2085 sub biblio_search_isbn {
2086 my( $self, $client, $isbn ) = @_;
2087 $logger->debug("Searching ISBN $isbn");
2088 my $recs = $U->storagereq('open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn);
2089 return { ids => $recs, count => scalar(@$recs) };
2092 __PACKAGE__->register_method(
2093 method => "biblio_search_isbn_batch",
2094 api_name => "open-ils.search.biblio.isbn_list",
2097 sub biblio_search_isbn_batch {
2098 my( $self, $client, $isbn_list ) = @_;
2099 $logger->debug("Searching ISBNs @$isbn_list");
2100 my @recs = (); my %rec_set = ();
2101 foreach my $isbn ( @$isbn_list ) {
2102 foreach my $rec ( @{ $U->storagereq(
2103 'open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn )
2105 if (! $rec_set{ $rec }) {
2106 $rec_set{ $rec } = 1;
2111 return { ids => \@recs, count => scalar(@recs) };
2114 __PACKAGE__->register_method(
2115 method => "biblio_search_issn",
2116 api_name => "open-ils.search.biblio.issn",
2118 desc => 'Retrieve biblio IDs for a given ISSN',
2120 {desc => 'ISBN', type => 'string'}
2123 desc => 'Results object like: { "count": $i, "ids": [...] }',
2129 sub biblio_search_issn {
2130 my( $self, $client, $issn ) = @_;
2131 $logger->debug("Searching ISSN $issn");
2132 my $e = new_editor();
2134 my $recs = $U->storagereq(
2135 'open-ils.storage.id_list.biblio.record_entry.search.issn.atomic', $issn );
2136 return { ids => $recs, count => scalar(@$recs) };
2140 __PACKAGE__->register_method(
2141 method => "fetch_mods_by_copy",
2142 api_name => "open-ils.search.biblio.mods_from_copy",
2145 desc => 'Retrieve MODS record given an attached copy ID',
2147 { desc => 'Copy ID', type => 'number' }
2150 desc => 'MODS record, event on error or uncataloged item'
2155 sub fetch_mods_by_copy {
2156 my( $self, $client, $copyid ) = @_;
2157 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2158 return $evt if $evt;
2159 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2160 return $apputils->record_to_mvr($record);
2164 # -------------------------------------------------------------------------------------
2166 __PACKAGE__->register_method(
2167 method => "cn_browse",
2168 api_name => "open-ils.search.callnumber.browse.target",
2169 notes => "Starts a callnumber browse"
2172 __PACKAGE__->register_method(
2173 method => "cn_browse",
2174 api_name => "open-ils.search.callnumber.browse.page_up",
2175 notes => "Returns the previous page of callnumbers",
2178 __PACKAGE__->register_method(
2179 method => "cn_browse",
2180 api_name => "open-ils.search.callnumber.browse.page_down",
2181 notes => "Returns the next page of callnumbers",
2185 # RETURNS array of arrays like so: label, owning_lib, record, id
2187 my( $self, $client, @params ) = @_;
2190 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
2191 if( $self->api_name =~ /target/ );
2192 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2193 if( $self->api_name =~ /page_up/ );
2194 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2195 if( $self->api_name =~ /page_down/ );
2197 return $apputils->simplereq( 'open-ils.storage', $method, @params );
2199 # -------------------------------------------------------------------------------------
2201 __PACKAGE__->register_method(
2202 method => "fetch_cn",
2203 api_name => "open-ils.search.callnumber.retrieve",
2205 notes => "retrieves a callnumber based on ID",
2209 my( $self, $client, $id ) = @_;
2210 my( $cn, $evt ) = $apputils->fetch_callnumber( $id );
2211 return $evt if $evt;
2215 __PACKAGE__->register_method(
2216 method => "fetch_copy_by_cn",
2217 api_name => 'open-ils.search.copies_by_call_number.retrieve',
2219 Returns an array of copy ID's by callnumber ID
2220 @param cnid The callnumber ID
2221 @return An array of copy IDs
2225 sub fetch_copy_by_cn {
2226 my( $self, $conn, $cnid ) = @_;
2227 return $U->cstorereq(
2228 'open-ils.cstore.direct.asset.copy.id_list.atomic',
2229 { call_number => $cnid, deleted => 'f' } );
2232 __PACKAGE__->register_method(
2233 method => 'fetch_cn_by_info',
2234 api_name => 'open-ils.search.call_number.retrieve_by_info',
2236 @param label The callnumber label
2237 @param record The record the cn is attached to
2238 @param org The owning library of the cn
2239 @return The callnumber object
2244 sub fetch_cn_by_info {
2245 my( $self, $conn, $label, $record, $org ) = @_;
2246 return $U->cstorereq(
2247 'open-ils.cstore.direct.asset.call_number.search',
2248 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2253 __PACKAGE__->register_method(
2254 method => 'bib_extras',
2255 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all'
2257 __PACKAGE__->register_method(
2258 method => 'bib_extras',
2259 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all'
2261 __PACKAGE__->register_method(
2262 method => 'bib_extras',
2263 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all'
2265 __PACKAGE__->register_method(
2266 method => 'bib_extras',
2267 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all'
2269 __PACKAGE__->register_method(
2270 method => 'bib_extras',
2271 api_name => 'open-ils.search.biblio.audience_map.retrieve.all'
2277 my $e = new_editor();
2279 return $e->retrieve_all_config_lit_form_map()
2280 if( $self->api_name =~ /lit_form/ );
2282 return $e->retrieve_all_config_item_form_map()
2283 if( $self->api_name =~ /item_form_map/ );
2285 return $e->retrieve_all_config_item_type_map()
2286 if( $self->api_name =~ /item_type_map/ );
2288 return $e->retrieve_all_config_bib_level_map()
2289 if( $self->api_name =~ /bib_level_map/ );
2291 return $e->retrieve_all_config_audience_map()
2292 if( $self->api_name =~ /audience_map/ );
2299 __PACKAGE__->register_method(
2300 method => 'fetch_slim_record',
2301 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2303 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2305 { desc => 'Array of Record IDs', type => 'array' }
2308 desc => 'Array of biblio records, event on error'
2313 sub fetch_slim_record {
2314 my( $self, $conn, $ids ) = @_;
2316 #my $editor = OpenILS::Utils::Editor->new;
2317 my $editor = new_editor();
2320 return $editor->event unless
2321 my $r = $editor->retrieve_biblio_record_entry($_);
2330 __PACKAGE__->register_method(
2331 method => 'rec_to_mr_rec_descriptors',
2332 api_name => 'open-ils.search.metabib.record_to_descriptors',
2334 specialized method...
2335 Given a biblio record id or a metarecord id,
2336 this returns a list of metabib.record_descriptor
2337 objects that live within the same metarecord
2338 @param args Object of args including:
2342 sub rec_to_mr_rec_descriptors {
2343 my( $self, $conn, $args ) = @_;
2345 my $rec = $$args{record};
2346 my $mrec = $$args{metarecord};
2347 my $item_forms = $$args{item_forms};
2348 my $item_types = $$args{item_types};
2349 my $item_lang = $$args{item_lang};
2351 my $e = new_editor();
2355 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2356 return $e->event unless @$map;
2357 $mrec = $$map[0]->metarecord;
2360 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2361 return $e->event unless @$recs;
2363 my @recs = map { $_->source } @$recs;
2364 my $search = { record => \@recs };
2365 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2366 $search->{item_type} = $item_types if $item_types and @$item_types;
2367 $search->{item_lang} = $item_lang if $item_lang;
2369 my $desc = $e->search_metabib_record_descriptor($search);
2371 return { metarecord => $mrec, descriptors => $desc };
2375 __PACKAGE__->register_method(
2376 method => 'fetch_age_protect',
2377 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2380 sub fetch_age_protect {
2381 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2385 __PACKAGE__->register_method(
2386 method => 'copies_by_cn_label',
2387 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2390 __PACKAGE__->register_method(
2391 method => 'copies_by_cn_label',
2392 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2395 sub copies_by_cn_label {
2396 my( $self, $conn, $record, $label, $circ_lib ) = @_;
2397 my $e = new_editor();
2398 my $cns = $e->search_asset_call_number({record => $record, label => $label, deleted => 'f'}, {idlist=>1});
2399 return [] unless @$cns;
2401 # show all non-deleted copies in the staff client ...
2402 if ($self->api_name =~ /staff$/o) {
2403 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2406 # ... otherwise, grab the copies ...
2407 my $copies = $e->search_asset_copy(
2408 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2409 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2413 # ... and test for location and status visibility
2414 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];