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 # XXX This stops the session locale from doing the right thing.
793 # XXX Revisit this and have it translate to a lang instead of a locale.
794 #$arghash->{preferred_language} = $U->get_org_locale($arghash->{org_unit})
795 # unless $arghash->{preferred_language};
797 $method = $self->method_lookup($method);
798 my ($data) = $method->run($arghash, $docache);
800 $arghash->{searches} = $search if (!$data->{complex_query});
802 $arghash->{limit} = $ol if $ol;
803 $data->{compiled_search} = $arghash;
804 $data->{query} = $orig_query;
806 $logger->info("compiled search is " . OpenSRF::Utils::JSON->perl2JSON($arghash));
811 __PACKAGE__->register_method(
812 method => 'cat_search_z_style_wrapper',
813 api_name => 'open-ils.search.biblio.zstyle',
815 signature => q/@see open-ils.search.biblio.multiclass/
818 __PACKAGE__->register_method(
819 method => 'cat_search_z_style_wrapper',
820 api_name => 'open-ils.search.biblio.zstyle.staff',
822 signature => q/@see open-ils.search.biblio.multiclass/
825 sub cat_search_z_style_wrapper {
828 my $authtoken = shift;
831 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
833 my $ou = $cstore->request(
834 'open-ils.cstore.direct.actor.org_unit.search',
835 { parent_ou => undef }
838 my $result = { service => 'native-evergreen-catalog', records => [] };
839 my $searchhash = { limit => $$args{limit}, offset => $$args{offset}, org_unit => $ou->id };
841 $$searchhash{searches}{title}{term} = $$args{search}{title} if $$args{search}{title};
842 $$searchhash{searches}{author}{term} = $$args{search}{author} if $$args{search}{author};
843 $$searchhash{searches}{subject}{term} = $$args{search}{subject} if $$args{search}{subject};
844 $$searchhash{searches}{keyword}{term} = $$args{search}{keyword} if $$args{search}{keyword};
846 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{tcn} if $$args{search}{tcn};
847 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{isbn} if $$args{search}{isbn};
848 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{issn} if $$args{search}{issn};
849 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{publisher} if $$args{search}{publisher};
850 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{pubdate} if $$args{search}{pubdate};
851 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{item_type} if $$args{search}{item_type};
853 my $list = the_quest_for_knowledge( $self, $client, $searchhash );
855 if ($list->{count} > 0) {
856 $result->{count} = $list->{count};
858 my $records = $cstore->request(
859 'open-ils.cstore.direct.biblio.record_entry.search.atomic',
860 { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
863 for my $rec ( @$records ) {
865 my $u = OpenILS::Utils::ModsParser->new();
866 $u->start_mods_batch( $rec->marc );
867 my $mods = $u->finish_mods_batch();
869 push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
875 $cstore->disconnect();
879 # ----------------------------------------------------------------------------
880 # These are the main OPAC search methods
881 # ----------------------------------------------------------------------------
883 __PACKAGE__->register_method(
884 method => 'the_quest_for_knowledge',
885 api_name => 'open-ils.search.biblio.multiclass',
887 desc => "Performs a multi class biblio or metabib search",
890 desc => "A search hash with keys: "
891 . "searches, org_unit, depth, limit, offset, format, sort, sort_dir. "
892 . "See perldoc " . __PACKAGE__ . " for more detail",
896 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
901 desc => 'An object of the form: '
902 . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
907 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
909 The search-hash argument can have the following elements:
911 searches: { "$class" : "$value", ...} [REQUIRED]
912 org_unit: The org id to focus the search at
913 depth : The org depth
914 limit : The search limit default: 10
915 offset : The search offset default: 0
916 format : The MARC format
917 sort : What field to sort the results on? [ author | title | pubdate ]
918 sort_dir: What direction do we sort? [ asc | desc ]
919 tag_circulated_records : Boolean, if true, records that are in the user's visible checkout history
920 will be tagged with an additional value ("1") as the last value in the record ID array for
921 each record. Requires the 'authtoken'
922 authtoken : Authentication token string; When actions are performed that require a user login
923 (e.g. tagging circulated records), the authentication token is required
925 The searches element is required, must have a hashref value, and the hashref must contain at least one
926 of the following classes as a key:
934 The value paired with a key is the associated search string.
936 The docache argument enables/disables searching and saving results in cache (default OFF).
938 The return object, if successful, will look like:
940 { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
944 __PACKAGE__->register_method(
945 method => 'the_quest_for_knowledge',
946 api_name => 'open-ils.search.biblio.multiclass.staff',
947 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
949 __PACKAGE__->register_method(
950 method => 'the_quest_for_knowledge',
951 api_name => 'open-ils.search.metabib.multiclass',
952 signature => q/@see open-ils.search.biblio.multiclass/
954 __PACKAGE__->register_method(
955 method => 'the_quest_for_knowledge',
956 api_name => 'open-ils.search.metabib.multiclass.staff',
957 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
960 sub the_quest_for_knowledge {
961 my( $self, $conn, $searchhash, $docache ) = @_;
963 return { count => 0 } unless $searchhash and
964 ref $searchhash->{searches} eq 'HASH';
966 my $method = 'open-ils.storage.biblio.multiclass.search_fts';
970 if($self->api_name =~ /metabib/) {
972 $method =~ s/biblio/metabib/o;
975 # do some simple sanity checking
976 if(!$searchhash->{searches} or
977 ( !grep { /^(?:title|author|subject|series|keyword)/ } keys %{$searchhash->{searches}} ) ) {
978 return { count => 0 };
981 my $offset = $searchhash->{offset} || 0; # user value or default in local var now
982 my $limit = $searchhash->{limit} || 10; # user value or default in local var now
983 my $end = $offset + $limit - 1;
986 $searchhash->{offset} = 0; # possible user value overwritten in hash
987 $searchhash->{limit} = $maxlimit; # possible user value overwritten in hash
989 return { count => 0 } if $offset > $maxlimit;
992 push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
993 my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
994 my $ckey = $pfx . md5_hex($method . $s);
996 $logger->info("bib search for: $s");
998 $searchhash->{limit} -= $offset;
1002 my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
1006 $method .= ".staff" if($self->api_name =~ /staff/);
1007 $method .= ".atomic";
1009 for (keys %$searchhash) {
1010 delete $$searchhash{$_}
1011 unless defined $$searchhash{$_};
1014 $result = $U->storagereq( $method, %$searchhash );
1018 $docache = 0; # results came FROM cache, so we don't write back
1021 return {count => 0} unless ($result && $$result[0]);
1025 my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
1028 # If we didn't get this data from the cache, put it into the cache
1029 # then return the correct offset of records
1030 $logger->debug("putting search cache $ckey\n");
1031 put_cache($ckey, $count, \@recs);
1035 # if we have the full set of data, trim out
1036 # the requested chunk based on limit and offset
1038 for ($offset..$end) {
1039 last unless $recs[$_];
1040 push(@t, $recs[$_]);
1045 return { ids => \@recs, count => $count };
1049 __PACKAGE__->register_method(
1050 method => 'staged_search',
1051 api_name => 'open-ils.search.biblio.multiclass.staged',
1053 desc => 'Staged search filters out unavailable items. This means that it relies on an estimation strategy for determining ' .
1054 'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering. See "estimation_strategy" in your SRF config.',
1057 desc => "A search hash with keys: "
1058 . "searches, limit, offset. The others are optional, but the 'searches' key/value pair is required, with the value being a hashref. "
1059 . "See perldoc " . __PACKAGE__ . " for more detail",
1063 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
1068 desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids. '
1069 . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
1074 __PACKAGE__->register_method(
1075 method => 'staged_search',
1076 api_name => 'open-ils.search.biblio.multiclass.staged.staff',
1077 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1079 __PACKAGE__->register_method(
1080 method => 'staged_search',
1081 api_name => 'open-ils.search.metabib.multiclass.staged',
1082 signature => q/@see open-ils.search.biblio.multiclass.staged/
1084 __PACKAGE__->register_method(
1085 method => 'staged_search',
1086 api_name => 'open-ils.search.metabib.multiclass.staged.staff',
1087 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1091 my($self, $conn, $search_hash, $docache) = @_;
1093 my $IAmMetabib = ($self->api_name =~ /metabib/) ? 1 : 0;
1095 my $method = $IAmMetabib?
1096 'open-ils.storage.metabib.multiclass.staged.search_fts':
1097 'open-ils.storage.biblio.multiclass.staged.search_fts';
1099 $method .= '.staff' if $self->api_name =~ /staff$/;
1100 $method .= '.atomic';
1102 return {count => 0} unless (
1104 $search_hash->{searches} and
1105 scalar( keys %{$search_hash->{searches}} ));
1107 my $search_duration;
1108 my $user_offset = $search_hash->{offset} || 0; # user-specified offset
1109 my $user_limit = $search_hash->{limit} || 10;
1110 $user_offset = ($user_offset >= 0) ? $user_offset : 0;
1111 $user_limit = ($user_limit >= 0) ? $user_limit : 10;
1114 # we're grabbing results on a per-superpage basis, which means the
1115 # limit and offset should coincide with superpage boundaries
1116 $search_hash->{offset} = 0;
1117 $search_hash->{limit} = $superpage_size;
1119 # force a well-known check_limit
1120 $search_hash->{check_limit} = $superpage_size;
1121 # restrict total tested to superpage size * number of superpages
1122 $search_hash->{core_limit} = $superpage_size * $max_superpages;
1124 # Set the configured estimation strategy, defaults to 'inclusion'.
1125 my $estimation_strategy = OpenSRF::Utils::SettingsClient
1128 apps => 'open-ils.search', app_settings => 'estimation_strategy'
1130 $search_hash->{estimation_strategy} = $estimation_strategy;
1132 # pull any existing results from the cache
1133 my $key = search_cache_key($method, $search_hash);
1134 my $facet_key = $key.'_facets';
1135 my $cache_data = $cache->get_cache($key) || {};
1137 # keep retrieving results until we find enough to
1138 # fulfill the user-specified limit and offset
1139 my $all_results = [];
1140 my $page; # current superpage
1141 my $est_hit_count = 0;
1142 my $current_page_summary = {};
1143 my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1144 my $is_real_hit_count = 0;
1147 for($page = 0; $page < $max_superpages; $page++) {
1149 my $data = $cache_data->{$page};
1153 $logger->debug("staged search: analyzing superpage $page");
1156 # this window of results is already cached
1157 $logger->debug("staged search: found cached results");
1158 $summary = $data->{summary};
1159 $results = $data->{results};
1162 # retrieve the window of results from the database
1163 $logger->debug("staged search: fetching results from the database");
1164 $search_hash->{skip_check} = $page * $superpage_size;
1166 $results = $U->storagereq($method, %$search_hash);
1167 $search_duration = time - $start;
1168 $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1169 $summary = shift(@$results) if $results;
1172 $logger->info("search timed out: duration=$search_duration: params=".
1173 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1174 return {count => 0};
1177 my $hc = $summary->{estimated_hit_count} || $summary->{visible};
1179 $logger->info("search returned 0 results: duration=$search_duration: params=".
1180 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1183 # Create backwards-compatible result structures
1185 $results = [map {[$_->{id}, $_->{rel}, $_->{record}]} @$results];
1187 $results = [map {[$_->{id}]} @$results];
1190 tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib)
1191 if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1193 push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1194 $results = [grep {defined $_->[0]} @$results];
1195 cache_staged_search_page($key, $page, $summary, $results) if $docache;
1198 $current_page_summary = $summary;
1200 # add the new set of results to the set under construction
1201 push(@$all_results, @$results);
1203 my $current_count = scalar(@$all_results);
1205 $est_hit_count = $summary->{estimated_hit_count} || $summary->{visible}
1208 $logger->debug("staged search: located $current_count, with estimated hits=".
1209 $summary->{estimated_hit_count}." : visible=".$summary->{visible}.", checked=".$summary->{checked});
1211 if (defined($summary->{estimated_hit_count})) {
1212 foreach (qw/ checked visible excluded deleted /) {
1213 $global_summary->{$_} += $summary->{$_};
1215 $global_summary->{total} = $summary->{total};
1218 # we've found all the possible hits
1219 last if $current_count == $summary->{visible}
1220 and not defined $summary->{estimated_hit_count};
1222 # we've found enough results to satisfy the requested limit/offset
1223 last if $current_count >= ($user_limit + $user_offset);
1225 # we've scanned all possible hits
1226 if($summary->{checked} < $superpage_size) {
1227 $est_hit_count = scalar(@$all_results);
1228 # we have all possible results in hand, so we know the final hit count
1229 $is_real_hit_count = 1;
1234 my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1236 # refine the estimate if we have more than one superpage
1237 if ($page > 0 and not $is_real_hit_count) {
1238 if ($global_summary->{checked} >= $global_summary->{total}) {
1239 $est_hit_count = $global_summary->{visible};
1241 my $updated_hit_count = $U->storagereq(
1242 'open-ils.storage.fts_paging_estimate',
1243 $global_summary->{checked},
1244 $global_summary->{visible},
1245 $global_summary->{excluded},
1246 $global_summary->{deleted},
1247 $global_summary->{total}
1249 $est_hit_count = $updated_hit_count->{$estimation_strategy};
1253 $conn->respond_complete(
1255 count => $est_hit_count,
1256 core_limit => $search_hash->{core_limit},
1257 superpage_size => $search_hash->{check_limit},
1258 superpage_summary => $current_page_summary,
1259 facet_key => $facet_key,
1264 cache_facets($facet_key, $new_ids, $IAmMetabib) if $docache;
1269 sub tag_circulated_records {
1270 my ($auth, $results, $metabib) = @_;
1271 my $e = new_editor(authtoken => $auth);
1272 return $results unless $e->checkauth;
1275 select => { acn => [{ column => 'record', alias => 'tagme' }] },
1276 from => { acp => 'acn' },
1277 where => { id => { in => { from => ['action.usr_visible_circ_copies', $e->requestor->id] } } },
1283 select => { mmsm => [{ column => 'metarecord', alias => 'tagme' }] },
1285 where => { source => { in => $query } },
1290 # Give me the distinct set of bib records that exist in the user's visible circulation history
1291 my $circ_recs = $e->json_query( $query );
1293 # if the record appears in the circ history, push a 1 onto
1294 # the rec array structure to indicate truthiness
1295 for my $rec (@$results) {
1296 push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1302 # creates a unique token to represent the query in the cache
1303 sub search_cache_key {
1305 my $search_hash = shift;
1307 for my $key (sort keys %$search_hash) {
1308 push(@sorted, ($key => $$search_hash{$key}))
1309 unless $key eq 'limit' or
1311 $key eq 'skip_check';
1313 my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1314 return $pfx . md5_hex($method . $s);
1317 sub retrieve_cached_facets {
1323 return undef unless ($key and $key =~ /_facets$/);
1325 my $blob = $cache->get_cache($key) || {};
1329 for my $f ( keys %$blob ) {
1330 my @sorted = map{ { $$_[1] => $$_[0] } } sort {$$b[0] <=> $$a[0] || $$a[1] cmp $$b[1]} map { [$$blob{$f}{$_}, $_] } keys %{ $$blob{$f} };
1331 @sorted = @sorted[0 .. $limit - 1] if (scalar(@sorted) > $limit);
1332 for my $s ( @sorted ) {
1333 my ($k) = keys(%$s);
1334 my ($v) = values(%$s);
1335 $$facets{$f}{$k} = $v;
1345 __PACKAGE__->register_method(
1346 method => "retrieve_cached_facets",
1347 api_name => "open-ils.search.facet_cache.retrieve",
1349 desc => 'Returns facet data derived from a specific search based on a key '.
1350 'generated by open-ils.search.biblio.multiclass.staged and friends.',
1353 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1358 desc => 'Two level hash of facet values. Top level key is the facet id defined on the config.metabib_field table. '.
1359 'Second level key is a string facet value. Datum attached to each facet value is the number of distinct records, '.
1360 'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1361 'facet retrieval. These counts are calculated for all superpages that have been checked for visibility.',
1369 # add facets for this search to the facet cache
1370 my($key, $results, $metabib) = @_;
1371 my $data = $cache->get_cache($key);
1374 return undef unless (@$results);
1376 # The query we're constructing
1378 # select mfae.field as id,
1380 # count(distinct mmrsm.appropriate-id-field )
1381 # from metabib.facet_entry mfae
1382 # join metabib.metarecord_sourc_map mmrsm on (mfae.source = mmrsm.source)
1383 # where mmrsm.appropriate-id-field in IDLIST
1386 my $count_field = $metabib ? 'metarecord' : 'source';
1387 my $facets = $U->cstorereq( "open-ils.cstore.json_query.atomic",
1389 mfae => [ { column => 'field', alias => 'id'}, 'value' ],
1391 transform => 'count',
1393 column => $count_field,
1400 mmrsm => { field => 'source', fkey => 'source' }
1404 '+mmrsm' => { $count_field => $results }
1409 for my $facet (@$facets) {
1410 next unless ($facet->{value});
1411 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1414 $logger->info("facet compilation: cached with key=$key");
1416 $cache->put_cache($key, $data, $cache_timeout);
1419 sub cache_staged_search_page {
1420 # puts this set of results into the cache
1421 my($key, $page, $summary, $results) = @_;
1422 my $data = $cache->get_cache($key);
1425 summary => $summary,
1429 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1430 $summary->{estimated_hit_count}.", visible=".$summary->{visible});
1432 $cache->put_cache($key, $data, $cache_timeout);
1440 my $start = $offset;
1441 my $end = $offset + $limit - 1;
1443 $logger->debug("searching cache for $key : $start..$end\n");
1445 return undef unless $cache;
1446 my $data = $cache->get_cache($key);
1448 return undef unless $data;
1450 my $count = $data->[0];
1453 return undef unless $offset < $count;
1456 for( my $i = $offset; $i <= $end; $i++ ) {
1457 last unless my $d = $$data[$i];
1458 push( @result, $d );
1461 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1468 my( $key, $count, $data ) = @_;
1469 return undef unless $cache;
1470 $logger->debug("search_cache putting ".
1471 scalar(@$data)." items at key $key with timeout $cache_timeout");
1472 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1476 __PACKAGE__->register_method(
1477 method => "biblio_mrid_to_modsbatch_batch",
1478 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1481 sub biblio_mrid_to_modsbatch_batch {
1482 my( $self, $client, $mrids) = @_;
1483 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1485 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1486 for my $id (@$mrids) {
1487 next unless defined $id;
1488 my ($m) = $method->run($id);
1495 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1496 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1498 __PACKAGE__->register_method(
1499 method => "biblio_mrid_to_modsbatch",
1502 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1503 . "As usual, the .staff version of this method will include otherwise hidden records.",
1505 { desc => 'Metarecord ID', type => 'number' },
1506 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1509 desc => 'MVR Object, event on error',
1515 sub biblio_mrid_to_modsbatch {
1516 my( $self, $client, $mrid, $args) = @_;
1518 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1520 my ($mr, $evt) = _grab_metarecord($mrid);
1521 return $evt unless $mr;
1523 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1524 biblio_mrid_make_modsbatch($self, $client, $mr);
1526 return $mvr unless ref($args);
1528 # Here we find the lead record appropriate for the given filters
1529 # and use that for the title and author of the metarecord
1530 my $format = $$args{format};
1531 my $org = $$args{org};
1532 my $depth = $$args{depth};
1534 return $mvr unless $format or $org or $depth;
1536 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1537 $method = "$method.staff" if $self->api_name =~ /staff/o;
1539 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1541 if( my $mods = $U->record_to_mvr($rec) ) {
1543 $mvr->title( $mods->title );
1544 $mvr->author($mods->author);
1545 $logger->debug("mods_slim updating title and ".
1546 "author in mvr with ".$mods->title." : ".$mods->author);
1552 # converts a metarecord to an mvr
1555 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1556 return Fieldmapper::metabib::virtual_record->new($perl);
1559 # checks to see if a metarecord has mods, if so returns true;
1561 __PACKAGE__->register_method(
1562 method => "biblio_mrid_check_mvr",
1563 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1564 notes => "Takes a metarecord ID or a metarecord object and returns true "
1565 . "if the metarecord already has an mvr associated with it."
1568 sub biblio_mrid_check_mvr {
1569 my( $self, $client, $mrid ) = @_;
1573 if(ref($mrid)) { $mr = $mrid; }
1574 else { ($mr, $evt) = _grab_metarecord($mrid); }
1575 return $evt if $evt;
1577 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1579 return _mr_to_mvr($mr) if $mr->mods();
1583 sub _grab_metarecord {
1585 #my $e = OpenILS::Utils::Editor->new;
1586 my $e = new_editor();
1587 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1592 __PACKAGE__->register_method(
1593 method => "biblio_mrid_make_modsbatch",
1594 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1595 notes => "Takes either a metarecord ID or a metarecord object. "
1596 . "Forces the creations of an mvr for the given metarecord. "
1597 . "The created mvr is returned."
1600 sub biblio_mrid_make_modsbatch {
1601 my( $self, $client, $mrid ) = @_;
1603 #my $e = OpenILS::Utils::Editor->new;
1604 my $e = new_editor();
1611 $mr = $e->retrieve_metabib_metarecord($mrid)
1612 or return $e->event;
1615 my $masterid = $mr->master_record;
1616 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1618 my $ids = $U->storagereq(
1619 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1620 return undef unless @$ids;
1622 my $master = $e->retrieve_biblio_record_entry($masterid)
1623 or return $e->event;
1625 # start the mods batch
1626 my $u = OpenILS::Utils::ModsParser->new();
1627 $u->start_mods_batch( $master->marc );
1629 # grab all of the sub-records and shove them into the batch
1630 my @ids = grep { $_ ne $masterid } @$ids;
1631 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1636 my $r = $e->retrieve_biblio_record_entry($i);
1637 push( @$subrecs, $r ) if $r;
1642 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1643 $u->push_mods_batch( $_->marc ) if $_->marc;
1647 # finish up and send to the client
1648 my $mods = $u->finish_mods_batch();
1649 $mods->doc_id($mrid);
1650 $client->respond_complete($mods);
1653 # now update the mods string in the db
1654 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1657 #$e = OpenILS::Utils::Editor->new(xact => 1);
1658 $e = new_editor(xact => 1);
1659 $e->update_metabib_metarecord($mr)
1660 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1667 # converts a mr id into a list of record ids
1669 foreach (qw/open-ils.search.biblio.metarecord_to_records
1670 open-ils.search.biblio.metarecord_to_records.staff/)
1672 __PACKAGE__->register_method(
1673 method => "biblio_mrid_to_record_ids",
1676 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1677 . "As usual, the .staff version of this method will include otherwise hidden records.",
1679 { desc => 'Metarecord ID', type => 'number' },
1680 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1683 desc => 'Results object like {count => $i, ids =>[...]}',
1691 sub biblio_mrid_to_record_ids {
1692 my( $self, $client, $mrid, $args ) = @_;
1694 my $format = $$args{format};
1695 my $org = $$args{org};
1696 my $depth = $$args{depth};
1698 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1699 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1700 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1702 return { count => scalar(@$recs), ids => $recs };
1706 __PACKAGE__->register_method(
1707 method => "biblio_record_to_marc_html",
1708 api_name => "open-ils.search.biblio.record.html"
1711 __PACKAGE__->register_method(
1712 method => "biblio_record_to_marc_html",
1713 api_name => "open-ils.search.authority.to_html"
1716 # Persistent parsers and setting objects
1717 my $parser = XML::LibXML->new();
1718 my $xslt = XML::LibXSLT->new();
1720 my $slim_marc_sheet;
1721 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1723 sub biblio_record_to_marc_html {
1724 my($self, $client, $recordid, $slim, $marcxml) = @_;
1727 my $dir = $settings_client->config_value("dirs", "xsl");
1730 unless($slim_marc_sheet) {
1731 my $xsl = $settings_client->config_value(
1732 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1734 $xsl = $parser->parse_file("$dir/$xsl");
1735 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1738 $sheet = $slim_marc_sheet;
1742 unless($marc_sheet) {
1743 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1744 my $xsl = $settings_client->config_value(
1745 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1746 $xsl = $parser->parse_file("$dir/$xsl");
1747 $marc_sheet = $xslt->parse_stylesheet($xsl);
1749 $sheet = $marc_sheet;
1754 my $e = new_editor();
1755 if($self->api_name =~ /authority/) {
1756 $record = $e->retrieve_authority_record_entry($recordid)
1757 or return $e->event;
1759 $record = $e->retrieve_biblio_record_entry($recordid)
1760 or return $e->event;
1762 $marcxml = $record->marc;
1765 my $xmldoc = $parser->parse_string($marcxml);
1766 my $html = $sheet->transform($xmldoc);
1767 return $html->documentElement->toString();
1770 __PACKAGE__->register_method(
1771 method => "format_biblio_record_entry",
1772 api_name => "open-ils.search.biblio.record.print",
1774 desc => 'Returns a printable version of the specified bib record',
1776 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1779 desc => q/An action_trigger.event object or error event./,
1784 __PACKAGE__->register_method(
1785 method => "format_biblio_record_entry",
1786 api_name => "open-ils.search.biblio.record.email",
1788 desc => 'Emails an A/T templated version of the specified bib records to the authorized user',
1790 { desc => 'Authentication token', type => 'string'},
1791 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1794 desc => q/Undefined on success, otherwise an error event./,
1800 sub format_biblio_record_entry {
1801 my($self, $conn, $arg1, $arg2) = @_;
1803 my $for_print = ($self->api_name =~ /print/);
1804 my $for_email = ($self->api_name =~ /email/);
1806 my $e; my $auth; my $bib_id; my $context_org;
1810 $context_org = $arg2 || $U->fetch_org_tree->id;
1811 $e = new_editor(xact => 1);
1812 } elsif ($for_email) {
1815 $e = new_editor(authtoken => $auth, xact => 1);
1816 return $e->die_event unless $e->checkauth;
1817 $context_org = $e->requestor->home_ou;
1821 if (ref $bib_id ne 'ARRAY') {
1822 $bib_ids = [ $bib_id ];
1827 my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1828 $bucket->btype('temp');
1829 $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1831 $bucket->owner($e->requestor)
1835 my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1837 for my $id (@$bib_ids) {
1839 my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1841 my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1842 $bucket_entry->target_biblio_record_entry($bib);
1843 $bucket_entry->bucket($bucket_obj->id);
1844 $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
1851 return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
1853 } elsif ($for_email) {
1855 $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
1862 __PACKAGE__->register_method(
1863 method => "retrieve_all_copy_statuses",
1864 api_name => "open-ils.search.config.copy_status.retrieve.all"
1867 sub retrieve_all_copy_statuses {
1868 my( $self, $client ) = @_;
1869 return new_editor()->retrieve_all_config_copy_status();
1873 __PACKAGE__->register_method(
1874 method => "copy_counts_per_org",
1875 api_name => "open-ils.search.biblio.copy_counts.retrieve"
1878 __PACKAGE__->register_method(
1879 method => "copy_counts_per_org",
1880 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1883 sub copy_counts_per_org {
1884 my( $self, $client, $record_id ) = @_;
1886 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1888 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1889 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1891 my $counts = $apputils->simple_scalar_request(
1892 "open-ils.storage", $method, $record_id );
1894 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1899 __PACKAGE__->register_method(
1900 method => "copy_count_summary",
1901 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
1902 notes => "returns an array of these: "
1903 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1904 . "where statusx is a copy status name. The statuses are sorted by ID.",
1908 sub copy_count_summary {
1909 my( $self, $client, $rid, $org, $depth ) = @_;
1912 my $data = $U->storagereq(
1913 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
1915 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1918 __PACKAGE__->register_method(
1919 method => "copy_location_count_summary",
1920 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
1921 notes => "returns an array of these: "
1922 . "[ org_id, callnumber_label, copy_location, <status1_count>, <status2_count>,...] "
1923 . "where statusx is a copy status name. The statuses are sorted by ID.",
1926 sub copy_location_count_summary {
1927 my( $self, $client, $rid, $org, $depth ) = @_;
1930 my $data = $U->storagereq(
1931 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1933 return [ sort { $a->[1] cmp $b->[1] || $a->[2] cmp $b->[2] } @$data ];
1936 __PACKAGE__->register_method(
1937 method => "copy_count_location_summary",
1938 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
1939 notes => "returns an array of these: "
1940 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1941 . "where statusx is a copy status name. The statuses are sorted by ID."
1944 sub copy_count_location_summary {
1945 my( $self, $client, $rid, $org, $depth ) = @_;
1948 my $data = $U->storagereq(
1949 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1950 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1954 foreach (qw/open-ils.search.biblio.marc
1955 open-ils.search.biblio.marc.staff/)
1957 __PACKAGE__->register_method(
1958 method => "marc_search",
1961 desc => 'Fetch biblio IDs based on MARC record criteria. '
1962 . 'As usual, the .staff version of the search includes otherwise hidden records',
1965 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
1966 'See perldoc ' . __PACKAGE__ . ' for more detail.',
1969 {desc => 'limit (optional)', type => 'number'},
1970 {desc => 'offset (optional)', type => 'number'}
1973 desc => 'Results object like: { "count": $i, "ids": [...] }',
1980 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
1982 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
1984 searches: complex query object (required)
1985 org_unit: The org ID to focus the search at
1986 depth : The org depth
1987 limit : integer search limit default: 10
1988 offset : integer search offset default: 0
1989 sort : What field to sort the results on? [ author | title | pubdate ]
1990 sort_dir: In what direction do we sort? [ asc | desc ]
1992 Additional keys to refine search criteria:
1995 language : Language (code)
1996 lit_form : Literary form
1997 item_form: Item form
1998 item_type: Item type
1999 format : The MARC format
2001 Please note that the specific strings to be used in the "addtional keys" will be entirely
2002 dependent on your loaded data.
2004 All keys except "searches" are optional.
2005 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
2007 For example, an arg hash might look like:
2029 The arghash is eventually passed to the SRF call:
2030 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2032 Presently, search uses the cache unconditionally.
2036 # FIXME: that example above isn't actually tested.
2037 # TODO: docache option?
2039 my( $self, $conn, $args, $limit, $offset ) = @_;
2041 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2042 $method .= ".staff" if $self->api_name =~ /staff/;
2043 $method .= ".atomic";
2045 $limit ||= 10; # FIXME: what about $args->{limit} ?
2046 $offset ||= 0; # FIXME: what about $args->{offset} ?
2049 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2050 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2052 my $recs = search_cache($ckey, $offset, $limit);
2055 $recs = $U->storagereq($method, %$args) || [];
2057 put_cache($ckey, scalar(@$recs), $recs);
2058 $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
2065 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2066 my @recs = map { $_->[0] } @$recs;
2068 return { ids => \@recs, count => $count };
2072 __PACKAGE__->register_method(
2073 method => "biblio_search_isbn",
2074 api_name => "open-ils.search.biblio.isbn",
2076 desc => 'Retrieve biblio IDs for a given ISBN',
2078 {desc => 'ISBN', type => 'string'} # or number maybe? How normalized is our storage data?
2081 desc => 'Results object like: { "count": $i, "ids": [...] }',
2087 sub biblio_search_isbn {
2088 my( $self, $client, $isbn ) = @_;
2089 $logger->debug("Searching ISBN $isbn");
2090 my $recs = $U->storagereq('open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn);
2091 return { ids => $recs, count => scalar(@$recs) };
2094 __PACKAGE__->register_method(
2095 method => "biblio_search_isbn_batch",
2096 api_name => "open-ils.search.biblio.isbn_list",
2099 sub biblio_search_isbn_batch {
2100 my( $self, $client, $isbn_list ) = @_;
2101 $logger->debug("Searching ISBNs @$isbn_list");
2102 my @recs = (); my %rec_set = ();
2103 foreach my $isbn ( @$isbn_list ) {
2104 foreach my $rec ( @{ $U->storagereq(
2105 'open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn )
2107 if (! $rec_set{ $rec }) {
2108 $rec_set{ $rec } = 1;
2113 return { ids => \@recs, count => scalar(@recs) };
2116 __PACKAGE__->register_method(
2117 method => "biblio_search_issn",
2118 api_name => "open-ils.search.biblio.issn",
2120 desc => 'Retrieve biblio IDs for a given ISSN',
2122 {desc => 'ISBN', type => 'string'}
2125 desc => 'Results object like: { "count": $i, "ids": [...] }',
2131 sub biblio_search_issn {
2132 my( $self, $client, $issn ) = @_;
2133 $logger->debug("Searching ISSN $issn");
2134 my $e = new_editor();
2136 my $recs = $U->storagereq(
2137 'open-ils.storage.id_list.biblio.record_entry.search.issn.atomic', $issn );
2138 return { ids => $recs, count => scalar(@$recs) };
2142 __PACKAGE__->register_method(
2143 method => "fetch_mods_by_copy",
2144 api_name => "open-ils.search.biblio.mods_from_copy",
2147 desc => 'Retrieve MODS record given an attached copy ID',
2149 { desc => 'Copy ID', type => 'number' }
2152 desc => 'MODS record, event on error or uncataloged item'
2157 sub fetch_mods_by_copy {
2158 my( $self, $client, $copyid ) = @_;
2159 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2160 return $evt if $evt;
2161 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2162 return $apputils->record_to_mvr($record);
2166 # -------------------------------------------------------------------------------------
2168 __PACKAGE__->register_method(
2169 method => "cn_browse",
2170 api_name => "open-ils.search.callnumber.browse.target",
2171 notes => "Starts a callnumber browse"
2174 __PACKAGE__->register_method(
2175 method => "cn_browse",
2176 api_name => "open-ils.search.callnumber.browse.page_up",
2177 notes => "Returns the previous page of callnumbers",
2180 __PACKAGE__->register_method(
2181 method => "cn_browse",
2182 api_name => "open-ils.search.callnumber.browse.page_down",
2183 notes => "Returns the next page of callnumbers",
2187 # RETURNS array of arrays like so: label, owning_lib, record, id
2189 my( $self, $client, @params ) = @_;
2192 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
2193 if( $self->api_name =~ /target/ );
2194 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2195 if( $self->api_name =~ /page_up/ );
2196 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2197 if( $self->api_name =~ /page_down/ );
2199 return $apputils->simplereq( 'open-ils.storage', $method, @params );
2201 # -------------------------------------------------------------------------------------
2203 __PACKAGE__->register_method(
2204 method => "fetch_cn",
2205 api_name => "open-ils.search.callnumber.retrieve",
2207 notes => "retrieves a callnumber based on ID",
2211 my( $self, $client, $id ) = @_;
2212 my( $cn, $evt ) = $apputils->fetch_callnumber( $id );
2213 return $evt if $evt;
2217 __PACKAGE__->register_method(
2218 method => "fetch_copy_by_cn",
2219 api_name => 'open-ils.search.copies_by_call_number.retrieve',
2221 Returns an array of copy ID's by callnumber ID
2222 @param cnid The callnumber ID
2223 @return An array of copy IDs
2227 sub fetch_copy_by_cn {
2228 my( $self, $conn, $cnid ) = @_;
2229 return $U->cstorereq(
2230 'open-ils.cstore.direct.asset.copy.id_list.atomic',
2231 { call_number => $cnid, deleted => 'f' } );
2234 __PACKAGE__->register_method(
2235 method => 'fetch_cn_by_info',
2236 api_name => 'open-ils.search.call_number.retrieve_by_info',
2238 @param label The callnumber label
2239 @param record The record the cn is attached to
2240 @param org The owning library of the cn
2241 @return The callnumber object
2246 sub fetch_cn_by_info {
2247 my( $self, $conn, $label, $record, $org ) = @_;
2248 return $U->cstorereq(
2249 'open-ils.cstore.direct.asset.call_number.search',
2250 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2255 __PACKAGE__->register_method(
2256 method => 'bib_extras',
2257 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all'
2259 __PACKAGE__->register_method(
2260 method => 'bib_extras',
2261 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all'
2263 __PACKAGE__->register_method(
2264 method => 'bib_extras',
2265 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all'
2267 __PACKAGE__->register_method(
2268 method => 'bib_extras',
2269 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all'
2271 __PACKAGE__->register_method(
2272 method => 'bib_extras',
2273 api_name => 'open-ils.search.biblio.audience_map.retrieve.all'
2279 my $e = new_editor();
2281 return $e->retrieve_all_config_lit_form_map()
2282 if( $self->api_name =~ /lit_form/ );
2284 return $e->retrieve_all_config_item_form_map()
2285 if( $self->api_name =~ /item_form_map/ );
2287 return $e->retrieve_all_config_item_type_map()
2288 if( $self->api_name =~ /item_type_map/ );
2290 return $e->retrieve_all_config_bib_level_map()
2291 if( $self->api_name =~ /bib_level_map/ );
2293 return $e->retrieve_all_config_audience_map()
2294 if( $self->api_name =~ /audience_map/ );
2301 __PACKAGE__->register_method(
2302 method => 'fetch_slim_record',
2303 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2305 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2307 { desc => 'Array of Record IDs', type => 'array' }
2310 desc => 'Array of biblio records, event on error'
2315 sub fetch_slim_record {
2316 my( $self, $conn, $ids ) = @_;
2318 #my $editor = OpenILS::Utils::Editor->new;
2319 my $editor = new_editor();
2322 return $editor->event unless
2323 my $r = $editor->retrieve_biblio_record_entry($_);
2332 __PACKAGE__->register_method(
2333 method => 'rec_to_mr_rec_descriptors',
2334 api_name => 'open-ils.search.metabib.record_to_descriptors',
2336 specialized method...
2337 Given a biblio record id or a metarecord id,
2338 this returns a list of metabib.record_descriptor
2339 objects that live within the same metarecord
2340 @param args Object of args including:
2344 sub rec_to_mr_rec_descriptors {
2345 my( $self, $conn, $args ) = @_;
2347 my $rec = $$args{record};
2348 my $mrec = $$args{metarecord};
2349 my $item_forms = $$args{item_forms};
2350 my $item_types = $$args{item_types};
2351 my $item_lang = $$args{item_lang};
2353 my $e = new_editor();
2357 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2358 return $e->event unless @$map;
2359 $mrec = $$map[0]->metarecord;
2362 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2363 return $e->event unless @$recs;
2365 my @recs = map { $_->source } @$recs;
2366 my $search = { record => \@recs };
2367 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2368 $search->{item_type} = $item_types if $item_types and @$item_types;
2369 $search->{item_lang} = $item_lang if $item_lang;
2371 my $desc = $e->search_metabib_record_descriptor($search);
2373 return { metarecord => $mrec, descriptors => $desc };
2377 __PACKAGE__->register_method(
2378 method => 'fetch_age_protect',
2379 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2382 sub fetch_age_protect {
2383 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2387 __PACKAGE__->register_method(
2388 method => 'copies_by_cn_label',
2389 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2392 __PACKAGE__->register_method(
2393 method => 'copies_by_cn_label',
2394 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2397 sub copies_by_cn_label {
2398 my( $self, $conn, $record, $label, $circ_lib ) = @_;
2399 my $e = new_editor();
2400 my $cns = $e->search_asset_call_number({record => $record, label => $label, deleted => 'f'}, {idlist=>1});
2401 return [] unless @$cns;
2403 # show all non-deleted copies in the staff client ...
2404 if ($self->api_name =~ /staff$/o) {
2405 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2408 # ... otherwise, grab the copies ...
2409 my $copies = $e->search_asset_copy(
2410 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2411 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2415 # ... and test for location and status visibility
2416 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];