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",
213 note => "Retrieve a record by TCN",
216 sub biblio_search_tcn {
218 my( $self, $client, $tcn, $include_deleted ) = @_;
220 $tcn =~ s/^\s+|\s+$//og;
222 my $e = new_editor();
223 my $search = {tcn_value => $tcn};
224 $search->{deleted} = 'f' unless $include_deleted;
225 my $recs = $e->search_biblio_record_entry( $search, {idlist =>1} );
227 return { count => scalar(@$recs), ids => $recs };
231 # --------------------------------------------------------------------------------
233 __PACKAGE__->register_method(
234 method => "biblio_barcode_to_copy",
235 api_name => "open-ils.search.asset.copy.find_by_barcode",
237 sub biblio_barcode_to_copy {
238 my( $self, $client, $barcode ) = @_;
239 my( $copy, $evt ) = $U->fetch_copy_by_barcode($barcode);
244 __PACKAGE__->register_method(
245 method => "biblio_id_to_copy",
246 api_name => "open-ils.search.asset.copy.batch.retrieve",
248 sub biblio_id_to_copy {
249 my( $self, $client, $ids ) = @_;
250 $logger->info("Fetching copies @$ids");
251 return $U->cstorereq(
252 "open-ils.cstore.direct.asset.copy.search.atomic", { id => $ids } );
256 __PACKAGE__->register_method(
257 method => "biblio_id_to_uris",
258 api_name=> "open-ils.search.asset.uri.retrieve_by_bib",
262 @param BibID Which bib record contains the URIs
263 @param OrgID Where to look for URIs
264 @param OrgDepth Range adjustment for OrgID
265 @return A stream or list of 'auri' objects
269 sub biblio_id_to_uris {
270 my( $self, $client, $bib, $org, $depth ) = @_;
271 die "Org ID required" unless defined($org);
272 die "Bib ID required" unless defined($bib);
275 push @params, $depth if (defined $depth);
277 my $ids = $U->cstorereq( "open-ils.cstore.json_query.atomic",
278 { select => { auri => [ 'id' ] },
282 field => 'call_number',
288 filter => { active => 't' }
299 select => { aou => [ { column => 'id', transform => 'actor.org_unit_descendants', params => \@params, result_field => 'id' } ] },
301 where => { id => $org },
311 my $uris = $U->cstorereq(
312 "open-ils.cstore.direct.asset.uri.search.atomic",
313 { id => [ map { (values %$_) } @$ids ] }
316 $client->respond($_) for (@$uris);
322 __PACKAGE__->register_method(
323 method => "copy_retrieve",
324 api_name => "open-ils.search.asset.copy.retrieve",
327 my( $self, $client, $cid ) = @_;
328 my( $copy, $evt ) = $U->fetch_copy($cid);
333 __PACKAGE__->register_method(
334 method => "volume_retrieve",
335 api_name => "open-ils.search.asset.call_number.retrieve"
337 sub volume_retrieve {
338 my( $self, $client, $vid ) = @_;
339 my $e = new_editor();
340 my $vol = $e->retrieve_asset_call_number($vid) or return $e->event;
344 __PACKAGE__->register_method(
345 method => "fleshed_copy_retrieve_batch",
346 api_name => "open-ils.search.asset.copy.fleshed.batch.retrieve",
350 sub fleshed_copy_retrieve_batch {
351 my( $self, $client, $ids ) = @_;
352 $logger->info("Fetching fleshed copies @$ids");
353 return $U->cstorereq(
354 "open-ils.cstore.direct.asset.copy.search.atomic",
357 flesh_fields => { acp => [ qw/ circ_lib location status stat_cat_entries / ] }
362 __PACKAGE__->register_method(
363 method => "fleshed_copy_retrieve",
364 api_name => "open-ils.search.asset.copy.fleshed.retrieve",
367 sub fleshed_copy_retrieve {
368 my( $self, $client, $id ) = @_;
369 my( $c, $e) = $U->fetch_fleshed_copy($id);
375 __PACKAGE__->register_method(
376 method => 'fleshed_by_barcode',
377 api_name => "open-ils.search.asset.copy.fleshed2.find_by_barcode",
380 sub fleshed_by_barcode {
381 my( $self, $conn, $barcode ) = @_;
382 my $e = new_editor();
383 my $copyid = $e->search_asset_copy(
384 {barcode => $barcode, deleted => 'f'}, {idlist=>1})->[0]
386 return fleshed_copy_retrieve2( $self, $conn, $copyid);
390 __PACKAGE__->register_method(
391 method => "fleshed_copy_retrieve2",
392 api_name => "open-ils.search.asset.copy.fleshed2.retrieve",
396 sub fleshed_copy_retrieve2 {
397 my( $self, $client, $id ) = @_;
398 my $e = new_editor();
399 my $copy = $e->retrieve_asset_copy(
406 qw/ location status stat_cat_entry_copy_maps notes age_protect /
408 ascecm => [qw/ stat_cat stat_cat_entry /],
412 ) or return $e->event;
414 # For backwards compatibility
415 #$copy->stat_cat_entries($copy->stat_cat_entry_copy_maps);
417 if( $copy->status->id == OILS_COPY_STATUS_CHECKED_OUT ) {
419 $e->search_action_circulation(
421 { target_copy => $copy->id },
423 order_by => { circ => 'xact_start desc' },
435 __PACKAGE__->register_method(
436 method => 'flesh_copy_custom',
437 api_name => 'open-ils.search.asset.copy.fleshed.custom',
441 sub flesh_copy_custom {
442 my( $self, $conn, $copyid, $fields ) = @_;
443 my $e = new_editor();
444 my $copy = $e->retrieve_asset_copy(
454 ) or return $e->event;
459 __PACKAGE__->register_method(
460 method => "biblio_barcode_to_title",
461 api_name => "open-ils.search.biblio.find_by_barcode",
464 sub biblio_barcode_to_title {
465 my( $self, $client, $barcode ) = @_;
467 my $title = $apputils->simple_scalar_request(
469 "open-ils.storage.biblio.record_entry.retrieve_by_barcode", $barcode );
471 return { ids => [ $title->id ], count => 1 } if $title;
472 return { count => 0 };
475 __PACKAGE__->register_method(
476 method => 'title_id_by_item_barcode',
477 api_name => 'open-ils.search.bib_id.by_barcode',
480 desc => 'Retrieve copy object with fleshed record, given the barcode',
482 { desc => 'Item barcode', type => 'string' }
485 desc => 'Asset copy object with fleshed record and callnumber, or event on error or null set'
490 sub title_id_by_item_barcode {
491 my( $self, $conn, $barcode ) = @_;
492 my $e = new_editor();
493 my $copies = $e->search_asset_copy(
495 { deleted => 'f', barcode => $barcode },
499 acp => [ 'call_number' ],
506 return $e->event unless @$copies;
507 return $$copies[0]->call_number->record->id;
511 __PACKAGE__->register_method(
512 method => "biblio_copy_to_mods",
513 api_name => "open-ils.search.biblio.copy.mods.retrieve",
516 # takes a copy object and returns it fleshed mods object
517 sub biblio_copy_to_mods {
518 my( $self, $client, $copy ) = @_;
520 my $volume = $U->cstorereq(
521 "open-ils.cstore.direct.asset.call_number.retrieve",
522 $copy->call_number() );
524 my $mods = _records_to_mods($volume->record());
525 $mods = shift @$mods;
526 $volume->copies([$copy]);
527 push @{$mods->call_numbers()}, $volume;
535 OpenILS::Application::Search::Biblio
541 =head3 open-ils.search.biblio.multiclass.query (arghash, query, docache)
543 For arghash and docache, see B<open-ils.search.biblio.multiclass>.
545 The query argument is a string, but built like a hash with key: value pairs.
546 Recognized search keys include:
548 keyword (kw) - search keyword(s) *
549 author (au) - search author(s) *
550 name (au) - same as author *
551 title (ti) - search title *
552 subject (su) - search subject *
553 series (se) - search series *
554 lang - limit by language (specifiy multiple langs with lang:l1 lang:l2 ...)
555 site - search at specified org unit, corresponds to actor.org_unit.shortname
556 sort - sort type (title, author, pubdate)
557 dir - sort direction (asc, desc)
558 available - if set to anything other than "false" or "0", limits to available items
560 * Searching keyword, author, title, subject, and series supports additional search
561 subclasses, specified with a "|". For example, C<title|proper:gone with the wind>.
563 For more, see B<config.metabib_field>.
567 foreach (qw/open-ils.search.biblio.multiclass.query
568 open-ils.search.biblio.multiclass.query.staff
569 open-ils.search.metabib.multiclass.query
570 open-ils.search.metabib.multiclass.query.staff/)
572 __PACKAGE__->register_method(
574 method => 'multiclass_query',
576 desc => 'Perform a search query. The .staff version of the call includes otherwise hidden hits.',
578 {name => 'arghash', desc => 'Arg hash (see open-ils.search.biblio.multiclass)', type => 'object'},
579 {name => 'query', desc => 'Raw human-readable query (see perldoc '. __PACKAGE__ .')', type => 'string'},
580 {name => 'docache', desc => 'Flag for caching (see open-ils.search.biblio.multiclass)', type => 'object'},
583 desc => 'Search results from query, like: { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
584 type => 'object', # TODO: update as miker's new elements are included
590 sub multiclass_query {
591 my($self, $conn, $arghash, $query, $docache) = @_;
593 $logger->debug("initial search query => $query");
594 my $orig_query = $query;
598 $query =~ s/^\s+//go;
600 # convert convenience classes (e.g. kw for keyword) to the full class name
601 $query =~ s/kw(:|\|)/keyword$1/go;
602 $query =~ s/ti(:|\|)/title$1/go;
603 $query =~ s/au(:|\|)/author$1/go;
604 $query =~ s/su(:|\|)/subject$1/go;
605 $query =~ s/se(:|\|)/series$1/go;
606 $query =~ s/name(:|\|)/author$1/og;
608 $logger->debug("cleansed query string => $query");
611 my $simple_class_re = qr/((?:\w+(?:\|\w+)?):[^:]+?)$/;
612 my $class_list_re = qr/(?:keyword|title|author|subject|series)/;
613 my $modifier_list_re = qr/(?:site|dir|sort|lang|available)/;
616 while ($query =~ s/$simple_class_re//so) {
619 my $where = index($qpart,':');
620 my $type = substr($qpart, 0, $where++);
621 my $value = substr($qpart, $where);
623 if ($type !~ /^(?:$class_list_re|$modifier_list_re)/o) {
624 $tmp_value = "$qpart $tmp_value";
628 if ($type =~ /$class_list_re/o ) {
629 $value .= $tmp_value;
633 next unless $type and $value;
635 $value =~ s/^\s*//og;
636 $value =~ s/\s*$//og;
637 $type = 'sort_dir' if $type eq 'dir';
639 if($type eq 'site') {
640 # 'site' is the org shortname. when using this, we also want
641 # to search at the requested org's depth
642 my $e = new_editor();
643 if(my $org = $e->search_actor_org_unit({shortname => $value})->[0]) {
644 $arghash->{org_unit} = $org->id if $org;
645 $arghash->{depth} = $e->retrieve_actor_org_unit_type($org->ou_type)->depth;
647 $logger->warn("'site:' query used on invalid org shortname: $value ... ignoring");
650 } elsif($type eq 'available') {
652 $arghash->{available} = 1 unless $value eq 'false' or $value eq '0';
654 } elsif($type eq 'lang') {
655 # collect languages into an array of languages
656 $arghash->{language} = [] unless $arghash->{language};
657 push(@{$arghash->{language}}, $value);
659 } elsif($type =~ /^sort/o) {
660 # sort and sort_dir modifiers
661 $arghash->{$type} = $value;
664 # append the search term to the term under construction
665 $search->{$type} = {} unless $search->{$type};
666 $search->{$type}->{term} =
667 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $value" : $value;
671 $query .= " $tmp_value";
672 $query =~ s/\s+/ /go;
673 $query =~ s/^\s+//go;
674 $query =~ s/\s+$//go;
676 my $type = $arghash->{default_class} || 'keyword';
677 $type = ($type eq '-') ? 'keyword' : $type;
678 $type = ($type !~ /^(title|author|keyword|subject|series)(?:\|\w+)?$/o) ? 'keyword' : $type;
681 # This is the front part of the string before any special tokens were
682 # parsed OR colon-separated strings that do not denote a class.
683 # Add this data to the default search class
684 $search->{$type} = {} unless $search->{$type};
685 $search->{$type}->{term} =
686 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $query" : $query;
688 my $real_search = $arghash->{searches} = { $type => { term => $orig_query } };
690 # capture the original limit because the search method alters the limit internally
691 my $ol = $arghash->{limit};
693 my $sclient = OpenSRF::Utils::SettingsClient->new;
695 (my $method = $self->api_name) =~ s/\.query//o;
697 $method =~ s/multiclass/multiclass.staged/
698 if $sclient->config_value(apps => 'open-ils.search',
699 app_settings => 'use_staged_search') =~ /true/i;
701 $arghash->{preferred_language} = $U->get_org_locale($arghash->{org_unit})
702 unless $arghash->{preferred_language};
704 $method = $self->method_lookup($method);
705 my ($data) = $method->run($arghash, $docache);
707 $arghash->{searches} = $search if (!$data->{complex_query});
709 $arghash->{limit} = $ol if $ol;
710 $data->{compiled_search} = $arghash;
711 $data->{query} = $orig_query;
713 $logger->info("compiled search is " . OpenSRF::Utils::JSON->perl2JSON($arghash));
718 __PACKAGE__->register_method(
719 method => 'cat_search_z_style_wrapper',
720 api_name => 'open-ils.search.biblio.zstyle',
722 signature => q/@see open-ils.search.biblio.multiclass/
725 __PACKAGE__->register_method(
726 method => 'cat_search_z_style_wrapper',
727 api_name => 'open-ils.search.biblio.zstyle.staff',
729 signature => q/@see open-ils.search.biblio.multiclass/
732 sub cat_search_z_style_wrapper {
735 my $authtoken = shift;
738 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
740 my $ou = $cstore->request(
741 'open-ils.cstore.direct.actor.org_unit.search',
742 { parent_ou => undef }
745 my $result = { service => 'native-evergreen-catalog', records => [] };
746 my $searchhash = { limit => $$args{limit}, offset => $$args{offset}, org_unit => $ou->id };
748 $$searchhash{searches}{title}{term} = $$args{search}{title} if $$args{search}{title};
749 $$searchhash{searches}{author}{term} = $$args{search}{author} if $$args{search}{author};
750 $$searchhash{searches}{subject}{term} = $$args{search}{subject} if $$args{search}{subject};
751 $$searchhash{searches}{keyword}{term} = $$args{search}{keyword} if $$args{search}{keyword};
753 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{tcn} if $$args{search}{tcn};
754 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{isbn} if $$args{search}{isbn};
755 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{issn} if $$args{search}{issn};
756 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{publisher} if $$args{search}{publisher};
757 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{pubdate} if $$args{search}{pubdate};
758 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{item_type} if $$args{search}{item_type};
760 my $list = the_quest_for_knowledge( $self, $client, $searchhash );
762 if ($list->{count} > 0) {
763 $result->{count} = $list->{count};
765 my $records = $cstore->request(
766 'open-ils.cstore.direct.biblio.record_entry.search.atomic',
767 { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
770 for my $rec ( @$records ) {
772 my $u = OpenILS::Utils::ModsParser->new();
773 $u->start_mods_batch( $rec->marc );
774 my $mods = $u->finish_mods_batch();
776 push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
782 $cstore->disconnect();
786 # ----------------------------------------------------------------------------
787 # These are the main OPAC search methods
788 # ----------------------------------------------------------------------------
790 __PACKAGE__->register_method(
791 method => 'the_quest_for_knowledge',
792 api_name => 'open-ils.search.biblio.multiclass',
794 desc => "Performs a multi class biblio or metabib search",
797 desc => "A search hash with keys: "
798 . "searches, org_unit, depth, limit, offset, format, sort, sort_dir. "
799 . "See perldoc " . __PACKAGE__ . " for more detail",
803 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
808 desc => 'An object of the form: '
809 . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
814 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
816 The search-hash argument can have the following elements:
818 searches: { "$class" : "$value", ...} [REQUIRED]
819 org_unit: The org id to focus the search at
820 depth : The org depth
821 limit : The search limit default: 10
822 offset : The search offset default: 0
823 format : The MARC format
824 sort : What field to sort the results on? [ author | title | pubdate ]
825 sort_dir: What direction do we sort? [ asc | desc ]
827 The searches element is required, must have a hashref value, and the hashref must contain at least one
828 of the following classes as a key:
836 The value paired with a key is the associated search string.
838 The docache argument enables/disables searching and saving results in cache (default OFF).
840 The return object, if successful, will look like:
842 { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
846 __PACKAGE__->register_method(
847 method => 'the_quest_for_knowledge',
848 api_name => 'open-ils.search.biblio.multiclass.staff',
849 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
851 __PACKAGE__->register_method(
852 method => 'the_quest_for_knowledge',
853 api_name => 'open-ils.search.metabib.multiclass',
854 signature => q/@see open-ils.search.biblio.multiclass/
856 __PACKAGE__->register_method(
857 method => 'the_quest_for_knowledge',
858 api_name => 'open-ils.search.metabib.multiclass.staff',
859 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
862 sub the_quest_for_knowledge {
863 my( $self, $conn, $searchhash, $docache ) = @_;
865 return { count => 0 } unless $searchhash and
866 ref $searchhash->{searches} eq 'HASH';
868 my $method = 'open-ils.storage.biblio.multiclass.search_fts';
872 if($self->api_name =~ /metabib/) {
874 $method =~ s/biblio/metabib/o;
877 # do some simple sanity checking
878 if(!$searchhash->{searches} or
879 ( !grep { /^(?:title|author|subject|series|keyword)/ } keys %{$searchhash->{searches}} ) ) {
880 return { count => 0 };
883 my $offset = $searchhash->{offset} || 0; # user value or default in local var now
884 my $limit = $searchhash->{limit} || 10; # user value or default in local var now
885 my $end = $offset + $limit - 1;
888 $searchhash->{offset} = 0; # possible user value overwritten in hash
889 $searchhash->{limit} = $maxlimit; # possible user value overwritten in hash
891 return { count => 0 } if $offset > $maxlimit;
894 push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
895 my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
896 my $ckey = $pfx . md5_hex($method . $s);
898 $logger->info("bib search for: $s");
900 $searchhash->{limit} -= $offset;
904 my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
908 $method .= ".staff" if($self->api_name =~ /staff/);
909 $method .= ".atomic";
911 for (keys %$searchhash) {
912 delete $$searchhash{$_}
913 unless defined $$searchhash{$_};
916 $result = $U->storagereq( $method, %$searchhash );
920 $docache = 0; # results came FROM cache, so we don't write back
923 return {count => 0} unless ($result && $$result[0]);
927 my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
930 # If we didn't get this data from the cache, put it into the cache
931 # then return the correct offset of records
932 $logger->debug("putting search cache $ckey\n");
933 put_cache($ckey, $count, \@recs);
937 # if we have the full set of data, trim out
938 # the requested chunk based on limit and offset
940 for ($offset..$end) {
941 last unless $recs[$_];
947 return { ids => \@recs, count => $count };
951 __PACKAGE__->register_method(
952 method => 'staged_search',
953 api_name => 'open-ils.search.biblio.multiclass.staged',
955 desc => 'Staged search filters out unavailable items. This means that it relies on an estimation strategy for determining ' .
956 'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering. See "estimation_strategy" in your SRF config.',
959 desc => "A search hash with keys: "
960 . "searches, limit, offset. The others are optional, but the 'searches' key/value pair is required, with the value being a hashref. "
961 . "See perldoc " . __PACKAGE__ . " for more detail",
965 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
970 desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids. '
971 . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
976 __PACKAGE__->register_method(
977 method => 'staged_search',
978 api_name => 'open-ils.search.biblio.multiclass.staged.staff',
979 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
981 __PACKAGE__->register_method(
982 method => 'staged_search',
983 api_name => 'open-ils.search.metabib.multiclass.staged',
984 signature => q/@see open-ils.search.biblio.multiclass.staged/
986 __PACKAGE__->register_method(
987 method => 'staged_search',
988 api_name => 'open-ils.search.metabib.multiclass.staged.staff',
989 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
993 my($self, $conn, $search_hash, $docache) = @_;
995 my $method = ($self->api_name =~ /metabib/) ?
996 'open-ils.storage.metabib.multiclass.staged.search_fts':
997 'open-ils.storage.biblio.multiclass.staged.search_fts';
999 $method .= '.staff' if $self->api_name =~ /staff$/;
1000 $method .= '.atomic';
1002 return {count => 0} unless (
1004 $search_hash->{searches} and
1005 scalar( keys %{$search_hash->{searches}} ));
1007 my $search_duration;
1008 my $user_offset = $search_hash->{offset} || 0; # user-specified offset
1009 my $user_limit = $search_hash->{limit} || 10;
1010 $user_offset = ($user_offset >= 0) ? $user_offset : 0;
1011 $user_limit = ($user_limit >= 0) ? $user_limit : 10;
1014 # we're grabbing results on a per-superpage basis, which means the
1015 # limit and offset should coincide with superpage boundaries
1016 $search_hash->{offset} = 0;
1017 $search_hash->{limit} = $superpage_size;
1019 # force a well-known check_limit
1020 $search_hash->{check_limit} = $superpage_size;
1021 # restrict total tested to superpage size * number of superpages
1022 $search_hash->{core_limit} = $superpage_size * $max_superpages;
1024 # Set the configured estimation strategy, defaults to 'inclusion'.
1025 my $estimation_strategy = OpenSRF::Utils::SettingsClient
1028 apps => 'open-ils.search', app_settings => 'estimation_strategy'
1030 $search_hash->{estimation_strategy} = $estimation_strategy;
1032 # pull any existing results from the cache
1033 my $key = search_cache_key($method, $search_hash);
1034 my $facet_key = $key.'_facets';
1035 my $cache_data = $cache->get_cache($key) || {};
1037 # keep retrieving results until we find enough to
1038 # fulfill the user-specified limit and offset
1039 my $all_results = [];
1040 my $page; # current superpage
1041 my $est_hit_count = 0;
1042 my $current_page_summary = {};
1043 my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1044 my $is_real_hit_count = 0;
1047 for($page = 0; $page < $max_superpages; $page++) {
1049 my $data = $cache_data->{$page};
1053 $logger->debug("staged search: analyzing superpage $page");
1056 # this window of results is already cached
1057 $logger->debug("staged search: found cached results");
1058 $summary = $data->{summary};
1059 $results = $data->{results};
1062 # retrieve the window of results from the database
1063 $logger->debug("staged search: fetching results from the database");
1064 $search_hash->{skip_check} = $page * $superpage_size;
1066 $results = $U->storagereq($method, %$search_hash);
1067 $search_duration = time - $start;
1068 $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1069 $summary = shift(@$results);
1072 $logger->info("search timed out: duration=$search_duration: params=".
1073 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1074 return {count => 0};
1077 my $hc = $summary->{estimated_hit_count} || $summary->{visible};
1079 $logger->info("search returned 0 results: duration=$search_duration: params=".
1080 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1083 # Create backwards-compatible result structures
1084 if($self->api_name =~ /biblio/) {
1085 $results = [map {[$_->{id}]} @$results];
1087 $results = [map {[$_->{id}, $_->{rel}, $_->{record}]} @$results];
1090 push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1091 $results = [grep {defined $_->[0]} @$results];
1092 cache_staged_search_page($key, $page, $summary, $results) if $docache;
1095 $current_page_summary = $summary;
1097 # add the new set of results to the set under construction
1098 push(@$all_results, @$results);
1100 my $current_count = scalar(@$all_results);
1102 $est_hit_count = $summary->{estimated_hit_count} || $summary->{visible}
1105 $logger->debug("staged search: located $current_count, with estimated hits=".
1106 $summary->{estimated_hit_count}." : visible=".$summary->{visible}.", checked=".$summary->{checked});
1108 if (defined($summary->{estimated_hit_count})) {
1109 foreach (qw/ checked visible excluded deleted /) {
1110 $global_summary->{$_} += $summary->{$_};
1112 $global_summary->{total} = $summary->{total};
1115 # we've found all the possible hits
1116 last if $current_count == $summary->{visible}
1117 and not defined $summary->{estimated_hit_count};
1119 # we've found enough results to satisfy the requested limit/offset
1120 last if $current_count >= ($user_limit + $user_offset);
1122 # we've scanned all possible hits
1123 if($summary->{checked} < $superpage_size) {
1124 $est_hit_count = scalar(@$all_results);
1125 # we have all possible results in hand, so we know the final hit count
1126 $is_real_hit_count = 1;
1131 my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1133 # refine the estimate if we have more than one superpage
1134 if ($page > 0 and not $is_real_hit_count) {
1135 if ($global_summary->{checked} >= $global_summary->{total}) {
1136 $est_hit_count = $global_summary->{visible};
1138 my $updated_hit_count = $U->storagereq(
1139 'open-ils.storage.fts_paging_estimate',
1140 $global_summary->{checked},
1141 $global_summary->{visible},
1142 $global_summary->{excluded},
1143 $global_summary->{deleted},
1144 $global_summary->{total}
1146 $est_hit_count = $updated_hit_count->{$estimation_strategy};
1150 $conn->respond_complete(
1152 count => $est_hit_count,
1153 core_limit => $search_hash->{core_limit},
1154 superpage_size => $search_hash->{check_limit},
1155 superpage_summary => $current_page_summary,
1156 facet_key => $facet_key,
1161 cache_facets($facet_key, $new_ids) if $docache;
1165 # creates a unique token to represent the query in the cache
1166 sub search_cache_key {
1168 my $search_hash = shift;
1170 for my $key (sort keys %$search_hash) {
1171 push(@sorted, ($key => $$search_hash{$key}))
1172 unless $key eq 'limit' or
1174 $key eq 'skip_check';
1176 my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1177 return $pfx . md5_hex($method . $s);
1180 sub retrieve_cached_facets {
1185 return undef unless ($key and $key =~ /_facets$/);
1187 return $cache->get_cache($key) || {};
1190 __PACKAGE__->register_method(
1191 method => "retrieve_cached_facets",
1192 api_name => "open-ils.search.facet_cache.retrieve"
1197 # add facets for this search to the facet cache
1198 my($key, $results) = @_;
1199 my $data = $cache->get_cache($key);
1202 return undef unless (@$results);
1204 # The query we're constructing
1208 # count(distinct mfae.source)
1209 # from metabib.facet_entry mfae
1210 # join config.metabib_field cmf on (mfae.field = cmf.id)
1211 # where cmf.facet_field
1212 # and mfae.source in IDLIST
1215 my $facets = $U->cstorereq( "open-ils.cstore.json_query.atomic",
1221 transform => 'count',
1229 from => { mfae => 'cmf' },
1230 where => { '+cmf' => 'facet_field', '+mfae' => { source => $results } }
1234 for my $facet (@$facets) {
1235 next unless ($facet->{value});
1236 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1239 $logger->info("facet compilation: cached with key=$key");
1241 $cache->put_cache($key, $data, $cache_timeout);
1244 sub cache_staged_search_page {
1245 # puts this set of results into the cache
1246 my($key, $page, $summary, $results) = @_;
1247 my $data = $cache->get_cache($key);
1250 summary => $summary,
1254 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1255 $summary->{estimated_hit_count}.", visible=".$summary->{visible});
1257 $cache->put_cache($key, $data, $cache_timeout);
1265 my $start = $offset;
1266 my $end = $offset + $limit - 1;
1268 $logger->debug("searching cache for $key : $start..$end\n");
1270 return undef unless $cache;
1271 my $data = $cache->get_cache($key);
1273 return undef unless $data;
1275 my $count = $data->[0];
1278 return undef unless $offset < $count;
1281 for( my $i = $offset; $i <= $end; $i++ ) {
1282 last unless my $d = $$data[$i];
1283 push( @result, $d );
1286 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1293 my( $key, $count, $data ) = @_;
1294 return undef unless $cache;
1295 $logger->debug("search_cache putting ".
1296 scalar(@$data)." items at key $key with timeout $cache_timeout");
1297 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1301 __PACKAGE__->register_method(
1302 method => "biblio_mrid_to_modsbatch_batch",
1303 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1306 sub biblio_mrid_to_modsbatch_batch {
1307 my( $self, $client, $mrids) = @_;
1308 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1310 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1311 for my $id (@$mrids) {
1312 next unless defined $id;
1313 my ($m) = $method->run($id);
1320 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1321 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1323 __PACKAGE__->register_method(
1324 method => "biblio_mrid_to_modsbatch",
1327 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1328 . "As usual, the .staff version of this method will include otherwise hidden records.",
1330 { desc => 'Metarecord ID', type => 'number' },
1331 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1334 desc => 'MVR Object, event on error',
1340 sub biblio_mrid_to_modsbatch {
1341 my( $self, $client, $mrid, $args) = @_;
1343 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1345 my ($mr, $evt) = _grab_metarecord($mrid);
1346 return $evt unless $mr;
1348 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1349 biblio_mrid_make_modsbatch($self, $client, $mr);
1351 return $mvr unless ref($args);
1353 # Here we find the lead record appropriate for the given filters
1354 # and use that for the title and author of the metarecord
1355 my $format = $$args{format};
1356 my $org = $$args{org};
1357 my $depth = $$args{depth};
1359 return $mvr unless $format or $org or $depth;
1361 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1362 $method = "$method.staff" if $self->api_name =~ /staff/o;
1364 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1366 if( my $mods = $U->record_to_mvr($rec) ) {
1368 $mvr->title( $mods->title );
1369 $mvr->author($mods->author);
1370 $logger->debug("mods_slim updating title and ".
1371 "author in mvr with ".$mods->title." : ".$mods->author);
1377 # converts a metarecord to an mvr
1380 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1381 return Fieldmapper::metabib::virtual_record->new($perl);
1384 # checks to see if a metarecord has mods, if so returns true;
1386 __PACKAGE__->register_method(
1387 method => "biblio_mrid_check_mvr",
1388 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1389 notes => "Takes a metarecord ID or a metarecord object and returns true "
1390 . "if the metarecord already has an mvr associated with it."
1393 sub biblio_mrid_check_mvr {
1394 my( $self, $client, $mrid ) = @_;
1398 if(ref($mrid)) { $mr = $mrid; }
1399 else { ($mr, $evt) = _grab_metarecord($mrid); }
1400 return $evt if $evt;
1402 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1404 return _mr_to_mvr($mr) if $mr->mods();
1408 sub _grab_metarecord {
1410 #my $e = OpenILS::Utils::Editor->new;
1411 my $e = new_editor();
1412 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1417 __PACKAGE__->register_method(
1418 method => "biblio_mrid_make_modsbatch",
1419 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1420 notes => "Takes either a metarecord ID or a metarecord object. "
1421 . "Forces the creations of an mvr for the given metarecord. "
1422 . "The created mvr is returned."
1425 sub biblio_mrid_make_modsbatch {
1426 my( $self, $client, $mrid ) = @_;
1428 #my $e = OpenILS::Utils::Editor->new;
1429 my $e = new_editor();
1436 $mr = $e->retrieve_metabib_metarecord($mrid)
1437 or return $e->event;
1440 my $masterid = $mr->master_record;
1441 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1443 my $ids = $U->storagereq(
1444 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1445 return undef unless @$ids;
1447 my $master = $e->retrieve_biblio_record_entry($masterid)
1448 or return $e->event;
1450 # start the mods batch
1451 my $u = OpenILS::Utils::ModsParser->new();
1452 $u->start_mods_batch( $master->marc );
1454 # grab all of the sub-records and shove them into the batch
1455 my @ids = grep { $_ ne $masterid } @$ids;
1456 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1461 my $r = $e->retrieve_biblio_record_entry($i);
1462 push( @$subrecs, $r ) if $r;
1467 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1468 $u->push_mods_batch( $_->marc ) if $_->marc;
1472 # finish up and send to the client
1473 my $mods = $u->finish_mods_batch();
1474 $mods->doc_id($mrid);
1475 $client->respond_complete($mods);
1478 # now update the mods string in the db
1479 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1482 #$e = OpenILS::Utils::Editor->new(xact => 1);
1483 $e = new_editor(xact => 1);
1484 $e->update_metabib_metarecord($mr)
1485 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1492 # converts a mr id into a list of record ids
1494 foreach (qw/open-ils.search.biblio.metarecord_to_records
1495 open-ils.search.biblio.metarecord_to_records.staff/)
1497 __PACKAGE__->register_method(
1498 method => "biblio_mrid_to_record_ids",
1501 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1502 . "As usual, the .staff version of this method will include otherwise hidden records.",
1504 { desc => 'Metarecord ID', type => 'number' },
1505 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1508 desc => 'Results object like {count => $i, ids =>[...]}',
1516 sub biblio_mrid_to_record_ids {
1517 my( $self, $client, $mrid, $args ) = @_;
1519 my $format = $$args{format};
1520 my $org = $$args{org};
1521 my $depth = $$args{depth};
1523 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1524 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1525 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1527 return { count => scalar(@$recs), ids => $recs };
1531 __PACKAGE__->register_method(
1532 method => "biblio_record_to_marc_html",
1533 api_name => "open-ils.search.biblio.record.html"
1536 __PACKAGE__->register_method(
1537 method => "biblio_record_to_marc_html",
1538 api_name => "open-ils.search.authority.to_html"
1541 # Persistent parsers and setting objects
1542 my $parser = XML::LibXML->new();
1543 my $xslt = XML::LibXSLT->new();
1545 my $slim_marc_sheet;
1546 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1548 sub biblio_record_to_marc_html {
1549 my($self, $client, $recordid, $slim, $marcxml) = @_;
1552 my $dir = $settings_client->config_value("dirs", "xsl");
1555 unless($slim_marc_sheet) {
1556 my $xsl = $settings_client->config_value(
1557 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1559 $xsl = $parser->parse_file("$dir/$xsl");
1560 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1563 $sheet = $slim_marc_sheet;
1567 unless($marc_sheet) {
1568 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1569 my $xsl = $settings_client->config_value(
1570 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1571 $xsl = $parser->parse_file("$dir/$xsl");
1572 $marc_sheet = $xslt->parse_stylesheet($xsl);
1574 $sheet = $marc_sheet;
1579 my $e = new_editor();
1580 if($self->api_name =~ /authority/) {
1581 $record = $e->retrieve_authority_record_entry($recordid)
1582 or return $e->event;
1584 $record = $e->retrieve_biblio_record_entry($recordid)
1585 or return $e->event;
1587 $marcxml = $record->marc;
1590 my $xmldoc = $parser->parse_string($marcxml);
1591 my $html = $sheet->transform($xmldoc);
1592 return $html->documentElement->toString();
1597 __PACKAGE__->register_method(
1598 method => "retrieve_all_copy_statuses",
1599 api_name => "open-ils.search.config.copy_status.retrieve.all"
1602 sub retrieve_all_copy_statuses {
1603 my( $self, $client ) = @_;
1604 return new_editor()->retrieve_all_config_copy_status();
1608 __PACKAGE__->register_method(
1609 method => "copy_counts_per_org",
1610 api_name => "open-ils.search.biblio.copy_counts.retrieve"
1613 __PACKAGE__->register_method(
1614 method => "copy_counts_per_org",
1615 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1618 sub copy_counts_per_org {
1619 my( $self, $client, $record_id ) = @_;
1621 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1623 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1624 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1626 my $counts = $apputils->simple_scalar_request(
1627 "open-ils.storage", $method, $record_id );
1629 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1634 __PACKAGE__->register_method(
1635 method => "copy_count_summary",
1636 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
1637 notes => "returns an array of these: "
1638 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1639 . "where statusx is a copy status name. The statuses are sorted by ID.",
1643 sub copy_count_summary {
1644 my( $self, $client, $rid, $org, $depth ) = @_;
1647 my $data = $U->storagereq(
1648 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
1650 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1653 __PACKAGE__->register_method(
1654 method => "copy_location_count_summary",
1655 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
1656 notes => "returns an array of these: "
1657 . "[ org_id, callnumber_label, copy_location, <status1_count>, <status2_count>,...] "
1658 . "where statusx is a copy status name. The statuses are sorted by ID.",
1661 sub copy_location_count_summary {
1662 my( $self, $client, $rid, $org, $depth ) = @_;
1665 my $data = $U->storagereq(
1666 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1668 return [ sort { $a->[1] cmp $b->[1] || $a->[2] cmp $b->[2] } @$data ];
1671 __PACKAGE__->register_method(
1672 method => "copy_count_location_summary",
1673 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
1674 notes => "returns an array of these: "
1675 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1676 . "where statusx is a copy status name. The statuses are sorted by ID."
1679 sub copy_count_location_summary {
1680 my( $self, $client, $rid, $org, $depth ) = @_;
1683 my $data = $U->storagereq(
1684 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1685 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1689 __PACKAGE__->register_method(
1690 method => "marc_search",
1691 api_name => "open-ils.search.biblio.marc.staff",
1694 __PACKAGE__->register_method(
1695 method => "marc_search",
1696 api_name => "open-ils.search.biblio.marc",
1698 desc => 'Fetch biblio IDs based on MARC record criteria',
1701 desc => 'Search hash with possible elements: searches, limit, offset, sort, sort_dir. (required). ' .
1702 'See perldoc ' . __PACKAGE__ . ' for more detail.',
1705 {desc => 'limit (optional)', type => 'number'},
1706 {desc => 'offset (optional)', type => 'number'}
1709 desc => 'Results object like: { "count": $i, "ids": [...] }',
1715 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
1717 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
1719 searches: complex query object (required)
1720 org_unit: The org ID to focus the search at
1721 depth : The org depth
1722 limit : integer search limit default: 10
1723 offset : integer search offset default: 0
1724 sort : What field to sort the results on? [ author | title | pubdate ]
1725 sort_dir: In what direction do we sort? [ asc | desc ]
1727 Additional keys to refine search criteria:
1730 language : Language (code)
1731 lit_form : Literary form
1732 item_form: Item form
1733 item_type: Item type
1734 format : The MARC format
1736 Please note that the specific strings to be used in the "addtional keys" will be entirely
1737 dependent on your loaded data.
1739 All keys except "searches" are optional.
1740 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
1742 For example, an arg hash might look like:
1764 The arghash is eventually passed to the SRF call:
1765 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
1767 Presently, search uses the cache unconditionally.
1771 # FIXME: that example above isn't actually tested.
1772 # TODO: docache option?
1774 my( $self, $conn, $args, $limit, $offset ) = @_;
1776 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
1777 $method .= ".staff" if $self->api_name =~ /staff/;
1778 $method .= ".atomic";
1780 $limit ||= 10; # FIXME: what about $args->{limit} ?
1781 $offset ||= 0; # FIXME: what about $args->{offset} ?
1784 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
1785 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
1787 my $recs = search_cache($ckey, $offset, $limit);
1790 $recs = $U->storagereq($method, %$args) || [];
1792 put_cache($ckey, scalar(@$recs), $recs);
1793 $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
1800 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
1801 my @recs = map { $_->[0] } @$recs;
1803 return { ids => \@recs, count => $count };
1807 __PACKAGE__->register_method(
1808 method => "biblio_search_isbn",
1809 api_name => "open-ils.search.biblio.isbn",
1811 desc => 'Retrieve biblio IDs for a given ISBN',
1813 {desc => 'ISBN', type => 'string'} # or number maybe? How normalized is our storage data?
1816 desc => 'Results object like: { "count": $i, "ids": [...] }',
1822 sub biblio_search_isbn {
1823 my( $self, $client, $isbn ) = @_;
1824 $logger->debug("Searching ISBN $isbn");
1825 my $recs = $U->storagereq('open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn);
1826 return { ids => $recs, count => scalar(@$recs) };
1829 __PACKAGE__->register_method(
1830 method => "biblio_search_isbn_batch",
1831 api_name => "open-ils.search.biblio.isbn_list",
1834 sub biblio_search_isbn_batch {
1835 my( $self, $client, $isbn_list ) = @_;
1836 $logger->debug("Searching ISBNs @$isbn_list");
1837 my @recs = (); my %rec_set = ();
1838 foreach my $isbn ( @$isbn_list ) {
1839 foreach my $rec ( @{ $U->storagereq(
1840 'open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn )
1842 if (! $rec_set{ $rec }) {
1843 $rec_set{ $rec } = 1;
1848 return { ids => \@recs, count => scalar(@recs) };
1851 __PACKAGE__->register_method(
1852 method => "biblio_search_issn",
1853 api_name => "open-ils.search.biblio.issn",
1855 desc => 'Retrieve biblio IDs for a given ISSN',
1857 {desc => 'ISBN', type => 'string'}
1860 desc => 'Results object like: { "count": $i, "ids": [...] }',
1866 sub biblio_search_issn {
1867 my( $self, $client, $issn ) = @_;
1868 $logger->debug("Searching ISSN $issn");
1869 my $e = new_editor();
1871 my $recs = $U->storagereq(
1872 'open-ils.storage.id_list.biblio.record_entry.search.issn.atomic', $issn );
1873 return { ids => $recs, count => scalar(@$recs) };
1877 __PACKAGE__->register_method(
1878 method => "fetch_mods_by_copy",
1879 api_name => "open-ils.search.biblio.mods_from_copy",
1882 sub fetch_mods_by_copy {
1883 my( $self, $client, $copyid ) = @_;
1884 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
1885 return $evt if $evt;
1886 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
1887 return $apputils->record_to_mvr($record);
1892 # -------------------------------------------------------------------------------------
1894 __PACKAGE__->register_method(
1895 method => "cn_browse",
1896 api_name => "open-ils.search.callnumber.browse.target",
1897 notes => "Starts a callnumber browse"
1900 __PACKAGE__->register_method(
1901 method => "cn_browse",
1902 api_name => "open-ils.search.callnumber.browse.page_up",
1903 notes => "Returns the previous page of callnumbers",
1906 __PACKAGE__->register_method(
1907 method => "cn_browse",
1908 api_name => "open-ils.search.callnumber.browse.page_down",
1909 notes => "Returns the next page of callnumbers",
1913 # RETURNS array of arrays like so: label, owning_lib, record, id
1915 my( $self, $client, @params ) = @_;
1918 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
1919 if( $self->api_name =~ /target/ );
1920 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
1921 if( $self->api_name =~ /page_up/ );
1922 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
1923 if( $self->api_name =~ /page_down/ );
1925 return $apputils->simplereq( 'open-ils.storage', $method, @params );
1927 # -------------------------------------------------------------------------------------
1929 __PACKAGE__->register_method(
1930 method => "fetch_cn",
1931 api_name => "open-ils.search.callnumber.retrieve",
1933 notes => "retrieves a callnumber based on ID",
1937 my( $self, $client, $id ) = @_;
1938 my( $cn, $evt ) = $apputils->fetch_callnumber( $id );
1939 return $evt if $evt;
1943 __PACKAGE__->register_method(
1944 method => "fetch_copy_by_cn",
1945 api_name => 'open-ils.search.copies_by_call_number.retrieve',
1947 Returns an array of copy ID's by callnumber ID
1948 @param cnid The callnumber ID
1949 @return An array of copy IDs
1953 sub fetch_copy_by_cn {
1954 my( $self, $conn, $cnid ) = @_;
1955 return $U->cstorereq(
1956 'open-ils.cstore.direct.asset.copy.id_list.atomic',
1957 { call_number => $cnid, deleted => 'f' } );
1960 __PACKAGE__->register_method(
1961 method => 'fetch_cn_by_info',
1962 api_name => 'open-ils.search.call_number.retrieve_by_info',
1964 @param label The callnumber label
1965 @param record The record the cn is attached to
1966 @param org The owning library of the cn
1967 @return The callnumber object
1972 sub fetch_cn_by_info {
1973 my( $self, $conn, $label, $record, $org ) = @_;
1974 return $U->cstorereq(
1975 'open-ils.cstore.direct.asset.call_number.search',
1976 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
1981 __PACKAGE__->register_method(
1982 method => 'bib_extras',
1983 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all'
1985 __PACKAGE__->register_method(
1986 method => 'bib_extras',
1987 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all'
1989 __PACKAGE__->register_method(
1990 method => 'bib_extras',
1991 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all'
1993 __PACKAGE__->register_method(
1994 method => 'bib_extras',
1995 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all'
1997 __PACKAGE__->register_method(
1998 method => 'bib_extras',
1999 api_name => 'open-ils.search.biblio.audience_map.retrieve.all'
2005 my $e = new_editor();
2007 return $e->retrieve_all_config_lit_form_map()
2008 if( $self->api_name =~ /lit_form/ );
2010 return $e->retrieve_all_config_item_form_map()
2011 if( $self->api_name =~ /item_form_map/ );
2013 return $e->retrieve_all_config_item_type_map()
2014 if( $self->api_name =~ /item_type_map/ );
2016 return $e->retrieve_all_config_bib_level_map()
2017 if( $self->api_name =~ /bib_level_map/ );
2019 return $e->retrieve_all_config_audience_map()
2020 if( $self->api_name =~ /audience_map/ );
2027 __PACKAGE__->register_method(
2028 method => 'fetch_slim_record',
2029 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2031 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2033 { desc => 'Array of Record IDs', type => 'array' }
2036 desc => 'Array of biblio records, event on error'
2041 sub fetch_slim_record {
2042 my( $self, $conn, $ids ) = @_;
2044 #my $editor = OpenILS::Utils::Editor->new;
2045 my $editor = new_editor();
2048 return $editor->event unless
2049 my $r = $editor->retrieve_biblio_record_entry($_);
2058 __PACKAGE__->register_method(
2059 method => 'rec_to_mr_rec_descriptors',
2060 api_name => 'open-ils.search.metabib.record_to_descriptors',
2062 specialized method...
2063 Given a biblio record id or a metarecord id,
2064 this returns a list of metabib.record_descriptor
2065 objects that live within the same metarecord
2066 @param args Object of args including:
2070 sub rec_to_mr_rec_descriptors {
2071 my( $self, $conn, $args ) = @_;
2073 my $rec = $$args{record};
2074 my $mrec = $$args{metarecord};
2075 my $item_forms = $$args{item_forms};
2076 my $item_types = $$args{item_types};
2077 my $item_lang = $$args{item_lang};
2079 my $e = new_editor();
2083 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2084 return $e->event unless @$map;
2085 $mrec = $$map[0]->metarecord;
2088 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2089 return $e->event unless @$recs;
2091 my @recs = map { $_->source } @$recs;
2092 my $search = { record => \@recs };
2093 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2094 $search->{item_type} = $item_types if $item_types and @$item_types;
2095 $search->{item_lang} = $item_lang if $item_lang;
2097 my $desc = $e->search_metabib_record_descriptor($search);
2099 return { metarecord => $mrec, descriptors => $desc };
2103 __PACKAGE__->register_method(
2104 method => 'fetch_age_protect',
2105 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2108 sub fetch_age_protect {
2109 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2113 __PACKAGE__->register_method(
2114 method => 'copies_by_cn_label',
2115 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2118 __PACKAGE__->register_method(
2119 method => 'copies_by_cn_label',
2120 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2123 sub copies_by_cn_label {
2124 my( $self, $conn, $record, $label, $circ_lib ) = @_;
2125 my $e = new_editor();
2126 my $cns = $e->search_asset_call_number({record => $record, label => $label, deleted => 'f'}, {idlist=>1});
2127 return [] unless @$cns;
2129 # show all non-deleted copies in the staff client ...
2130 if ($self->api_name =~ /staff$/o) {
2131 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2134 # ... otherwise, grab the copies ...
2135 my $copies = $e->search_asset_copy(
2136 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2137 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2141 # ... and test for location and status visibility
2142 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];