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 # ensure that the convenience class isn't part of a word (e.g. 'playhouse')
693 $query =~ s/(^|\s)kw(:|\|)/$1keyword$2/go;
694 $query =~ s/(^|\s)ti(:|\|)/$1title$2/go;
695 $query =~ s/(^|\s)au(:|\|)/$1author$2/go;
696 $query =~ s/(^|\s)su(:|\|)/$1subject$2/go;
697 $query =~ s/(^|\s)se(:|\|)/$1series$2/go;
698 $query =~ s/(^|\s)name(:|\|)/$1author$2/og;
700 $logger->debug("cleansed query string => $query");
703 my $simple_class_re = qr/((?:\w+(?:\|\w+)?):[^:]+?)$/;
704 my $class_list_re = qr/(?:keyword|title|author|subject|series)/;
705 my $modifier_list_re = qr/(?:site|dir|sort|lang|available)/;
708 while ($query =~ s/$simple_class_re//so) {
711 my $where = index($qpart,':');
712 my $type = substr($qpart, 0, $where++);
713 my $value = substr($qpart, $where);
715 if ($type !~ /^(?:$class_list_re|$modifier_list_re)/o) {
716 $tmp_value = "$qpart $tmp_value";
720 if ($type =~ /$class_list_re/o ) {
721 $value .= $tmp_value;
725 next unless $type and $value;
727 $value =~ s/^\s*//og;
728 $value =~ s/\s*$//og;
729 $type = 'sort_dir' if $type eq 'dir';
731 if($type eq 'site') {
732 # 'site' is the org shortname. when using this, we also want
733 # to search at the requested org's depth
734 my $e = new_editor();
735 if(my $org = $e->search_actor_org_unit({shortname => $value})->[0]) {
736 $arghash->{org_unit} = $org->id if $org;
737 $arghash->{depth} = $e->retrieve_actor_org_unit_type($org->ou_type)->depth;
739 $logger->warn("'site:' query used on invalid org shortname: $value ... ignoring");
742 } elsif($type eq 'available') {
744 $arghash->{available} = 1 unless $value eq 'false' or $value eq '0';
746 } elsif($type eq 'lang') {
747 # collect languages into an array of languages
748 $arghash->{language} = [] unless $arghash->{language};
749 push(@{$arghash->{language}}, $value);
751 } elsif($type =~ /^sort/o) {
752 # sort and sort_dir modifiers
753 $arghash->{$type} = $value;
756 # append the search term to the term under construction
757 $search->{$type} = {} unless $search->{$type};
758 $search->{$type}->{term} =
759 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $value" : $value;
763 $query .= " $tmp_value";
764 $query =~ s/\s+/ /go;
765 $query =~ s/^\s+//go;
766 $query =~ s/\s+$//go;
768 my $type = $arghash->{default_class} || 'keyword';
769 $type = ($type eq '-') ? 'keyword' : $type;
770 $type = ($type !~ /^(title|author|keyword|subject|series)(?:\|\w+)?$/o) ? 'keyword' : $type;
773 # This is the front part of the string before any special tokens were
774 # parsed OR colon-separated strings that do not denote a class.
775 # Add this data to the default search class
776 $search->{$type} = {} unless $search->{$type};
777 $search->{$type}->{term} =
778 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $query" : $query;
780 my $real_search = $arghash->{searches} = { $type => { term => $orig_query } };
782 # capture the original limit because the search method alters the limit internally
783 my $ol = $arghash->{limit};
785 my $sclient = OpenSRF::Utils::SettingsClient->new;
787 (my $method = $self->api_name) =~ s/\.query//o;
789 $method =~ s/multiclass/multiclass.staged/
790 if $sclient->config_value(apps => 'open-ils.search',
791 app_settings => 'use_staged_search') =~ /true/i;
793 # XXX This stops the session locale from doing the right thing.
794 # XXX Revisit this and have it translate to a lang instead of a locale.
795 #$arghash->{preferred_language} = $U->get_org_locale($arghash->{org_unit})
796 # unless $arghash->{preferred_language};
798 $method = $self->method_lookup($method);
799 my ($data) = $method->run($arghash, $docache);
801 $arghash->{searches} = $search if (!$data->{complex_query});
803 $arghash->{limit} = $ol if $ol;
804 $data->{compiled_search} = $arghash;
805 $data->{query} = $orig_query;
807 $logger->info("compiled search is " . OpenSRF::Utils::JSON->perl2JSON($arghash));
812 __PACKAGE__->register_method(
813 method => 'cat_search_z_style_wrapper',
814 api_name => 'open-ils.search.biblio.zstyle',
816 signature => q/@see open-ils.search.biblio.multiclass/
819 __PACKAGE__->register_method(
820 method => 'cat_search_z_style_wrapper',
821 api_name => 'open-ils.search.biblio.zstyle.staff',
823 signature => q/@see open-ils.search.biblio.multiclass/
826 sub cat_search_z_style_wrapper {
829 my $authtoken = shift;
832 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
834 my $ou = $cstore->request(
835 'open-ils.cstore.direct.actor.org_unit.search',
836 { parent_ou => undef }
839 my $result = { service => 'native-evergreen-catalog', records => [] };
840 my $searchhash = { limit => $$args{limit}, offset => $$args{offset}, org_unit => $ou->id };
842 $$searchhash{searches}{title}{term} = $$args{search}{title} if $$args{search}{title};
843 $$searchhash{searches}{author}{term} = $$args{search}{author} if $$args{search}{author};
844 $$searchhash{searches}{subject}{term} = $$args{search}{subject} if $$args{search}{subject};
845 $$searchhash{searches}{keyword}{term} = $$args{search}{keyword} if $$args{search}{keyword};
847 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{tcn} if $$args{search}{tcn};
848 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{isbn} if $$args{search}{isbn};
849 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{issn} if $$args{search}{issn};
850 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{publisher} if $$args{search}{publisher};
851 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{pubdate} if $$args{search}{pubdate};
852 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{item_type} if $$args{search}{item_type};
854 my $list = the_quest_for_knowledge( $self, $client, $searchhash );
856 if ($list->{count} > 0) {
857 $result->{count} = $list->{count};
859 my $records = $cstore->request(
860 'open-ils.cstore.direct.biblio.record_entry.search.atomic',
861 { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
864 for my $rec ( @$records ) {
866 my $u = OpenILS::Utils::ModsParser->new();
867 $u->start_mods_batch( $rec->marc );
868 my $mods = $u->finish_mods_batch();
870 push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
876 $cstore->disconnect();
880 # ----------------------------------------------------------------------------
881 # These are the main OPAC search methods
882 # ----------------------------------------------------------------------------
884 __PACKAGE__->register_method(
885 method => 'the_quest_for_knowledge',
886 api_name => 'open-ils.search.biblio.multiclass',
888 desc => "Performs a multi class biblio or metabib search",
891 desc => "A search hash with keys: "
892 . "searches, org_unit, depth, limit, offset, format, sort, sort_dir. "
893 . "See perldoc " . __PACKAGE__ . " for more detail",
897 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
902 desc => 'An object of the form: '
903 . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
908 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
910 The search-hash argument can have the following elements:
912 searches: { "$class" : "$value", ...} [REQUIRED]
913 org_unit: The org id to focus the search at
914 depth : The org depth
915 limit : The search limit default: 10
916 offset : The search offset default: 0
917 format : The MARC format
918 sort : What field to sort the results on? [ author | title | pubdate ]
919 sort_dir: What direction do we sort? [ asc | desc ]
920 tag_circulated_records : Boolean, if true, records that are in the user's visible checkout history
921 will be tagged with an additional value ("1") as the last value in the record ID array for
922 each record. Requires the 'authtoken'
923 authtoken : Authentication token string; When actions are performed that require a user login
924 (e.g. tagging circulated records), the authentication token is required
926 The searches element is required, must have a hashref value, and the hashref must contain at least one
927 of the following classes as a key:
935 The value paired with a key is the associated search string.
937 The docache argument enables/disables searching and saving results in cache (default OFF).
939 The return object, if successful, will look like:
941 { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
945 __PACKAGE__->register_method(
946 method => 'the_quest_for_knowledge',
947 api_name => 'open-ils.search.biblio.multiclass.staff',
948 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
950 __PACKAGE__->register_method(
951 method => 'the_quest_for_knowledge',
952 api_name => 'open-ils.search.metabib.multiclass',
953 signature => q/@see open-ils.search.biblio.multiclass/
955 __PACKAGE__->register_method(
956 method => 'the_quest_for_knowledge',
957 api_name => 'open-ils.search.metabib.multiclass.staff',
958 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
961 sub the_quest_for_knowledge {
962 my( $self, $conn, $searchhash, $docache ) = @_;
964 return { count => 0 } unless $searchhash and
965 ref $searchhash->{searches} eq 'HASH';
967 my $method = 'open-ils.storage.biblio.multiclass.search_fts';
971 if($self->api_name =~ /metabib/) {
973 $method =~ s/biblio/metabib/o;
976 # do some simple sanity checking
977 if(!$searchhash->{searches} or
978 ( !grep { /^(?:title|author|subject|series|keyword)/ } keys %{$searchhash->{searches}} ) ) {
979 return { count => 0 };
982 my $offset = $searchhash->{offset} || 0; # user value or default in local var now
983 my $limit = $searchhash->{limit} || 10; # user value or default in local var now
984 my $end = $offset + $limit - 1;
987 $searchhash->{offset} = 0; # possible user value overwritten in hash
988 $searchhash->{limit} = $maxlimit; # possible user value overwritten in hash
990 return { count => 0 } if $offset > $maxlimit;
993 push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
994 my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
995 my $ckey = $pfx . md5_hex($method . $s);
997 $logger->info("bib search for: $s");
999 $searchhash->{limit} -= $offset;
1003 my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
1007 $method .= ".staff" if($self->api_name =~ /staff/);
1008 $method .= ".atomic";
1010 for (keys %$searchhash) {
1011 delete $$searchhash{$_}
1012 unless defined $$searchhash{$_};
1015 $result = $U->storagereq( $method, %$searchhash );
1019 $docache = 0; # results came FROM cache, so we don't write back
1022 return {count => 0} unless ($result && $$result[0]);
1026 my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
1029 # If we didn't get this data from the cache, put it into the cache
1030 # then return the correct offset of records
1031 $logger->debug("putting search cache $ckey\n");
1032 put_cache($ckey, $count, \@recs);
1036 # if we have the full set of data, trim out
1037 # the requested chunk based on limit and offset
1039 for ($offset..$end) {
1040 last unless $recs[$_];
1041 push(@t, $recs[$_]);
1046 return { ids => \@recs, count => $count };
1050 __PACKAGE__->register_method(
1051 method => 'staged_search',
1052 api_name => 'open-ils.search.biblio.multiclass.staged',
1054 desc => 'Staged search filters out unavailable items. This means that it relies on an estimation strategy for determining ' .
1055 'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering. See "estimation_strategy" in your SRF config.',
1058 desc => "A search hash with keys: "
1059 . "searches, limit, offset. The others are optional, but the 'searches' key/value pair is required, with the value being a hashref. "
1060 . "See perldoc " . __PACKAGE__ . " for more detail",
1064 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
1069 desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids. '
1070 . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
1075 __PACKAGE__->register_method(
1076 method => 'staged_search',
1077 api_name => 'open-ils.search.biblio.multiclass.staged.staff',
1078 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1080 __PACKAGE__->register_method(
1081 method => 'staged_search',
1082 api_name => 'open-ils.search.metabib.multiclass.staged',
1083 signature => q/@see open-ils.search.biblio.multiclass.staged/
1085 __PACKAGE__->register_method(
1086 method => 'staged_search',
1087 api_name => 'open-ils.search.metabib.multiclass.staged.staff',
1088 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1092 my($self, $conn, $search_hash, $docache) = @_;
1094 my $IAmMetabib = ($self->api_name =~ /metabib/) ? 1 : 0;
1096 my $method = $IAmMetabib?
1097 'open-ils.storage.metabib.multiclass.staged.search_fts':
1098 'open-ils.storage.biblio.multiclass.staged.search_fts';
1100 $method .= '.staff' if $self->api_name =~ /staff$/;
1101 $method .= '.atomic';
1103 return {count => 0} unless (
1105 $search_hash->{searches} and
1106 scalar( keys %{$search_hash->{searches}} ));
1108 my $search_duration;
1109 my $user_offset = $search_hash->{offset} || 0; # user-specified offset
1110 my $user_limit = $search_hash->{limit} || 10;
1111 my $ignore_facet_classes = $search_hash->{ignore_facet_classes};
1112 $user_offset = ($user_offset >= 0) ? $user_offset : 0;
1113 $user_limit = ($user_limit >= 0) ? $user_limit : 10;
1116 # we're grabbing results on a per-superpage basis, which means the
1117 # limit and offset should coincide with superpage boundaries
1118 $search_hash->{offset} = 0;
1119 $search_hash->{limit} = $superpage_size;
1121 # force a well-known check_limit
1122 $search_hash->{check_limit} = $superpage_size;
1123 # restrict total tested to superpage size * number of superpages
1124 $search_hash->{core_limit} = $superpage_size * $max_superpages;
1126 # Set the configured estimation strategy, defaults to 'inclusion'.
1127 my $estimation_strategy = OpenSRF::Utils::SettingsClient
1130 apps => 'open-ils.search', app_settings => 'estimation_strategy'
1132 $search_hash->{estimation_strategy} = $estimation_strategy;
1134 # pull any existing results from the cache
1135 my $key = search_cache_key($method, $search_hash);
1136 my $facet_key = $key.'_facets';
1137 my $cache_data = $cache->get_cache($key) || {};
1139 # keep retrieving results until we find enough to
1140 # fulfill the user-specified limit and offset
1141 my $all_results = [];
1142 my $page; # current superpage
1143 my $est_hit_count = 0;
1144 my $current_page_summary = {};
1145 my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1146 my $is_real_hit_count = 0;
1149 for($page = 0; $page < $max_superpages; $page++) {
1151 my $data = $cache_data->{$page};
1155 $logger->debug("staged search: analyzing superpage $page");
1158 # this window of results is already cached
1159 $logger->debug("staged search: found cached results");
1160 $summary = $data->{summary};
1161 $results = $data->{results};
1164 # retrieve the window of results from the database
1165 $logger->debug("staged search: fetching results from the database");
1166 $search_hash->{skip_check} = $page * $superpage_size;
1168 $results = $U->storagereq($method, %$search_hash);
1169 $search_duration = time - $start;
1170 $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1171 $summary = shift(@$results) if $results;
1174 $logger->info("search timed out: duration=$search_duration: params=".
1175 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1176 return {count => 0};
1179 my $hc = $summary->{estimated_hit_count} || $summary->{visible};
1181 $logger->info("search returned 0 results: duration=$search_duration: params=".
1182 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1185 # Create backwards-compatible result structures
1187 $results = [map {[$_->{id}, $_->{rel}, $_->{record}]} @$results];
1189 $results = [map {[$_->{id}]} @$results];
1192 tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib)
1193 if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1195 push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1196 $results = [grep {defined $_->[0]} @$results];
1197 cache_staged_search_page($key, $page, $summary, $results) if $docache;
1200 $current_page_summary = $summary;
1202 # add the new set of results to the set under construction
1203 push(@$all_results, @$results);
1205 my $current_count = scalar(@$all_results);
1207 $est_hit_count = $summary->{estimated_hit_count} || $summary->{visible}
1210 $logger->debug("staged search: located $current_count, with estimated hits=".
1211 $summary->{estimated_hit_count}." : visible=".$summary->{visible}.", checked=".$summary->{checked});
1213 if (defined($summary->{estimated_hit_count})) {
1214 foreach (qw/ checked visible excluded deleted /) {
1215 $global_summary->{$_} += $summary->{$_};
1217 $global_summary->{total} = $summary->{total};
1220 # we've found all the possible hits
1221 last if $current_count == $summary->{visible}
1222 and not defined $summary->{estimated_hit_count};
1224 # we've found enough results to satisfy the requested limit/offset
1225 last if $current_count >= ($user_limit + $user_offset);
1227 # we've scanned all possible hits
1228 if($summary->{checked} < $superpage_size) {
1229 $est_hit_count = scalar(@$all_results);
1230 # we have all possible results in hand, so we know the final hit count
1231 $is_real_hit_count = 1;
1236 my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1238 # refine the estimate if we have more than one superpage
1239 if ($page > 0 and not $is_real_hit_count) {
1240 if ($global_summary->{checked} >= $global_summary->{total}) {
1241 $est_hit_count = $global_summary->{visible};
1243 my $updated_hit_count = $U->storagereq(
1244 'open-ils.storage.fts_paging_estimate',
1245 $global_summary->{checked},
1246 $global_summary->{visible},
1247 $global_summary->{excluded},
1248 $global_summary->{deleted},
1249 $global_summary->{total}
1251 $est_hit_count = $updated_hit_count->{$estimation_strategy};
1255 $conn->respond_complete(
1257 count => $est_hit_count,
1258 core_limit => $search_hash->{core_limit},
1259 superpage_size => $search_hash->{check_limit},
1260 superpage_summary => $current_page_summary,
1261 facet_key => $facet_key,
1266 cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
1271 sub tag_circulated_records {
1272 my ($auth, $results, $metabib) = @_;
1273 my $e = new_editor(authtoken => $auth);
1274 return $results unless $e->checkauth;
1277 select => { acn => [{ column => 'record', alias => 'tagme' }] },
1278 from => { acp => 'acn' },
1279 where => { id => { in => { from => ['action.usr_visible_circ_copies', $e->requestor->id] } } },
1285 select => { mmsm => [{ column => 'metarecord', alias => 'tagme' }] },
1287 where => { source => { in => $query } },
1292 # Give me the distinct set of bib records that exist in the user's visible circulation history
1293 my $circ_recs = $e->json_query( $query );
1295 # if the record appears in the circ history, push a 1 onto
1296 # the rec array structure to indicate truthiness
1297 for my $rec (@$results) {
1298 push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1304 # creates a unique token to represent the query in the cache
1305 sub search_cache_key {
1307 my $search_hash = shift;
1309 for my $key (sort keys %$search_hash) {
1310 push(@sorted, ($key => $$search_hash{$key}))
1311 unless $key eq 'limit' or
1313 $key eq 'skip_check';
1315 my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1316 return $pfx . md5_hex($method . $s);
1319 sub retrieve_cached_facets {
1325 return undef unless ($key and $key =~ /_facets$/);
1327 my $blob = $cache->get_cache($key) || {};
1331 for my $f ( keys %$blob ) {
1332 my @sorted = map{ { $$_[1] => $$_[0] } } sort {$$b[0] <=> $$a[0] || $$a[1] cmp $$b[1]} map { [$$blob{$f}{$_}, $_] } keys %{ $$blob{$f} };
1333 @sorted = @sorted[0 .. $limit - 1] if (scalar(@sorted) > $limit);
1334 for my $s ( @sorted ) {
1335 my ($k) = keys(%$s);
1336 my ($v) = values(%$s);
1337 $$facets{$f}{$k} = $v;
1347 __PACKAGE__->register_method(
1348 method => "retrieve_cached_facets",
1349 api_name => "open-ils.search.facet_cache.retrieve",
1351 desc => 'Returns facet data derived from a specific search based on a key '.
1352 'generated by open-ils.search.biblio.multiclass.staged and friends.',
1355 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1360 desc => 'Two level hash of facet values. Top level key is the facet id defined on the config.metabib_field table. '.
1361 'Second level key is a string facet value. Datum attached to each facet value is the number of distinct records, '.
1362 'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1363 'facet retrieval. These counts are calculated for all superpages that have been checked for visibility.',
1371 # add facets for this search to the facet cache
1372 my($key, $results, $metabib, $ignore) = @_;
1373 my $data = $cache->get_cache($key);
1376 if (!ref($ignore)) {
1377 $ignore = ['identifier']; # ignore the identifier class by default
1380 return undef unless (@$results);
1382 # The query we're constructing
1384 # select mfae.field as id,
1386 # count(distinct mmrsm.appropriate-id-field )
1387 # from metabib.facet_entry mfae
1388 # join metabib.metarecord_sourc_map mmrsm on (mfae.source = mmrsm.source)
1389 # where mmrsm.appropriate-id-field in IDLIST
1392 my $count_field = $metabib ? 'metarecord' : 'source';
1393 my $facets = $U->cstorereq( "open-ils.cstore.json_query.atomic",
1395 mfae => [ { column => 'field', alias => 'id'}, 'value' ],
1397 transform => 'count',
1399 column => $count_field,
1406 mmrsm => { field => 'source', fkey => 'source' },
1407 cmf => { field => 'id', fkey => 'field' }
1411 '+mmrsm' => { $count_field => $results },
1412 '+cmf' => { field_class => { 'not in' => $ignore } }
1417 for my $facet (@$facets) {
1418 next unless ($facet->{value});
1419 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1422 $logger->info("facet compilation: cached with key=$key");
1424 $cache->put_cache($key, $data, $cache_timeout);
1427 sub cache_staged_search_page {
1428 # puts this set of results into the cache
1429 my($key, $page, $summary, $results) = @_;
1430 my $data = $cache->get_cache($key);
1433 summary => $summary,
1437 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1438 $summary->{estimated_hit_count}.", visible=".$summary->{visible});
1440 $cache->put_cache($key, $data, $cache_timeout);
1448 my $start = $offset;
1449 my $end = $offset + $limit - 1;
1451 $logger->debug("searching cache for $key : $start..$end\n");
1453 return undef unless $cache;
1454 my $data = $cache->get_cache($key);
1456 return undef unless $data;
1458 my $count = $data->[0];
1461 return undef unless $offset < $count;
1464 for( my $i = $offset; $i <= $end; $i++ ) {
1465 last unless my $d = $$data[$i];
1466 push( @result, $d );
1469 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1476 my( $key, $count, $data ) = @_;
1477 return undef unless $cache;
1478 $logger->debug("search_cache putting ".
1479 scalar(@$data)." items at key $key with timeout $cache_timeout");
1480 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1484 __PACKAGE__->register_method(
1485 method => "biblio_mrid_to_modsbatch_batch",
1486 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1489 sub biblio_mrid_to_modsbatch_batch {
1490 my( $self, $client, $mrids) = @_;
1491 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1493 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1494 for my $id (@$mrids) {
1495 next unless defined $id;
1496 my ($m) = $method->run($id);
1503 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1504 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1506 __PACKAGE__->register_method(
1507 method => "biblio_mrid_to_modsbatch",
1510 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1511 . "As usual, the .staff version of this method will include otherwise hidden records.",
1513 { desc => 'Metarecord ID', type => 'number' },
1514 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1517 desc => 'MVR Object, event on error',
1523 sub biblio_mrid_to_modsbatch {
1524 my( $self, $client, $mrid, $args) = @_;
1526 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1528 my ($mr, $evt) = _grab_metarecord($mrid);
1529 return $evt unless $mr;
1531 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1532 biblio_mrid_make_modsbatch($self, $client, $mr);
1534 return $mvr unless ref($args);
1536 # Here we find the lead record appropriate for the given filters
1537 # and use that for the title and author of the metarecord
1538 my $format = $$args{format};
1539 my $org = $$args{org};
1540 my $depth = $$args{depth};
1542 return $mvr unless $format or $org or $depth;
1544 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1545 $method = "$method.staff" if $self->api_name =~ /staff/o;
1547 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1549 if( my $mods = $U->record_to_mvr($rec) ) {
1551 $mvr->title( $mods->title );
1552 $mvr->author($mods->author);
1553 $logger->debug("mods_slim updating title and ".
1554 "author in mvr with ".$mods->title." : ".$mods->author);
1560 # converts a metarecord to an mvr
1563 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1564 return Fieldmapper::metabib::virtual_record->new($perl);
1567 # checks to see if a metarecord has mods, if so returns true;
1569 __PACKAGE__->register_method(
1570 method => "biblio_mrid_check_mvr",
1571 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1572 notes => "Takes a metarecord ID or a metarecord object and returns true "
1573 . "if the metarecord already has an mvr associated with it."
1576 sub biblio_mrid_check_mvr {
1577 my( $self, $client, $mrid ) = @_;
1581 if(ref($mrid)) { $mr = $mrid; }
1582 else { ($mr, $evt) = _grab_metarecord($mrid); }
1583 return $evt if $evt;
1585 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1587 return _mr_to_mvr($mr) if $mr->mods();
1591 sub _grab_metarecord {
1593 #my $e = OpenILS::Utils::Editor->new;
1594 my $e = new_editor();
1595 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1600 __PACKAGE__->register_method(
1601 method => "biblio_mrid_make_modsbatch",
1602 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1603 notes => "Takes either a metarecord ID or a metarecord object. "
1604 . "Forces the creations of an mvr for the given metarecord. "
1605 . "The created mvr is returned."
1608 sub biblio_mrid_make_modsbatch {
1609 my( $self, $client, $mrid ) = @_;
1611 #my $e = OpenILS::Utils::Editor->new;
1612 my $e = new_editor();
1619 $mr = $e->retrieve_metabib_metarecord($mrid)
1620 or return $e->event;
1623 my $masterid = $mr->master_record;
1624 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1626 my $ids = $U->storagereq(
1627 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1628 return undef unless @$ids;
1630 my $master = $e->retrieve_biblio_record_entry($masterid)
1631 or return $e->event;
1633 # start the mods batch
1634 my $u = OpenILS::Utils::ModsParser->new();
1635 $u->start_mods_batch( $master->marc );
1637 # grab all of the sub-records and shove them into the batch
1638 my @ids = grep { $_ ne $masterid } @$ids;
1639 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1644 my $r = $e->retrieve_biblio_record_entry($i);
1645 push( @$subrecs, $r ) if $r;
1650 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1651 $u->push_mods_batch( $_->marc ) if $_->marc;
1655 # finish up and send to the client
1656 my $mods = $u->finish_mods_batch();
1657 $mods->doc_id($mrid);
1658 $client->respond_complete($mods);
1661 # now update the mods string in the db
1662 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1665 #$e = OpenILS::Utils::Editor->new(xact => 1);
1666 $e = new_editor(xact => 1);
1667 $e->update_metabib_metarecord($mr)
1668 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1675 # converts a mr id into a list of record ids
1677 foreach (qw/open-ils.search.biblio.metarecord_to_records
1678 open-ils.search.biblio.metarecord_to_records.staff/)
1680 __PACKAGE__->register_method(
1681 method => "biblio_mrid_to_record_ids",
1684 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1685 . "As usual, the .staff version of this method will include otherwise hidden records.",
1687 { desc => 'Metarecord ID', type => 'number' },
1688 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1691 desc => 'Results object like {count => $i, ids =>[...]}',
1699 sub biblio_mrid_to_record_ids {
1700 my( $self, $client, $mrid, $args ) = @_;
1702 my $format = $$args{format};
1703 my $org = $$args{org};
1704 my $depth = $$args{depth};
1706 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1707 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1708 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1710 return { count => scalar(@$recs), ids => $recs };
1714 __PACKAGE__->register_method(
1715 method => "biblio_record_to_marc_html",
1716 api_name => "open-ils.search.biblio.record.html"
1719 __PACKAGE__->register_method(
1720 method => "biblio_record_to_marc_html",
1721 api_name => "open-ils.search.authority.to_html"
1724 # Persistent parsers and setting objects
1725 my $parser = XML::LibXML->new();
1726 my $xslt = XML::LibXSLT->new();
1728 my $slim_marc_sheet;
1729 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1731 sub biblio_record_to_marc_html {
1732 my($self, $client, $recordid, $slim, $marcxml) = @_;
1735 my $dir = $settings_client->config_value("dirs", "xsl");
1738 unless($slim_marc_sheet) {
1739 my $xsl = $settings_client->config_value(
1740 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1742 $xsl = $parser->parse_file("$dir/$xsl");
1743 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1746 $sheet = $slim_marc_sheet;
1750 unless($marc_sheet) {
1751 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1752 my $xsl = $settings_client->config_value(
1753 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1754 $xsl = $parser->parse_file("$dir/$xsl");
1755 $marc_sheet = $xslt->parse_stylesheet($xsl);
1757 $sheet = $marc_sheet;
1762 my $e = new_editor();
1763 if($self->api_name =~ /authority/) {
1764 $record = $e->retrieve_authority_record_entry($recordid)
1765 or return $e->event;
1767 $record = $e->retrieve_biblio_record_entry($recordid)
1768 or return $e->event;
1770 $marcxml = $record->marc;
1773 my $xmldoc = $parser->parse_string($marcxml);
1774 my $html = $sheet->transform($xmldoc);
1775 return $html->documentElement->toString();
1778 __PACKAGE__->register_method(
1779 method => "format_biblio_record_entry",
1780 api_name => "open-ils.search.biblio.record.print",
1782 desc => 'Returns a printable version of the specified bib record',
1784 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1787 desc => q/An action_trigger.event object or error event./,
1792 __PACKAGE__->register_method(
1793 method => "format_biblio_record_entry",
1794 api_name => "open-ils.search.biblio.record.email",
1796 desc => 'Emails an A/T templated version of the specified bib records to the authorized user',
1798 { desc => 'Authentication token', type => 'string'},
1799 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1802 desc => q/Undefined on success, otherwise an error event./,
1808 sub format_biblio_record_entry {
1809 my($self, $conn, $arg1, $arg2) = @_;
1811 my $for_print = ($self->api_name =~ /print/);
1812 my $for_email = ($self->api_name =~ /email/);
1814 my $e; my $auth; my $bib_id; my $context_org;
1818 $context_org = $arg2 || $U->fetch_org_tree->id;
1819 $e = new_editor(xact => 1);
1820 } elsif ($for_email) {
1823 $e = new_editor(authtoken => $auth, xact => 1);
1824 return $e->die_event unless $e->checkauth;
1825 $context_org = $e->requestor->home_ou;
1829 if (ref $bib_id ne 'ARRAY') {
1830 $bib_ids = [ $bib_id ];
1835 my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1836 $bucket->btype('temp');
1837 $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1839 $bucket->owner($e->requestor)
1843 my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1845 for my $id (@$bib_ids) {
1847 my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1849 my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1850 $bucket_entry->target_biblio_record_entry($bib);
1851 $bucket_entry->bucket($bucket_obj->id);
1852 $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
1859 return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
1861 } elsif ($for_email) {
1863 $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
1870 __PACKAGE__->register_method(
1871 method => "retrieve_all_copy_statuses",
1872 api_name => "open-ils.search.config.copy_status.retrieve.all"
1875 sub retrieve_all_copy_statuses {
1876 my( $self, $client ) = @_;
1877 return new_editor()->retrieve_all_config_copy_status();
1881 __PACKAGE__->register_method(
1882 method => "copy_counts_per_org",
1883 api_name => "open-ils.search.biblio.copy_counts.retrieve"
1886 __PACKAGE__->register_method(
1887 method => "copy_counts_per_org",
1888 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1891 sub copy_counts_per_org {
1892 my( $self, $client, $record_id ) = @_;
1894 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1896 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1897 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1899 my $counts = $apputils->simple_scalar_request(
1900 "open-ils.storage", $method, $record_id );
1902 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1907 __PACKAGE__->register_method(
1908 method => "copy_count_summary",
1909 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
1910 notes => "returns an array of these: "
1911 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1912 . "where statusx is a copy status name. The statuses are sorted by ID.",
1916 sub copy_count_summary {
1917 my( $self, $client, $rid, $org, $depth ) = @_;
1920 my $data = $U->storagereq(
1921 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
1923 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1926 __PACKAGE__->register_method(
1927 method => "copy_location_count_summary",
1928 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
1929 notes => "returns an array of these: "
1930 . "[ org_id, callnumber_label, copy_location, <status1_count>, <status2_count>,...] "
1931 . "where statusx is a copy status name. The statuses are sorted by ID.",
1934 sub copy_location_count_summary {
1935 my( $self, $client, $rid, $org, $depth ) = @_;
1938 my $data = $U->storagereq(
1939 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1941 return [ sort { $a->[1] cmp $b->[1] || $a->[2] cmp $b->[2] } @$data ];
1944 __PACKAGE__->register_method(
1945 method => "copy_count_location_summary",
1946 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
1947 notes => "returns an array of these: "
1948 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1949 . "where statusx is a copy status name. The statuses are sorted by ID."
1952 sub copy_count_location_summary {
1953 my( $self, $client, $rid, $org, $depth ) = @_;
1956 my $data = $U->storagereq(
1957 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1958 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1962 foreach (qw/open-ils.search.biblio.marc
1963 open-ils.search.biblio.marc.staff/)
1965 __PACKAGE__->register_method(
1966 method => "marc_search",
1969 desc => 'Fetch biblio IDs based on MARC record criteria. '
1970 . 'As usual, the .staff version of the search includes otherwise hidden records',
1973 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
1974 'See perldoc ' . __PACKAGE__ . ' for more detail.',
1977 {desc => 'limit (optional)', type => 'number'},
1978 {desc => 'offset (optional)', type => 'number'}
1981 desc => 'Results object like: { "count": $i, "ids": [...] }',
1988 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
1990 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
1992 searches: complex query object (required)
1993 org_unit: The org ID to focus the search at
1994 depth : The org depth
1995 limit : integer search limit default: 10
1996 offset : integer search offset default: 0
1997 sort : What field to sort the results on? [ author | title | pubdate ]
1998 sort_dir: In what direction do we sort? [ asc | desc ]
2000 Additional keys to refine search criteria:
2003 language : Language (code)
2004 lit_form : Literary form
2005 item_form: Item form
2006 item_type: Item type
2007 format : The MARC format
2009 Please note that the specific strings to be used in the "addtional keys" will be entirely
2010 dependent on your loaded data.
2012 All keys except "searches" are optional.
2013 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
2015 For example, an arg hash might look like:
2037 The arghash is eventually passed to the SRF call:
2038 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2040 Presently, search uses the cache unconditionally.
2044 # FIXME: that example above isn't actually tested.
2045 # TODO: docache option?
2047 my( $self, $conn, $args, $limit, $offset ) = @_;
2049 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2050 $method .= ".staff" if $self->api_name =~ /staff/;
2051 $method .= ".atomic";
2053 $limit ||= 10; # FIXME: what about $args->{limit} ?
2054 $offset ||= 0; # FIXME: what about $args->{offset} ?
2057 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2058 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2060 my $recs = search_cache($ckey, $offset, $limit);
2063 $recs = $U->storagereq($method, %$args) || [];
2065 put_cache($ckey, scalar(@$recs), $recs);
2066 $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
2073 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2074 my @recs = map { $_->[0] } @$recs;
2076 return { ids => \@recs, count => $count };
2080 __PACKAGE__->register_method(
2081 method => "biblio_search_isbn",
2082 api_name => "open-ils.search.biblio.isbn",
2084 desc => 'Retrieve biblio IDs for a given ISBN',
2086 {desc => 'ISBN', type => 'string'} # or number maybe? How normalized is our storage data?
2089 desc => 'Results object like: { "count": $i, "ids": [...] }',
2095 sub biblio_search_isbn {
2096 my( $self, $client, $isbn ) = @_;
2097 $logger->debug("Searching ISBN $isbn");
2098 my $recs = $U->storagereq('open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn);
2099 return { ids => $recs, count => scalar(@$recs) };
2102 __PACKAGE__->register_method(
2103 method => "biblio_search_isbn_batch",
2104 api_name => "open-ils.search.biblio.isbn_list",
2107 sub biblio_search_isbn_batch {
2108 my( $self, $client, $isbn_list ) = @_;
2109 $logger->debug("Searching ISBNs @$isbn_list");
2110 my @recs = (); my %rec_set = ();
2111 foreach my $isbn ( @$isbn_list ) {
2112 foreach my $rec ( @{ $U->storagereq(
2113 'open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn )
2115 if (! $rec_set{ $rec }) {
2116 $rec_set{ $rec } = 1;
2121 return { ids => \@recs, count => scalar(@recs) };
2124 __PACKAGE__->register_method(
2125 method => "biblio_search_issn",
2126 api_name => "open-ils.search.biblio.issn",
2128 desc => 'Retrieve biblio IDs for a given ISSN',
2130 {desc => 'ISBN', type => 'string'}
2133 desc => 'Results object like: { "count": $i, "ids": [...] }',
2139 sub biblio_search_issn {
2140 my( $self, $client, $issn ) = @_;
2141 $logger->debug("Searching ISSN $issn");
2142 my $e = new_editor();
2144 my $recs = $U->storagereq(
2145 'open-ils.storage.id_list.biblio.record_entry.search.issn.atomic', $issn );
2146 return { ids => $recs, count => scalar(@$recs) };
2150 __PACKAGE__->register_method(
2151 method => "fetch_mods_by_copy",
2152 api_name => "open-ils.search.biblio.mods_from_copy",
2155 desc => 'Retrieve MODS record given an attached copy ID',
2157 { desc => 'Copy ID', type => 'number' }
2160 desc => 'MODS record, event on error or uncataloged item'
2165 sub fetch_mods_by_copy {
2166 my( $self, $client, $copyid ) = @_;
2167 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2168 return $evt if $evt;
2169 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2170 return $apputils->record_to_mvr($record);
2174 # -------------------------------------------------------------------------------------
2176 __PACKAGE__->register_method(
2177 method => "cn_browse",
2178 api_name => "open-ils.search.callnumber.browse.target",
2179 notes => "Starts a callnumber browse"
2182 __PACKAGE__->register_method(
2183 method => "cn_browse",
2184 api_name => "open-ils.search.callnumber.browse.page_up",
2185 notes => "Returns the previous page of callnumbers",
2188 __PACKAGE__->register_method(
2189 method => "cn_browse",
2190 api_name => "open-ils.search.callnumber.browse.page_down",
2191 notes => "Returns the next page of callnumbers",
2195 # RETURNS array of arrays like so: label, owning_lib, record, id
2197 my( $self, $client, @params ) = @_;
2200 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
2201 if( $self->api_name =~ /target/ );
2202 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2203 if( $self->api_name =~ /page_up/ );
2204 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2205 if( $self->api_name =~ /page_down/ );
2207 return $apputils->simplereq( 'open-ils.storage', $method, @params );
2209 # -------------------------------------------------------------------------------------
2211 __PACKAGE__->register_method(
2212 method => "fetch_cn",
2213 api_name => "open-ils.search.callnumber.retrieve",
2215 notes => "retrieves a callnumber based on ID",
2219 my( $self, $client, $id ) = @_;
2220 my( $cn, $evt ) = $apputils->fetch_callnumber( $id );
2221 return $evt if $evt;
2225 __PACKAGE__->register_method(
2226 method => "fetch_copy_by_cn",
2227 api_name => 'open-ils.search.copies_by_call_number.retrieve',
2229 Returns an array of copy ID's by callnumber ID
2230 @param cnid The callnumber ID
2231 @return An array of copy IDs
2235 sub fetch_copy_by_cn {
2236 my( $self, $conn, $cnid ) = @_;
2237 return $U->cstorereq(
2238 'open-ils.cstore.direct.asset.copy.id_list.atomic',
2239 { call_number => $cnid, deleted => 'f' } );
2242 __PACKAGE__->register_method(
2243 method => 'fetch_cn_by_info',
2244 api_name => 'open-ils.search.call_number.retrieve_by_info',
2246 @param label The callnumber label
2247 @param record The record the cn is attached to
2248 @param org The owning library of the cn
2249 @return The callnumber object
2254 sub fetch_cn_by_info {
2255 my( $self, $conn, $label, $record, $org ) = @_;
2256 return $U->cstorereq(
2257 'open-ils.cstore.direct.asset.call_number.search',
2258 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2263 __PACKAGE__->register_method(
2264 method => 'bib_extras',
2265 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all'
2267 __PACKAGE__->register_method(
2268 method => 'bib_extras',
2269 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all'
2271 __PACKAGE__->register_method(
2272 method => 'bib_extras',
2273 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all'
2275 __PACKAGE__->register_method(
2276 method => 'bib_extras',
2277 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all'
2279 __PACKAGE__->register_method(
2280 method => 'bib_extras',
2281 api_name => 'open-ils.search.biblio.audience_map.retrieve.all'
2287 my $e = new_editor();
2289 return $e->retrieve_all_config_lit_form_map()
2290 if( $self->api_name =~ /lit_form/ );
2292 return $e->retrieve_all_config_item_form_map()
2293 if( $self->api_name =~ /item_form_map/ );
2295 return $e->retrieve_all_config_item_type_map()
2296 if( $self->api_name =~ /item_type_map/ );
2298 return $e->retrieve_all_config_bib_level_map()
2299 if( $self->api_name =~ /bib_level_map/ );
2301 return $e->retrieve_all_config_audience_map()
2302 if( $self->api_name =~ /audience_map/ );
2309 __PACKAGE__->register_method(
2310 method => 'fetch_slim_record',
2311 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2313 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2315 { desc => 'Array of Record IDs', type => 'array' }
2318 desc => 'Array of biblio records, event on error'
2323 sub fetch_slim_record {
2324 my( $self, $conn, $ids ) = @_;
2326 #my $editor = OpenILS::Utils::Editor->new;
2327 my $editor = new_editor();
2330 return $editor->event unless
2331 my $r = $editor->retrieve_biblio_record_entry($_);
2340 __PACKAGE__->register_method(
2341 method => 'rec_to_mr_rec_descriptors',
2342 api_name => 'open-ils.search.metabib.record_to_descriptors',
2344 specialized method...
2345 Given a biblio record id or a metarecord id,
2346 this returns a list of metabib.record_descriptor
2347 objects that live within the same metarecord
2348 @param args Object of args including:
2352 sub rec_to_mr_rec_descriptors {
2353 my( $self, $conn, $args ) = @_;
2355 my $rec = $$args{record};
2356 my $mrec = $$args{metarecord};
2357 my $item_forms = $$args{item_forms};
2358 my $item_types = $$args{item_types};
2359 my $item_lang = $$args{item_lang};
2361 my $e = new_editor();
2365 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2366 return $e->event unless @$map;
2367 $mrec = $$map[0]->metarecord;
2370 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2371 return $e->event unless @$recs;
2373 my @recs = map { $_->source } @$recs;
2374 my $search = { record => \@recs };
2375 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2376 $search->{item_type} = $item_types if $item_types and @$item_types;
2377 $search->{item_lang} = $item_lang if $item_lang;
2379 my $desc = $e->search_metabib_record_descriptor($search);
2381 return { metarecord => $mrec, descriptors => $desc };
2385 __PACKAGE__->register_method(
2386 method => 'fetch_age_protect',
2387 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2390 sub fetch_age_protect {
2391 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2395 __PACKAGE__->register_method(
2396 method => 'copies_by_cn_label',
2397 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2400 __PACKAGE__->register_method(
2401 method => 'copies_by_cn_label',
2402 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2405 sub copies_by_cn_label {
2406 my( $self, $conn, $record, $label, $circ_lib ) = @_;
2407 my $e = new_editor();
2408 my $cns = $e->search_asset_call_number({record => $record, label => $label, deleted => 'f'}, {idlist=>1});
2409 return [] unless @$cns;
2411 # show all non-deleted copies in the staff client ...
2412 if ($self->api_name =~ /staff$/o) {
2413 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2416 # ... otherwise, grab the copies ...
2417 my $copies = $e->search_asset_copy(
2418 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2419 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2423 # ... and test for location and status visibility
2424 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];