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",
171 __PACKAGE__->register_method(
172 method => "record_id_to_copy_count",
173 api_name => "open-ils.search.biblio.record.copy_count.staff",
177 __PACKAGE__->register_method(
178 method => "record_id_to_copy_count",
179 api_name => "open-ils.search.biblio.metarecord.copy_count",
182 __PACKAGE__->register_method(
183 method => "record_id_to_copy_count",
184 api_name => "open-ils.search.biblio.metarecord.copy_count.staff",
186 sub record_id_to_copy_count {
187 my( $self, $client, $org_id, $record_id, $format ) = @_;
189 return [] unless $record_id;
190 $format = undef if (!$format or $format eq 'all');
192 my $method = "open-ils.storage.biblio.record_entry.copy_count.atomic";
195 if($self->api_name =~ /metarecord/) {
196 $method = "open-ils.storage.metabib.metarecord.copy_count.atomic";
200 $method =~ s/atomic/staff\.atomic/og if($self->api_name =~ /staff/ );
202 my $count = $U->storagereq( $method,
203 org_unit => $org_id, $key => $record_id, format => $format );
205 return [ sort { $a->{depth} <=> $b->{depth} } @$count ];
209 __PACKAGE__->register_method(
210 method => "biblio_search_tcn",
211 api_name => "open-ils.search.biblio.tcn",
214 desc => "Retrieve related record ID(s) given a TCN",
216 { desc => 'TCN', type => 'string' },
217 { desc => 'Flag indicating to include deleted records', type => 'string' }
220 desc => 'Results object like: { "count": $i, "ids": [...] }',
227 sub biblio_search_tcn {
229 my( $self, $client, $tcn, $include_deleted ) = @_;
231 $tcn =~ s/^\s+|\s+$//og;
233 my $e = new_editor();
234 my $search = {tcn_value => $tcn};
235 $search->{deleted} = 'f' unless $include_deleted;
236 my $recs = $e->search_biblio_record_entry( $search, {idlist =>1} );
238 return { count => scalar(@$recs), ids => $recs };
242 # --------------------------------------------------------------------------------
244 __PACKAGE__->register_method(
245 method => "biblio_barcode_to_copy",
246 api_name => "open-ils.search.asset.copy.find_by_barcode",
248 sub biblio_barcode_to_copy {
249 my( $self, $client, $barcode ) = @_;
250 my( $copy, $evt ) = $U->fetch_copy_by_barcode($barcode);
255 __PACKAGE__->register_method(
256 method => "biblio_id_to_copy",
257 api_name => "open-ils.search.asset.copy.batch.retrieve",
259 sub biblio_id_to_copy {
260 my( $self, $client, $ids ) = @_;
261 $logger->info("Fetching copies @$ids");
262 return $U->cstorereq(
263 "open-ils.cstore.direct.asset.copy.search.atomic", { id => $ids } );
267 __PACKAGE__->register_method(
268 method => "biblio_id_to_uris",
269 api_name=> "open-ils.search.asset.uri.retrieve_by_bib",
273 @param BibID Which bib record contains the URIs
274 @param OrgID Where to look for URIs
275 @param OrgDepth Range adjustment for OrgID
276 @return A stream or list of 'auri' objects
280 sub biblio_id_to_uris {
281 my( $self, $client, $bib, $org, $depth ) = @_;
282 die "Org ID required" unless defined($org);
283 die "Bib ID required" unless defined($bib);
286 push @params, $depth if (defined $depth);
288 my $ids = $U->cstorereq( "open-ils.cstore.json_query.atomic",
289 { select => { auri => [ 'id' ] },
293 field => 'call_number',
299 filter => { active => 't' }
310 select => { aou => [ { column => 'id', transform => 'actor.org_unit_descendants', params => \@params, result_field => 'id' } ] },
312 where => { id => $org },
322 my $uris = $U->cstorereq(
323 "open-ils.cstore.direct.asset.uri.search.atomic",
324 { id => [ map { (values %$_) } @$ids ] }
327 $client->respond($_) for (@$uris);
333 __PACKAGE__->register_method(
334 method => "copy_retrieve",
335 api_name => "open-ils.search.asset.copy.retrieve",
338 desc => 'Retrieve a copy object based on the Copy ID',
340 { desc => 'Copy ID', type => 'number'}
343 desc => 'Copy object, event on error'
349 my( $self, $client, $cid ) = @_;
350 my( $copy, $evt ) = $U->fetch_copy($cid);
351 return $evt || $copy;
354 __PACKAGE__->register_method(
355 method => "volume_retrieve",
356 api_name => "open-ils.search.asset.call_number.retrieve"
358 sub volume_retrieve {
359 my( $self, $client, $vid ) = @_;
360 my $e = new_editor();
361 my $vol = $e->retrieve_asset_call_number($vid) or return $e->event;
365 __PACKAGE__->register_method(
366 method => "fleshed_copy_retrieve_batch",
367 api_name => "open-ils.search.asset.copy.fleshed.batch.retrieve",
371 sub fleshed_copy_retrieve_batch {
372 my( $self, $client, $ids ) = @_;
373 $logger->info("Fetching fleshed copies @$ids");
374 return $U->cstorereq(
375 "open-ils.cstore.direct.asset.copy.search.atomic",
378 flesh_fields => { acp => [ qw/ circ_lib location status stat_cat_entries / ] }
383 __PACKAGE__->register_method(
384 method => "fleshed_copy_retrieve",
385 api_name => "open-ils.search.asset.copy.fleshed.retrieve",
388 sub fleshed_copy_retrieve {
389 my( $self, $client, $id ) = @_;
390 my( $c, $e) = $U->fetch_fleshed_copy($id);
395 __PACKAGE__->register_method(
396 method => 'fleshed_by_barcode',
397 api_name => "open-ils.search.asset.copy.fleshed2.find_by_barcode",
400 sub fleshed_by_barcode {
401 my( $self, $conn, $barcode ) = @_;
402 my $e = new_editor();
403 my $copyid = $e->search_asset_copy(
404 {barcode => $barcode, deleted => 'f'}, {idlist=>1})->[0]
406 return fleshed_copy_retrieve2( $self, $conn, $copyid);
410 __PACKAGE__->register_method(
411 method => "fleshed_copy_retrieve2",
412 api_name => "open-ils.search.asset.copy.fleshed2.retrieve",
416 sub fleshed_copy_retrieve2 {
417 my( $self, $client, $id ) = @_;
418 my $e = new_editor();
419 my $copy = $e->retrieve_asset_copy(
426 qw/ location status stat_cat_entry_copy_maps notes age_protect /
428 ascecm => [qw/ stat_cat stat_cat_entry /],
432 ) or return $e->event;
434 # For backwards compatibility
435 #$copy->stat_cat_entries($copy->stat_cat_entry_copy_maps);
437 if( $copy->status->id == OILS_COPY_STATUS_CHECKED_OUT ) {
439 $e->search_action_circulation(
441 { target_copy => $copy->id },
443 order_by => { circ => 'xact_start desc' },
455 __PACKAGE__->register_method(
456 method => 'flesh_copy_custom',
457 api_name => 'open-ils.search.asset.copy.fleshed.custom',
461 sub flesh_copy_custom {
462 my( $self, $conn, $copyid, $fields ) = @_;
463 my $e = new_editor();
464 my $copy = $e->retrieve_asset_copy(
474 ) or return $e->event;
479 __PACKAGE__->register_method(
480 method => "biblio_barcode_to_title",
481 api_name => "open-ils.search.biblio.find_by_barcode",
484 sub biblio_barcode_to_title {
485 my( $self, $client, $barcode ) = @_;
487 my $title = $apputils->simple_scalar_request(
489 "open-ils.storage.biblio.record_entry.retrieve_by_barcode", $barcode );
491 return { ids => [ $title->id ], count => 1 } if $title;
492 return { count => 0 };
495 __PACKAGE__->register_method(
496 method => 'title_id_by_item_barcode',
497 api_name => 'open-ils.search.bib_id.by_barcode',
500 desc => 'Retrieve copy object with fleshed record, given the barcode',
502 { desc => 'Item barcode', type => 'string' }
505 desc => 'Asset copy object with fleshed record and callnumber, or event on error or null set'
510 sub title_id_by_item_barcode {
511 my( $self, $conn, $barcode ) = @_;
512 my $e = new_editor();
513 my $copies = $e->search_asset_copy(
515 { deleted => 'f', barcode => $barcode },
519 acp => [ 'call_number' ],
526 return $e->event unless @$copies;
527 return $$copies[0]->call_number->record->id;
531 __PACKAGE__->register_method(
532 method => "biblio_copy_to_mods",
533 api_name => "open-ils.search.biblio.copy.mods.retrieve",
536 # takes a copy object and returns it fleshed mods object
537 sub biblio_copy_to_mods {
538 my( $self, $client, $copy ) = @_;
540 my $volume = $U->cstorereq(
541 "open-ils.cstore.direct.asset.call_number.retrieve",
542 $copy->call_number() );
544 my $mods = _records_to_mods($volume->record());
545 $mods = shift @$mods;
546 $volume->copies([$copy]);
547 push @{$mods->call_numbers()}, $volume;
555 OpenILS::Application::Search::Biblio
561 =head3 open-ils.search.biblio.multiclass.query (arghash, query, docache)
563 For arghash and docache, see B<open-ils.search.biblio.multiclass>.
565 The query argument is a string, but built like a hash with key: value pairs.
566 Recognized search keys include:
568 keyword (kw) - search keyword(s) *
569 author (au) - search author(s) *
570 name (au) - same as author *
571 title (ti) - search title *
572 subject (su) - search subject *
573 series (se) - search series *
574 lang - limit by language (specifiy multiple langs with lang:l1 lang:l2 ...)
575 site - search at specified org unit, corresponds to actor.org_unit.shortname
576 sort - sort type (title, author, pubdate)
577 dir - sort direction (asc, desc)
578 available - if set to anything other than "false" or "0", limits to available items
580 * Searching keyword, author, title, subject, and series supports additional search
581 subclasses, specified with a "|". For example, C<title|proper:gone with the wind>.
583 For more, see B<config.metabib_field>.
587 foreach (qw/open-ils.search.biblio.multiclass.query
588 open-ils.search.biblio.multiclass.query.staff
589 open-ils.search.metabib.multiclass.query
590 open-ils.search.metabib.multiclass.query.staff/)
592 __PACKAGE__->register_method(
594 method => 'multiclass_query',
596 desc => 'Perform a search query. The .staff version of the call includes otherwise hidden hits.',
598 {name => 'arghash', desc => 'Arg hash (see open-ils.search.biblio.multiclass)', type => 'object'},
599 {name => 'query', desc => 'Raw human-readable query (see perldoc '. __PACKAGE__ .')', type => 'string'},
600 {name => 'docache', desc => 'Flag for caching (see open-ils.search.biblio.multiclass)', type => 'object'},
603 desc => 'Search results from query, like: { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
604 type => 'object', # TODO: update as miker's new elements are included
610 sub multiclass_query {
611 my($self, $conn, $arghash, $query, $docache) = @_;
613 $logger->debug("initial search query => $query");
614 my $orig_query = $query;
618 $query =~ s/^\s+//go;
620 # convert convenience classes (e.g. kw for keyword) to the full class name
621 $query =~ s/kw(:|\|)/keyword$1/go;
622 $query =~ s/ti(:|\|)/title$1/go;
623 $query =~ s/au(:|\|)/author$1/go;
624 $query =~ s/su(:|\|)/subject$1/go;
625 $query =~ s/se(:|\|)/series$1/go;
626 $query =~ s/name(:|\|)/author$1/og;
628 $logger->debug("cleansed query string => $query");
631 my $simple_class_re = qr/((?:\w+(?:\|\w+)?):[^:]+?)$/;
632 my $class_list_re = qr/(?:keyword|title|author|subject|series)/;
633 my $modifier_list_re = qr/(?:site|dir|sort|lang|available)/;
636 while ($query =~ s/$simple_class_re//so) {
639 my $where = index($qpart,':');
640 my $type = substr($qpart, 0, $where++);
641 my $value = substr($qpart, $where);
643 if ($type !~ /^(?:$class_list_re|$modifier_list_re)/o) {
644 $tmp_value = "$qpart $tmp_value";
648 if ($type =~ /$class_list_re/o ) {
649 $value .= $tmp_value;
653 next unless $type and $value;
655 $value =~ s/^\s*//og;
656 $value =~ s/\s*$//og;
657 $type = 'sort_dir' if $type eq 'dir';
659 if($type eq 'site') {
660 # 'site' is the org shortname. when using this, we also want
661 # to search at the requested org's depth
662 my $e = new_editor();
663 if(my $org = $e->search_actor_org_unit({shortname => $value})->[0]) {
664 $arghash->{org_unit} = $org->id if $org;
665 $arghash->{depth} = $e->retrieve_actor_org_unit_type($org->ou_type)->depth;
667 $logger->warn("'site:' query used on invalid org shortname: $value ... ignoring");
670 } elsif($type eq 'available') {
672 $arghash->{available} = 1 unless $value eq 'false' or $value eq '0';
674 } elsif($type eq 'lang') {
675 # collect languages into an array of languages
676 $arghash->{language} = [] unless $arghash->{language};
677 push(@{$arghash->{language}}, $value);
679 } elsif($type =~ /^sort/o) {
680 # sort and sort_dir modifiers
681 $arghash->{$type} = $value;
684 # append the search term to the term under construction
685 $search->{$type} = {} unless $search->{$type};
686 $search->{$type}->{term} =
687 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $value" : $value;
691 $query .= " $tmp_value";
692 $query =~ s/\s+/ /go;
693 $query =~ s/^\s+//go;
694 $query =~ s/\s+$//go;
696 my $type = $arghash->{default_class} || 'keyword';
697 $type = ($type eq '-') ? 'keyword' : $type;
698 $type = ($type !~ /^(title|author|keyword|subject|series)(?:\|\w+)?$/o) ? 'keyword' : $type;
701 # This is the front part of the string before any special tokens were
702 # parsed OR colon-separated strings that do not denote a class.
703 # Add this data to the default search class
704 $search->{$type} = {} unless $search->{$type};
705 $search->{$type}->{term} =
706 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $query" : $query;
708 my $real_search = $arghash->{searches} = { $type => { term => $orig_query } };
710 # capture the original limit because the search method alters the limit internally
711 my $ol = $arghash->{limit};
713 my $sclient = OpenSRF::Utils::SettingsClient->new;
715 (my $method = $self->api_name) =~ s/\.query//o;
717 $method =~ s/multiclass/multiclass.staged/
718 if $sclient->config_value(apps => 'open-ils.search',
719 app_settings => 'use_staged_search') =~ /true/i;
721 $arghash->{preferred_language} = $U->get_org_locale($arghash->{org_unit})
722 unless $arghash->{preferred_language};
724 $method = $self->method_lookup($method);
725 my ($data) = $method->run($arghash, $docache);
727 $arghash->{searches} = $search if (!$data->{complex_query});
729 $arghash->{limit} = $ol if $ol;
730 $data->{compiled_search} = $arghash;
731 $data->{query} = $orig_query;
733 $logger->info("compiled search is " . OpenSRF::Utils::JSON->perl2JSON($arghash));
738 __PACKAGE__->register_method(
739 method => 'cat_search_z_style_wrapper',
740 api_name => 'open-ils.search.biblio.zstyle',
742 signature => q/@see open-ils.search.biblio.multiclass/
745 __PACKAGE__->register_method(
746 method => 'cat_search_z_style_wrapper',
747 api_name => 'open-ils.search.biblio.zstyle.staff',
749 signature => q/@see open-ils.search.biblio.multiclass/
752 sub cat_search_z_style_wrapper {
755 my $authtoken = shift;
758 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
760 my $ou = $cstore->request(
761 'open-ils.cstore.direct.actor.org_unit.search',
762 { parent_ou => undef }
765 my $result = { service => 'native-evergreen-catalog', records => [] };
766 my $searchhash = { limit => $$args{limit}, offset => $$args{offset}, org_unit => $ou->id };
768 $$searchhash{searches}{title}{term} = $$args{search}{title} if $$args{search}{title};
769 $$searchhash{searches}{author}{term} = $$args{search}{author} if $$args{search}{author};
770 $$searchhash{searches}{subject}{term} = $$args{search}{subject} if $$args{search}{subject};
771 $$searchhash{searches}{keyword}{term} = $$args{search}{keyword} if $$args{search}{keyword};
773 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{tcn} if $$args{search}{tcn};
774 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{isbn} if $$args{search}{isbn};
775 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{issn} if $$args{search}{issn};
776 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{publisher} if $$args{search}{publisher};
777 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{pubdate} if $$args{search}{pubdate};
778 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{item_type} if $$args{search}{item_type};
780 my $list = the_quest_for_knowledge( $self, $client, $searchhash );
782 if ($list->{count} > 0) {
783 $result->{count} = $list->{count};
785 my $records = $cstore->request(
786 'open-ils.cstore.direct.biblio.record_entry.search.atomic',
787 { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
790 for my $rec ( @$records ) {
792 my $u = OpenILS::Utils::ModsParser->new();
793 $u->start_mods_batch( $rec->marc );
794 my $mods = $u->finish_mods_batch();
796 push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
802 $cstore->disconnect();
806 # ----------------------------------------------------------------------------
807 # These are the main OPAC search methods
808 # ----------------------------------------------------------------------------
810 __PACKAGE__->register_method(
811 method => 'the_quest_for_knowledge',
812 api_name => 'open-ils.search.biblio.multiclass',
814 desc => "Performs a multi class biblio or metabib search",
817 desc => "A search hash with keys: "
818 . "searches, org_unit, depth, limit, offset, format, sort, sort_dir. "
819 . "See perldoc " . __PACKAGE__ . " for more detail",
823 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
828 desc => 'An object of the form: '
829 . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
834 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
836 The search-hash argument can have the following elements:
838 searches: { "$class" : "$value", ...} [REQUIRED]
839 org_unit: The org id to focus the search at
840 depth : The org depth
841 limit : The search limit default: 10
842 offset : The search offset default: 0
843 format : The MARC format
844 sort : What field to sort the results on? [ author | title | pubdate ]
845 sort_dir: What direction do we sort? [ asc | desc ]
847 The searches element is required, must have a hashref value, and the hashref must contain at least one
848 of the following classes as a key:
856 The value paired with a key is the associated search string.
858 The docache argument enables/disables searching and saving results in cache (default OFF).
860 The return object, if successful, will look like:
862 { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
866 __PACKAGE__->register_method(
867 method => 'the_quest_for_knowledge',
868 api_name => 'open-ils.search.biblio.multiclass.staff',
869 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
871 __PACKAGE__->register_method(
872 method => 'the_quest_for_knowledge',
873 api_name => 'open-ils.search.metabib.multiclass',
874 signature => q/@see open-ils.search.biblio.multiclass/
876 __PACKAGE__->register_method(
877 method => 'the_quest_for_knowledge',
878 api_name => 'open-ils.search.metabib.multiclass.staff',
879 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
882 sub the_quest_for_knowledge {
883 my( $self, $conn, $searchhash, $docache ) = @_;
885 return { count => 0 } unless $searchhash and
886 ref $searchhash->{searches} eq 'HASH';
888 my $method = 'open-ils.storage.biblio.multiclass.search_fts';
892 if($self->api_name =~ /metabib/) {
894 $method =~ s/biblio/metabib/o;
897 # do some simple sanity checking
898 if(!$searchhash->{searches} or
899 ( !grep { /^(?:title|author|subject|series|keyword)/ } keys %{$searchhash->{searches}} ) ) {
900 return { count => 0 };
903 my $offset = $searchhash->{offset} || 0; # user value or default in local var now
904 my $limit = $searchhash->{limit} || 10; # user value or default in local var now
905 my $end = $offset + $limit - 1;
908 $searchhash->{offset} = 0; # possible user value overwritten in hash
909 $searchhash->{limit} = $maxlimit; # possible user value overwritten in hash
911 return { count => 0 } if $offset > $maxlimit;
914 push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
915 my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
916 my $ckey = $pfx . md5_hex($method . $s);
918 $logger->info("bib search for: $s");
920 $searchhash->{limit} -= $offset;
924 my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
928 $method .= ".staff" if($self->api_name =~ /staff/);
929 $method .= ".atomic";
931 for (keys %$searchhash) {
932 delete $$searchhash{$_}
933 unless defined $$searchhash{$_};
936 $result = $U->storagereq( $method, %$searchhash );
940 $docache = 0; # results came FROM cache, so we don't write back
943 return {count => 0} unless ($result && $$result[0]);
947 my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
950 # If we didn't get this data from the cache, put it into the cache
951 # then return the correct offset of records
952 $logger->debug("putting search cache $ckey\n");
953 put_cache($ckey, $count, \@recs);
957 # if we have the full set of data, trim out
958 # the requested chunk based on limit and offset
960 for ($offset..$end) {
961 last unless $recs[$_];
967 return { ids => \@recs, count => $count };
971 __PACKAGE__->register_method(
972 method => 'staged_search',
973 api_name => 'open-ils.search.biblio.multiclass.staged',
975 desc => 'Staged search filters out unavailable items. This means that it relies on an estimation strategy for determining ' .
976 'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering. See "estimation_strategy" in your SRF config.',
979 desc => "A search hash with keys: "
980 . "searches, limit, offset. The others are optional, but the 'searches' key/value pair is required, with the value being a hashref. "
981 . "See perldoc " . __PACKAGE__ . " for more detail",
985 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
990 desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids. '
991 . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
996 __PACKAGE__->register_method(
997 method => 'staged_search',
998 api_name => 'open-ils.search.biblio.multiclass.staged.staff',
999 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1001 __PACKAGE__->register_method(
1002 method => 'staged_search',
1003 api_name => 'open-ils.search.metabib.multiclass.staged',
1004 signature => q/@see open-ils.search.biblio.multiclass.staged/
1006 __PACKAGE__->register_method(
1007 method => 'staged_search',
1008 api_name => 'open-ils.search.metabib.multiclass.staged.staff',
1009 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1013 my($self, $conn, $search_hash, $docache) = @_;
1015 my $method = ($self->api_name =~ /metabib/) ?
1016 'open-ils.storage.metabib.multiclass.staged.search_fts':
1017 'open-ils.storage.biblio.multiclass.staged.search_fts';
1019 $method .= '.staff' if $self->api_name =~ /staff$/;
1020 $method .= '.atomic';
1022 return {count => 0} unless (
1024 $search_hash->{searches} and
1025 scalar( keys %{$search_hash->{searches}} ));
1027 my $search_duration;
1028 my $user_offset = $search_hash->{offset} || 0; # user-specified offset
1029 my $user_limit = $search_hash->{limit} || 10;
1030 $user_offset = ($user_offset >= 0) ? $user_offset : 0;
1031 $user_limit = ($user_limit >= 0) ? $user_limit : 10;
1034 # we're grabbing results on a per-superpage basis, which means the
1035 # limit and offset should coincide with superpage boundaries
1036 $search_hash->{offset} = 0;
1037 $search_hash->{limit} = $superpage_size;
1039 # force a well-known check_limit
1040 $search_hash->{check_limit} = $superpage_size;
1041 # restrict total tested to superpage size * number of superpages
1042 $search_hash->{core_limit} = $superpage_size * $max_superpages;
1044 # Set the configured estimation strategy, defaults to 'inclusion'.
1045 my $estimation_strategy = OpenSRF::Utils::SettingsClient
1048 apps => 'open-ils.search', app_settings => 'estimation_strategy'
1050 $search_hash->{estimation_strategy} = $estimation_strategy;
1052 # pull any existing results from the cache
1053 my $key = search_cache_key($method, $search_hash);
1054 my $facet_key = $key.'_facets';
1055 my $cache_data = $cache->get_cache($key) || {};
1057 # keep retrieving results until we find enough to
1058 # fulfill the user-specified limit and offset
1059 my $all_results = [];
1060 my $page; # current superpage
1061 my $est_hit_count = 0;
1062 my $current_page_summary = {};
1063 my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1064 my $is_real_hit_count = 0;
1067 for($page = 0; $page < $max_superpages; $page++) {
1069 my $data = $cache_data->{$page};
1073 $logger->debug("staged search: analyzing superpage $page");
1076 # this window of results is already cached
1077 $logger->debug("staged search: found cached results");
1078 $summary = $data->{summary};
1079 $results = $data->{results};
1082 # retrieve the window of results from the database
1083 $logger->debug("staged search: fetching results from the database");
1084 $search_hash->{skip_check} = $page * $superpage_size;
1086 $results = $U->storagereq($method, %$search_hash);
1087 $search_duration = time - $start;
1088 $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1089 $summary = shift(@$results);
1092 $logger->info("search timed out: duration=$search_duration: params=".
1093 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1094 return {count => 0};
1097 my $hc = $summary->{estimated_hit_count} || $summary->{visible};
1099 $logger->info("search returned 0 results: duration=$search_duration: params=".
1100 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1103 # Create backwards-compatible result structures
1104 if($self->api_name =~ /biblio/) {
1105 $results = [map {[$_->{id}]} @$results];
1107 $results = [map {[$_->{id}, $_->{rel}, $_->{record}]} @$results];
1110 push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1111 $results = [grep {defined $_->[0]} @$results];
1112 cache_staged_search_page($key, $page, $summary, $results) if $docache;
1115 $current_page_summary = $summary;
1117 # add the new set of results to the set under construction
1118 push(@$all_results, @$results);
1120 my $current_count = scalar(@$all_results);
1122 $est_hit_count = $summary->{estimated_hit_count} || $summary->{visible}
1125 $logger->debug("staged search: located $current_count, with estimated hits=".
1126 $summary->{estimated_hit_count}." : visible=".$summary->{visible}.", checked=".$summary->{checked});
1128 if (defined($summary->{estimated_hit_count})) {
1129 foreach (qw/ checked visible excluded deleted /) {
1130 $global_summary->{$_} += $summary->{$_};
1132 $global_summary->{total} = $summary->{total};
1135 # we've found all the possible hits
1136 last if $current_count == $summary->{visible}
1137 and not defined $summary->{estimated_hit_count};
1139 # we've found enough results to satisfy the requested limit/offset
1140 last if $current_count >= ($user_limit + $user_offset);
1142 # we've scanned all possible hits
1143 if($summary->{checked} < $superpage_size) {
1144 $est_hit_count = scalar(@$all_results);
1145 # we have all possible results in hand, so we know the final hit count
1146 $is_real_hit_count = 1;
1151 my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1153 # refine the estimate if we have more than one superpage
1154 if ($page > 0 and not $is_real_hit_count) {
1155 if ($global_summary->{checked} >= $global_summary->{total}) {
1156 $est_hit_count = $global_summary->{visible};
1158 my $updated_hit_count = $U->storagereq(
1159 'open-ils.storage.fts_paging_estimate',
1160 $global_summary->{checked},
1161 $global_summary->{visible},
1162 $global_summary->{excluded},
1163 $global_summary->{deleted},
1164 $global_summary->{total}
1166 $est_hit_count = $updated_hit_count->{$estimation_strategy};
1170 $conn->respond_complete(
1172 count => $est_hit_count,
1173 core_limit => $search_hash->{core_limit},
1174 superpage_size => $search_hash->{check_limit},
1175 superpage_summary => $current_page_summary,
1176 facet_key => $facet_key,
1181 cache_facets($facet_key, $new_ids, ($self->api_name =~ /metabib/) ? 1 : 0) if $docache;
1186 # creates a unique token to represent the query in the cache
1187 sub search_cache_key {
1189 my $search_hash = shift;
1191 for my $key (sort keys %$search_hash) {
1192 push(@sorted, ($key => $$search_hash{$key}))
1193 unless $key eq 'limit' or
1195 $key eq 'skip_check';
1197 my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1198 return $pfx . md5_hex($method . $s);
1201 sub retrieve_cached_facets {
1206 return undef unless ($key and $key =~ /_facets$/);
1208 return $cache->get_cache($key) || {};
1211 __PACKAGE__->register_method(
1212 method => "retrieve_cached_facets",
1213 api_name => "open-ils.search.facet_cache.retrieve"
1218 # add facets for this search to the facet cache
1219 my($key, $results, $metabib) = @_;
1220 my $data = $cache->get_cache($key);
1223 return undef unless (@$results);
1225 # The query we're constructing
1229 # count(distinct mmrsm.appropriate-id-field )
1230 # from metabib.facet_entry mfae
1231 # join config.metabib_field cmf on (mfae.field = cmf.id)
1232 # join metabib.metarecord_sourc_map mmrsm on (mfae.source = mmrsm.source)
1233 # where cmf.facet_field
1234 # and mmrsm.appropriate-id-field in IDLIST
1237 my $count_field = $metabib ? 'metarecord' : 'source';
1238 my $facets = $U->cstorereq( "open-ils.cstore.json_query.atomic",
1241 mfae => [ 'value' ],
1243 transform => 'count',
1245 column => $count_field,
1252 cmf => { field => 'id', fkey => 'field' },
1253 mmrsm => { field => 'source', fkey => 'source' }
1257 '+cmf' => 'facet_field',
1258 '+mmrsm' => { $count_field => $results }
1263 for my $facet (@$facets) {
1264 next unless ($facet->{value});
1265 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1268 $logger->info("facet compilation: cached with key=$key");
1270 $cache->put_cache($key, $data, $cache_timeout);
1273 sub cache_staged_search_page {
1274 # puts this set of results into the cache
1275 my($key, $page, $summary, $results) = @_;
1276 my $data = $cache->get_cache($key);
1279 summary => $summary,
1283 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1284 $summary->{estimated_hit_count}.", visible=".$summary->{visible});
1286 $cache->put_cache($key, $data, $cache_timeout);
1294 my $start = $offset;
1295 my $end = $offset + $limit - 1;
1297 $logger->debug("searching cache for $key : $start..$end\n");
1299 return undef unless $cache;
1300 my $data = $cache->get_cache($key);
1302 return undef unless $data;
1304 my $count = $data->[0];
1307 return undef unless $offset < $count;
1310 for( my $i = $offset; $i <= $end; $i++ ) {
1311 last unless my $d = $$data[$i];
1312 push( @result, $d );
1315 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1322 my( $key, $count, $data ) = @_;
1323 return undef unless $cache;
1324 $logger->debug("search_cache putting ".
1325 scalar(@$data)." items at key $key with timeout $cache_timeout");
1326 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1330 __PACKAGE__->register_method(
1331 method => "biblio_mrid_to_modsbatch_batch",
1332 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1335 sub biblio_mrid_to_modsbatch_batch {
1336 my( $self, $client, $mrids) = @_;
1337 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1339 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1340 for my $id (@$mrids) {
1341 next unless defined $id;
1342 my ($m) = $method->run($id);
1349 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1350 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1352 __PACKAGE__->register_method(
1353 method => "biblio_mrid_to_modsbatch",
1356 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1357 . "As usual, the .staff version of this method will include otherwise hidden records.",
1359 { desc => 'Metarecord ID', type => 'number' },
1360 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1363 desc => 'MVR Object, event on error',
1369 sub biblio_mrid_to_modsbatch {
1370 my( $self, $client, $mrid, $args) = @_;
1372 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1374 my ($mr, $evt) = _grab_metarecord($mrid);
1375 return $evt unless $mr;
1377 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1378 biblio_mrid_make_modsbatch($self, $client, $mr);
1380 return $mvr unless ref($args);
1382 # Here we find the lead record appropriate for the given filters
1383 # and use that for the title and author of the metarecord
1384 my $format = $$args{format};
1385 my $org = $$args{org};
1386 my $depth = $$args{depth};
1388 return $mvr unless $format or $org or $depth;
1390 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1391 $method = "$method.staff" if $self->api_name =~ /staff/o;
1393 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1395 if( my $mods = $U->record_to_mvr($rec) ) {
1397 $mvr->title( $mods->title );
1398 $mvr->author($mods->author);
1399 $logger->debug("mods_slim updating title and ".
1400 "author in mvr with ".$mods->title." : ".$mods->author);
1406 # converts a metarecord to an mvr
1409 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1410 return Fieldmapper::metabib::virtual_record->new($perl);
1413 # checks to see if a metarecord has mods, if so returns true;
1415 __PACKAGE__->register_method(
1416 method => "biblio_mrid_check_mvr",
1417 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1418 notes => "Takes a metarecord ID or a metarecord object and returns true "
1419 . "if the metarecord already has an mvr associated with it."
1422 sub biblio_mrid_check_mvr {
1423 my( $self, $client, $mrid ) = @_;
1427 if(ref($mrid)) { $mr = $mrid; }
1428 else { ($mr, $evt) = _grab_metarecord($mrid); }
1429 return $evt if $evt;
1431 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1433 return _mr_to_mvr($mr) if $mr->mods();
1437 sub _grab_metarecord {
1439 #my $e = OpenILS::Utils::Editor->new;
1440 my $e = new_editor();
1441 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1446 __PACKAGE__->register_method(
1447 method => "biblio_mrid_make_modsbatch",
1448 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1449 notes => "Takes either a metarecord ID or a metarecord object. "
1450 . "Forces the creations of an mvr for the given metarecord. "
1451 . "The created mvr is returned."
1454 sub biblio_mrid_make_modsbatch {
1455 my( $self, $client, $mrid ) = @_;
1457 #my $e = OpenILS::Utils::Editor->new;
1458 my $e = new_editor();
1465 $mr = $e->retrieve_metabib_metarecord($mrid)
1466 or return $e->event;
1469 my $masterid = $mr->master_record;
1470 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1472 my $ids = $U->storagereq(
1473 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1474 return undef unless @$ids;
1476 my $master = $e->retrieve_biblio_record_entry($masterid)
1477 or return $e->event;
1479 # start the mods batch
1480 my $u = OpenILS::Utils::ModsParser->new();
1481 $u->start_mods_batch( $master->marc );
1483 # grab all of the sub-records and shove them into the batch
1484 my @ids = grep { $_ ne $masterid } @$ids;
1485 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1490 my $r = $e->retrieve_biblio_record_entry($i);
1491 push( @$subrecs, $r ) if $r;
1496 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1497 $u->push_mods_batch( $_->marc ) if $_->marc;
1501 # finish up and send to the client
1502 my $mods = $u->finish_mods_batch();
1503 $mods->doc_id($mrid);
1504 $client->respond_complete($mods);
1507 # now update the mods string in the db
1508 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1511 #$e = OpenILS::Utils::Editor->new(xact => 1);
1512 $e = new_editor(xact => 1);
1513 $e->update_metabib_metarecord($mr)
1514 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1521 # converts a mr id into a list of record ids
1523 foreach (qw/open-ils.search.biblio.metarecord_to_records
1524 open-ils.search.biblio.metarecord_to_records.staff/)
1526 __PACKAGE__->register_method(
1527 method => "biblio_mrid_to_record_ids",
1530 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1531 . "As usual, the .staff version of this method will include otherwise hidden records.",
1533 { desc => 'Metarecord ID', type => 'number' },
1534 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1537 desc => 'Results object like {count => $i, ids =>[...]}',
1545 sub biblio_mrid_to_record_ids {
1546 my( $self, $client, $mrid, $args ) = @_;
1548 my $format = $$args{format};
1549 my $org = $$args{org};
1550 my $depth = $$args{depth};
1552 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1553 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1554 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1556 return { count => scalar(@$recs), ids => $recs };
1560 __PACKAGE__->register_method(
1561 method => "biblio_record_to_marc_html",
1562 api_name => "open-ils.search.biblio.record.html"
1565 __PACKAGE__->register_method(
1566 method => "biblio_record_to_marc_html",
1567 api_name => "open-ils.search.authority.to_html"
1570 # Persistent parsers and setting objects
1571 my $parser = XML::LibXML->new();
1572 my $xslt = XML::LibXSLT->new();
1574 my $slim_marc_sheet;
1575 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1577 sub biblio_record_to_marc_html {
1578 my($self, $client, $recordid, $slim, $marcxml) = @_;
1581 my $dir = $settings_client->config_value("dirs", "xsl");
1584 unless($slim_marc_sheet) {
1585 my $xsl = $settings_client->config_value(
1586 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1588 $xsl = $parser->parse_file("$dir/$xsl");
1589 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1592 $sheet = $slim_marc_sheet;
1596 unless($marc_sheet) {
1597 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1598 my $xsl = $settings_client->config_value(
1599 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1600 $xsl = $parser->parse_file("$dir/$xsl");
1601 $marc_sheet = $xslt->parse_stylesheet($xsl);
1603 $sheet = $marc_sheet;
1608 my $e = new_editor();
1609 if($self->api_name =~ /authority/) {
1610 $record = $e->retrieve_authority_record_entry($recordid)
1611 or return $e->event;
1613 $record = $e->retrieve_biblio_record_entry($recordid)
1614 or return $e->event;
1616 $marcxml = $record->marc;
1619 my $xmldoc = $parser->parse_string($marcxml);
1620 my $html = $sheet->transform($xmldoc);
1621 return $html->documentElement->toString();
1626 __PACKAGE__->register_method(
1627 method => "retrieve_all_copy_statuses",
1628 api_name => "open-ils.search.config.copy_status.retrieve.all"
1631 sub retrieve_all_copy_statuses {
1632 my( $self, $client ) = @_;
1633 return new_editor()->retrieve_all_config_copy_status();
1637 __PACKAGE__->register_method(
1638 method => "copy_counts_per_org",
1639 api_name => "open-ils.search.biblio.copy_counts.retrieve"
1642 __PACKAGE__->register_method(
1643 method => "copy_counts_per_org",
1644 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1647 sub copy_counts_per_org {
1648 my( $self, $client, $record_id ) = @_;
1650 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1652 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1653 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1655 my $counts = $apputils->simple_scalar_request(
1656 "open-ils.storage", $method, $record_id );
1658 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1663 __PACKAGE__->register_method(
1664 method => "copy_count_summary",
1665 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
1666 notes => "returns an array of these: "
1667 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1668 . "where statusx is a copy status name. The statuses are sorted by ID.",
1672 sub copy_count_summary {
1673 my( $self, $client, $rid, $org, $depth ) = @_;
1676 my $data = $U->storagereq(
1677 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
1679 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1682 __PACKAGE__->register_method(
1683 method => "copy_location_count_summary",
1684 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
1685 notes => "returns an array of these: "
1686 . "[ org_id, callnumber_label, copy_location, <status1_count>, <status2_count>,...] "
1687 . "where statusx is a copy status name. The statuses are sorted by ID.",
1690 sub copy_location_count_summary {
1691 my( $self, $client, $rid, $org, $depth ) = @_;
1694 my $data = $U->storagereq(
1695 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1697 return [ sort { $a->[1] cmp $b->[1] || $a->[2] cmp $b->[2] } @$data ];
1700 __PACKAGE__->register_method(
1701 method => "copy_count_location_summary",
1702 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
1703 notes => "returns an array of these: "
1704 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1705 . "where statusx is a copy status name. The statuses are sorted by ID."
1708 sub copy_count_location_summary {
1709 my( $self, $client, $rid, $org, $depth ) = @_;
1712 my $data = $U->storagereq(
1713 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1714 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1718 foreach (qw/open-ils.search.biblio.marc
1719 open-ils.search.biblio.marc.staff/)
1721 __PACKAGE__->register_method(
1722 method => "marc_search",
1725 desc => 'Fetch biblio IDs based on MARC record criteria. '
1726 . 'As usual, the .staff version of the search includes otherwise hidden records',
1729 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
1730 'See perldoc ' . __PACKAGE__ . ' for more detail.',
1733 {desc => 'limit (optional)', type => 'number'},
1734 {desc => 'offset (optional)', type => 'number'}
1737 desc => 'Results object like: { "count": $i, "ids": [...] }',
1744 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
1746 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
1748 searches: complex query object (required)
1749 org_unit: The org ID to focus the search at
1750 depth : The org depth
1751 limit : integer search limit default: 10
1752 offset : integer search offset default: 0
1753 sort : What field to sort the results on? [ author | title | pubdate ]
1754 sort_dir: In what direction do we sort? [ asc | desc ]
1756 Additional keys to refine search criteria:
1759 language : Language (code)
1760 lit_form : Literary form
1761 item_form: Item form
1762 item_type: Item type
1763 format : The MARC format
1765 Please note that the specific strings to be used in the "addtional keys" will be entirely
1766 dependent on your loaded data.
1768 All keys except "searches" are optional.
1769 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
1771 For example, an arg hash might look like:
1793 The arghash is eventually passed to the SRF call:
1794 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
1796 Presently, search uses the cache unconditionally.
1800 # FIXME: that example above isn't actually tested.
1801 # TODO: docache option?
1803 my( $self, $conn, $args, $limit, $offset ) = @_;
1805 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
1806 $method .= ".staff" if $self->api_name =~ /staff/;
1807 $method .= ".atomic";
1809 $limit ||= 10; # FIXME: what about $args->{limit} ?
1810 $offset ||= 0; # FIXME: what about $args->{offset} ?
1813 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
1814 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
1816 my $recs = search_cache($ckey, $offset, $limit);
1819 $recs = $U->storagereq($method, %$args) || [];
1821 put_cache($ckey, scalar(@$recs), $recs);
1822 $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
1829 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
1830 my @recs = map { $_->[0] } @$recs;
1832 return { ids => \@recs, count => $count };
1836 __PACKAGE__->register_method(
1837 method => "biblio_search_isbn",
1838 api_name => "open-ils.search.biblio.isbn",
1840 desc => 'Retrieve biblio IDs for a given ISBN',
1842 {desc => 'ISBN', type => 'string'} # or number maybe? How normalized is our storage data?
1845 desc => 'Results object like: { "count": $i, "ids": [...] }',
1851 sub biblio_search_isbn {
1852 my( $self, $client, $isbn ) = @_;
1853 $logger->debug("Searching ISBN $isbn");
1854 my $recs = $U->storagereq('open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn);
1855 return { ids => $recs, count => scalar(@$recs) };
1858 __PACKAGE__->register_method(
1859 method => "biblio_search_isbn_batch",
1860 api_name => "open-ils.search.biblio.isbn_list",
1863 sub biblio_search_isbn_batch {
1864 my( $self, $client, $isbn_list ) = @_;
1865 $logger->debug("Searching ISBNs @$isbn_list");
1866 my @recs = (); my %rec_set = ();
1867 foreach my $isbn ( @$isbn_list ) {
1868 foreach my $rec ( @{ $U->storagereq(
1869 'open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn )
1871 if (! $rec_set{ $rec }) {
1872 $rec_set{ $rec } = 1;
1877 return { ids => \@recs, count => scalar(@recs) };
1880 __PACKAGE__->register_method(
1881 method => "biblio_search_issn",
1882 api_name => "open-ils.search.biblio.issn",
1884 desc => 'Retrieve biblio IDs for a given ISSN',
1886 {desc => 'ISBN', type => 'string'}
1889 desc => 'Results object like: { "count": $i, "ids": [...] }',
1895 sub biblio_search_issn {
1896 my( $self, $client, $issn ) = @_;
1897 $logger->debug("Searching ISSN $issn");
1898 my $e = new_editor();
1900 my $recs = $U->storagereq(
1901 'open-ils.storage.id_list.biblio.record_entry.search.issn.atomic', $issn );
1902 return { ids => $recs, count => scalar(@$recs) };
1906 __PACKAGE__->register_method(
1907 method => "fetch_mods_by_copy",
1908 api_name => "open-ils.search.biblio.mods_from_copy",
1911 desc => 'Retrieve MODS record given an attached copy ID',
1913 { desc => 'Copy ID', type => 'number' }
1916 desc => 'MODS record, event on error or uncataloged item'
1921 sub fetch_mods_by_copy {
1922 my( $self, $client, $copyid ) = @_;
1923 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
1924 return $evt if $evt;
1925 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
1926 return $apputils->record_to_mvr($record);
1930 # -------------------------------------------------------------------------------------
1932 __PACKAGE__->register_method(
1933 method => "cn_browse",
1934 api_name => "open-ils.search.callnumber.browse.target",
1935 notes => "Starts a callnumber browse"
1938 __PACKAGE__->register_method(
1939 method => "cn_browse",
1940 api_name => "open-ils.search.callnumber.browse.page_up",
1941 notes => "Returns the previous page of callnumbers",
1944 __PACKAGE__->register_method(
1945 method => "cn_browse",
1946 api_name => "open-ils.search.callnumber.browse.page_down",
1947 notes => "Returns the next page of callnumbers",
1951 # RETURNS array of arrays like so: label, owning_lib, record, id
1953 my( $self, $client, @params ) = @_;
1956 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
1957 if( $self->api_name =~ /target/ );
1958 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
1959 if( $self->api_name =~ /page_up/ );
1960 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
1961 if( $self->api_name =~ /page_down/ );
1963 return $apputils->simplereq( 'open-ils.storage', $method, @params );
1965 # -------------------------------------------------------------------------------------
1967 __PACKAGE__->register_method(
1968 method => "fetch_cn",
1969 api_name => "open-ils.search.callnumber.retrieve",
1971 notes => "retrieves a callnumber based on ID",
1975 my( $self, $client, $id ) = @_;
1976 my( $cn, $evt ) = $apputils->fetch_callnumber( $id );
1977 return $evt if $evt;
1981 __PACKAGE__->register_method(
1982 method => "fetch_copy_by_cn",
1983 api_name => 'open-ils.search.copies_by_call_number.retrieve',
1985 Returns an array of copy ID's by callnumber ID
1986 @param cnid The callnumber ID
1987 @return An array of copy IDs
1991 sub fetch_copy_by_cn {
1992 my( $self, $conn, $cnid ) = @_;
1993 return $U->cstorereq(
1994 'open-ils.cstore.direct.asset.copy.id_list.atomic',
1995 { call_number => $cnid, deleted => 'f' } );
1998 __PACKAGE__->register_method(
1999 method => 'fetch_cn_by_info',
2000 api_name => 'open-ils.search.call_number.retrieve_by_info',
2002 @param label The callnumber label
2003 @param record The record the cn is attached to
2004 @param org The owning library of the cn
2005 @return The callnumber object
2010 sub fetch_cn_by_info {
2011 my( $self, $conn, $label, $record, $org ) = @_;
2012 return $U->cstorereq(
2013 'open-ils.cstore.direct.asset.call_number.search',
2014 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2019 __PACKAGE__->register_method(
2020 method => 'bib_extras',
2021 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all'
2023 __PACKAGE__->register_method(
2024 method => 'bib_extras',
2025 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all'
2027 __PACKAGE__->register_method(
2028 method => 'bib_extras',
2029 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all'
2031 __PACKAGE__->register_method(
2032 method => 'bib_extras',
2033 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all'
2035 __PACKAGE__->register_method(
2036 method => 'bib_extras',
2037 api_name => 'open-ils.search.biblio.audience_map.retrieve.all'
2043 my $e = new_editor();
2045 return $e->retrieve_all_config_lit_form_map()
2046 if( $self->api_name =~ /lit_form/ );
2048 return $e->retrieve_all_config_item_form_map()
2049 if( $self->api_name =~ /item_form_map/ );
2051 return $e->retrieve_all_config_item_type_map()
2052 if( $self->api_name =~ /item_type_map/ );
2054 return $e->retrieve_all_config_bib_level_map()
2055 if( $self->api_name =~ /bib_level_map/ );
2057 return $e->retrieve_all_config_audience_map()
2058 if( $self->api_name =~ /audience_map/ );
2065 __PACKAGE__->register_method(
2066 method => 'fetch_slim_record',
2067 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2069 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2071 { desc => 'Array of Record IDs', type => 'array' }
2074 desc => 'Array of biblio records, event on error'
2079 sub fetch_slim_record {
2080 my( $self, $conn, $ids ) = @_;
2082 #my $editor = OpenILS::Utils::Editor->new;
2083 my $editor = new_editor();
2086 return $editor->event unless
2087 my $r = $editor->retrieve_biblio_record_entry($_);
2096 __PACKAGE__->register_method(
2097 method => 'rec_to_mr_rec_descriptors',
2098 api_name => 'open-ils.search.metabib.record_to_descriptors',
2100 specialized method...
2101 Given a biblio record id or a metarecord id,
2102 this returns a list of metabib.record_descriptor
2103 objects that live within the same metarecord
2104 @param args Object of args including:
2108 sub rec_to_mr_rec_descriptors {
2109 my( $self, $conn, $args ) = @_;
2111 my $rec = $$args{record};
2112 my $mrec = $$args{metarecord};
2113 my $item_forms = $$args{item_forms};
2114 my $item_types = $$args{item_types};
2115 my $item_lang = $$args{item_lang};
2117 my $e = new_editor();
2121 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2122 return $e->event unless @$map;
2123 $mrec = $$map[0]->metarecord;
2126 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2127 return $e->event unless @$recs;
2129 my @recs = map { $_->source } @$recs;
2130 my $search = { record => \@recs };
2131 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2132 $search->{item_type} = $item_types if $item_types and @$item_types;
2133 $search->{item_lang} = $item_lang if $item_lang;
2135 my $desc = $e->search_metabib_record_descriptor($search);
2137 return { metarecord => $mrec, descriptors => $desc };
2141 __PACKAGE__->register_method(
2142 method => 'fetch_age_protect',
2143 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2146 sub fetch_age_protect {
2147 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2151 __PACKAGE__->register_method(
2152 method => 'copies_by_cn_label',
2153 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2156 __PACKAGE__->register_method(
2157 method => 'copies_by_cn_label',
2158 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2161 sub copies_by_cn_label {
2162 my( $self, $conn, $record, $label, $circ_lib ) = @_;
2163 my $e = new_editor();
2164 my $cns = $e->search_asset_call_number({record => $record, label => $label, deleted => 'f'}, {idlist=>1});
2165 return [] unless @$cns;
2167 # show all non-deleted copies in the staff client ...
2168 if ($self->api_name =~ /staff$/o) {
2169 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2172 # ... otherwise, grab the copies ...
2173 my $copies = $e->search_asset_copy(
2174 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2175 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2179 # ... and test for location and status visibility
2180 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];