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"
1328 # add facets for this search to the facet cache
1329 my($key, $results, $metabib) = @_;
1330 my $data = $cache->get_cache($key);
1333 return undef unless (@$results);
1335 # The query we're constructing
1339 # count(distinct mmrsm.appropriate-id-field )
1340 # from metabib.facet_entry mfae
1341 # join config.metabib_field cmf on (mfae.field = cmf.id)
1342 # join metabib.metarecord_sourc_map mmrsm on (mfae.source = mmrsm.source)
1343 # where cmf.facet_field
1344 # and mmrsm.appropriate-id-field in IDLIST
1347 my $count_field = $metabib ? 'metarecord' : 'source';
1348 my $facets = $U->cstorereq( "open-ils.cstore.json_query.atomic",
1351 mfae => [ 'value' ],
1353 transform => 'count',
1355 column => $count_field,
1362 mmrsm => { field => 'source', fkey => 'source' }
1366 '+mmrsm' => { $count_field => $results }
1371 for my $facet (@$facets) {
1372 next unless ($facet->{value});
1373 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1376 $logger->info("facet compilation: cached with key=$key");
1378 $cache->put_cache($key, $data, $cache_timeout);
1381 sub cache_staged_search_page {
1382 # puts this set of results into the cache
1383 my($key, $page, $summary, $results) = @_;
1384 my $data = $cache->get_cache($key);
1387 summary => $summary,
1391 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1392 $summary->{estimated_hit_count}.", visible=".$summary->{visible});
1394 $cache->put_cache($key, $data, $cache_timeout);
1402 my $start = $offset;
1403 my $end = $offset + $limit - 1;
1405 $logger->debug("searching cache for $key : $start..$end\n");
1407 return undef unless $cache;
1408 my $data = $cache->get_cache($key);
1410 return undef unless $data;
1412 my $count = $data->[0];
1415 return undef unless $offset < $count;
1418 for( my $i = $offset; $i <= $end; $i++ ) {
1419 last unless my $d = $$data[$i];
1420 push( @result, $d );
1423 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1430 my( $key, $count, $data ) = @_;
1431 return undef unless $cache;
1432 $logger->debug("search_cache putting ".
1433 scalar(@$data)." items at key $key with timeout $cache_timeout");
1434 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1438 __PACKAGE__->register_method(
1439 method => "biblio_mrid_to_modsbatch_batch",
1440 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1443 sub biblio_mrid_to_modsbatch_batch {
1444 my( $self, $client, $mrids) = @_;
1445 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1447 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1448 for my $id (@$mrids) {
1449 next unless defined $id;
1450 my ($m) = $method->run($id);
1457 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1458 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1460 __PACKAGE__->register_method(
1461 method => "biblio_mrid_to_modsbatch",
1464 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1465 . "As usual, the .staff version of this method will include otherwise hidden records.",
1467 { desc => 'Metarecord ID', type => 'number' },
1468 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1471 desc => 'MVR Object, event on error',
1477 sub biblio_mrid_to_modsbatch {
1478 my( $self, $client, $mrid, $args) = @_;
1480 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1482 my ($mr, $evt) = _grab_metarecord($mrid);
1483 return $evt unless $mr;
1485 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1486 biblio_mrid_make_modsbatch($self, $client, $mr);
1488 return $mvr unless ref($args);
1490 # Here we find the lead record appropriate for the given filters
1491 # and use that for the title and author of the metarecord
1492 my $format = $$args{format};
1493 my $org = $$args{org};
1494 my $depth = $$args{depth};
1496 return $mvr unless $format or $org or $depth;
1498 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1499 $method = "$method.staff" if $self->api_name =~ /staff/o;
1501 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1503 if( my $mods = $U->record_to_mvr($rec) ) {
1505 $mvr->title( $mods->title );
1506 $mvr->author($mods->author);
1507 $logger->debug("mods_slim updating title and ".
1508 "author in mvr with ".$mods->title." : ".$mods->author);
1514 # converts a metarecord to an mvr
1517 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1518 return Fieldmapper::metabib::virtual_record->new($perl);
1521 # checks to see if a metarecord has mods, if so returns true;
1523 __PACKAGE__->register_method(
1524 method => "biblio_mrid_check_mvr",
1525 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1526 notes => "Takes a metarecord ID or a metarecord object and returns true "
1527 . "if the metarecord already has an mvr associated with it."
1530 sub biblio_mrid_check_mvr {
1531 my( $self, $client, $mrid ) = @_;
1535 if(ref($mrid)) { $mr = $mrid; }
1536 else { ($mr, $evt) = _grab_metarecord($mrid); }
1537 return $evt if $evt;
1539 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1541 return _mr_to_mvr($mr) if $mr->mods();
1545 sub _grab_metarecord {
1547 #my $e = OpenILS::Utils::Editor->new;
1548 my $e = new_editor();
1549 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1554 __PACKAGE__->register_method(
1555 method => "biblio_mrid_make_modsbatch",
1556 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1557 notes => "Takes either a metarecord ID or a metarecord object. "
1558 . "Forces the creations of an mvr for the given metarecord. "
1559 . "The created mvr is returned."
1562 sub biblio_mrid_make_modsbatch {
1563 my( $self, $client, $mrid ) = @_;
1565 #my $e = OpenILS::Utils::Editor->new;
1566 my $e = new_editor();
1573 $mr = $e->retrieve_metabib_metarecord($mrid)
1574 or return $e->event;
1577 my $masterid = $mr->master_record;
1578 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1580 my $ids = $U->storagereq(
1581 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1582 return undef unless @$ids;
1584 my $master = $e->retrieve_biblio_record_entry($masterid)
1585 or return $e->event;
1587 # start the mods batch
1588 my $u = OpenILS::Utils::ModsParser->new();
1589 $u->start_mods_batch( $master->marc );
1591 # grab all of the sub-records and shove them into the batch
1592 my @ids = grep { $_ ne $masterid } @$ids;
1593 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1598 my $r = $e->retrieve_biblio_record_entry($i);
1599 push( @$subrecs, $r ) if $r;
1604 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1605 $u->push_mods_batch( $_->marc ) if $_->marc;
1609 # finish up and send to the client
1610 my $mods = $u->finish_mods_batch();
1611 $mods->doc_id($mrid);
1612 $client->respond_complete($mods);
1615 # now update the mods string in the db
1616 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1619 #$e = OpenILS::Utils::Editor->new(xact => 1);
1620 $e = new_editor(xact => 1);
1621 $e->update_metabib_metarecord($mr)
1622 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1629 # converts a mr id into a list of record ids
1631 foreach (qw/open-ils.search.biblio.metarecord_to_records
1632 open-ils.search.biblio.metarecord_to_records.staff/)
1634 __PACKAGE__->register_method(
1635 method => "biblio_mrid_to_record_ids",
1638 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1639 . "As usual, the .staff version of this method will include otherwise hidden records.",
1641 { desc => 'Metarecord ID', type => 'number' },
1642 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1645 desc => 'Results object like {count => $i, ids =>[...]}',
1653 sub biblio_mrid_to_record_ids {
1654 my( $self, $client, $mrid, $args ) = @_;
1656 my $format = $$args{format};
1657 my $org = $$args{org};
1658 my $depth = $$args{depth};
1660 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1661 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1662 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1664 return { count => scalar(@$recs), ids => $recs };
1668 __PACKAGE__->register_method(
1669 method => "biblio_record_to_marc_html",
1670 api_name => "open-ils.search.biblio.record.html"
1673 __PACKAGE__->register_method(
1674 method => "biblio_record_to_marc_html",
1675 api_name => "open-ils.search.authority.to_html"
1678 # Persistent parsers and setting objects
1679 my $parser = XML::LibXML->new();
1680 my $xslt = XML::LibXSLT->new();
1682 my $slim_marc_sheet;
1683 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1685 sub biblio_record_to_marc_html {
1686 my($self, $client, $recordid, $slim, $marcxml) = @_;
1689 my $dir = $settings_client->config_value("dirs", "xsl");
1692 unless($slim_marc_sheet) {
1693 my $xsl = $settings_client->config_value(
1694 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1696 $xsl = $parser->parse_file("$dir/$xsl");
1697 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1700 $sheet = $slim_marc_sheet;
1704 unless($marc_sheet) {
1705 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1706 my $xsl = $settings_client->config_value(
1707 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1708 $xsl = $parser->parse_file("$dir/$xsl");
1709 $marc_sheet = $xslt->parse_stylesheet($xsl);
1711 $sheet = $marc_sheet;
1716 my $e = new_editor();
1717 if($self->api_name =~ /authority/) {
1718 $record = $e->retrieve_authority_record_entry($recordid)
1719 or return $e->event;
1721 $record = $e->retrieve_biblio_record_entry($recordid)
1722 or return $e->event;
1724 $marcxml = $record->marc;
1727 my $xmldoc = $parser->parse_string($marcxml);
1728 my $html = $sheet->transform($xmldoc);
1729 return $html->documentElement->toString();
1732 __PACKAGE__->register_method(
1733 method => "format_biblio_record_entry",
1734 api_name => "open-ils.search.biblio.record.print",
1736 desc => 'Returns a printable version of the specified bib record',
1738 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1741 desc => q/An action_trigger.event object or error event./,
1746 __PACKAGE__->register_method(
1747 method => "format_biblio_record_entry",
1748 api_name => "open-ils.search.biblio.record.email",
1750 desc => 'Emails an A/T templated version of the specified bib records to the authorized user',
1752 { desc => 'Authentication token', type => 'string'},
1753 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1756 desc => q/Undefined on success, otherwise an error event./,
1762 sub format_biblio_record_entry {
1763 my($self, $conn, $arg1, $arg2) = @_;
1765 my $for_print = ($self->api_name =~ /print/);
1766 my $for_email = ($self->api_name =~ /email/);
1768 my $e; my $auth; my $bib_id; my $context_org;
1772 $context_org = $arg2 || $U->fetch_org_tree->id;
1773 $e = new_editor(xact => 1);
1774 } elsif ($for_email) {
1777 $e = new_editor(authtoken => $auth, xact => 1);
1778 return $e->die_event unless $e->checkauth;
1779 $context_org = $e->requestor->home_ou;
1783 if (ref $bib_id ne 'ARRAY') {
1784 $bib_ids = [ $bib_id ];
1789 my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1790 $bucket->btype('temp');
1791 $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1793 $bucket->owner($e->requestor)
1797 my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1799 for my $id (@$bib_ids) {
1801 my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1803 my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1804 $bucket_entry->target_biblio_record_entry($bib);
1805 $bucket_entry->bucket($bucket_obj->id);
1806 $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
1813 return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
1815 } elsif ($for_email) {
1817 $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
1824 __PACKAGE__->register_method(
1825 method => "retrieve_all_copy_statuses",
1826 api_name => "open-ils.search.config.copy_status.retrieve.all"
1829 sub retrieve_all_copy_statuses {
1830 my( $self, $client ) = @_;
1831 return new_editor()->retrieve_all_config_copy_status();
1835 __PACKAGE__->register_method(
1836 method => "copy_counts_per_org",
1837 api_name => "open-ils.search.biblio.copy_counts.retrieve"
1840 __PACKAGE__->register_method(
1841 method => "copy_counts_per_org",
1842 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1845 sub copy_counts_per_org {
1846 my( $self, $client, $record_id ) = @_;
1848 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1850 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1851 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1853 my $counts = $apputils->simple_scalar_request(
1854 "open-ils.storage", $method, $record_id );
1856 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1861 __PACKAGE__->register_method(
1862 method => "copy_count_summary",
1863 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
1864 notes => "returns an array of these: "
1865 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1866 . "where statusx is a copy status name. The statuses are sorted by ID.",
1870 sub copy_count_summary {
1871 my( $self, $client, $rid, $org, $depth ) = @_;
1874 my $data = $U->storagereq(
1875 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
1877 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1880 __PACKAGE__->register_method(
1881 method => "copy_location_count_summary",
1882 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
1883 notes => "returns an array of these: "
1884 . "[ org_id, callnumber_label, copy_location, <status1_count>, <status2_count>,...] "
1885 . "where statusx is a copy status name. The statuses are sorted by ID.",
1888 sub copy_location_count_summary {
1889 my( $self, $client, $rid, $org, $depth ) = @_;
1892 my $data = $U->storagereq(
1893 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1895 return [ sort { $a->[1] cmp $b->[1] || $a->[2] cmp $b->[2] } @$data ];
1898 __PACKAGE__->register_method(
1899 method => "copy_count_location_summary",
1900 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
1901 notes => "returns an array of these: "
1902 . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1903 . "where statusx is a copy status name. The statuses are sorted by ID."
1906 sub copy_count_location_summary {
1907 my( $self, $client, $rid, $org, $depth ) = @_;
1910 my $data = $U->storagereq(
1911 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1912 return [ sort { $a->[1] cmp $b->[1] } @$data ];
1916 foreach (qw/open-ils.search.biblio.marc
1917 open-ils.search.biblio.marc.staff/)
1919 __PACKAGE__->register_method(
1920 method => "marc_search",
1923 desc => 'Fetch biblio IDs based on MARC record criteria. '
1924 . 'As usual, the .staff version of the search includes otherwise hidden records',
1927 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
1928 'See perldoc ' . __PACKAGE__ . ' for more detail.',
1931 {desc => 'limit (optional)', type => 'number'},
1932 {desc => 'offset (optional)', type => 'number'}
1935 desc => 'Results object like: { "count": $i, "ids": [...] }',
1942 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
1944 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
1946 searches: complex query object (required)
1947 org_unit: The org ID to focus the search at
1948 depth : The org depth
1949 limit : integer search limit default: 10
1950 offset : integer search offset default: 0
1951 sort : What field to sort the results on? [ author | title | pubdate ]
1952 sort_dir: In what direction do we sort? [ asc | desc ]
1954 Additional keys to refine search criteria:
1957 language : Language (code)
1958 lit_form : Literary form
1959 item_form: Item form
1960 item_type: Item type
1961 format : The MARC format
1963 Please note that the specific strings to be used in the "addtional keys" will be entirely
1964 dependent on your loaded data.
1966 All keys except "searches" are optional.
1967 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
1969 For example, an arg hash might look like:
1991 The arghash is eventually passed to the SRF call:
1992 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
1994 Presently, search uses the cache unconditionally.
1998 # FIXME: that example above isn't actually tested.
1999 # TODO: docache option?
2001 my( $self, $conn, $args, $limit, $offset ) = @_;
2003 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2004 $method .= ".staff" if $self->api_name =~ /staff/;
2005 $method .= ".atomic";
2007 $limit ||= 10; # FIXME: what about $args->{limit} ?
2008 $offset ||= 0; # FIXME: what about $args->{offset} ?
2011 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2012 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2014 my $recs = search_cache($ckey, $offset, $limit);
2017 $recs = $U->storagereq($method, %$args) || [];
2019 put_cache($ckey, scalar(@$recs), $recs);
2020 $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
2027 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2028 my @recs = map { $_->[0] } @$recs;
2030 return { ids => \@recs, count => $count };
2034 __PACKAGE__->register_method(
2035 method => "biblio_search_isbn",
2036 api_name => "open-ils.search.biblio.isbn",
2038 desc => 'Retrieve biblio IDs for a given ISBN',
2040 {desc => 'ISBN', type => 'string'} # or number maybe? How normalized is our storage data?
2043 desc => 'Results object like: { "count": $i, "ids": [...] }',
2049 sub biblio_search_isbn {
2050 my( $self, $client, $isbn ) = @_;
2051 $logger->debug("Searching ISBN $isbn");
2052 my $recs = $U->storagereq('open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn);
2053 return { ids => $recs, count => scalar(@$recs) };
2056 __PACKAGE__->register_method(
2057 method => "biblio_search_isbn_batch",
2058 api_name => "open-ils.search.biblio.isbn_list",
2061 sub biblio_search_isbn_batch {
2062 my( $self, $client, $isbn_list ) = @_;
2063 $logger->debug("Searching ISBNs @$isbn_list");
2064 my @recs = (); my %rec_set = ();
2065 foreach my $isbn ( @$isbn_list ) {
2066 foreach my $rec ( @{ $U->storagereq(
2067 'open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn )
2069 if (! $rec_set{ $rec }) {
2070 $rec_set{ $rec } = 1;
2075 return { ids => \@recs, count => scalar(@recs) };
2078 __PACKAGE__->register_method(
2079 method => "biblio_search_issn",
2080 api_name => "open-ils.search.biblio.issn",
2082 desc => 'Retrieve biblio IDs for a given ISSN',
2084 {desc => 'ISBN', type => 'string'}
2087 desc => 'Results object like: { "count": $i, "ids": [...] }',
2093 sub biblio_search_issn {
2094 my( $self, $client, $issn ) = @_;
2095 $logger->debug("Searching ISSN $issn");
2096 my $e = new_editor();
2098 my $recs = $U->storagereq(
2099 'open-ils.storage.id_list.biblio.record_entry.search.issn.atomic', $issn );
2100 return { ids => $recs, count => scalar(@$recs) };
2104 __PACKAGE__->register_method(
2105 method => "fetch_mods_by_copy",
2106 api_name => "open-ils.search.biblio.mods_from_copy",
2109 desc => 'Retrieve MODS record given an attached copy ID',
2111 { desc => 'Copy ID', type => 'number' }
2114 desc => 'MODS record, event on error or uncataloged item'
2119 sub fetch_mods_by_copy {
2120 my( $self, $client, $copyid ) = @_;
2121 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2122 return $evt if $evt;
2123 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2124 return $apputils->record_to_mvr($record);
2128 # -------------------------------------------------------------------------------------
2130 __PACKAGE__->register_method(
2131 method => "cn_browse",
2132 api_name => "open-ils.search.callnumber.browse.target",
2133 notes => "Starts a callnumber browse"
2136 __PACKAGE__->register_method(
2137 method => "cn_browse",
2138 api_name => "open-ils.search.callnumber.browse.page_up",
2139 notes => "Returns the previous page of callnumbers",
2142 __PACKAGE__->register_method(
2143 method => "cn_browse",
2144 api_name => "open-ils.search.callnumber.browse.page_down",
2145 notes => "Returns the next page of callnumbers",
2149 # RETURNS array of arrays like so: label, owning_lib, record, id
2151 my( $self, $client, @params ) = @_;
2154 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
2155 if( $self->api_name =~ /target/ );
2156 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2157 if( $self->api_name =~ /page_up/ );
2158 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2159 if( $self->api_name =~ /page_down/ );
2161 return $apputils->simplereq( 'open-ils.storage', $method, @params );
2163 # -------------------------------------------------------------------------------------
2165 __PACKAGE__->register_method(
2166 method => "fetch_cn",
2167 api_name => "open-ils.search.callnumber.retrieve",
2169 notes => "retrieves a callnumber based on ID",
2173 my( $self, $client, $id ) = @_;
2174 my( $cn, $evt ) = $apputils->fetch_callnumber( $id );
2175 return $evt if $evt;
2179 __PACKAGE__->register_method(
2180 method => "fetch_copy_by_cn",
2181 api_name => 'open-ils.search.copies_by_call_number.retrieve',
2183 Returns an array of copy ID's by callnumber ID
2184 @param cnid The callnumber ID
2185 @return An array of copy IDs
2189 sub fetch_copy_by_cn {
2190 my( $self, $conn, $cnid ) = @_;
2191 return $U->cstorereq(
2192 'open-ils.cstore.direct.asset.copy.id_list.atomic',
2193 { call_number => $cnid, deleted => 'f' } );
2196 __PACKAGE__->register_method(
2197 method => 'fetch_cn_by_info',
2198 api_name => 'open-ils.search.call_number.retrieve_by_info',
2200 @param label The callnumber label
2201 @param record The record the cn is attached to
2202 @param org The owning library of the cn
2203 @return The callnumber object
2208 sub fetch_cn_by_info {
2209 my( $self, $conn, $label, $record, $org ) = @_;
2210 return $U->cstorereq(
2211 'open-ils.cstore.direct.asset.call_number.search',
2212 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2217 __PACKAGE__->register_method(
2218 method => 'bib_extras',
2219 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all'
2221 __PACKAGE__->register_method(
2222 method => 'bib_extras',
2223 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all'
2225 __PACKAGE__->register_method(
2226 method => 'bib_extras',
2227 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all'
2229 __PACKAGE__->register_method(
2230 method => 'bib_extras',
2231 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all'
2233 __PACKAGE__->register_method(
2234 method => 'bib_extras',
2235 api_name => 'open-ils.search.biblio.audience_map.retrieve.all'
2241 my $e = new_editor();
2243 return $e->retrieve_all_config_lit_form_map()
2244 if( $self->api_name =~ /lit_form/ );
2246 return $e->retrieve_all_config_item_form_map()
2247 if( $self->api_name =~ /item_form_map/ );
2249 return $e->retrieve_all_config_item_type_map()
2250 if( $self->api_name =~ /item_type_map/ );
2252 return $e->retrieve_all_config_bib_level_map()
2253 if( $self->api_name =~ /bib_level_map/ );
2255 return $e->retrieve_all_config_audience_map()
2256 if( $self->api_name =~ /audience_map/ );
2263 __PACKAGE__->register_method(
2264 method => 'fetch_slim_record',
2265 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2267 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2269 { desc => 'Array of Record IDs', type => 'array' }
2272 desc => 'Array of biblio records, event on error'
2277 sub fetch_slim_record {
2278 my( $self, $conn, $ids ) = @_;
2280 #my $editor = OpenILS::Utils::Editor->new;
2281 my $editor = new_editor();
2284 return $editor->event unless
2285 my $r = $editor->retrieve_biblio_record_entry($_);
2294 __PACKAGE__->register_method(
2295 method => 'rec_to_mr_rec_descriptors',
2296 api_name => 'open-ils.search.metabib.record_to_descriptors',
2298 specialized method...
2299 Given a biblio record id or a metarecord id,
2300 this returns a list of metabib.record_descriptor
2301 objects that live within the same metarecord
2302 @param args Object of args including:
2306 sub rec_to_mr_rec_descriptors {
2307 my( $self, $conn, $args ) = @_;
2309 my $rec = $$args{record};
2310 my $mrec = $$args{metarecord};
2311 my $item_forms = $$args{item_forms};
2312 my $item_types = $$args{item_types};
2313 my $item_lang = $$args{item_lang};
2315 my $e = new_editor();
2319 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2320 return $e->event unless @$map;
2321 $mrec = $$map[0]->metarecord;
2324 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2325 return $e->event unless @$recs;
2327 my @recs = map { $_->source } @$recs;
2328 my $search = { record => \@recs };
2329 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2330 $search->{item_type} = $item_types if $item_types and @$item_types;
2331 $search->{item_lang} = $item_lang if $item_lang;
2333 my $desc = $e->search_metabib_record_descriptor($search);
2335 return { metarecord => $mrec, descriptors => $desc };
2339 __PACKAGE__->register_method(
2340 method => 'fetch_age_protect',
2341 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2344 sub fetch_age_protect {
2345 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2349 __PACKAGE__->register_method(
2350 method => 'copies_by_cn_label',
2351 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2354 __PACKAGE__->register_method(
2355 method => 'copies_by_cn_label',
2356 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2359 sub copies_by_cn_label {
2360 my( $self, $conn, $record, $label, $circ_lib ) = @_;
2361 my $e = new_editor();
2362 my $cns = $e->search_asset_call_number({record => $record, label => $label, deleted => 'f'}, {idlist=>1});
2363 return [] unless @$cns;
2365 # show all non-deleted copies in the staff client ...
2366 if ($self->api_name =~ /staff$/o) {
2367 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2370 # ... otherwise, grab the copies ...
2371 my $copies = $e->search_asset_copy(
2372 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2373 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2377 # ... and test for location and status visibility
2378 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];