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) if $docache;
1185 # creates a unique token to represent the query in the cache
1186 sub search_cache_key {
1188 my $search_hash = shift;
1190 for my $key (sort keys %$search_hash) {
1191 push(@sorted, ($key => $$search_hash{$key}))
1192 unless $key eq 'limit' or
1194 $key eq 'skip_check';
1196 my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1197 return $pfx . md5_hex($method . $s);
1200 sub retrieve_cached_facets {
1205 return undef unless ($key and $key =~ /_facets$/);
1207 return $cache->get_cache($key) || {};
1210 __PACKAGE__->register_method(
1211 method => "retrieve_cached_facets",
1212 api_name => "open-ils.search.facet_cache.retrieve"
1217 # add facets for this search to the facet cache
1218 my($key, $results) = @_;
1219 my $data = $cache->get_cache($key);
1222 return undef unless (@$results);
1224 # The query we're constructing
1228 # count(distinct mfae.source)
1229 # from metabib.facet_entry mfae
1230 # join config.metabib_field cmf on (mfae.field = cmf.id)
1231 # where cmf.facet_field
1232 # and mfae.source in IDLIST
1235 my $facets = $U->cstorereq( "open-ils.cstore.json_query.atomic",
1241 transform => 'count',
1249 from => { mfae => 'cmf' },
1250 where => { '+cmf' => 'facet_field', '+mfae' => { source => $results } }
1254 for my $facet (@$facets) {
1255 next unless ($facet->{value});
1256 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1259 $logger->info("facet compilation: cached with key=$key");
1261 $cache->put_cache($key, $data, $cache_timeout);
1264 sub cache_staged_search_page {
1265 # puts this set of results into the cache
1266 my($key, $page, $summary, $results) = @_;
1267 my $data = $cache->get_cache($key);
1270 summary => $summary,
1274 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1275 $summary->{estimated_hit_count}.", visible=".$summary->{visible});
1277 $cache->put_cache($key, $data, $cache_timeout);
1285 my $start = $offset;
1286 my $end = $offset + $limit - 1;
1288 $logger->debug("searching cache for $key : $start..$end\n");
1290 return undef unless $cache;
1291 my $data = $cache->get_cache($key);
1293 return undef unless $data;
1295 my $count = $data->[0];
1298 return undef unless $offset < $count;
1301 for( my $i = $offset; $i <= $end; $i++ ) {
1302 last unless my $d = $$data[$i];
1303 push( @result, $d );
1306 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1313 my( $key, $count, $data ) = @_;
1314 return undef unless $cache;
1315 $logger->debug("search_cache putting ".
1316 scalar(@$data)." items at key $key with timeout $cache_timeout");
1317 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1321 __PACKAGE__->register_method(
1322 method => "biblio_mrid_to_modsbatch_batch",
1323 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1326 sub biblio_mrid_to_modsbatch_batch {
1327 my( $self, $client, $mrids) = @_;
1328 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1330 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1331 for my $id (@$mrids) {
1332 next unless defined $id;
1333 my ($m) = $method->run($id);
1340 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1341 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1343 __PACKAGE__->register_method(
1344 method => "biblio_mrid_to_modsbatch",
1347 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1348 . "As usual, the .staff version of this method will include otherwise hidden records.",
1350 { desc => 'Metarecord ID', type => 'number' },
1351 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1354 desc => 'MVR Object, event on error',
1360 sub biblio_mrid_to_modsbatch {
1361 my( $self, $client, $mrid, $args) = @_;
1363 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1365 my ($mr, $evt) = _grab_metarecord($mrid);
1366 return $evt unless $mr;
1368 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1369 biblio_mrid_make_modsbatch($self, $client, $mr);
1371 return $mvr unless ref($args);
1373 # Here we find the lead record appropriate for the given filters
1374 # and use that for the title and author of the metarecord
1375 my $format = $$args{format};
1376 my $org = $$args{org};
1377 my $depth = $$args{depth};
1379 return $mvr unless $format or $org or $depth;
1381 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1382 $method = "$method.staff" if $self->api_name =~ /staff/o;
1384 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1386 if( my $mods = $U->record_to_mvr($rec) ) {
1388 $mvr->title( $mods->title );
1389 $mvr->author($mods->author);
1390 $logger->debug("mods_slim updating title and ".
1391 "author in mvr with ".$mods->title." : ".$mods->author);
1397 # converts a metarecord to an mvr
1400 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1401 return Fieldmapper::metabib::virtual_record->new($perl);
1404 # checks to see if a metarecord has mods, if so returns true;
1406 __PACKAGE__->register_method(
1407 method => "biblio_mrid_check_mvr",
1408 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1409 notes => "Takes a metarecord ID or a metarecord object and returns true "
1410 . "if the metarecord already has an mvr associated with it."
1413 sub biblio_mrid_check_mvr {
1414 my( $self, $client, $mrid ) = @_;
1418 if(ref($mrid)) { $mr = $mrid; }
1419 else { ($mr, $evt) = _grab_metarecord($mrid); }
1420 return $evt if $evt;
1422 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1424 return _mr_to_mvr($mr) if $mr->mods();
1428 sub _grab_metarecord {
1430 #my $e = OpenILS::Utils::Editor->new;
1431 my $e = new_editor();
1432 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1437 __PACKAGE__->register_method(
1438 method => "biblio_mrid_make_modsbatch",
1439 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1440 notes => "Takes either a metarecord ID or a metarecord object. "
1441 . "Forces the creations of an mvr for the given metarecord. "
1442 . "The created mvr is returned."
1445 sub biblio_mrid_make_modsbatch {
1446 my( $self, $client, $mrid ) = @_;
1448 #my $e = OpenILS::Utils::Editor->new;
1449 my $e = new_editor();
1456 $mr = $e->retrieve_metabib_metarecord($mrid)
1457 or return $e->event;
1460 my $masterid = $mr->master_record;
1461 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1463 my $ids = $U->storagereq(
1464 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1465 return undef unless @$ids;
1467 my $master = $e->retrieve_biblio_record_entry($masterid)
1468 or return $e->event;
1470 # start the mods batch
1471 my $u = OpenILS::Utils::ModsParser->new();
1472 $u->start_mods_batch( $master->marc );
1474 # grab all of the sub-records and shove them into the batch
1475 my @ids = grep { $_ ne $masterid } @$ids;
1476 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1481 my $r = $e->retrieve_biblio_record_entry($i);
1482 push( @$subrecs, $r ) if $r;
1487 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1488 $u->push_mods_batch( $_->marc ) if $_->marc;
1492 # finish up and send to the client
1493 my $mods = $u->finish_mods_batch();
1494 $mods->doc_id($mrid);
1495 $client->respond_complete($mods);
1498 # now update the mods string in the db
1499 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1502 #$e = OpenILS::Utils::Editor->new(xact => 1);
1503 $e = new_editor(xact => 1);
1504 $e->update_metabib_metarecord($mr)
1505 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1512 # converts a mr id into a list of record ids
1514 foreach (qw/open-ils.search.biblio.metarecord_to_records
1515 open-ils.search.biblio.metarecord_to_records.staff/)
1517 __PACKAGE__->register_method(
1518 method => "biblio_mrid_to_record_ids",
1521 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1522 . "As usual, the .staff version of this method will include otherwise hidden records.",
1524 { desc => 'Metarecord ID', type => 'number' },
1525 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1528 desc => 'Results object like {count => $i, ids =>[...]}',
1536 sub biblio_mrid_to_record_ids {
1537 my( $self, $client, $mrid, $args ) = @_;
1539 my $format = $$args{format};
1540 my $org = $$args{org};
1541 my $depth = $$args{depth};
1543 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1544 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1545 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1547 return { count => scalar(@$recs), ids => $recs };
1551 __PACKAGE__->register_method(
1552 method => "biblio_record_to_marc_html",
1553 api_name => "open-ils.search.biblio.record.html"
1556 __PACKAGE__->register_method(
1557 method => "biblio_record_to_marc_html",
1558 api_name => "open-ils.search.authority.to_html"
1561 # Persistent parsers and setting objects
1562 my $parser = XML::LibXML->new();
1563 my $xslt = XML::LibXSLT->new();
1565 my $slim_marc_sheet;
1566 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1568 sub biblio_record_to_marc_html {
1569 my($self, $client, $recordid, $slim, $marcxml) = @_;
1572 my $dir = $settings_client->config_value("dirs", "xsl");
1575 unless($slim_marc_sheet) {
1576 my $xsl = $settings_client->config_value(
1577 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1579 $xsl = $parser->parse_file("$dir/$xsl");
1580 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1583 $sheet = $slim_marc_sheet;
1587 unless($marc_sheet) {
1588 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1589 my $xsl = $settings_client->config_value(
1590 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1591 $xsl = $parser->parse_file("$dir/$xsl");
1592 $marc_sheet = $xslt->parse_stylesheet($xsl);
1594 $sheet = $marc_sheet;
1599 my $e = new_editor();
1600 if($self->api_name =~ /authority/) {
1601 $record = $e->retrieve_authority_record_entry($recordid)
1602 or return $e->event;
1604 $record = $e->retrieve_biblio_record_entry($recordid)
1605 or return $e->event;
1607 $marcxml = $record->marc;
1610 my $xmldoc = $parser->parse_string($marcxml);
1611 my $html = $sheet->transform($xmldoc);
1612 return $html->documentElement->toString();
1617 __PACKAGE__->register_method(
1618 method => "retrieve_all_copy_statuses",
1619 api_name => "open-ils.search.config.copy_status.retrieve.all"
1622 sub retrieve_all_copy_statuses {
1623 my( $self, $client ) = @_;
1624 return new_editor()->retrieve_all_config_copy_status();
1628 __PACKAGE__->register_method(
1629 method => "copy_counts_per_org",
1630 api_name => "open-ils.search.biblio.copy_counts.retrieve"
1633 __PACKAGE__->register_method(
1634 method => "copy_counts_per_org",
1635 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1638 sub copy_counts_per_org {
1639 my( $self, $client, $record_id ) = @_;
1641 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1643 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1644 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1646 my $counts = $apputils->simple_scalar_request(
1647 "open-ils.storage", $method, $record_id );
1649 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1654 __PACKAGE__->register_method(
1655 method => "copy_count_summary",
1656 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
1657 notes => "returns an array of these: "
1658 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1659 . "where statusx is a copy status name. The statuses are sorted by ID.",
1663 sub copy_count_summary {
1664 my( $self, $client, $rid, $org, $depth ) = @_;
1667 my $data = $U->storagereq(
1668 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
1670 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1673 __PACKAGE__->register_method(
1674 method => "copy_location_count_summary",
1675 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
1676 notes => "returns an array of these: "
1677 . "[ org_id, callnumber_label, copy_location, <status1_count>, <status2_count>,...] "
1678 . "where statusx is a copy status name. The statuses are sorted by ID.",
1681 sub copy_location_count_summary {
1682 my( $self, $client, $rid, $org, $depth ) = @_;
1685 my $data = $U->storagereq(
1686 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1688 return [ sort { $a->[1] cmp $b->[1] || $a->[2] cmp $b->[2] } @$data ];
1691 __PACKAGE__->register_method(
1692 method => "copy_count_location_summary",
1693 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
1694 notes => "returns an array of these: "
1695 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1696 . "where statusx is a copy status name. The statuses are sorted by ID."
1699 sub copy_count_location_summary {
1700 my( $self, $client, $rid, $org, $depth ) = @_;
1703 my $data = $U->storagereq(
1704 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1705 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1709 foreach (qw/open-ils.search.biblio.marc
1710 open-ils.search.biblio.marc.staff/)
1712 __PACKAGE__->register_method(
1713 method => "marc_search",
1716 desc => 'Fetch biblio IDs based on MARC record criteria. '
1717 . 'As usual, the .staff version of the search includes otherwise hidden records',
1720 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
1721 'See perldoc ' . __PACKAGE__ . ' for more detail.',
1724 {desc => 'limit (optional)', type => 'number'},
1725 {desc => 'offset (optional)', type => 'number'}
1728 desc => 'Results object like: { "count": $i, "ids": [...] }',
1735 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
1737 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
1739 searches: complex query object (required)
1740 org_unit: The org ID to focus the search at
1741 depth : The org depth
1742 limit : integer search limit default: 10
1743 offset : integer search offset default: 0
1744 sort : What field to sort the results on? [ author | title | pubdate ]
1745 sort_dir: In what direction do we sort? [ asc | desc ]
1747 Additional keys to refine search criteria:
1750 language : Language (code)
1751 lit_form : Literary form
1752 item_form: Item form
1753 item_type: Item type
1754 format : The MARC format
1756 Please note that the specific strings to be used in the "addtional keys" will be entirely
1757 dependent on your loaded data.
1759 All keys except "searches" are optional.
1760 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
1762 For example, an arg hash might look like:
1784 The arghash is eventually passed to the SRF call:
1785 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
1787 Presently, search uses the cache unconditionally.
1791 # FIXME: that example above isn't actually tested.
1792 # TODO: docache option?
1794 my( $self, $conn, $args, $limit, $offset ) = @_;
1796 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
1797 $method .= ".staff" if $self->api_name =~ /staff/;
1798 $method .= ".atomic";
1800 $limit ||= 10; # FIXME: what about $args->{limit} ?
1801 $offset ||= 0; # FIXME: what about $args->{offset} ?
1804 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
1805 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
1807 my $recs = search_cache($ckey, $offset, $limit);
1810 $recs = $U->storagereq($method, %$args) || [];
1812 put_cache($ckey, scalar(@$recs), $recs);
1813 $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
1820 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
1821 my @recs = map { $_->[0] } @$recs;
1823 return { ids => \@recs, count => $count };
1827 __PACKAGE__->register_method(
1828 method => "biblio_search_isbn",
1829 api_name => "open-ils.search.biblio.isbn",
1831 desc => 'Retrieve biblio IDs for a given ISBN',
1833 {desc => 'ISBN', type => 'string'} # or number maybe? How normalized is our storage data?
1836 desc => 'Results object like: { "count": $i, "ids": [...] }',
1842 sub biblio_search_isbn {
1843 my( $self, $client, $isbn ) = @_;
1844 $logger->debug("Searching ISBN $isbn");
1845 my $recs = $U->storagereq('open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn);
1846 return { ids => $recs, count => scalar(@$recs) };
1849 __PACKAGE__->register_method(
1850 method => "biblio_search_isbn_batch",
1851 api_name => "open-ils.search.biblio.isbn_list",
1854 sub biblio_search_isbn_batch {
1855 my( $self, $client, $isbn_list ) = @_;
1856 $logger->debug("Searching ISBNs @$isbn_list");
1857 my @recs = (); my %rec_set = ();
1858 foreach my $isbn ( @$isbn_list ) {
1859 foreach my $rec ( @{ $U->storagereq(
1860 'open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn )
1862 if (! $rec_set{ $rec }) {
1863 $rec_set{ $rec } = 1;
1868 return { ids => \@recs, count => scalar(@recs) };
1871 __PACKAGE__->register_method(
1872 method => "biblio_search_issn",
1873 api_name => "open-ils.search.biblio.issn",
1875 desc => 'Retrieve biblio IDs for a given ISSN',
1877 {desc => 'ISBN', type => 'string'}
1880 desc => 'Results object like: { "count": $i, "ids": [...] }',
1886 sub biblio_search_issn {
1887 my( $self, $client, $issn ) = @_;
1888 $logger->debug("Searching ISSN $issn");
1889 my $e = new_editor();
1891 my $recs = $U->storagereq(
1892 'open-ils.storage.id_list.biblio.record_entry.search.issn.atomic', $issn );
1893 return { ids => $recs, count => scalar(@$recs) };
1897 __PACKAGE__->register_method(
1898 method => "fetch_mods_by_copy",
1899 api_name => "open-ils.search.biblio.mods_from_copy",
1902 desc => 'Retrieve MODS record given an attached copy ID',
1904 { desc => 'Copy ID', type => 'number' }
1907 desc => 'MODS record, event on error or uncataloged item'
1912 sub fetch_mods_by_copy {
1913 my( $self, $client, $copyid ) = @_;
1914 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
1915 return $evt if $evt;
1916 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
1917 return $apputils->record_to_mvr($record);
1921 # -------------------------------------------------------------------------------------
1923 __PACKAGE__->register_method(
1924 method => "cn_browse",
1925 api_name => "open-ils.search.callnumber.browse.target",
1926 notes => "Starts a callnumber browse"
1929 __PACKAGE__->register_method(
1930 method => "cn_browse",
1931 api_name => "open-ils.search.callnumber.browse.page_up",
1932 notes => "Returns the previous page of callnumbers",
1935 __PACKAGE__->register_method(
1936 method => "cn_browse",
1937 api_name => "open-ils.search.callnumber.browse.page_down",
1938 notes => "Returns the next page of callnumbers",
1942 # RETURNS array of arrays like so: label, owning_lib, record, id
1944 my( $self, $client, @params ) = @_;
1947 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
1948 if( $self->api_name =~ /target/ );
1949 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
1950 if( $self->api_name =~ /page_up/ );
1951 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
1952 if( $self->api_name =~ /page_down/ );
1954 return $apputils->simplereq( 'open-ils.storage', $method, @params );
1956 # -------------------------------------------------------------------------------------
1958 __PACKAGE__->register_method(
1959 method => "fetch_cn",
1960 api_name => "open-ils.search.callnumber.retrieve",
1962 notes => "retrieves a callnumber based on ID",
1966 my( $self, $client, $id ) = @_;
1967 my( $cn, $evt ) = $apputils->fetch_callnumber( $id );
1968 return $evt if $evt;
1972 __PACKAGE__->register_method(
1973 method => "fetch_copy_by_cn",
1974 api_name => 'open-ils.search.copies_by_call_number.retrieve',
1976 Returns an array of copy ID's by callnumber ID
1977 @param cnid The callnumber ID
1978 @return An array of copy IDs
1982 sub fetch_copy_by_cn {
1983 my( $self, $conn, $cnid ) = @_;
1984 return $U->cstorereq(
1985 'open-ils.cstore.direct.asset.copy.id_list.atomic',
1986 { call_number => $cnid, deleted => 'f' } );
1989 __PACKAGE__->register_method(
1990 method => 'fetch_cn_by_info',
1991 api_name => 'open-ils.search.call_number.retrieve_by_info',
1993 @param label The callnumber label
1994 @param record The record the cn is attached to
1995 @param org The owning library of the cn
1996 @return The callnumber object
2001 sub fetch_cn_by_info {
2002 my( $self, $conn, $label, $record, $org ) = @_;
2003 return $U->cstorereq(
2004 'open-ils.cstore.direct.asset.call_number.search',
2005 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2010 __PACKAGE__->register_method(
2011 method => 'bib_extras',
2012 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all'
2014 __PACKAGE__->register_method(
2015 method => 'bib_extras',
2016 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all'
2018 __PACKAGE__->register_method(
2019 method => 'bib_extras',
2020 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all'
2022 __PACKAGE__->register_method(
2023 method => 'bib_extras',
2024 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all'
2026 __PACKAGE__->register_method(
2027 method => 'bib_extras',
2028 api_name => 'open-ils.search.biblio.audience_map.retrieve.all'
2034 my $e = new_editor();
2036 return $e->retrieve_all_config_lit_form_map()
2037 if( $self->api_name =~ /lit_form/ );
2039 return $e->retrieve_all_config_item_form_map()
2040 if( $self->api_name =~ /item_form_map/ );
2042 return $e->retrieve_all_config_item_type_map()
2043 if( $self->api_name =~ /item_type_map/ );
2045 return $e->retrieve_all_config_bib_level_map()
2046 if( $self->api_name =~ /bib_level_map/ );
2048 return $e->retrieve_all_config_audience_map()
2049 if( $self->api_name =~ /audience_map/ );
2056 __PACKAGE__->register_method(
2057 method => 'fetch_slim_record',
2058 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2060 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2062 { desc => 'Array of Record IDs', type => 'array' }
2065 desc => 'Array of biblio records, event on error'
2070 sub fetch_slim_record {
2071 my( $self, $conn, $ids ) = @_;
2073 #my $editor = OpenILS::Utils::Editor->new;
2074 my $editor = new_editor();
2077 return $editor->event unless
2078 my $r = $editor->retrieve_biblio_record_entry($_);
2087 __PACKAGE__->register_method(
2088 method => 'rec_to_mr_rec_descriptors',
2089 api_name => 'open-ils.search.metabib.record_to_descriptors',
2091 specialized method...
2092 Given a biblio record id or a metarecord id,
2093 this returns a list of metabib.record_descriptor
2094 objects that live within the same metarecord
2095 @param args Object of args including:
2099 sub rec_to_mr_rec_descriptors {
2100 my( $self, $conn, $args ) = @_;
2102 my $rec = $$args{record};
2103 my $mrec = $$args{metarecord};
2104 my $item_forms = $$args{item_forms};
2105 my $item_types = $$args{item_types};
2106 my $item_lang = $$args{item_lang};
2108 my $e = new_editor();
2112 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2113 return $e->event unless @$map;
2114 $mrec = $$map[0]->metarecord;
2117 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2118 return $e->event unless @$recs;
2120 my @recs = map { $_->source } @$recs;
2121 my $search = { record => \@recs };
2122 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2123 $search->{item_type} = $item_types if $item_types and @$item_types;
2124 $search->{item_lang} = $item_lang if $item_lang;
2126 my $desc = $e->search_metabib_record_descriptor($search);
2128 return { metarecord => $mrec, descriptors => $desc };
2132 __PACKAGE__->register_method(
2133 method => 'fetch_age_protect',
2134 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2137 sub fetch_age_protect {
2138 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2142 __PACKAGE__->register_method(
2143 method => 'copies_by_cn_label',
2144 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2147 __PACKAGE__->register_method(
2148 method => 'copies_by_cn_label',
2149 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2152 sub copies_by_cn_label {
2153 my( $self, $conn, $record, $label, $circ_lib ) = @_;
2154 my $e = new_editor();
2155 my $cns = $e->search_asset_call_number({record => $record, label => $label, deleted => 'f'}, {idlist=>1});
2156 return [] unless @$cns;
2158 # show all non-deleted copies in the staff client ...
2159 if ($self->api_name =~ /staff$/o) {
2160 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2163 # ... otherwise, grab the copies ...
2164 my $copies = $e->search_asset_copy(
2165 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2166 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2170 # ... and test for location and status visibility
2171 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];