1 package OpenILS::Application::Search::Biblio;
2 use base qw/OpenILS::Application/;
3 use strict; use warnings;
6 use OpenSRF::Utils::JSON;
7 use OpenILS::Utils::Fieldmapper;
8 use OpenILS::Utils::ModsParser;
9 use OpenSRF::Utils::SettingsClient;
10 use OpenILS::Utils::CStoreEditor q/:funcs/;
11 use OpenSRF::Utils::Cache;
14 use OpenSRF::Utils::Logger qw/:logger/;
17 use OpenSRF::Utils::JSON;
19 use Time::HiRes qw(time);
20 use OpenSRF::EX qw(:try);
21 use Digest::MD5 qw(md5_hex);
27 $Data::Dumper::Indent = 0;
29 use OpenILS::Const qw/:const/;
31 use OpenILS::Application::AppUtils;
32 my $apputils = "OpenILS::Application::AppUtils";
35 my $pfx = "open-ils.search_";
43 $cache = OpenSRF::Utils::Cache->new('global');
44 my $sclient = OpenSRF::Utils::SettingsClient->new();
45 $cache_timeout = $sclient->config_value(
46 "apps", "open-ils.search", "app_settings", "cache_timeout" ) || 300;
48 $superpage_size = $sclient->config_value(
49 "apps", "open-ils.search", "app_settings", "superpage_size" ) || 500;
51 $max_superpages = $sclient->config_value(
52 "apps", "open-ils.search", "app_settings", "max_superpages" ) || 20;
54 $logger->info("Search cache timeout is $cache_timeout, ".
55 " superpage_size is $superpage_size, max_superpages is $max_superpages");
60 # ---------------------------------------------------------------------------
61 # takes a list of record id's and turns the docs into friendly
62 # mods structures. Creates one MODS structure for each doc id.
63 # ---------------------------------------------------------------------------
64 sub _records_to_mods {
70 my $session = OpenSRF::AppSession->create("open-ils.cstore");
71 my $request = $session->request(
72 "open-ils.cstore.direct.biblio.record_entry.search", { id => \@ids } );
74 while( my $resp = $request->recv ) {
75 my $content = $resp->content;
76 next if $content->id == OILS_PRECAT_RECORD;
77 my $u = OpenILS::Utils::ModsParser->new(); # FIXME: we really need a new parser for each object?
78 $u->start_mods_batch( $content->marc );
79 my $mods = $u->finish_mods_batch();
80 $mods->doc_id($content->id());
81 $mods->tcn($content->tcn_value);
85 $session->disconnect();
89 __PACKAGE__->register_method(
90 method => "record_id_to_mods",
91 api_name => "open-ils.search.biblio.record.mods.retrieve",
94 desc => "Provide ID, we provide the MODS object with copy count. "
95 . "Note: this method does NOT take an array of IDs like mods_slim.retrieve", # FIXME: do it here too
97 { desc => 'Record ID', type => 'number' }
100 desc => 'MODS object', type => 'object'
105 # converts a record into a mods object with copy counts attached
106 sub record_id_to_mods {
108 my( $self, $client, $org_id, $id ) = @_;
110 my $mods_list = _records_to_mods( $id );
111 my $mods_obj = $mods_list->[0];
112 my $cmethod = $self->method_lookup("open-ils.search.biblio.record.copy_count");
113 my ($count) = $cmethod->run($org_id, $id);
114 $mods_obj->copy_count($count);
121 __PACKAGE__->register_method(
122 method => "record_id_to_mods_slim",
123 api_name => "open-ils.search.biblio.record.mods_slim.retrieve",
127 desc => "Provide ID(s), we provide the MODS",
129 { desc => 'Record ID or array of IDs' }
132 desc => 'MODS object(s), event on error'
137 # converts a record into a mods object with NO copy counts attached
138 sub record_id_to_mods_slim {
139 my( $self, $client, $id ) = @_;
140 return undef unless defined $id;
142 if(ref($id) and ref($id) == 'ARRAY') {
143 return _records_to_mods( @$id );
145 my $mods_list = _records_to_mods( $id );
146 my $mods_obj = $mods_list->[0];
147 return OpenILS::Event->new('BIBLIO_RECORD_ENTRY_NOT_FOUND') unless $mods_obj;
153 __PACKAGE__->register_method(
154 method => "record_id_to_mods_slim_batch",
155 api_name => "open-ils.search.biblio.record.mods_slim.batch.retrieve",
158 sub record_id_to_mods_slim_batch {
159 my($self, $conn, $id_list) = @_;
160 $conn->respond(_records_to_mods($_)->[0]) for @$id_list;
165 # Returns the number of copies attached to a record based on org location
166 __PACKAGE__->register_method(
167 method => "record_id_to_copy_count",
168 api_name => "open-ils.search.biblio.record.copy_count",
170 desc => q/Returns a copy summary for the given record for the context org
171 unit and all ancestor org units/,
173 {desc => 'Context org unit id', type => 'number'},
174 {desc => 'Record ID', type => 'number'}
177 desc => q/summary object per org unit in the set, where the set
178 includes the context org unit and all parent org units.
179 Object includes the keys "transcendant", "count", "org_unit", "depth",
180 "unshadow", "available". Each is a count, except "org_unit" which is
181 the context org unit and "depth" which is the depth of the context org unit
188 __PACKAGE__->register_method(
189 method => "record_id_to_copy_count",
190 api_name => "open-ils.search.biblio.record.copy_count.staff",
193 desc => q/Returns a copy summary for the given record for the context org
194 unit and all ancestor org units/,
196 {desc => 'Context org unit id', type => 'number'},
197 {desc => 'Record ID', type => 'number'}
200 desc => q/summary object per org unit in the set, where the set
201 includes the context org unit and all parent org units.
202 Object includes the keys "transcendant", "count", "org_unit", "depth",
203 "unshadow", "available". Each is a count, except "org_unit" which is
204 the context org unit and "depth" which is the depth of the context org unit
211 __PACKAGE__->register_method(
212 method => "record_id_to_copy_count",
213 api_name => "open-ils.search.biblio.metarecord.copy_count",
215 desc => q/Returns a copy summary for the given record for the context org
216 unit and all ancestor org units/,
218 {desc => 'Context org unit id', type => 'number'},
219 {desc => 'Record ID', type => 'number'}
222 desc => q/summary object per org unit in the set, where the set
223 includes the context org unit and all parent org units.
224 Object includes the keys "transcendant", "count", "org_unit", "depth",
225 "unshadow", "available". Each is a count, except "org_unit" which is
226 the context org unit and "depth" which is the depth of the context org unit
233 __PACKAGE__->register_method(
234 method => "record_id_to_copy_count",
235 api_name => "open-ils.search.biblio.metarecord.copy_count.staff",
237 desc => q/Returns a copy summary for the given record for the context org
238 unit and all ancestor org units/,
240 {desc => 'Context org unit id', type => 'number'},
241 {desc => 'Record ID', type => 'number'}
244 desc => q/summary object per org unit in the set, where the set
245 includes the context org unit and all parent org units.
246 Object includes the keys "transcendant", "count", "org_unit", "depth",
247 "unshadow", "available". Each is a count, except "org_unit" which is
248 the context org unit and "depth" which is the depth of the context org unit
255 sub record_id_to_copy_count {
256 my( $self, $client, $org_id, $record_id ) = @_;
258 return [] unless $record_id;
260 my $method = "open-ils.storage.biblio.record_entry.copy_count.atomic";
263 if($self->api_name =~ /metarecord/) {
264 $method = "open-ils.storage.metabib.metarecord.copy_count.atomic";
268 $method =~ s/atomic/staff\.atomic/og if($self->api_name =~ /staff/ );
270 my $count = $U->storagereq($method, org_unit => $org_id, $key => $record_id);
272 return [ sort { $a->{depth} <=> $b->{depth} } @$count ];
276 __PACKAGE__->register_method(
277 method => "biblio_search_tcn",
278 api_name => "open-ils.search.biblio.tcn",
281 desc => "Retrieve related record ID(s) given a TCN",
283 { desc => 'TCN', type => 'string' },
284 { desc => 'Flag indicating to include deleted records', type => 'string' }
287 desc => 'Results object like: { "count": $i, "ids": [...] }',
294 sub biblio_search_tcn {
296 my( $self, $client, $tcn, $include_deleted ) = @_;
298 $tcn =~ s/^\s+|\s+$//og;
300 my $e = new_editor();
301 my $search = {tcn_value => $tcn};
302 $search->{deleted} = 'f' unless $include_deleted;
303 my $recs = $e->search_biblio_record_entry( $search, {idlist =>1} );
305 return { count => scalar(@$recs), ids => $recs };
309 # --------------------------------------------------------------------------------
311 __PACKAGE__->register_method(
312 method => "biblio_barcode_to_copy",
313 api_name => "open-ils.search.asset.copy.find_by_barcode",
315 sub biblio_barcode_to_copy {
316 my( $self, $client, $barcode ) = @_;
317 my( $copy, $evt ) = $U->fetch_copy_by_barcode($barcode);
322 __PACKAGE__->register_method(
323 method => "biblio_id_to_copy",
324 api_name => "open-ils.search.asset.copy.batch.retrieve",
326 sub biblio_id_to_copy {
327 my( $self, $client, $ids ) = @_;
328 $logger->info("Fetching copies @$ids");
329 return $U->cstorereq(
330 "open-ils.cstore.direct.asset.copy.search.atomic", { id => $ids } );
334 __PACKAGE__->register_method(
335 method => "biblio_id_to_uris",
336 api_name=> "open-ils.search.asset.uri.retrieve_by_bib",
340 @param BibID Which bib record contains the URIs
341 @param OrgID Where to look for URIs
342 @param OrgDepth Range adjustment for OrgID
343 @return A stream or list of 'auri' objects
347 sub biblio_id_to_uris {
348 my( $self, $client, $bib, $org, $depth ) = @_;
349 die "Org ID required" unless defined($org);
350 die "Bib ID required" unless defined($bib);
353 push @params, $depth if (defined $depth);
355 my $ids = $U->cstorereq( "open-ils.cstore.json_query.atomic",
356 { select => { auri => [ 'id' ] },
360 field => 'call_number',
366 filter => { active => 't' }
377 select => { aou => [ { column => 'id', transform => 'actor.org_unit_descendants', params => \@params, result_field => 'id' } ] },
379 where => { id => $org },
389 my $uris = $U->cstorereq(
390 "open-ils.cstore.direct.asset.uri.search.atomic",
391 { id => [ map { (values %$_) } @$ids ] }
394 $client->respond($_) for (@$uris);
400 __PACKAGE__->register_method(
401 method => "copy_retrieve",
402 api_name => "open-ils.search.asset.copy.retrieve",
405 desc => 'Retrieve a copy object based on the Copy ID',
407 { desc => 'Copy ID', type => 'number'}
410 desc => 'Copy object, event on error'
416 my( $self, $client, $cid ) = @_;
417 my( $copy, $evt ) = $U->fetch_copy($cid);
418 return $evt || $copy;
421 __PACKAGE__->register_method(
422 method => "volume_retrieve",
423 api_name => "open-ils.search.asset.call_number.retrieve"
425 sub volume_retrieve {
426 my( $self, $client, $vid ) = @_;
427 my $e = new_editor();
428 my $vol = $e->retrieve_asset_call_number($vid) or return $e->event;
432 __PACKAGE__->register_method(
433 method => "fleshed_copy_retrieve_batch",
434 api_name => "open-ils.search.asset.copy.fleshed.batch.retrieve",
438 sub fleshed_copy_retrieve_batch {
439 my( $self, $client, $ids ) = @_;
440 $logger->info("Fetching fleshed copies @$ids");
441 return $U->cstorereq(
442 "open-ils.cstore.direct.asset.copy.search.atomic",
445 flesh_fields => { acp => [ qw/ circ_lib location status stat_cat_entries / ] }
450 __PACKAGE__->register_method(
451 method => "fleshed_copy_retrieve",
452 api_name => "open-ils.search.asset.copy.fleshed.retrieve",
455 sub fleshed_copy_retrieve {
456 my( $self, $client, $id ) = @_;
457 my( $c, $e) = $U->fetch_fleshed_copy($id);
462 __PACKAGE__->register_method(
463 method => 'fleshed_by_barcode',
464 api_name => "open-ils.search.asset.copy.fleshed2.find_by_barcode",
467 sub fleshed_by_barcode {
468 my( $self, $conn, $barcode ) = @_;
469 my $e = new_editor();
470 my $copyid = $e->search_asset_copy(
471 {barcode => $barcode, deleted => 'f'}, {idlist=>1})->[0]
473 return fleshed_copy_retrieve2( $self, $conn, $copyid);
477 __PACKAGE__->register_method(
478 method => "fleshed_copy_retrieve2",
479 api_name => "open-ils.search.asset.copy.fleshed2.retrieve",
483 sub fleshed_copy_retrieve2 {
484 my( $self, $client, $id ) = @_;
485 my $e = new_editor();
486 my $copy = $e->retrieve_asset_copy(
493 qw/ location status stat_cat_entry_copy_maps notes age_protect /
495 ascecm => [qw/ stat_cat stat_cat_entry /],
499 ) or return $e->event;
501 # For backwards compatibility
502 #$copy->stat_cat_entries($copy->stat_cat_entry_copy_maps);
504 if( $copy->status->id == OILS_COPY_STATUS_CHECKED_OUT ) {
506 $e->search_action_circulation(
508 { target_copy => $copy->id },
510 order_by => { circ => 'xact_start desc' },
522 __PACKAGE__->register_method(
523 method => 'flesh_copy_custom',
524 api_name => 'open-ils.search.asset.copy.fleshed.custom',
528 sub flesh_copy_custom {
529 my( $self, $conn, $copyid, $fields ) = @_;
530 my $e = new_editor();
531 my $copy = $e->retrieve_asset_copy(
541 ) or return $e->event;
546 __PACKAGE__->register_method(
547 method => "biblio_barcode_to_title",
548 api_name => "open-ils.search.biblio.find_by_barcode",
551 sub biblio_barcode_to_title {
552 my( $self, $client, $barcode ) = @_;
554 my $title = $apputils->simple_scalar_request(
556 "open-ils.storage.biblio.record_entry.retrieve_by_barcode", $barcode );
558 return { ids => [ $title->id ], count => 1 } if $title;
559 return { count => 0 };
562 __PACKAGE__->register_method(
563 method => 'title_id_by_item_barcode',
564 api_name => 'open-ils.search.bib_id.by_barcode',
567 desc => 'Retrieve copy object with fleshed record, given the barcode',
569 { desc => 'Item barcode', type => 'string' }
572 desc => 'Asset copy object with fleshed record and callnumber, or event on error or null set'
577 sub title_id_by_item_barcode {
578 my( $self, $conn, $barcode ) = @_;
579 my $e = new_editor();
580 my $copies = $e->search_asset_copy(
582 { deleted => 'f', barcode => $barcode },
586 acp => [ 'call_number' ],
593 return $e->event unless @$copies;
594 return $$copies[0]->call_number->record->id;
598 __PACKAGE__->register_method(
599 method => "biblio_copy_to_mods",
600 api_name => "open-ils.search.biblio.copy.mods.retrieve",
603 # takes a copy object and returns it fleshed mods object
604 sub biblio_copy_to_mods {
605 my( $self, $client, $copy ) = @_;
607 my $volume = $U->cstorereq(
608 "open-ils.cstore.direct.asset.call_number.retrieve",
609 $copy->call_number() );
611 my $mods = _records_to_mods($volume->record());
612 $mods = shift @$mods;
613 $volume->copies([$copy]);
614 push @{$mods->call_numbers()}, $volume;
622 OpenILS::Application::Search::Biblio
628 =head3 open-ils.search.biblio.multiclass.query (arghash, query, docache)
630 For arghash and docache, see B<open-ils.search.biblio.multiclass>.
632 The query argument is a string, but built like a hash with key: value pairs.
633 Recognized search keys include:
635 keyword (kw) - search keyword(s) *
636 author (au) - search author(s) *
637 name (au) - same as author *
638 title (ti) - search title *
639 subject (su) - search subject *
640 series (se) - search series *
641 lang - limit by language (specifiy multiple langs with lang:l1 lang:l2 ...)
642 site - search at specified org unit, corresponds to actor.org_unit.shortname
643 sort - sort type (title, author, pubdate)
644 dir - sort direction (asc, desc)
645 available - if set to anything other than "false" or "0", limits to available items
647 * Searching keyword, author, title, subject, and series supports additional search
648 subclasses, specified with a "|". For example, C<title|proper:gone with the wind>.
650 For more, see B<config.metabib_field>.
654 foreach (qw/open-ils.search.biblio.multiclass.query
655 open-ils.search.biblio.multiclass.query.staff
656 open-ils.search.metabib.multiclass.query
657 open-ils.search.metabib.multiclass.query.staff/)
659 __PACKAGE__->register_method(
661 method => 'multiclass_query',
663 desc => 'Perform a search query. The .staff version of the call includes otherwise hidden hits.',
665 {name => 'arghash', desc => 'Arg hash (see open-ils.search.biblio.multiclass)', type => 'object'},
666 {name => 'query', desc => 'Raw human-readable query (see perldoc '. __PACKAGE__ .')', type => 'string'},
667 {name => 'docache', desc => 'Flag for caching (see open-ils.search.biblio.multiclass)', type => 'object'},
670 desc => 'Search results from query, like: { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
671 type => 'object', # TODO: update as miker's new elements are included
677 sub multiclass_query {
678 my($self, $conn, $arghash, $query, $docache) = @_;
680 $logger->debug("initial search query => $query");
681 my $orig_query = $query;
685 $query =~ s/^\s+//go;
687 # convert convenience classes (e.g. kw for keyword) to the full class name
688 $query =~ s/kw(:|\|)/keyword$1/go;
689 $query =~ s/ti(:|\|)/title$1/go;
690 $query =~ s/au(:|\|)/author$1/go;
691 $query =~ s/su(:|\|)/subject$1/go;
692 $query =~ s/se(:|\|)/series$1/go;
693 $query =~ s/name(:|\|)/author$1/og;
695 $logger->debug("cleansed query string => $query");
698 my $simple_class_re = qr/((?:\w+(?:\|\w+)?):[^:]+?)$/;
699 my $class_list_re = qr/(?:keyword|title|author|subject|series)/;
700 my $modifier_list_re = qr/(?:site|dir|sort|lang|available)/;
703 while ($query =~ s/$simple_class_re//so) {
706 my $where = index($qpart,':');
707 my $type = substr($qpart, 0, $where++);
708 my $value = substr($qpart, $where);
710 if ($type !~ /^(?:$class_list_re|$modifier_list_re)/o) {
711 $tmp_value = "$qpart $tmp_value";
715 if ($type =~ /$class_list_re/o ) {
716 $value .= $tmp_value;
720 next unless $type and $value;
722 $value =~ s/^\s*//og;
723 $value =~ s/\s*$//og;
724 $type = 'sort_dir' if $type eq 'dir';
726 if($type eq 'site') {
727 # 'site' is the org shortname. when using this, we also want
728 # to search at the requested org's depth
729 my $e = new_editor();
730 if(my $org = $e->search_actor_org_unit({shortname => $value})->[0]) {
731 $arghash->{org_unit} = $org->id if $org;
732 $arghash->{depth} = $e->retrieve_actor_org_unit_type($org->ou_type)->depth;
734 $logger->warn("'site:' query used on invalid org shortname: $value ... ignoring");
737 } elsif($type eq 'available') {
739 $arghash->{available} = 1 unless $value eq 'false' or $value eq '0';
741 } elsif($type eq 'lang') {
742 # collect languages into an array of languages
743 $arghash->{language} = [] unless $arghash->{language};
744 push(@{$arghash->{language}}, $value);
746 } elsif($type =~ /^sort/o) {
747 # sort and sort_dir modifiers
748 $arghash->{$type} = $value;
751 # append the search term to the term under construction
752 $search->{$type} = {} unless $search->{$type};
753 $search->{$type}->{term} =
754 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $value" : $value;
758 $query .= " $tmp_value";
759 $query =~ s/\s+/ /go;
760 $query =~ s/^\s+//go;
761 $query =~ s/\s+$//go;
763 my $type = $arghash->{default_class} || 'keyword';
764 $type = ($type eq '-') ? 'keyword' : $type;
765 $type = ($type !~ /^(title|author|keyword|subject|series)(?:\|\w+)?$/o) ? 'keyword' : $type;
768 # This is the front part of the string before any special tokens were
769 # parsed OR colon-separated strings that do not denote a class.
770 # Add this data to the default search class
771 $search->{$type} = {} unless $search->{$type};
772 $search->{$type}->{term} =
773 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $query" : $query;
775 my $real_search = $arghash->{searches} = { $type => { term => $orig_query } };
777 # capture the original limit because the search method alters the limit internally
778 my $ol = $arghash->{limit};
780 my $sclient = OpenSRF::Utils::SettingsClient->new;
782 (my $method = $self->api_name) =~ s/\.query//o;
784 $method =~ s/multiclass/multiclass.staged/
785 if $sclient->config_value(apps => 'open-ils.search',
786 app_settings => 'use_staged_search') =~ /true/i;
788 $arghash->{preferred_language} = $U->get_org_locale($arghash->{org_unit})
789 unless $arghash->{preferred_language};
791 $method = $self->method_lookup($method);
792 my ($data) = $method->run($arghash, $docache);
794 $arghash->{searches} = $search if (!$data->{complex_query});
796 $arghash->{limit} = $ol if $ol;
797 $data->{compiled_search} = $arghash;
798 $data->{query} = $orig_query;
800 $logger->info("compiled search is " . OpenSRF::Utils::JSON->perl2JSON($arghash));
805 __PACKAGE__->register_method(
806 method => 'cat_search_z_style_wrapper',
807 api_name => 'open-ils.search.biblio.zstyle',
809 signature => q/@see open-ils.search.biblio.multiclass/
812 __PACKAGE__->register_method(
813 method => 'cat_search_z_style_wrapper',
814 api_name => 'open-ils.search.biblio.zstyle.staff',
816 signature => q/@see open-ils.search.biblio.multiclass/
819 sub cat_search_z_style_wrapper {
822 my $authtoken = shift;
825 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
827 my $ou = $cstore->request(
828 'open-ils.cstore.direct.actor.org_unit.search',
829 { parent_ou => undef }
832 my $result = { service => 'native-evergreen-catalog', records => [] };
833 my $searchhash = { limit => $$args{limit}, offset => $$args{offset}, org_unit => $ou->id };
835 $$searchhash{searches}{title}{term} = $$args{search}{title} if $$args{search}{title};
836 $$searchhash{searches}{author}{term} = $$args{search}{author} if $$args{search}{author};
837 $$searchhash{searches}{subject}{term} = $$args{search}{subject} if $$args{search}{subject};
838 $$searchhash{searches}{keyword}{term} = $$args{search}{keyword} if $$args{search}{keyword};
840 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{tcn} if $$args{search}{tcn};
841 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{isbn} if $$args{search}{isbn};
842 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{issn} if $$args{search}{issn};
843 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{publisher} if $$args{search}{publisher};
844 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{pubdate} if $$args{search}{pubdate};
845 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{item_type} if $$args{search}{item_type};
847 my $list = the_quest_for_knowledge( $self, $client, $searchhash );
849 if ($list->{count} > 0) {
850 $result->{count} = $list->{count};
852 my $records = $cstore->request(
853 'open-ils.cstore.direct.biblio.record_entry.search.atomic',
854 { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
857 for my $rec ( @$records ) {
859 my $u = OpenILS::Utils::ModsParser->new();
860 $u->start_mods_batch( $rec->marc );
861 my $mods = $u->finish_mods_batch();
863 push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
869 $cstore->disconnect();
873 # ----------------------------------------------------------------------------
874 # These are the main OPAC search methods
875 # ----------------------------------------------------------------------------
877 __PACKAGE__->register_method(
878 method => 'the_quest_for_knowledge',
879 api_name => 'open-ils.search.biblio.multiclass',
881 desc => "Performs a multi class biblio or metabib search",
884 desc => "A search hash with keys: "
885 . "searches, org_unit, depth, limit, offset, format, sort, sort_dir. "
886 . "See perldoc " . __PACKAGE__ . " for more detail",
890 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
895 desc => 'An object of the form: '
896 . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
901 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
903 The search-hash argument can have the following elements:
905 searches: { "$class" : "$value", ...} [REQUIRED]
906 org_unit: The org id to focus the search at
907 depth : The org depth
908 limit : The search limit default: 10
909 offset : The search offset default: 0
910 format : The MARC format
911 sort : What field to sort the results on? [ author | title | pubdate ]
912 sort_dir: What direction do we sort? [ asc | desc ]
913 tag_circulated_records : Boolean, if true, records that are in the user's visible checkout history
914 will be tagged with an additional value ("1") as the last value in the record ID array for
915 each record. Requires the 'authtoken'
916 authtoken : Authentication token string; When actions are performed that require a user login
917 (e.g. tagging circulated records), the authentication token is required
919 The searches element is required, must have a hashref value, and the hashref must contain at least one
920 of the following classes as a key:
928 The value paired with a key is the associated search string.
930 The docache argument enables/disables searching and saving results in cache (default OFF).
932 The return object, if successful, will look like:
934 { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
938 __PACKAGE__->register_method(
939 method => 'the_quest_for_knowledge',
940 api_name => 'open-ils.search.biblio.multiclass.staff',
941 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
943 __PACKAGE__->register_method(
944 method => 'the_quest_for_knowledge',
945 api_name => 'open-ils.search.metabib.multiclass',
946 signature => q/@see open-ils.search.biblio.multiclass/
948 __PACKAGE__->register_method(
949 method => 'the_quest_for_knowledge',
950 api_name => 'open-ils.search.metabib.multiclass.staff',
951 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
954 sub the_quest_for_knowledge {
955 my( $self, $conn, $searchhash, $docache ) = @_;
957 return { count => 0 } unless $searchhash and
958 ref $searchhash->{searches} eq 'HASH';
960 my $method = 'open-ils.storage.biblio.multiclass.search_fts';
964 if($self->api_name =~ /metabib/) {
966 $method =~ s/biblio/metabib/o;
969 # do some simple sanity checking
970 if(!$searchhash->{searches} or
971 ( !grep { /^(?:title|author|subject|series|keyword)/ } keys %{$searchhash->{searches}} ) ) {
972 return { count => 0 };
975 my $offset = $searchhash->{offset} || 0; # user value or default in local var now
976 my $limit = $searchhash->{limit} || 10; # user value or default in local var now
977 my $end = $offset + $limit - 1;
980 $searchhash->{offset} = 0; # possible user value overwritten in hash
981 $searchhash->{limit} = $maxlimit; # possible user value overwritten in hash
983 return { count => 0 } if $offset > $maxlimit;
986 push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
987 my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
988 my $ckey = $pfx . md5_hex($method . $s);
990 $logger->info("bib search for: $s");
992 $searchhash->{limit} -= $offset;
996 my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
1000 $method .= ".staff" if($self->api_name =~ /staff/);
1001 $method .= ".atomic";
1003 for (keys %$searchhash) {
1004 delete $$searchhash{$_}
1005 unless defined $$searchhash{$_};
1008 $result = $U->storagereq( $method, %$searchhash );
1012 $docache = 0; # results came FROM cache, so we don't write back
1015 return {count => 0} unless ($result && $$result[0]);
1019 my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
1022 # If we didn't get this data from the cache, put it into the cache
1023 # then return the correct offset of records
1024 $logger->debug("putting search cache $ckey\n");
1025 put_cache($ckey, $count, \@recs);
1029 # if we have the full set of data, trim out
1030 # the requested chunk based on limit and offset
1032 for ($offset..$end) {
1033 last unless $recs[$_];
1034 push(@t, $recs[$_]);
1039 return { ids => \@recs, count => $count };
1043 __PACKAGE__->register_method(
1044 method => 'staged_search',
1045 api_name => 'open-ils.search.biblio.multiclass.staged',
1047 desc => 'Staged search filters out unavailable items. This means that it relies on an estimation strategy for determining ' .
1048 'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering. See "estimation_strategy" in your SRF config.',
1051 desc => "A search hash with keys: "
1052 . "searches, limit, offset. The others are optional, but the 'searches' key/value pair is required, with the value being a hashref. "
1053 . "See perldoc " . __PACKAGE__ . " for more detail",
1057 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
1062 desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids. '
1063 . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
1068 __PACKAGE__->register_method(
1069 method => 'staged_search',
1070 api_name => 'open-ils.search.biblio.multiclass.staged.staff',
1071 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1073 __PACKAGE__->register_method(
1074 method => 'staged_search',
1075 api_name => 'open-ils.search.metabib.multiclass.staged',
1076 signature => q/@see open-ils.search.biblio.multiclass.staged/
1078 __PACKAGE__->register_method(
1079 method => 'staged_search',
1080 api_name => 'open-ils.search.metabib.multiclass.staged.staff',
1081 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1085 my($self, $conn, $search_hash, $docache) = @_;
1087 my $IAmMetabib = ($self->api_name =~ /metabib/) ? 1 : 0;
1089 my $method = $IAmMetabib?
1090 'open-ils.storage.metabib.multiclass.staged.search_fts':
1091 'open-ils.storage.biblio.multiclass.staged.search_fts';
1093 $method .= '.staff' if $self->api_name =~ /staff$/;
1094 $method .= '.atomic';
1096 return {count => 0} unless (
1098 $search_hash->{searches} and
1099 scalar( keys %{$search_hash->{searches}} ));
1101 my $search_duration;
1102 my $user_offset = $search_hash->{offset} || 0; # user-specified offset
1103 my $user_limit = $search_hash->{limit} || 10;
1104 $user_offset = ($user_offset >= 0) ? $user_offset : 0;
1105 $user_limit = ($user_limit >= 0) ? $user_limit : 10;
1108 # we're grabbing results on a per-superpage basis, which means the
1109 # limit and offset should coincide with superpage boundaries
1110 $search_hash->{offset} = 0;
1111 $search_hash->{limit} = $superpage_size;
1113 # force a well-known check_limit
1114 $search_hash->{check_limit} = $superpage_size;
1115 # restrict total tested to superpage size * number of superpages
1116 $search_hash->{core_limit} = $superpage_size * $max_superpages;
1118 # Set the configured estimation strategy, defaults to 'inclusion'.
1119 my $estimation_strategy = OpenSRF::Utils::SettingsClient
1122 apps => 'open-ils.search', app_settings => 'estimation_strategy'
1124 $search_hash->{estimation_strategy} = $estimation_strategy;
1126 # pull any existing results from the cache
1127 my $key = search_cache_key($method, $search_hash);
1128 my $facet_key = $key.'_facets';
1129 my $cache_data = $cache->get_cache($key) || {};
1131 # keep retrieving results until we find enough to
1132 # fulfill the user-specified limit and offset
1133 my $all_results = [];
1134 my $page; # current superpage
1135 my $est_hit_count = 0;
1136 my $current_page_summary = {};
1137 my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1138 my $is_real_hit_count = 0;
1141 for($page = 0; $page < $max_superpages; $page++) {
1143 my $data = $cache_data->{$page};
1147 $logger->debug("staged search: analyzing superpage $page");
1150 # this window of results is already cached
1151 $logger->debug("staged search: found cached results");
1152 $summary = $data->{summary};
1153 $results = $data->{results};
1156 # retrieve the window of results from the database
1157 $logger->debug("staged search: fetching results from the database");
1158 $search_hash->{skip_check} = $page * $superpage_size;
1160 $results = $U->storagereq($method, %$search_hash);
1161 $search_duration = time - $start;
1162 $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1163 $summary = shift(@$results) if $results;
1166 $logger->info("search timed out: duration=$search_duration: params=".
1167 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1168 return {count => 0};
1171 my $hc = $summary->{estimated_hit_count} || $summary->{visible};
1173 $logger->info("search returned 0 results: duration=$search_duration: params=".
1174 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1177 # Create backwards-compatible result structures
1179 $results = [map {[$_->{id}, $_->{rel}, $_->{record}]} @$results];
1181 $results = [map {[$_->{id}]} @$results];
1184 tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib)
1185 if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1187 push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1188 $results = [grep {defined $_->[0]} @$results];
1189 cache_staged_search_page($key, $page, $summary, $results) if $docache;
1192 $current_page_summary = $summary;
1194 # add the new set of results to the set under construction
1195 push(@$all_results, @$results);
1197 my $current_count = scalar(@$all_results);
1199 $est_hit_count = $summary->{estimated_hit_count} || $summary->{visible}
1202 $logger->debug("staged search: located $current_count, with estimated hits=".
1203 $summary->{estimated_hit_count}." : visible=".$summary->{visible}.", checked=".$summary->{checked});
1205 if (defined($summary->{estimated_hit_count})) {
1206 foreach (qw/ checked visible excluded deleted /) {
1207 $global_summary->{$_} += $summary->{$_};
1209 $global_summary->{total} = $summary->{total};
1212 # we've found all the possible hits
1213 last if $current_count == $summary->{visible}
1214 and not defined $summary->{estimated_hit_count};
1216 # we've found enough results to satisfy the requested limit/offset
1217 last if $current_count >= ($user_limit + $user_offset);
1219 # we've scanned all possible hits
1220 if($summary->{checked} < $superpage_size) {
1221 $est_hit_count = scalar(@$all_results);
1222 # we have all possible results in hand, so we know the final hit count
1223 $is_real_hit_count = 1;
1228 my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1230 # refine the estimate if we have more than one superpage
1231 if ($page > 0 and not $is_real_hit_count) {
1232 if ($global_summary->{checked} >= $global_summary->{total}) {
1233 $est_hit_count = $global_summary->{visible};
1235 my $updated_hit_count = $U->storagereq(
1236 'open-ils.storage.fts_paging_estimate',
1237 $global_summary->{checked},
1238 $global_summary->{visible},
1239 $global_summary->{excluded},
1240 $global_summary->{deleted},
1241 $global_summary->{total}
1243 $est_hit_count = $updated_hit_count->{$estimation_strategy};
1247 $conn->respond_complete(
1249 count => $est_hit_count,
1250 core_limit => $search_hash->{core_limit},
1251 superpage_size => $search_hash->{check_limit},
1252 superpage_summary => $current_page_summary,
1253 facet_key => $facet_key,
1258 cache_facets($facet_key, $new_ids, $IAmMetabib) if $docache;
1263 sub tag_circulated_records {
1264 my ($auth, $results, $metabib) = @_;
1265 my $e = new_editor(authtoken => $auth);
1266 return $results unless $e->checkauth;
1269 select => { acn => [{ column => 'record', alias => 'tagme' }] },
1270 from => { acp => 'acn' },
1271 where => { id => { in => { from => ['action.usr_visible_circ_copies', $e->requestor->id] } } },
1277 select => { mmsm => [{ column => 'metarecord', alias => 'tagme' }] },
1279 where => { source => { in => $query } },
1284 # Give me the distinct set of bib records that exist in the user's visible circulation history
1285 my $circ_recs = $e->json_query( $query );
1287 # if the record appears in the circ history, push a 1 onto
1288 # the rec array structure to indicate truthiness
1289 for my $rec (@$results) {
1290 push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1296 # creates a unique token to represent the query in the cache
1297 sub search_cache_key {
1299 my $search_hash = shift;
1301 for my $key (sort keys %$search_hash) {
1302 push(@sorted, ($key => $$search_hash{$key}))
1303 unless $key eq 'limit' or
1305 $key eq 'skip_check';
1307 my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1308 return $pfx . md5_hex($method . $s);
1311 sub retrieve_cached_facets {
1316 return undef unless ($key and $key =~ /_facets$/);
1318 return $cache->get_cache($key) || {};
1321 __PACKAGE__->register_method(
1322 method => "retrieve_cached_facets",
1323 api_name => "open-ils.search.facet_cache.retrieve",
1325 desc => 'Returns facet data derived from a specific search based on a key '.
1326 'generated by open-ils.search.biblio.multiclass.staged and friends.',
1329 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1334 desc => 'Two level hash of facet values. Top level key is the facet id defined on the config.metabib_field table. '.
1335 'Second level key is a string facet value. Datum attached to each facet value is the number of distinct records, '.
1336 'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1337 'facet retrieval. These counts are calculated for all superpages that have been checked for visibility.',
1345 # add facets for this search to the facet cache
1346 my($key, $results, $metabib) = @_;
1347 my $data = $cache->get_cache($key);
1350 return undef unless (@$results);
1352 # The query we're constructing
1354 # select mfae.field as id,
1356 # count(distinct mmrsm.appropriate-id-field )
1357 # from metabib.facet_entry mfae
1358 # join metabib.metarecord_sourc_map mmrsm on (mfae.source = mmrsm.source)
1359 # where mmrsm.appropriate-id-field in IDLIST
1362 my $count_field = $metabib ? 'metarecord' : 'source';
1363 my $facets = $U->cstorereq( "open-ils.cstore.json_query.atomic",
1365 mfae => [ { column => 'field', alias => 'id'}, 'value' ],
1367 transform => 'count',
1369 column => $count_field,
1376 mmrsm => { field => 'source', fkey => 'source' }
1380 '+mmrsm' => { $count_field => $results }
1385 for my $facet (@$facets) {
1386 next unless ($facet->{value});
1387 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1390 $logger->info("facet compilation: cached with key=$key");
1392 $cache->put_cache($key, $data, $cache_timeout);
1395 sub cache_staged_search_page {
1396 # puts this set of results into the cache
1397 my($key, $page, $summary, $results) = @_;
1398 my $data = $cache->get_cache($key);
1401 summary => $summary,
1405 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1406 $summary->{estimated_hit_count}.", visible=".$summary->{visible});
1408 $cache->put_cache($key, $data, $cache_timeout);
1416 my $start = $offset;
1417 my $end = $offset + $limit - 1;
1419 $logger->debug("searching cache for $key : $start..$end\n");
1421 return undef unless $cache;
1422 my $data = $cache->get_cache($key);
1424 return undef unless $data;
1426 my $count = $data->[0];
1429 return undef unless $offset < $count;
1432 for( my $i = $offset; $i <= $end; $i++ ) {
1433 last unless my $d = $$data[$i];
1434 push( @result, $d );
1437 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1444 my( $key, $count, $data ) = @_;
1445 return undef unless $cache;
1446 $logger->debug("search_cache putting ".
1447 scalar(@$data)." items at key $key with timeout $cache_timeout");
1448 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1452 __PACKAGE__->register_method(
1453 method => "biblio_mrid_to_modsbatch_batch",
1454 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1457 sub biblio_mrid_to_modsbatch_batch {
1458 my( $self, $client, $mrids) = @_;
1459 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1461 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1462 for my $id (@$mrids) {
1463 next unless defined $id;
1464 my ($m) = $method->run($id);
1471 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1472 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1474 __PACKAGE__->register_method(
1475 method => "biblio_mrid_to_modsbatch",
1478 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1479 . "As usual, the .staff version of this method will include otherwise hidden records.",
1481 { desc => 'Metarecord ID', type => 'number' },
1482 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1485 desc => 'MVR Object, event on error',
1491 sub biblio_mrid_to_modsbatch {
1492 my( $self, $client, $mrid, $args) = @_;
1494 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1496 my ($mr, $evt) = _grab_metarecord($mrid);
1497 return $evt unless $mr;
1499 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1500 biblio_mrid_make_modsbatch($self, $client, $mr);
1502 return $mvr unless ref($args);
1504 # Here we find the lead record appropriate for the given filters
1505 # and use that for the title and author of the metarecord
1506 my $format = $$args{format};
1507 my $org = $$args{org};
1508 my $depth = $$args{depth};
1510 return $mvr unless $format or $org or $depth;
1512 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1513 $method = "$method.staff" if $self->api_name =~ /staff/o;
1515 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1517 if( my $mods = $U->record_to_mvr($rec) ) {
1519 $mvr->title( $mods->title );
1520 $mvr->author($mods->author);
1521 $logger->debug("mods_slim updating title and ".
1522 "author in mvr with ".$mods->title." : ".$mods->author);
1528 # converts a metarecord to an mvr
1531 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1532 return Fieldmapper::metabib::virtual_record->new($perl);
1535 # checks to see if a metarecord has mods, if so returns true;
1537 __PACKAGE__->register_method(
1538 method => "biblio_mrid_check_mvr",
1539 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1540 notes => "Takes a metarecord ID or a metarecord object and returns true "
1541 . "if the metarecord already has an mvr associated with it."
1544 sub biblio_mrid_check_mvr {
1545 my( $self, $client, $mrid ) = @_;
1549 if(ref($mrid)) { $mr = $mrid; }
1550 else { ($mr, $evt) = _grab_metarecord($mrid); }
1551 return $evt if $evt;
1553 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1555 return _mr_to_mvr($mr) if $mr->mods();
1559 sub _grab_metarecord {
1561 #my $e = OpenILS::Utils::Editor->new;
1562 my $e = new_editor();
1563 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1568 __PACKAGE__->register_method(
1569 method => "biblio_mrid_make_modsbatch",
1570 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1571 notes => "Takes either a metarecord ID or a metarecord object. "
1572 . "Forces the creations of an mvr for the given metarecord. "
1573 . "The created mvr is returned."
1576 sub biblio_mrid_make_modsbatch {
1577 my( $self, $client, $mrid ) = @_;
1579 #my $e = OpenILS::Utils::Editor->new;
1580 my $e = new_editor();
1587 $mr = $e->retrieve_metabib_metarecord($mrid)
1588 or return $e->event;
1591 my $masterid = $mr->master_record;
1592 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1594 my $ids = $U->storagereq(
1595 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1596 return undef unless @$ids;
1598 my $master = $e->retrieve_biblio_record_entry($masterid)
1599 or return $e->event;
1601 # start the mods batch
1602 my $u = OpenILS::Utils::ModsParser->new();
1603 $u->start_mods_batch( $master->marc );
1605 # grab all of the sub-records and shove them into the batch
1606 my @ids = grep { $_ ne $masterid } @$ids;
1607 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1612 my $r = $e->retrieve_biblio_record_entry($i);
1613 push( @$subrecs, $r ) if $r;
1618 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1619 $u->push_mods_batch( $_->marc ) if $_->marc;
1623 # finish up and send to the client
1624 my $mods = $u->finish_mods_batch();
1625 $mods->doc_id($mrid);
1626 $client->respond_complete($mods);
1629 # now update the mods string in the db
1630 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1633 #$e = OpenILS::Utils::Editor->new(xact => 1);
1634 $e = new_editor(xact => 1);
1635 $e->update_metabib_metarecord($mr)
1636 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1643 # converts a mr id into a list of record ids
1645 foreach (qw/open-ils.search.biblio.metarecord_to_records
1646 open-ils.search.biblio.metarecord_to_records.staff/)
1648 __PACKAGE__->register_method(
1649 method => "biblio_mrid_to_record_ids",
1652 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1653 . "As usual, the .staff version of this method will include otherwise hidden records.",
1655 { desc => 'Metarecord ID', type => 'number' },
1656 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1659 desc => 'Results object like {count => $i, ids =>[...]}',
1667 sub biblio_mrid_to_record_ids {
1668 my( $self, $client, $mrid, $args ) = @_;
1670 my $format = $$args{format};
1671 my $org = $$args{org};
1672 my $depth = $$args{depth};
1674 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1675 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1676 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1678 return { count => scalar(@$recs), ids => $recs };
1682 __PACKAGE__->register_method(
1683 method => "biblio_record_to_marc_html",
1684 api_name => "open-ils.search.biblio.record.html"
1687 __PACKAGE__->register_method(
1688 method => "biblio_record_to_marc_html",
1689 api_name => "open-ils.search.authority.to_html"
1692 # Persistent parsers and setting objects
1693 my $parser = XML::LibXML->new();
1694 my $xslt = XML::LibXSLT->new();
1696 my $slim_marc_sheet;
1697 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1699 sub biblio_record_to_marc_html {
1700 my($self, $client, $recordid, $slim, $marcxml) = @_;
1703 my $dir = $settings_client->config_value("dirs", "xsl");
1706 unless($slim_marc_sheet) {
1707 my $xsl = $settings_client->config_value(
1708 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1710 $xsl = $parser->parse_file("$dir/$xsl");
1711 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1714 $sheet = $slim_marc_sheet;
1718 unless($marc_sheet) {
1719 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1720 my $xsl = $settings_client->config_value(
1721 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1722 $xsl = $parser->parse_file("$dir/$xsl");
1723 $marc_sheet = $xslt->parse_stylesheet($xsl);
1725 $sheet = $marc_sheet;
1730 my $e = new_editor();
1731 if($self->api_name =~ /authority/) {
1732 $record = $e->retrieve_authority_record_entry($recordid)
1733 or return $e->event;
1735 $record = $e->retrieve_biblio_record_entry($recordid)
1736 or return $e->event;
1738 $marcxml = $record->marc;
1741 my $xmldoc = $parser->parse_string($marcxml);
1742 my $html = $sheet->transform($xmldoc);
1743 return $html->documentElement->toString();
1746 __PACKAGE__->register_method(
1747 method => "format_biblio_record_entry",
1748 api_name => "open-ils.search.biblio.record.print",
1750 desc => 'Returns a printable version of the specified bib record',
1752 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1755 desc => q/An action_trigger.event object or error event./,
1760 __PACKAGE__->register_method(
1761 method => "format_biblio_record_entry",
1762 api_name => "open-ils.search.biblio.record.email",
1764 desc => 'Emails an A/T templated version of the specified bib records to the authorized user',
1766 { desc => 'Authentication token', type => 'string'},
1767 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1770 desc => q/Undefined on success, otherwise an error event./,
1776 sub format_biblio_record_entry {
1777 my($self, $conn, $arg1, $arg2) = @_;
1779 my $for_print = ($self->api_name =~ /print/);
1780 my $for_email = ($self->api_name =~ /email/);
1782 my $e; my $auth; my $bib_id; my $context_org;
1786 $context_org = $arg2 || $U->fetch_org_tree->id;
1787 $e = new_editor(xact => 1);
1788 } elsif ($for_email) {
1791 $e = new_editor(authtoken => $auth, xact => 1);
1792 return $e->die_event unless $e->checkauth;
1793 $context_org = $e->requestor->home_ou;
1797 if (ref $bib_id ne 'ARRAY') {
1798 $bib_ids = [ $bib_id ];
1803 my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1804 $bucket->btype('temp');
1805 $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1807 $bucket->owner($e->requestor)
1811 my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1813 for my $id (@$bib_ids) {
1815 my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1817 my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1818 $bucket_entry->target_biblio_record_entry($bib);
1819 $bucket_entry->bucket($bucket_obj->id);
1820 $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
1827 return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
1829 } elsif ($for_email) {
1831 $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
1838 __PACKAGE__->register_method(
1839 method => "retrieve_all_copy_statuses",
1840 api_name => "open-ils.search.config.copy_status.retrieve.all"
1843 sub retrieve_all_copy_statuses {
1844 my( $self, $client ) = @_;
1845 return new_editor()->retrieve_all_config_copy_status();
1849 __PACKAGE__->register_method(
1850 method => "copy_counts_per_org",
1851 api_name => "open-ils.search.biblio.copy_counts.retrieve"
1854 __PACKAGE__->register_method(
1855 method => "copy_counts_per_org",
1856 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1859 sub copy_counts_per_org {
1860 my( $self, $client, $record_id ) = @_;
1862 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1864 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1865 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1867 my $counts = $apputils->simple_scalar_request(
1868 "open-ils.storage", $method, $record_id );
1870 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1875 __PACKAGE__->register_method(
1876 method => "copy_count_summary",
1877 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
1878 notes => "returns an array of these: "
1879 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1880 . "where statusx is a copy status name. The statuses are sorted by ID.",
1884 sub copy_count_summary {
1885 my( $self, $client, $rid, $org, $depth ) = @_;
1888 my $data = $U->storagereq(
1889 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
1891 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1894 __PACKAGE__->register_method(
1895 method => "copy_location_count_summary",
1896 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
1897 notes => "returns an array of these: "
1898 . "[ org_id, callnumber_label, copy_location, <status1_count>, <status2_count>,...] "
1899 . "where statusx is a copy status name. The statuses are sorted by ID.",
1902 sub copy_location_count_summary {
1903 my( $self, $client, $rid, $org, $depth ) = @_;
1906 my $data = $U->storagereq(
1907 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1909 return [ sort { $a->[1] cmp $b->[1] || $a->[2] cmp $b->[2] } @$data ];
1912 __PACKAGE__->register_method(
1913 method => "copy_count_location_summary",
1914 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
1915 notes => "returns an array of these: "
1916 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1917 . "where statusx is a copy status name. The statuses are sorted by ID."
1920 sub copy_count_location_summary {
1921 my( $self, $client, $rid, $org, $depth ) = @_;
1924 my $data = $U->storagereq(
1925 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1926 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1930 foreach (qw/open-ils.search.biblio.marc
1931 open-ils.search.biblio.marc.staff/)
1933 __PACKAGE__->register_method(
1934 method => "marc_search",
1937 desc => 'Fetch biblio IDs based on MARC record criteria. '
1938 . 'As usual, the .staff version of the search includes otherwise hidden records',
1941 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
1942 'See perldoc ' . __PACKAGE__ . ' for more detail.',
1945 {desc => 'limit (optional)', type => 'number'},
1946 {desc => 'offset (optional)', type => 'number'}
1949 desc => 'Results object like: { "count": $i, "ids": [...] }',
1956 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
1958 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
1960 searches: complex query object (required)
1961 org_unit: The org ID to focus the search at
1962 depth : The org depth
1963 limit : integer search limit default: 10
1964 offset : integer search offset default: 0
1965 sort : What field to sort the results on? [ author | title | pubdate ]
1966 sort_dir: In what direction do we sort? [ asc | desc ]
1968 Additional keys to refine search criteria:
1971 language : Language (code)
1972 lit_form : Literary form
1973 item_form: Item form
1974 item_type: Item type
1975 format : The MARC format
1977 Please note that the specific strings to be used in the "addtional keys" will be entirely
1978 dependent on your loaded data.
1980 All keys except "searches" are optional.
1981 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
1983 For example, an arg hash might look like:
2005 The arghash is eventually passed to the SRF call:
2006 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2008 Presently, search uses the cache unconditionally.
2012 # FIXME: that example above isn't actually tested.
2013 # TODO: docache option?
2015 my( $self, $conn, $args, $limit, $offset ) = @_;
2017 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2018 $method .= ".staff" if $self->api_name =~ /staff/;
2019 $method .= ".atomic";
2021 $limit ||= 10; # FIXME: what about $args->{limit} ?
2022 $offset ||= 0; # FIXME: what about $args->{offset} ?
2025 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2026 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2028 my $recs = search_cache($ckey, $offset, $limit);
2031 $recs = $U->storagereq($method, %$args) || [];
2033 put_cache($ckey, scalar(@$recs), $recs);
2034 $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
2041 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2042 my @recs = map { $_->[0] } @$recs;
2044 return { ids => \@recs, count => $count };
2048 __PACKAGE__->register_method(
2049 method => "biblio_search_isbn",
2050 api_name => "open-ils.search.biblio.isbn",
2052 desc => 'Retrieve biblio IDs for a given ISBN',
2054 {desc => 'ISBN', type => 'string'} # or number maybe? How normalized is our storage data?
2057 desc => 'Results object like: { "count": $i, "ids": [...] }',
2063 sub biblio_search_isbn {
2064 my( $self, $client, $isbn ) = @_;
2065 $logger->debug("Searching ISBN $isbn");
2066 my $recs = $U->storagereq('open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn);
2067 return { ids => $recs, count => scalar(@$recs) };
2070 __PACKAGE__->register_method(
2071 method => "biblio_search_isbn_batch",
2072 api_name => "open-ils.search.biblio.isbn_list",
2075 sub biblio_search_isbn_batch {
2076 my( $self, $client, $isbn_list ) = @_;
2077 $logger->debug("Searching ISBNs @$isbn_list");
2078 my @recs = (); my %rec_set = ();
2079 foreach my $isbn ( @$isbn_list ) {
2080 foreach my $rec ( @{ $U->storagereq(
2081 'open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn )
2083 if (! $rec_set{ $rec }) {
2084 $rec_set{ $rec } = 1;
2089 return { ids => \@recs, count => scalar(@recs) };
2092 __PACKAGE__->register_method(
2093 method => "biblio_search_issn",
2094 api_name => "open-ils.search.biblio.issn",
2096 desc => 'Retrieve biblio IDs for a given ISSN',
2098 {desc => 'ISBN', type => 'string'}
2101 desc => 'Results object like: { "count": $i, "ids": [...] }',
2107 sub biblio_search_issn {
2108 my( $self, $client, $issn ) = @_;
2109 $logger->debug("Searching ISSN $issn");
2110 my $e = new_editor();
2112 my $recs = $U->storagereq(
2113 'open-ils.storage.id_list.biblio.record_entry.search.issn.atomic', $issn );
2114 return { ids => $recs, count => scalar(@$recs) };
2118 __PACKAGE__->register_method(
2119 method => "fetch_mods_by_copy",
2120 api_name => "open-ils.search.biblio.mods_from_copy",
2123 desc => 'Retrieve MODS record given an attached copy ID',
2125 { desc => 'Copy ID', type => 'number' }
2128 desc => 'MODS record, event on error or uncataloged item'
2133 sub fetch_mods_by_copy {
2134 my( $self, $client, $copyid ) = @_;
2135 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2136 return $evt if $evt;
2137 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2138 return $apputils->record_to_mvr($record);
2142 # -------------------------------------------------------------------------------------
2144 __PACKAGE__->register_method(
2145 method => "cn_browse",
2146 api_name => "open-ils.search.callnumber.browse.target",
2147 notes => "Starts a callnumber browse"
2150 __PACKAGE__->register_method(
2151 method => "cn_browse",
2152 api_name => "open-ils.search.callnumber.browse.page_up",
2153 notes => "Returns the previous page of callnumbers",
2156 __PACKAGE__->register_method(
2157 method => "cn_browse",
2158 api_name => "open-ils.search.callnumber.browse.page_down",
2159 notes => "Returns the next page of callnumbers",
2163 # RETURNS array of arrays like so: label, owning_lib, record, id
2165 my( $self, $client, @params ) = @_;
2168 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
2169 if( $self->api_name =~ /target/ );
2170 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2171 if( $self->api_name =~ /page_up/ );
2172 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2173 if( $self->api_name =~ /page_down/ );
2175 return $apputils->simplereq( 'open-ils.storage', $method, @params );
2177 # -------------------------------------------------------------------------------------
2179 __PACKAGE__->register_method(
2180 method => "fetch_cn",
2181 api_name => "open-ils.search.callnumber.retrieve",
2183 notes => "retrieves a callnumber based on ID",
2187 my( $self, $client, $id ) = @_;
2188 my( $cn, $evt ) = $apputils->fetch_callnumber( $id );
2189 return $evt if $evt;
2193 __PACKAGE__->register_method(
2194 method => "fetch_copy_by_cn",
2195 api_name => 'open-ils.search.copies_by_call_number.retrieve',
2197 Returns an array of copy ID's by callnumber ID
2198 @param cnid The callnumber ID
2199 @return An array of copy IDs
2203 sub fetch_copy_by_cn {
2204 my( $self, $conn, $cnid ) = @_;
2205 return $U->cstorereq(
2206 'open-ils.cstore.direct.asset.copy.id_list.atomic',
2207 { call_number => $cnid, deleted => 'f' } );
2210 __PACKAGE__->register_method(
2211 method => 'fetch_cn_by_info',
2212 api_name => 'open-ils.search.call_number.retrieve_by_info',
2214 @param label The callnumber label
2215 @param record The record the cn is attached to
2216 @param org The owning library of the cn
2217 @return The callnumber object
2222 sub fetch_cn_by_info {
2223 my( $self, $conn, $label, $record, $org ) = @_;
2224 return $U->cstorereq(
2225 'open-ils.cstore.direct.asset.call_number.search',
2226 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2231 __PACKAGE__->register_method(
2232 method => 'bib_extras',
2233 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all'
2235 __PACKAGE__->register_method(
2236 method => 'bib_extras',
2237 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all'
2239 __PACKAGE__->register_method(
2240 method => 'bib_extras',
2241 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all'
2243 __PACKAGE__->register_method(
2244 method => 'bib_extras',
2245 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all'
2247 __PACKAGE__->register_method(
2248 method => 'bib_extras',
2249 api_name => 'open-ils.search.biblio.audience_map.retrieve.all'
2255 my $e = new_editor();
2257 return $e->retrieve_all_config_lit_form_map()
2258 if( $self->api_name =~ /lit_form/ );
2260 return $e->retrieve_all_config_item_form_map()
2261 if( $self->api_name =~ /item_form_map/ );
2263 return $e->retrieve_all_config_item_type_map()
2264 if( $self->api_name =~ /item_type_map/ );
2266 return $e->retrieve_all_config_bib_level_map()
2267 if( $self->api_name =~ /bib_level_map/ );
2269 return $e->retrieve_all_config_audience_map()
2270 if( $self->api_name =~ /audience_map/ );
2277 __PACKAGE__->register_method(
2278 method => 'fetch_slim_record',
2279 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2281 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2283 { desc => 'Array of Record IDs', type => 'array' }
2286 desc => 'Array of biblio records, event on error'
2291 sub fetch_slim_record {
2292 my( $self, $conn, $ids ) = @_;
2294 #my $editor = OpenILS::Utils::Editor->new;
2295 my $editor = new_editor();
2298 return $editor->event unless
2299 my $r = $editor->retrieve_biblio_record_entry($_);
2308 __PACKAGE__->register_method(
2309 method => 'rec_to_mr_rec_descriptors',
2310 api_name => 'open-ils.search.metabib.record_to_descriptors',
2312 specialized method...
2313 Given a biblio record id or a metarecord id,
2314 this returns a list of metabib.record_descriptor
2315 objects that live within the same metarecord
2316 @param args Object of args including:
2320 sub rec_to_mr_rec_descriptors {
2321 my( $self, $conn, $args ) = @_;
2323 my $rec = $$args{record};
2324 my $mrec = $$args{metarecord};
2325 my $item_forms = $$args{item_forms};
2326 my $item_types = $$args{item_types};
2327 my $item_lang = $$args{item_lang};
2329 my $e = new_editor();
2333 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2334 return $e->event unless @$map;
2335 $mrec = $$map[0]->metarecord;
2338 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2339 return $e->event unless @$recs;
2341 my @recs = map { $_->source } @$recs;
2342 my $search = { record => \@recs };
2343 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2344 $search->{item_type} = $item_types if $item_types and @$item_types;
2345 $search->{item_lang} = $item_lang if $item_lang;
2347 my $desc = $e->search_metabib_record_descriptor($search);
2349 return { metarecord => $mrec, descriptors => $desc };
2353 __PACKAGE__->register_method(
2354 method => 'fetch_age_protect',
2355 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2358 sub fetch_age_protect {
2359 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2363 __PACKAGE__->register_method(
2364 method => 'copies_by_cn_label',
2365 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2368 __PACKAGE__->register_method(
2369 method => 'copies_by_cn_label',
2370 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2373 sub copies_by_cn_label {
2374 my( $self, $conn, $record, $label, $circ_lib ) = @_;
2375 my $e = new_editor();
2376 my $cns = $e->search_asset_call_number({record => $record, label => $label, deleted => 'f'}, {idlist=>1});
2377 return [] unless @$cns;
2379 # show all non-deleted copies in the staff client ...
2380 if ($self->api_name =~ /staff$/o) {
2381 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2384 # ... otherwise, grab the copies ...
2385 my $copies = $e->search_asset_copy(
2386 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2387 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2391 # ... and test for location and status visibility
2392 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];