1 package OpenILS::Application::Storage::Publisher::metabib;
2 use base qw/OpenILS::Application::Storage::Publisher/;
4 use OpenSRF::EX qw/:try/;
5 use OpenILS::Application::Storage::FTS;
6 use OpenILS::Utils::Fieldmapper;
7 use OpenSRF::Utils::Logger qw/:level/;
8 use OpenILS::Application::AppUtils;
9 use OpenSRF::Utils::Cache;
10 use OpenSRF::Utils::JSON;
11 use List::MoreUtils qw(uniq);
13 use Digest::MD5 qw/md5_hex/;
15 use OpenILS::Application::Storage::QueryParser;
17 my $U = 'OpenILS::Application::AppUtils';
19 my $log = 'OpenSRF::Utils::Logger';
23 sub _initialize_parser {
26 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
28 config_record_attr_index_norm_map =>
30 'open-ils.cstore.direct.config.record_attr_index_norm_map.search.atomic',
31 { id => { "!=" => undef } },
32 { flesh => 1, flesh_fields => { crainm => [qw/norm/] }, order_by => [{ class => "crainm", field => "pos" }] }
34 search_relevance_adjustment =>
36 'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
37 { id => { "!=" => undef } }
39 config_metabib_field =>
41 'open-ils.cstore.direct.config.metabib_field.search.atomic',
42 { id => { "!=" => undef } }
44 config_metabib_field_virtual_map =>
46 'open-ils.cstore.direct.config.metabib_field_virtual_map.search.atomic',
47 { id => { "!=" => undef } }
49 config_metabib_search_alias =>
51 'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
52 { alias => { "!=" => undef } }
54 config_metabib_field_index_norm_map =>
56 'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
57 { id => { "!=" => undef } },
58 { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
60 config_record_attr_definition =>
62 'open-ils.cstore.direct.config.record_attr_definition.search.atomic',
63 { name => { "!=" => undef } }
65 config_metabib_class_ts_map =>
67 'open-ils.cstore.direct.config.metabib_class_ts_map.search.atomic',
70 config_metabib_field_ts_map =>
72 'open-ils.cstore.direct.config.metabib_field_ts_map.search.atomic',
75 config_metabib_class =>
77 'open-ils.cstore.direct.config.metabib_class.search.atomic',
78 { name => { "!=" => undef } }
83 my $cgf = $cstore->request(
84 'open-ils.cstore.direct.config.global_flag.retrieve',
85 'search.max_popularity_importance_multiplier'
87 $max_mult = $cgf->value if $cgf && $U->is_true($cgf->enabled);
89 $max_mult = 2.0 unless $max_mult =~ /^-?(?:\d+\.?|\.\d)\d*\z/; # just in case
90 $parser->max_popularity_importance_multiplier($max_mult);
91 $parser->dbh(biblio::record_entry->db_Main);
94 die("Cannot initialize $parser!") unless ($parser->initialization_complete);
97 sub ordered_records_from_metarecord { # XXX Replace with QP-based search-within-MR
101 my $formats = shift; # dead
105 my $copies_visible = 'LEFT JOIN asset.copy_vis_attr_cache vc ON (br.id = vc.record '.
106 'AND vc.vis_attr_vector @@ (SELECT c_attrs::query_int FROM asset.patron_default_visibility_mask() LIMIT 1))';
107 $copies_visible = '' if ($self->api_name =~ /staff/o);
109 my $copies_visible_count = ',COUNT(vc.id)';
110 $copies_visible_count = '' if ($self->api_name =~ /staff/o);
112 my $descendants = '';
114 $descendants = defined($depth) ?
115 ",actor.org_unit_descendants($org, $depth) d" :
116 ",actor.org_unit_descendants($org) d" ;
123 $copies_visible_count
124 FROM metabib.metarecord_source_map sm
125 JOIN biblio.record_entry br ON (sm.source = br.id AND NOT br.deleted)
126 LEFT JOIN metabib.record_sorter s ON (s.source = br.id AND s.attr = 'titlesort')
127 LEFT JOIN config.bib_source bs ON (br.source = bs.id)
130 WHERE sm.metarecord = ?
134 if ($copies_visible) {
135 $sql .= 'AND (bs.transcendant OR ';
137 $sql .= 'vc.circ_lib = d.id)';
139 $sql .= 'vc.id IS NOT NULL)'
141 $having = 'HAVING COUNT(vc.id) > 0';
149 s.value ASC NULLS LAST
152 my $ids = metabib::metarecord_source_map->db_Main->selectcol_arrayref($sql, {}, "$mr");
153 return $ids if ($self->api_name =~ /atomic$/o);
155 $client->respond( $_ ) for ( @$ids );
159 __PACKAGE__->register_method(
160 api_name => 'open-ils.storage.ordered.metabib.metarecord.records',
162 method => 'ordered_records_from_metarecord',
166 __PACKAGE__->register_method(
167 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff',
169 method => 'ordered_records_from_metarecord',
174 __PACKAGE__->register_method(
175 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.atomic',
177 method => 'ordered_records_from_metarecord',
181 __PACKAGE__->register_method(
182 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic',
184 method => 'ordered_records_from_metarecord',
189 # XXX: this subroutine and its two registered methods are marked for
190 # deprecation, as they do not work properly in 2.x (these tags are no longer
191 # normalized in mfr) and are not in known use
195 my $isxn = lc(shift());
199 $isxn =~ s/-//o if ($self->api_name =~ /isbn/o);
201 my $tag = ($self->api_name =~ /isbn/o) ? "'020' OR f.tag = '024'" : "'022'";
203 my $fr_table = metabib::full_rec->table;
204 my $bib_table = biblio::record_entry->table;
207 SELECT DISTINCT f.record
209 JOIN $bib_table b ON (b.id = f.record)
212 AND b.deleted IS FALSE
215 my $list = metabib::full_rec->db_Main->selectcol_arrayref($sql, {}, "$isxn%");
216 $client->respond($_) for (@$list);
219 __PACKAGE__->register_method(
220 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
222 method => 'isxn_search',
226 __PACKAGE__->register_method(
227 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.issn',
229 method => 'isxn_search',
234 sub metarecord_copy_count {
240 my $sm_table = metabib::metarecord_source_map->table;
241 my $rd_table = metabib::record_descriptor->table;
242 my $cn_table = asset::call_number->table;
243 my $cp_table = asset::copy->table;
244 my $br_table = biblio::record_entry->table;
245 my $src_table = config::bib_source->table;
246 my $cl_table = asset::copy_location->table;
247 my $cs_table = config::copy_status->table;
248 my $out_table = actor::org_unit_type->table;
250 my $descendants = "actor.org_unit_descendants(u.id)";
251 my $ancestors = "actor.org_unit_ancestors(?) u JOIN $out_table t ON (u.ou_type = t.id)";
253 if ($args{org_unit} < 0) {
254 $args{org_unit} *= -1;
255 $ancestors = "(select org_unit as id from actor.org_lasso_map where lasso = ?) u CROSS JOIN (SELECT -1 AS depth) t";
258 my $copies_visible = 'AND a.opac_visible IS TRUE AND cp.opac_visible IS TRUE AND cs.opac_visible IS TRUE AND cl.opac_visible IS TRUE';
259 $copies_visible = '' if ($self->api_name =~ /staff/o);
261 my (@types,@forms,@blvl);
262 my ($t_filter, $f_filter, $b_filter) = ('','','');
265 my ($t, $f, $b) = split '-', $args{format};
266 @types = split '', $t;
267 @forms = split '', $f;
268 @blvl = split '', $b;
271 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
275 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
279 $b_filter .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
289 JOIN $cn_table cn ON (cn.record = r.source)
290 JOIN $rd_table rd ON (cn.record = rd.record)
291 JOIN $cp_table cp ON (cn.id = cp.call_number)
292 JOIN $cs_table cs ON (cp.status = cs.id)
293 JOIN $cl_table cl ON (cp.location = cl.id)
294 JOIN $descendants a ON (cp.circ_lib = a.id)
295 WHERE r.metarecord = ?
296 AND cn.deleted IS FALSE
297 AND cp.deleted IS FALSE
307 JOIN $cn_table cn ON (cn.record = r.source)
308 JOIN $rd_table rd ON (cn.record = rd.record)
309 JOIN $cp_table cp ON (cn.id = cp.call_number)
310 JOIN $cs_table cs ON (cp.status = cs.id)
311 JOIN $cl_table cl ON (cp.location = cl.id)
312 JOIN $descendants a ON (cp.circ_lib = a.id)
313 WHERE r.metarecord = ?
314 AND cp.status IN (0,7,12)
315 AND cn.deleted IS FALSE
316 AND cp.deleted IS FALSE
326 JOIN $cn_table cn ON (cn.record = r.source)
327 JOIN $rd_table rd ON (cn.record = rd.record)
328 JOIN $cp_table cp ON (cn.id = cp.call_number)
329 JOIN $cs_table cs ON (cp.status = cs.id)
330 JOIN $cl_table cl ON (cp.location = cl.id)
331 WHERE r.metarecord = ?
332 AND cn.deleted IS FALSE
333 AND cp.deleted IS FALSE
334 AND cp.opac_visible IS TRUE
335 AND cs.opac_visible IS TRUE
336 AND cl.opac_visible IS TRUE
345 JOIN $br_table br ON (br.id = r.source)
346 JOIN $src_table src ON (src.id = br.source)
347 WHERE r.metarecord = ?
348 AND src.transcendant IS TRUE
356 my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
357 $sth->execute( ''.$args{metarecord},
361 ''.$args{metarecord},
365 ''.$args{metarecord},
369 ''.$args{metarecord},
373 while ( my $row = $sth->fetchrow_hashref ) {
374 $client->respond( $row );
378 __PACKAGE__->register_method(
379 api_name => 'open-ils.storage.metabib.metarecord.copy_count',
381 method => 'metarecord_copy_count',
386 __PACKAGE__->register_method(
387 api_name => 'open-ils.storage.metabib.metarecord.copy_count.staff',
389 method => 'metarecord_copy_count',
395 sub biblio_multi_search_full_rec {
400 my $class_join = $args{class_join} || 'AND';
401 my $limit = $args{limit} || 100;
402 my $offset = $args{offset} || 0;
403 my $sort = $args{'sort'};
404 my $sort_dir = $args{sort_dir} || 'DESC';
409 for my $arg (@{ $args{searches} }) {
410 my $term = $$arg{term};
411 my $limiters = $$arg{restrict};
413 my ($index_col) = metabib::full_rec->columns('FTS');
414 $index_col ||= 'value';
415 my $search_table = metabib::full_rec->table;
417 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
419 my $fts_where = $fts->sql_where_clause();
420 my @fts_ranks = $fts->fts_rank;
422 my $rank = join(' + ', @fts_ranks);
425 for my $limit (@$limiters) {
426 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
427 # MARC control field; mfr.subfield is NULL
428 push @wheres, "( tag = ? AND $fts_where )";
429 push @binds, $$limit{tag};
430 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
432 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
433 push @binds, $$limit{tag}, $$limit{subfield};
434 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
437 my $where = join(' OR ', @wheres);
439 push @selects, "SELECT record, AVG($rank) as sum FROM $search_table WHERE $where GROUP BY record";
443 my $descendants = defined($args{depth}) ?
444 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
445 "actor.org_unit_descendants($args{org_unit})" ;
448 my $metabib_record_descriptor = metabib::record_descriptor->table;
449 my $metabib_full_rec = metabib::full_rec->table;
450 my $asset_call_number_table = asset::call_number->table;
451 my $asset_copy_table = asset::copy->table;
452 my $cs_table = config::copy_status->table;
453 my $cl_table = asset::copy_location->table;
454 my $br_table = biblio::record_entry->table;
457 $cj = 'HAVING COUNT(x.record) = ' . scalar(@selects) if ($class_join eq 'AND');
460 '(SELECT x.record, sum(x.sum) FROM (('.
461 join(') UNION ALL (', @selects).
462 ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
464 my $has_vols = 'AND cn.owning_lib = d.id';
465 my $has_copies = 'AND cp.call_number = cn.id';
466 my $copies_visible = 'AND d.opac_visible IS TRUE AND cp.opac_visible IS TRUE AND cs.opac_visible IS TRUE AND cl.opac_visible IS TRUE';
468 if ($self->api_name =~ /staff/o) {
469 # Staff want to see all copies regardless of visibility
470 $copies_visible = '';
471 # When searching globally for staff avoid any copy filtering.
472 if ((defined $args{depth} && $args{depth} == 0)
473 || $args{org_unit} == $U->get_org_tree->id) {
479 my ($t_filter, $f_filter) = ('','');
480 my ($a_filter, $l_filter, $lf_filter) = ('','','');
483 if (my $a = $args{audience}) {
484 $a = [$a] if (!ref($a));
487 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
492 if (my $l = $args{language}) {
493 $l = [$l] if (!ref($l));
496 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
501 if (my $f = $args{lit_form}) {
502 $f = [$f] if (!ref($f));
505 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
506 push @binds, @lit_form;
510 if (my $f = $args{item_form}) {
511 $f = [$f] if (!ref($f));
514 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
519 if (my $t = $args{item_type}) {
520 $t = [$t] if (!ref($t));
523 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
530 my ($t, $f) = split '-', $args{format};
531 my @types = split '', $t;
532 my @forms = split '', $f;
534 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
539 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
542 push @binds, @types, @forms;
545 my $relevance = 'sum(f.sum)';
546 $relevance = 1 if (!$copies_visible);
548 my $string_default_sort = 'zzzz';
549 $string_default_sort = 'AAAA' if ($sort_dir =~ /^DESC$/i);
551 my $number_default_sort = '9999';
552 $number_default_sort = '0000' if ($sort_dir =~/^DESC$/i);
554 my $rank = $relevance;
555 if (lc($sort) eq 'pubdate') {
558 SELECT COALESCE(SUBSTRING(MAX(frp.value) FROM E'\\\\d{4}'), '$number_default_sort')::INT
559 FROM $metabib_full_rec frp
560 WHERE frp.record = f.record
562 AND frp.subfield = 'c'
566 } elsif (lc($sort) eq 'create_date') {
568 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
570 } elsif (lc($sort) eq 'edit_date') {
572 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
574 } elsif (lc($sort) =~ /^title/i) {
577 SELECT COALESCE(LTRIM(SUBSTR(MAX(frt.value), COALESCE(SUBSTRING(MAX(frt.ind2) FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
578 FROM $metabib_full_rec frt
579 WHERE frt.record = f.record
581 AND frt.subfield = 'a'
585 } elsif (lc($sort) =~ /^author/i) {
588 SELECT COALESCE(LTRIM(MAX(query.value)), '$string_default_sort')
591 FROM $metabib_full_rec fra
592 WHERE fra.record = f.record
593 AND fra.tag LIKE '1%'
594 AND fra.subfield = 'a'
595 ORDER BY fra.tag::text::int
604 my $rd_join = $use_rd ? "$metabib_record_descriptor rd," : '';
605 my $rd_filter = $use_rd ? 'AND rd.record = f.record' : '';
609 SELECT f.record, $relevance, count(DISTINCT cp.id), $rank
610 FROM $search_table f,
611 $asset_call_number_table cn,
612 $asset_copy_table cp,
618 WHERE br.id = f.record
619 AND cn.record = f.record
620 AND cp.status = cs.id
621 AND cp.location = cl.id
622 AND br.deleted IS FALSE
623 AND cn.deleted IS FALSE
624 AND cp.deleted IS FALSE
634 GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
635 ORDER BY 4 $sort_dir,3 DESC
639 SELECT f.record, 1, 1, $rank
640 FROM $search_table f,
643 WHERE br.id = f.record
644 AND br.deleted IS FALSE
657 $log->debug("Search SQL :: [$select]",DEBUG);
659 my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
660 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
663 $max = 1 if (!@$recs);
665 $max = $$_[1] if ($$_[1] > $max);
669 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
670 next unless ($$rec[0]);
671 my ($rid,$rank,$junk,$skip) = @$rec;
672 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
676 __PACKAGE__->register_method(
677 api_name => 'open-ils.storage.biblio.full_rec.multi_search',
679 method => 'biblio_multi_search_full_rec',
684 __PACKAGE__->register_method(
685 api_name => 'open-ils.storage.biblio.full_rec.multi_search.staff',
687 method => 'biblio_multi_search_full_rec',
693 sub search_full_rec {
699 my $term = $args{term};
700 my $limiters = $args{restrict};
702 my ($index_col) = metabib::full_rec->columns('FTS');
703 $index_col ||= 'value';
704 my $search_table = metabib::full_rec->table;
706 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
708 my $fts_where = $fts->sql_where_clause();
709 my @fts_ranks = $fts->fts_rank;
711 my $rank = join(' + ', @fts_ranks);
715 for my $limit (@$limiters) {
716 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
717 # MARC control field; mfr.subfield is NULL
718 push @wheres, "( tag = ? AND $fts_where )";
719 push @binds, $$limit{tag};
720 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
722 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
723 push @binds, $$limit{tag}, $$limit{subfield};
724 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
727 my $where = join(' OR ', @wheres);
729 my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
731 $log->debug("Search SQL :: [$select]",DEBUG);
733 my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
734 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
736 $client->respond($_) for (@$recs);
739 __PACKAGE__->register_method(
740 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
742 method => 'search_full_rec',
747 __PACKAGE__->register_method(
748 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
750 method => 'search_full_rec',
757 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
758 sub search_class_fts {
763 my $term = $args{term};
764 my $ou = $args{org_unit};
765 my $ou_type = $args{depth};
766 my $limit = $args{limit};
767 my $offset = $args{offset};
769 my $limit_clause = '';
770 my $offset_clause = '';
772 $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
773 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
776 my ($t_filter, $f_filter) = ('','');
779 my ($t, $f) = split '-', $args{format};
780 @types = split '', $t;
781 @forms = split '', $f;
783 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
787 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
793 my $descendants = defined($ou_type) ?
794 "actor.org_unit_descendants($ou, $ou_type)" :
795 "actor.org_unit_descendants($ou)";
797 my $class = $self->{cdbi};
798 my $search_table = $class->table;
800 my $metabib_record_descriptor = metabib::record_descriptor->table;
801 my $metabib_metarecord = metabib::metarecord->table;
802 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
803 my $asset_call_number_table = asset::call_number->table;
804 my $asset_copy_table = asset::copy->table;
805 my $cs_table = config::copy_status->table;
806 my $cl_table = asset::copy_location->table;
808 my ($index_col) = $class->columns('FTS');
809 $index_col ||= 'value';
811 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
812 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
814 my $fts_where = $fts->sql_where_clause;
815 my @fts_ranks = $fts->fts_rank;
817 my $rank = join(' + ', @fts_ranks);
819 my $has_vols = 'AND cn.owning_lib = d.id';
820 my $has_copies = 'AND cp.call_number = cn.id';
821 my $copies_visible = 'AND d.opac_visible IS TRUE AND cp.opac_visible IS TRUE AND cs.opac_visible IS TRUE AND cl.opac_visible IS TRUE';
823 my $visible_count = ', count(DISTINCT cp.id)';
824 my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
826 if ($self->api_name =~ /staff/o) {
827 $copies_visible = '';
828 $visible_count_test = '';
829 $has_copies = '' if ($ou_type == 0);
830 $has_vols = '' if ($ou_type == 0);
833 my $rank_calc = <<" RANK";
835 * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
836 * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
837 * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
838 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
841 $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
843 if ($copies_visible) {
845 SELECT m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
846 FROM $search_table f,
847 $metabib_metarecord_source_map_table m,
848 $asset_call_number_table cn,
849 $asset_copy_table cp,
852 $metabib_record_descriptor rd,
855 AND m.source = f.source
856 AND cn.record = m.source
857 AND rd.record = m.source
858 AND cp.status = cs.id
859 AND cp.location = cl.id
865 GROUP BY 1 $visible_count_test
867 $limit_clause $offset_clause
871 SELECT m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
872 FROM $search_table f,
873 $metabib_metarecord_source_map_table m,
874 $metabib_record_descriptor rd
876 AND m.source = f.source
877 AND rd.record = m.source
882 $limit_clause $offset_clause
886 $log->debug("Field Search SQL :: [$select]",DEBUG);
888 my $SQLstring = join('%',$fts->words);
889 my $REstring = join('\\s+',$fts->words);
890 my $first_word = ($fts->words)[0].'%';
891 my $recs = ($self->api_name =~ /unordered/o) ?
892 $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
893 $class->db_Main->selectall_arrayref($select, {},
894 '%'.lc($SQLstring).'%', # phrase order match
895 lc($first_word), # first word match
896 '^\\s*'.lc($REstring).'\\s*/?\s*$', # full exact match
900 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
902 $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
906 for my $class ( qw/title author subject keyword series identifier/ ) {
907 __PACKAGE__->register_method(
908 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord",
910 method => 'search_class_fts',
913 cdbi => "metabib::${class}_field_entry",
916 __PACKAGE__->register_method(
917 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
919 method => 'search_class_fts',
922 cdbi => "metabib::${class}_field_entry",
925 __PACKAGE__->register_method(
926 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
928 method => 'search_class_fts',
931 cdbi => "metabib::${class}_field_entry",
934 __PACKAGE__->register_method(
935 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
937 method => 'search_class_fts',
940 cdbi => "metabib::${class}_field_entry",
945 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
946 sub search_class_fts_count {
951 my $term = $args{term};
952 my $ou = $args{org_unit};
953 my $ou_type = $args{depth};
954 my $limit = $args{limit} || 100;
955 my $offset = $args{offset} || 0;
957 my $descendants = defined($ou_type) ?
958 "actor.org_unit_descendants($ou, $ou_type)" :
959 "actor.org_unit_descendants($ou)";
962 my ($t_filter, $f_filter) = ('','');
965 my ($t, $f) = split '-', $args{format};
966 @types = split '', $t;
967 @forms = split '', $f;
969 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
973 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
978 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
980 my $class = $self->{cdbi};
981 my $search_table = $class->table;
983 my $metabib_record_descriptor = metabib::record_descriptor->table;
984 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
985 my $asset_call_number_table = asset::call_number->table;
986 my $asset_copy_table = asset::copy->table;
987 my $cs_table = config::copy_status->table;
988 my $cl_table = asset::copy_location->table;
990 my ($index_col) = $class->columns('FTS');
991 $index_col ||= 'value';
993 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
995 my $fts_where = $fts->sql_where_clause;
997 my $has_vols = 'AND cn.owning_lib = d.id';
998 my $has_copies = 'AND cp.call_number = cn.id';
999 my $copies_visible = 'AND d.opac_visible IS TRUE AND cp.opac_visible IS TRUE AND cs.opac_visible IS TRUE AND cl.opac_visible IS TRUE';
1000 if ($self->api_name =~ /staff/o) {
1001 $copies_visible = '';
1002 $has_vols = '' if ($ou_type == 0);
1003 $has_copies = '' if ($ou_type == 0);
1006 # XXX test an "EXISTS version of descendant checking...
1008 if ($copies_visible) {
1010 SELECT count(distinct m.metarecord)
1011 FROM $search_table f,
1012 $metabib_metarecord_source_map_table m,
1013 $metabib_metarecord_source_map_table mr,
1014 $asset_call_number_table cn,
1015 $asset_copy_table cp,
1018 $metabib_record_descriptor rd,
1021 AND mr.source = f.source
1022 AND mr.metarecord = m.metarecord
1023 AND cn.record = m.source
1024 AND rd.record = m.source
1025 AND cp.status = cs.id
1026 AND cp.location = cl.id
1035 SELECT count(distinct m.metarecord)
1036 FROM $search_table f,
1037 $metabib_metarecord_source_map_table m,
1038 $metabib_metarecord_source_map_table mr,
1039 $metabib_record_descriptor rd
1041 AND mr.source = f.source
1042 AND mr.metarecord = m.metarecord
1043 AND rd.record = m.source
1049 $log->debug("Field Search Count SQL :: [$select]",DEBUG);
1051 my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1053 $log->debug("Count Search yielded $recs results.",DEBUG);
1058 for my $class ( qw/title author subject keyword series identifier/ ) {
1059 __PACKAGE__->register_method(
1060 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
1062 method => 'search_class_fts_count',
1065 cdbi => "metabib::${class}_field_entry",
1068 __PACKAGE__->register_method(
1069 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
1071 method => 'search_class_fts_count',
1074 cdbi => "metabib::${class}_field_entry",
1080 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1081 sub postfilter_search_class_fts {
1086 my $term = $args{term};
1087 my $sort = $args{'sort'};
1088 my $sort_dir = $args{sort_dir} || 'DESC';
1089 my $ou = $args{org_unit};
1090 my $ou_type = $args{depth};
1091 my $limit = $args{limit} || 10;
1092 my $visibility_limit = $args{visibility_limit} || 5000;
1093 my $offset = $args{offset} || 0;
1095 my $outer_limit = 1000;
1097 my $limit_clause = '';
1098 my $offset_clause = '';
1100 $limit_clause = "LIMIT $outer_limit";
1101 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1103 my (@types,@forms,@lang,@aud,@lit_form);
1104 my ($t_filter, $f_filter) = ('','');
1105 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1106 my ($ot_filter, $of_filter) = ('','');
1107 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1109 if (my $a = $args{audience}) {
1110 $a = [$a] if (!ref($a));
1113 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1114 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1117 if (my $l = $args{language}) {
1118 $l = [$l] if (!ref($l));
1121 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1122 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1125 if (my $f = $args{lit_form}) {
1126 $f = [$f] if (!ref($f));
1129 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1130 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1133 if ($args{format}) {
1134 my ($t, $f) = split '-', $args{format};
1135 @types = split '', $t;
1136 @forms = split '', $f;
1138 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1139 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1143 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1144 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1149 my $descendants = defined($ou_type) ?
1150 "actor.org_unit_descendants($ou, $ou_type)" :
1151 "actor.org_unit_descendants($ou)";
1153 my $class = $self->{cdbi};
1154 my $search_table = $class->table;
1156 my $metabib_full_rec = metabib::full_rec->table;
1157 my $metabib_record_descriptor = metabib::record_descriptor->table;
1158 my $metabib_metarecord = metabib::metarecord->table;
1159 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1160 my $asset_call_number_table = asset::call_number->table;
1161 my $asset_copy_table = asset::copy->table;
1162 my $cs_table = config::copy_status->table;
1163 my $cl_table = asset::copy_location->table;
1164 my $br_table = biblio::record_entry->table;
1166 my ($index_col) = $class->columns('FTS');
1167 $index_col ||= 'value';
1169 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1171 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1173 my $SQLstring = join('%',map { lc($_) } $fts->words);
1174 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1175 my $first_word = lc(($fts->words)[0]).'%';
1177 my $fts_where = $fts->sql_where_clause;
1178 my @fts_ranks = $fts->fts_rank;
1181 $bonus{'metabib::identifier_field_entry'} =
1182 $bonus{'metabib::keyword_field_entry'} = [
1183 { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring }
1186 $bonus{'metabib::title_field_entry'} =
1187 $bonus{'metabib::series_field_entry'} = [
1188 { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1189 { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1190 @{ $bonus{'metabib::keyword_field_entry'} }
1193 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1194 $bonus_list ||= '1';
1196 my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1198 my $relevance = join(' + ', @fts_ranks);
1199 $relevance = <<" RANK";
1200 (SUM( ( $relevance ) * ( $bonus_list ) )/COUNT(m.source))
1203 my $string_default_sort = 'zzzz';
1204 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1206 my $number_default_sort = '9999';
1207 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1209 my $rank = $relevance;
1210 if (lc($sort) eq 'pubdate') {
1213 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1214 FROM $metabib_full_rec frp
1215 WHERE frp.record = mr.master_record
1217 AND frp.subfield = 'c'
1221 } elsif (lc($sort) eq 'create_date') {
1223 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1225 } elsif (lc($sort) eq 'edit_date') {
1227 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1229 } elsif (lc($sort) eq 'title') {
1232 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1233 FROM $metabib_full_rec frt
1234 WHERE frt.record = mr.master_record
1236 AND frt.subfield = 'a'
1240 } elsif (lc($sort) eq 'author') {
1243 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1244 FROM $metabib_full_rec fra
1245 WHERE fra.record = mr.master_record
1246 AND fra.tag LIKE '1%'
1247 AND fra.subfield = 'a'
1248 ORDER BY fra.tag::text::int
1256 my $select = <<" SQL";
1257 SELECT m.metarecord,
1259 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1261 FROM $search_table f,
1262 $metabib_metarecord_source_map_table m,
1263 $metabib_metarecord_source_map_table smrs,
1264 $metabib_metarecord mr,
1265 $metabib_record_descriptor rd
1267 AND smrs.metarecord = mr.id
1268 AND m.source = f.source
1269 AND m.metarecord = mr.id
1270 AND rd.record = smrs.source
1276 GROUP BY m.metarecord
1277 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1278 LIMIT $visibility_limit
1285 FROM $asset_call_number_table cn,
1286 $metabib_metarecord_source_map_table mrs,
1287 $asset_copy_table cp,
1292 $metabib_record_descriptor ord,
1294 WHERE mrs.metarecord = s.metarecord
1295 AND br.id = mrs.source
1296 AND cn.record = mrs.source
1297 AND cp.status = cs.id
1298 AND cp.location = cl.id
1299 AND cn.owning_lib = d.id
1300 AND cp.call_number = cn.id
1301 AND cp.opac_visible IS TRUE
1302 AND cs.opac_visible IS TRUE
1303 AND cl.opac_visible IS TRUE
1304 AND d.opac_visible IS TRUE
1305 AND br.active IS TRUE
1306 AND br.deleted IS FALSE
1307 AND ord.record = mrs.source
1313 ORDER BY 4 $sort_dir
1315 } elsif ($self->api_name !~ /staff/o) {
1322 FROM $asset_call_number_table cn,
1323 $metabib_metarecord_source_map_table mrs,
1324 $asset_copy_table cp,
1329 $metabib_record_descriptor ord
1331 WHERE mrs.metarecord = s.metarecord
1332 AND br.id = mrs.source
1333 AND cn.record = mrs.source
1334 AND cp.status = cs.id
1335 AND cp.location = cl.id
1336 AND cp.circ_lib = d.id
1337 AND cp.call_number = cn.id
1338 AND cp.opac_visible IS TRUE
1339 AND cs.opac_visible IS TRUE
1340 AND cl.opac_visible IS TRUE
1341 AND d.opac_visible IS TRUE
1342 AND br.active IS TRUE
1343 AND br.deleted IS FALSE
1344 AND ord.record = mrs.source
1352 ORDER BY 4 $sort_dir
1361 FROM $asset_call_number_table cn,
1362 $asset_copy_table cp,
1363 $metabib_metarecord_source_map_table mrs,
1366 $metabib_record_descriptor ord
1368 WHERE mrs.metarecord = s.metarecord
1369 AND br.id = mrs.source
1370 AND cn.record = mrs.source
1371 AND cn.id = cp.call_number
1372 AND br.deleted IS FALSE
1373 AND cn.deleted IS FALSE
1374 AND ord.record = mrs.source
1375 AND ( cn.owning_lib = d.id
1376 OR ( cp.circ_lib = d.id
1377 AND cp.deleted IS FALSE
1389 FROM $asset_call_number_table cn,
1390 $metabib_metarecord_source_map_table mrs,
1391 $metabib_record_descriptor ord
1392 WHERE mrs.metarecord = s.metarecord
1393 AND cn.record = mrs.source
1394 AND ord.record = mrs.source
1402 ORDER BY 4 $sort_dir
1407 $log->debug("Field Search SQL :: [$select]",DEBUG);
1409 my $recs = $class->db_Main->selectall_arrayref(
1411 (@bonus_values > 0 ? @bonus_values : () ),
1412 ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1413 @types, @forms, @aud, @lang, @lit_form,
1414 @types, @forms, @aud, @lang, @lit_form,
1415 ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1417 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1420 $max = 1 if (!@$recs);
1422 $max = $$_[1] if ($$_[1] > $max);
1425 my $count = scalar(@$recs);
1426 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1427 my ($mrid,$rank,$skip) = @$rec;
1428 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1433 for my $class ( qw/title author subject keyword series identifier/ ) {
1434 __PACKAGE__->register_method(
1435 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1437 method => 'postfilter_search_class_fts',
1440 cdbi => "metabib::${class}_field_entry",
1443 __PACKAGE__->register_method(
1444 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1446 method => 'postfilter_search_class_fts',
1449 cdbi => "metabib::${class}_field_entry",
1456 my $_cdbi = { title => "metabib::title_field_entry",
1457 author => "metabib::author_field_entry",
1458 subject => "metabib::subject_field_entry",
1459 keyword => "metabib::keyword_field_entry",
1460 series => "metabib::series_field_entry",
1461 identifier => "metabib::identifier_field_entry",
1464 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1465 sub postfilter_search_multi_class_fts {
1470 my $sort = $args{'sort'};
1471 my $sort_dir = $args{sort_dir} || 'DESC';
1472 my $ou = $args{org_unit};
1473 my $ou_type = $args{depth};
1474 my $limit = $args{limit} || 10;
1475 my $offset = $args{offset} || 0;
1476 my $visibility_limit = $args{visibility_limit} || 5000;
1479 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1482 if (!defined($args{org_unit})) {
1483 die "No target organizational unit passed to ".$self->api_name;
1486 if (! scalar( keys %{$args{searches}} )) {
1487 die "No search arguments were passed to ".$self->api_name;
1490 my $outer_limit = 1000;
1492 my $limit_clause = '';
1493 my $offset_clause = '';
1495 $limit_clause = "LIMIT $outer_limit";
1496 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1498 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1499 my ($t_filter, $f_filter, $v_filter) = ('','','');
1500 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1501 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1502 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1504 if ($args{available}) {
1505 $avail_filter = ' AND cp.status IN (0,7,12)';
1508 if (my $a = $args{audience}) {
1509 $a = [$a] if (!ref($a));
1512 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1513 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1516 if (my $l = $args{language}) {
1517 $l = [$l] if (!ref($l));
1520 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1521 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1524 if (my $f = $args{lit_form}) {
1525 $f = [$f] if (!ref($f));
1528 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1529 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1532 if (my $f = $args{item_form}) {
1533 $f = [$f] if (!ref($f));
1536 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1537 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1540 if (my $t = $args{item_type}) {
1541 $t = [$t] if (!ref($t));
1544 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1545 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1548 if (my $v = $args{vr_format}) {
1549 $v = [$v] if (!ref($v));
1552 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1553 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1557 # XXX legacy format and item type support
1558 if ($args{format}) {
1559 my ($t, $f) = split '-', $args{format};
1560 @types = split '', $t;
1561 @forms = split '', $f;
1563 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1564 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1568 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1569 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1575 my $descendants = defined($ou_type) ?
1576 "actor.org_unit_descendants($ou, $ou_type)" :
1577 "actor.org_unit_descendants($ou)";
1579 my $search_table_list = '';
1581 my $join_table_list = '';
1584 my $field_table = config::metabib_field->table;
1588 my $prev_search_group;
1589 my $curr_search_group;
1593 for my $search_group (sort keys %{$args{searches}}) {
1594 (my $search_group_name = $search_group) =~ s/\|/_/gso;
1595 ($search_class,$search_field) = split /\|/, $search_group;
1596 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1598 if ($search_field) {
1599 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1600 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1605 $prev_search_group = $curr_search_group if ($curr_search_group);
1607 $curr_search_group = $search_group_name;
1609 my $class = $_cdbi->{$search_class};
1610 my $search_table = $class->table;
1612 my ($index_col) = $class->columns('FTS');
1613 $index_col ||= 'value';
1616 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1618 my $fts_where = $fts->sql_where_clause;
1619 my @fts_ranks = $fts->fts_rank;
1621 my $SQLstring = join('%',map { lc($_) } $fts->words);
1622 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1623 my $first_word = lc(($fts->words)[0]).'%';
1625 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1626 my $rank = join(' + ', @fts_ranks);
1629 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1630 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1632 $bonus{'series'} = [
1633 { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1634 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1637 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1639 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1640 $bonus_list ||= '1';
1642 push @bonus_lists, $bonus_list;
1643 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1646 #---------------------
1648 $search_table_list .= "$search_table $search_group_name, ";
1649 push @rank_list,$rank;
1650 $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1652 if ($metabib_field) {
1653 $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1654 $metabib_field = undef;
1657 if ($prev_search_group) {
1658 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1662 my $metabib_record_descriptor = metabib::record_descriptor->table;
1663 my $metabib_full_rec = metabib::full_rec->table;
1664 my $metabib_metarecord = metabib::metarecord->table;
1665 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1666 my $asset_call_number_table = asset::call_number->table;
1667 my $asset_copy_table = asset::copy->table;
1668 my $cs_table = config::copy_status->table;
1669 my $cl_table = asset::copy_location->table;
1670 my $br_table = biblio::record_entry->table;
1671 my $source_table = config::bib_source->table;
1673 my $bonuses = join (' * ', @bonus_lists);
1674 my $relevance = join (' + ', @rank_list);
1675 $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1677 my $string_default_sort = 'zzzz';
1678 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1680 my $number_default_sort = '9999';
1681 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1685 my $secondary_sort = <<" SORT";
1687 SELECT COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1688 FROM $metabib_full_rec sfrt,
1689 $metabib_metarecord mr
1690 WHERE sfrt.record = mr.master_record
1691 AND sfrt.tag = '245'
1692 AND sfrt.subfield = 'a'
1697 my $rank = $relevance;
1698 if (lc($sort) eq 'pubdate') {
1701 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1702 FROM $metabib_full_rec frp
1703 WHERE frp.record = mr.master_record
1705 AND frp.subfield = 'c'
1709 } elsif (lc($sort) eq 'create_date') {
1711 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1713 } elsif (lc($sort) eq 'edit_date') {
1715 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1717 } elsif (lc($sort) eq 'title') {
1720 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1721 FROM $metabib_full_rec frt
1722 WHERE frt.record = mr.master_record
1724 AND frt.subfield = 'a'
1728 $secondary_sort = <<" SORT";
1730 SELECT COALESCE(SUBSTRING(sfrp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1731 FROM $metabib_full_rec sfrp
1732 WHERE sfrp.record = mr.master_record
1733 AND sfrp.tag = '260'
1734 AND sfrp.subfield = 'c'
1738 } elsif (lc($sort) eq 'author') {
1741 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1742 FROM $metabib_full_rec fra
1743 WHERE fra.record = mr.master_record
1744 AND fra.tag LIKE '1%'
1745 AND fra.subfield = 'a'
1746 ORDER BY fra.tag::text::int
1751 push @bonus_values, @bonus_values;
1756 my $select = <<" SQL";
1757 SELECT m.metarecord,
1759 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1762 FROM $search_table_list
1763 $metabib_metarecord mr,
1764 $metabib_metarecord_source_map_table m,
1765 $metabib_metarecord_source_map_table smrs
1766 WHERE m.metarecord = smrs.metarecord
1767 AND mr.id = m.metarecord
1770 GROUP BY m.metarecord
1771 -- ORDER BY 4 $sort_dir
1772 LIMIT $visibility_limit
1775 if ($self->api_name !~ /staff/o) {
1782 FROM $asset_call_number_table cn,
1783 $metabib_metarecord_source_map_table mrs,
1784 $asset_copy_table cp,
1789 $metabib_record_descriptor ord
1790 WHERE mrs.metarecord = s.metarecord
1791 AND br.id = mrs.source
1792 AND cn.record = mrs.source
1793 AND cp.status = cs.id
1794 AND cp.location = cl.id
1795 AND cp.circ_lib = d.id
1796 AND cp.call_number = cn.id
1797 AND cp.opac_visible IS TRUE
1798 AND cs.opac_visible IS TRUE
1799 AND cl.opac_visible IS TRUE
1800 AND d.opac_visible IS TRUE
1801 AND br.active IS TRUE
1802 AND br.deleted IS FALSE
1803 AND cp.deleted IS FALSE
1804 AND cn.deleted IS FALSE
1805 AND ord.record = mrs.source
1818 $metabib_metarecord_source_map_table mrs,
1819 $metabib_record_descriptor ord,
1821 WHERE mrs.metarecord = s.metarecord
1822 AND ord.record = mrs.source
1823 AND br.id = mrs.source
1824 AND br.source = src.id
1825 AND src.transcendant IS TRUE
1833 ORDER BY 4 $sort_dir, 5
1840 $metabib_metarecord_source_map_table omrs,
1841 $metabib_record_descriptor ord
1842 WHERE omrs.metarecord = s.metarecord
1843 AND ord.record = omrs.source
1846 FROM $asset_call_number_table cn,
1847 $asset_copy_table cp,
1850 WHERE br.id = omrs.source
1851 AND cn.record = omrs.source
1852 AND br.deleted IS FALSE
1853 AND cn.deleted IS FALSE
1854 AND cp.call_number = cn.id
1855 AND ( cn.owning_lib = d.id
1856 OR ( cp.circ_lib = d.id
1857 AND cp.deleted IS FALSE
1865 FROM $asset_call_number_table cn
1866 WHERE cn.record = omrs.source
1867 AND cn.deleted IS FALSE
1873 $metabib_metarecord_source_map_table mrs,
1874 $metabib_record_descriptor ord,
1876 WHERE mrs.metarecord = s.metarecord
1877 AND br.id = mrs.source
1878 AND br.source = src.id
1879 AND src.transcendant IS TRUE
1895 ORDER BY 4 $sort_dir, 5
1900 $log->debug("Field Search SQL :: [$select]",DEBUG);
1902 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1905 @types, @forms, @vformats, @aud, @lang, @lit_form,
1906 @types, @forms, @vformats, @aud, @lang, @lit_form,
1907 # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1910 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1913 $max = 1 if (!@$recs);
1915 $max = $$_[1] if ($$_[1] > $max);
1918 my $count = scalar(@$recs);
1919 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1920 next unless ($$rec[0]);
1921 my ($mrid,$rank,$skip) = @$rec;
1922 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1927 __PACKAGE__->register_method(
1928 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1930 method => 'postfilter_search_multi_class_fts',
1935 __PACKAGE__->register_method(
1936 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1938 method => 'postfilter_search_multi_class_fts',
1944 __PACKAGE__->register_method(
1945 api_name => "open-ils.storage.metabib.multiclass.search_fts",
1947 method => 'postfilter_search_multi_class_fts',
1952 __PACKAGE__->register_method(
1953 api_name => "open-ils.storage.metabib.multiclass.search_fts.staff",
1955 method => 'postfilter_search_multi_class_fts',
1961 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1962 sub biblio_search_multi_class_fts {
1967 my $sort = $args{'sort'};
1968 my $sort_dir = $args{sort_dir} || 'DESC';
1969 my $ou = $args{org_unit};
1970 my $ou_type = $args{depth};
1971 my $limit = $args{limit} || 10;
1972 my $offset = $args{offset} || 0;
1973 my $pref_lang = $args{preferred_language} || 'eng';
1974 my $visibility_limit = $args{visibility_limit} || 5000;
1977 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1980 if (! scalar( keys %{$args{searches}} )) {
1981 die "No search arguments were passed to ".$self->api_name;
1984 my $outer_limit = 1000;
1986 my $limit_clause = '';
1987 my $offset_clause = '';
1989 $limit_clause = "LIMIT $outer_limit";
1990 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1992 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1993 my ($t_filter, $f_filter, $v_filter) = ('','','');
1994 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1995 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1996 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1998 if ($args{available}) {
1999 $avail_filter = ' AND cp.status IN (0,7,12)';
2002 if (my $a = $args{audience}) {
2003 $a = [$a] if (!ref($a));
2006 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
2007 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
2010 if (my $l = $args{language}) {
2011 $l = [$l] if (!ref($l));
2014 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
2015 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
2018 if (my $f = $args{lit_form}) {
2019 $f = [$f] if (!ref($f));
2022 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2023 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2026 if (my $f = $args{item_form}) {
2027 $f = [$f] if (!ref($f));
2030 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2031 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2034 if (my $t = $args{item_type}) {
2035 $t = [$t] if (!ref($t));
2038 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2039 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2042 if (my $v = $args{vr_format}) {
2043 $v = [$v] if (!ref($v));
2046 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
2047 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
2050 # XXX legacy format and item type support
2051 if ($args{format}) {
2052 my ($t, $f) = split '-', $args{format};
2053 @types = split '', $t;
2054 @forms = split '', $f;
2056 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2057 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2061 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2062 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2067 my $descendants = defined($ou_type) ?
2068 "actor.org_unit_descendants($ou, $ou_type)" :
2069 "actor.org_unit_descendants($ou)";
2071 my $search_table_list = '';
2073 my $join_table_list = '';
2076 my $field_table = config::metabib_field->table;
2080 my $prev_search_group;
2081 my $curr_search_group;
2085 for my $search_group (sort keys %{$args{searches}}) {
2086 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2087 ($search_class,$search_field) = split /\|/, $search_group;
2088 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2090 if ($search_field) {
2091 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2092 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2097 $prev_search_group = $curr_search_group if ($curr_search_group);
2099 $curr_search_group = $search_group_name;
2101 my $class = $_cdbi->{$search_class};
2102 my $search_table = $class->table;
2104 my ($index_col) = $class->columns('FTS');
2105 $index_col ||= 'value';
2108 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2110 my $fts_where = $fts->sql_where_clause;
2111 my @fts_ranks = $fts->fts_rank;
2113 my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2114 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2115 my $first_word = lc(($fts->words)[0]).'%';
2117 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2118 my $rank = join(' + ', @fts_ranks);
2121 $bonus{'subject'} = [];
2122 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2124 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2126 $bonus{'series'} = [
2127 { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2128 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2131 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2134 push @{ $bonus{'title'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2135 push @{ $bonus{'author'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2136 push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2137 push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2138 push @{ $bonus{'series'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2141 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2142 $bonus_list ||= '1';
2144 push @bonus_lists, $bonus_list;
2145 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2147 #---------------------
2149 $search_table_list .= "$search_table $search_group_name, ";
2150 push @rank_list,$rank;
2151 $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2153 if ($metabib_field) {
2154 $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2155 $metabib_field = undef;
2158 if ($prev_search_group) {
2159 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2163 my $metabib_record_descriptor = metabib::record_descriptor->table;
2164 my $metabib_full_rec = metabib::full_rec->table;
2165 my $metabib_metarecord = metabib::metarecord->table;
2166 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2167 my $asset_call_number_table = asset::call_number->table;
2168 my $asset_copy_table = asset::copy->table;
2169 my $cs_table = config::copy_status->table;
2170 my $cl_table = asset::copy_location->table;
2171 my $br_table = biblio::record_entry->table;
2172 my $source_table = config::bib_source->table;
2175 my $bonuses = join (' * ', @bonus_lists);
2176 my $relevance = join (' + ', @rank_list);
2177 $relevance = "AVG( ($relevance) * ($bonuses) )";
2179 my $string_default_sort = 'zzzz';
2180 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2182 my $number_default_sort = '9999';
2183 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2185 my $rank = $relevance;
2186 if (lc($sort) eq 'pubdate') {
2189 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d{4}'),'$number_default_sort')::INT
2190 FROM $metabib_full_rec frp
2191 WHERE frp.record = b.id
2193 AND frp.subfield = 'c'
2197 } elsif (lc($sort) eq 'create_date') {
2199 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2201 } elsif (lc($sort) eq 'edit_date') {
2203 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2205 } elsif (lc($sort) eq 'title') {
2208 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2209 FROM $metabib_full_rec frt
2210 WHERE frt.record = b.id
2212 AND frt.subfield = 'a'
2216 } elsif (lc($sort) eq 'author') {
2219 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
2220 FROM $metabib_full_rec fra
2221 WHERE fra.record = b.id
2222 AND fra.tag LIKE '1%'
2223 AND fra.subfield = 'a'
2224 ORDER BY fra.tag::text::int
2229 push @bonus_values, @bonus_values;
2234 my $select = <<" SQL";
2239 FROM $search_table_list
2240 $metabib_record_descriptor rd,
2243 WHERE rd.record = b.id
2244 AND b.active IS TRUE
2245 AND b.deleted IS FALSE
2254 GROUP BY b.id, b.source
2255 ORDER BY 3 $sort_dir
2256 LIMIT $visibility_limit
2259 if ($self->api_name !~ /staff/o) {
2264 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2267 FROM $asset_call_number_table cn,
2268 $asset_copy_table cp,
2272 WHERE cn.record = s.id
2273 AND cp.status = cs.id
2274 AND cp.location = cl.id
2275 AND cp.call_number = cn.id
2276 AND cp.opac_visible IS TRUE
2277 AND cs.opac_visible IS TRUE
2278 AND cl.opac_visible IS TRUE
2279 AND d.opac_visible IS TRUE
2280 AND cp.deleted IS FALSE
2281 AND cn.deleted IS FALSE
2282 AND cp.circ_lib = d.id
2286 OR src.transcendant IS TRUE
2287 ORDER BY 3 $sort_dir
2294 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2297 FROM $asset_call_number_table cn,
2298 $asset_copy_table cp,
2300 WHERE cn.record = s.id
2301 AND cp.call_number = cn.id
2302 AND cn.deleted IS FALSE
2303 AND cp.circ_lib = d.id
2304 AND cp.deleted IS FALSE
2310 FROM $asset_call_number_table cn
2311 WHERE cn.record = s.id
2314 OR src.transcendant IS TRUE
2315 ORDER BY 3 $sort_dir
2320 $log->debug("Field Search SQL :: [$select]",DEBUG);
2322 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2324 @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2327 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2329 my $count = scalar(@$recs);
2330 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2331 next unless ($$rec[0]);
2332 my ($mrid,$rank) = @$rec;
2333 $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2338 __PACKAGE__->register_method(
2339 api_name => "open-ils.storage.biblio.multiclass.search_fts.record",
2341 method => 'biblio_search_multi_class_fts',
2346 __PACKAGE__->register_method(
2347 api_name => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2349 method => 'biblio_search_multi_class_fts',
2354 __PACKAGE__->register_method(
2355 api_name => "open-ils.storage.biblio.multiclass.search_fts",
2357 method => 'biblio_search_multi_class_fts',
2362 __PACKAGE__->register_method(
2363 api_name => "open-ils.storage.biblio.multiclass.search_fts.staff",
2365 method => 'biblio_search_multi_class_fts',
2373 my $default_preferred_language;
2374 my $default_preferred_language_weight;
2376 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2382 if (!$locale_map{COMPLETE}) {
2384 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2385 for my $locale ( @locales ) {
2386 $locale_map{lc($locale->code)} = $locale->marc_code;
2388 $locale_map{COMPLETE} = 1;
2392 my $config = OpenSRF::Utils::SettingsClient->new();
2394 if (!$default_preferred_language) {
2396 $default_preferred_language = $config->config_value(
2397 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2398 ) || $config->config_value(
2399 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2404 if (!$default_preferred_language_weight) {
2406 $default_preferred_language_weight = $config->config_value(
2407 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2408 ) || $config->config_value(
2409 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2413 # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2414 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2416 my $ou = $args{org_unit};
2417 my $limit = $args{limit} || 10;
2418 my $offset = $args{offset} || 0;
2421 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2424 if (! scalar( keys %{$args{searches}} )) {
2425 die "No search arguments were passed to ".$self->api_name;
2428 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2430 if (!defined($args{preferred_language})) {
2431 my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2432 $args{preferred_language} =
2433 $locale_map{ lc($ses_locale) } || 'eng';
2436 if (!defined($args{preferred_language_weight})) {
2437 $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2440 if ($args{available}) {
2441 @statuses = (0,7,12);
2444 if (my $s = $args{locations}) {
2445 $s = [$s] if (!ref($s));
2449 if (my $b = $args{between}) {
2450 if (ref($b) && @$b == 2) {
2455 if (my $s = $args{statuses}) {
2456 $s = [$s] if (!ref($s));
2460 if (my $a = $args{audience}) {
2461 $a = [$a] if (!ref($a));
2465 if (my $l = $args{language}) {
2466 $l = [$l] if (!ref($l));
2470 if (my $f = $args{lit_form}) {
2471 $f = [$f] if (!ref($f));
2475 if (my $f = $args{item_form}) {
2476 $f = [$f] if (!ref($f));
2480 if (my $t = $args{item_type}) {
2481 $t = [$t] if (!ref($t));
2485 if (my $b = $args{bib_level}) {
2486 $b = [$b] if (!ref($b));
2490 if (my $v = $args{vr_format}) {
2491 $v = [$v] if (!ref($v));
2495 # XXX legacy format and item type support
2496 if ($args{format}) {
2497 my ($t, $f) = split '-', $args{format};
2498 @types = split '', $t;
2499 @forms = split '', $f;
2502 my %stored_proc_search_args;
2503 for my $search_group (sort keys %{$args{searches}}) {
2504 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2505 my ($search_class,$search_field) = split /\|/, $search_group;
2506 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2508 if ($search_field) {
2509 unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2510 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2515 my $class = $_cdbi->{$search_class};
2516 my $search_table = $class->table;
2518 my ($index_col) = $class->columns('FTS');
2519 $index_col ||= 'value';
2522 my $fts = OpenILS::Application::Storage::FTS->compile(
2523 $search_class => $args{searches}{$search_group}{term},
2524 $search_group_name.'.value',
2525 "$search_group_name.$index_col"
2527 $fts->sql_where_clause; # this builds the ranks for us
2529 my @fts_ranks = $fts->fts_rank;
2530 my @fts_queries = $fts->fts_query;
2531 my @phrases = map { lc($_) } $fts->phrases;
2532 my @words = map { lc($_) } $fts->words;
2534 $stored_proc_search_args{$search_group} = {
2535 fts_rank => \@fts_ranks,
2536 fts_query => \@fts_queries,
2537 phrase => \@phrases,
2543 my $param_search_ou = $ou;
2544 my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2545 my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2546 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2547 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2548 my $param_audience = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud ) . '}$$';
2549 my $param_language = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang ) . '}$$';
2550 my $param_lit_form = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2551 my $param_types = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types ) . '}$$';
2552 my $param_forms = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms ) . '}$$';
2553 my $param_vformats = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2554 my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2555 my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2556 my $param_after = $args{after} ; $param_after = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2557 my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2558 my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2559 my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2560 my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2561 my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2562 my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2563 my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2564 my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2565 my $param_rel_limit = $args{core_limit}; $param_rel_limit ||= 'NULL';
2566 my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2567 my $param_skip_chk = $args{skip_check}; $param_skip_chk ||= 'NULL';
2569 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2571 FROM search.staged_fts(
2572 $param_search_ou\:\:INT,
2573 $param_depth\:\:INT,
2574 $param_searches\:\:TEXT,
2575 $param_statuses\:\:INT[],
2576 $param_locations\:\:INT[],
2577 $param_audience\:\:TEXT[],
2578 $param_language\:\:TEXT[],
2579 $param_lit_form\:\:TEXT[],
2580 $param_types\:\:TEXT[],
2581 $param_forms\:\:TEXT[],
2582 $param_vformats\:\:TEXT[],
2583 $param_bib_level\:\:TEXT[],
2584 $param_before\:\:TEXT,
2585 $param_after\:\:TEXT,
2586 $param_during\:\:TEXT,
2587 $param_between\:\:TEXT[],
2588 $param_pref_lang\:\:TEXT,
2589 $param_pref_lang_multiplier\:\:REAL,
2590 $param_sort\:\:TEXT,
2591 $param_sort_desc\:\:BOOL,
2592 $metarecord\:\:BOOL,
2594 $param_rel_limit\:\:INT,
2595 $param_chk_limit\:\:INT,
2596 $param_skip_chk\:\:INT
2602 my $recs = $sth->fetchall_arrayref({});
2603 my $summary_row = pop @$recs;
2605 my $total = $$summary_row{total};
2606 my $checked = $$summary_row{checked};
2607 my $visible = $$summary_row{visible};
2608 my $deleted = $$summary_row{deleted};
2609 my $excluded = $$summary_row{excluded};
2611 my $estimate = $visible;
2612 if ( $total > $checked && $checked ) {
2614 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2615 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2619 delete $$summary_row{id};
2620 delete $$summary_row{rel};
2621 delete $$summary_row{record};
2623 $client->respond( $summary_row );
2625 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2627 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2628 delete $$rec{checked};
2629 delete $$rec{visible};
2630 delete $$rec{excluded};
2631 delete $$rec{deleted};
2632 delete $$rec{total};
2633 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2635 $client->respond( $rec );
2639 __PACKAGE__->register_method(
2640 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
2642 method => 'staged_fts',
2647 __PACKAGE__->register_method(
2648 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2650 method => 'staged_fts',
2655 __PACKAGE__->register_method(
2656 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
2658 method => 'staged_fts',
2663 __PACKAGE__->register_method(
2664 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2666 method => 'staged_fts',
2672 sub FTS_paging_estimate {
2676 my $checked = shift;
2677 my $visible = shift;
2678 my $excluded = shift;
2679 my $deleted = shift;
2682 my $deleted_ratio = $deleted / $checked;
2683 my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2685 my $exclusion_ratio = $excluded / $checked;
2686 my $delete_adjusted_exclusion_ratio = $checked - $deleted ? $excluded / ($checked - $deleted) : 1;
2688 my $inclusion_ratio = $visible / $checked;
2689 my $delete_adjusted_inclusion_ratio = $checked - $deleted ? $visible / ($checked - $deleted) : 0;
2692 exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2693 inclusion => int($delete_adjusted_total * $inclusion_ratio),
2694 delete_adjusted_exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2695 delete_adjusted_inclusion => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2698 __PACKAGE__->register_method(
2699 api_name => "open-ils.storage.fts_paging_estimate",
2701 method => 'FTS_paging_estimate',
2707 Hash of estimation values based on four variant estimation strategies:
2708 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2709 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2710 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2711 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2714 Helper method used to determin the approximate number of
2715 hits for a search that spans multiple superpages. For
2716 sparse superpages, the inclusion estimate will likely be the
2717 best estimate. The exclusion strategy is the original, but
2718 inclusion is the default.
2721 { name => 'checked',
2722 desc => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2725 { name => 'visible',
2726 desc => 'Number of records visible to the search location on the current superpage.',
2729 { name => 'excluded',
2730 desc => 'Number of records excluded from the search location on the current superpage.',
2733 { name => 'deleted',
2734 desc => 'Number of deleted records on the current superpage.',
2738 desc => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2751 my $term = $$args{term};
2752 my $limit = $$args{max} || 1;
2753 my $min = $$args{min} || 1;
2754 my @classes = @{$$args{class}};
2756 $limit = $min if ($min > $limit);
2759 @classes = ( qw/ title author subject series keyword / );
2763 my $bre_table = biblio::record_entry->table;
2764 my $cn_table = asset::call_number->table;
2765 my $cp_table = asset::copy->table;
2767 for my $search_class ( @classes ) {
2769 my $class = $_cdbi->{$search_class};
2770 my $search_table = $class->table;
2772 my ($index_col) = $class->columns('FTS');
2773 $index_col ||= 'value';
2776 my $where = OpenILS::Application::Storage::FTS
2777 ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2781 SELECT COUNT(DISTINCT X.source)
2782 FROM (SELECT $search_class.source
2783 FROM $search_table $search_class
2784 JOIN $bre_table b ON (b.id = $search_class.source)
2789 HAVING COUNT(DISTINCT X.source) >= $min;
2792 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2793 $matches{$search_class} = $res ? $res->[0] : 0;
2798 __PACKAGE__->register_method(
2799 api_name => "open-ils.storage.search.xref",
2801 method => 'xref_count',
2805 # Takes an abstract query object and recursively turns it back into a string
2807 sub abstract_query2str {
2808 my ($self, $conn, $query) = @_;
2810 return QueryParser::Canonicalize::abstract_query2str_impl($query, 0, $OpenILS::Application::Storage::QParser);
2813 __PACKAGE__->register_method(
2814 api_name => "open-ils.storage.query_parser.abstract_query.canonicalize",
2816 method => "abstract_query2str",
2821 Abstract query parser object, with complete config data. For example input,
2822 see the 'abstract_query' part of the output of an API call like
2823 open-ils.search.biblio.multiclass.query, when called with the return_abstract
2827 return => { type => "string", desc => "String representation of abstract query object" }
2831 sub str2abstract_query {
2832 my ($self, $conn, $query, $qp_opts, $with_config) = @_;
2834 my %use_opts = ( # reasonable defaults? should these even be hardcoded here?
2836 superpage_size => 1000,
2837 core_limit => 25000,
2839 (ref $opts eq 'HASH' ? %$opts : ())
2844 # grab the query parser and initialize it
2845 my $parser = $OpenILS::Application::Storage::QParser;
2848 _initialize_parser($parser) unless $parser->initialization_complete;
2850 my $query = $parser->new(%use_opts)->parse;
2852 return $query->parse_tree->to_abstract_query(with_config => $with_config);
2855 __PACKAGE__->register_method(
2856 api_name => "open-ils.storage.query_parser.abstract_query.from_string",
2858 method => "str2abstract_query",
2862 {desc => "Query", type => "string"},
2863 {desc => q/Arguments for initializing QueryParser (optional)/,
2865 {desc => q/Flag enabling inclusion of QP config in returned object (optional, default false)/,
2868 return => { type => "object", desc => "abstract representation of query parser query" }
2872 my @available_statuses_cache;
2873 sub available_statuses {
2874 if (!scalar(@available_statuses_cache)) {
2875 @available_statuses_cache = map { $_->id } config::copy_status->search_where({is_available => 't'});
2877 return @available_statuses_cache;
2880 sub query_parser_fts {
2886 # grab the query parser and initialize it
2887 my $parser = $OpenILS::Application::Storage::QParser;
2890 _initialize_parser($parser) unless $parser->initialization_complete;
2892 # populate the locale/language map
2893 if (!$locale_map{COMPLETE}) {
2895 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2896 for my $locale ( @locales ) {
2897 $locale_map{lc($locale->code)} = $locale->marc_code;
2899 $locale_map{COMPLETE} = 1;
2903 # I hope we have a query!
2904 if (! $args{query} ) {
2905 die "No query was passed to ".$self->api_name;
2908 my $default_CD_modifiers = OpenSRF::Utils::SettingsClient->new->config_value(
2909 apps => 'open-ils.search' => app_settings => 'default_CD_modifiers'
2912 # Protect against empty / missing default_CD_modifiers setting
2913 if ($default_CD_modifiers and !ref($default_CD_modifiers)) {
2914 $args{query} = "$default_CD_modifiers $args{query}";
2917 my $simple_plan = $args{_simple_plan};
2918 # remove bad chunks of the %args hash
2919 for my $bad ( grep { /^_/ } keys(%args)) {
2920 delete($args{$bad});
2924 # parse the query and supply any query-level %arg-based defaults
2925 # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2926 my $query = $parser->new( %args )->parse;
2928 my $config = OpenSRF::Utils::SettingsClient->new();
2930 # set the locale-based default preferred location
2931 if (!$query->parse_tree->find_filter('preferred_language')) {
2932 $parser->default_preferred_language( $args{preferred_language} );
2934 if (!$parser->default_preferred_language) {
2935 my $ses_locale = $client->session ? $client->session->session_locale : '';
2936 $parser->default_preferred_language( $locale_map{ lc($ses_locale) } );
2939 if (!$parser->default_preferred_language) { # still nothing...
2940 my $tmp_dpl = $config->config_value(
2941 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2942 ) || $config->config_value(
2943 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2946 $parser->default_preferred_language( $tmp_dpl )
2951 # set the global default language multiplier
2952 if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2955 if ($tmp_dplw = $args{preferred_language_weight} || $args{preferred_language_multiplier} ) {
2956 $parser->default_preferred_language_multiplier($tmp_dplw);
2959 $tmp_dplw = $config->config_value(
2960 apps => 'open-ils.search' => app_settings => 'default_preferred_language_weight'
2961 ) || $config->config_value(
2962 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2965 $parser->default_preferred_language_multiplier( $tmp_dplw );
2969 # gather the site, if one is specified, defaulting to the in-query version
2970 my $ou = $args{org_unit};
2971 if (my ($filter) = $query->parse_tree->find_filter('site')) {
2972 $ou = $filter->args->[0] if (@{$filter->args});
2974 $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^(-)?\d+$/);
2977 # # XXX The following, along with most of the surrounding code, is actually dead now. However, realigning to be more "true" and match surrounding.
2978 # # gather lasso, as with $ou
2979 my $lasso = $args{lasso};
2980 # if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
2981 # $lasso = $filter->args->[0] if (@{$filter->args});
2983 # # search by name if an id (number) wasn't given
2984 # $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
2986 # # gather lasso org list
2987 # my $lasso_orgs = [];
2988 # $lasso_orgs = [actor::org_lasso_map->search( { lasso => $lasso } )] if ($lasso);
2991 ## # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
2992 ## # gather user lasso, as with $ou and lasso
2993 my $mylasso = $args{my_lasso};
2994 ## if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
2995 ## $mylasso = $filter->args->[0] if (@{$filter->args});
2997 ## $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
3000 # # if we have a lasso, go with that, otherwise ... ou
3001 # $ou = $lasso if ($lasso);
3003 # gather the preferred OU, if one is specified, as with $ou
3004 my $pref_ou = $args{pref_ou};
3005 if (my ($filter) = $query->parse_tree->find_filter('pref_ou')) {
3006 $pref_ou = $filter->args->[0] if (@{$filter->args});
3008 $pref_ou = actor::org_unit->search( { shortname => $pref_ou } )->next->id if ($pref_ou and $pref_ou !~ /^(-)?\d+$/);
3010 # get the default $ou if we have nothing
3011 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
3014 # XXX when user lassos are here, check to make sure we don't have one -- it'll be passed in the depth, with an ou of 0
3015 # gather the depth, if one is specified, defaulting to the in-query version
3016 my $depth = $args{depth};
3017 if (my ($filter) = $query->parse_tree->find_filter('depth')) {
3018 $depth = $filter->args->[0] if (@{$filter->args});
3020 $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
3023 # gather the limit or default to 10
3024 my $limit = $args{check_limit};
3025 if (my ($filter) = $query->parse_tree->find_filter('limit')) {
3026 $limit = $filter->args->[0] if (@{$filter->args});
3028 if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
3029 $limit = $filter->args->[0] if (@{$filter->args});
3033 # gather the offset or default to 0
3034 my $offset = $args{skip_check} || $args{offset};
3035 if (my ($filter) = $query->parse_tree->find_filter('offset')) {
3036 $offset = $filter->args->[0] if (@{$filter->args});
3038 if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
3039 $offset = $filter->args->[0] if (@{$filter->args});
3043 # gather the estimation strategy or default to inclusion
3044 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
3045 if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
3046 $estimation_strategy = $filter->args->[0] if (@{$filter->args});
3050 # gather the estimation strategy or default to inclusion
3051 my $core_limit = $args{core_limit};
3052 if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
3053 $core_limit = $filter->args->[0] if (@{$filter->args});
3057 # gather statuses, and then forget those if we have an #available modifier
3059 if ($query->parse_tree->find_modifier('available')) {
3060 @statuses = available_statuses();
3061 } elsif (my ($filter) = $query->parse_tree->find_filter('statuses')) {
3062 @statuses = @{$filter->args} if (@{$filter->args});
3068 if (my ($filter) = $query->parse_tree->find_filter('locations')) {
3069 @location = @{$filter->args} if (@{$filter->args});
3072 # gather location_groups
3073 if (my ($filter) = $query->parse_tree->find_filter('location_groups')) {
3074 my @loc_groups = ();
3075 @loc_groups = @{$filter->args} if (@{$filter->args});
3077 # collect the mapped locations and add them to the locations() filter
3080 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3081 my $maps = $cstore->request(
3082 'open-ils.cstore.direct.asset.copy_location_group_map.search.atomic',
3083 {lgroup => \@loc_groups})->gather(1);
3085 push(@location, $_->location) for @$maps;
3090 my $param_check = $limit || $query->superpage_size || 'NULL';
3091 my $param_offset = $offset || 'NULL';
3092 my $param_limit = $core_limit || 'NULL';
3094 my $sp = $query->superpage || 1;
3096 $param_offset = ($sp - 1) * $sp_size;
3099 my $param_search_ou = $ou;
3100 my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
3101 my $param_core_query = $query->parse_tree->toSQL;
3102 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
3103 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
3104 my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
3105 my $deleted_search = ($query->parse_tree->find_modifier('deleted')) ? "'t'" : "'f'";
3106 my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
3107 my $param_pref_ou = $pref_ou || 'NULL';
3109 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3110 -- bib search: $args{query}
3116 my $recs = $sth->fetchall_arrayref({});
3117 my $summary_row = pop @$recs;
3119 my $total = $$summary_row{total};
3120 my $checked = $$summary_row{checked};
3121 my $visible = $$summary_row{visible};
3122 my $deleted = $$summary_row{deleted};
3123 my $excluded = $$summary_row{excluded};
3125 delete $$summary_row{id};
3126 delete $$summary_row{rel};
3127 delete $$summary_row{record};
3128 delete $$summary_row{badges};
3129 delete $$summary_row{popularity};
3131 if (defined($simple_plan)) {
3132 $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3134 $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3137 if ($args{return_query}) {
3138 $$summary_row{query_struct} = $query->parse_tree->to_abstract_query();
3139 $$summary_row{canonicalized_query} = QueryParser::Canonicalize::abstract_query2str_impl($$summary_row{query_struct}, 0, $parser);
3142 $client->respond( $summary_row );
3144 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $visible.",DEBUG);
3146 for my $rec (@$recs) {
3147 delete $$rec{checked};
3148 delete $$rec{visible};
3149 delete $$rec{excluded};
3150 delete $$rec{deleted};
3151 delete $$rec{total};
3152 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3154 $client->respond( $rec );
3158 __PACKAGE__->register_method(
3159 api_name => "open-ils.storage.query_parser_search",
3161 method => 'query_parser_fts',
3169 sub query_parser_fts_wrapper {
3174 $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3175 # grab the query parser and initialize it
3176 my $parser = $OpenILS::Application::Storage::QParser;
3179 _initialize_parser($parser) unless $parser->initialization_complete;
3181 $args{searches} ||= {};
3182 if (!scalar(keys(%{$args{searches}})) && !$args{query}) {
3183 die "No search arguments were passed to ".$self->api_name;
3186 $top_org ||= actor::org_unit->search( { parent_ou => undef } )->next;
3188 my $base_query = $args{query} || '';
3189 if (scalar(keys(%{$args{searches}}))) {
3190 $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3191 for my $sclass ( keys %{$args{searches}} ) {
3192 $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3193 $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3197 my $query = $base_query;
3198 $log->debug("Full base query: $base_query", DEBUG);
3200 $query = "$args{facets} $query" if ($args{facets});
3202 if (!$locale_map{COMPLETE}) {
3204 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3205 for my $locale ( @locales ) {
3206 $locale_map{lc($locale->code)} = $locale->marc_code;
3208 $locale_map{COMPLETE} = 1;
3212 my $base_plan = $parser->new( query => $base_query )->parse;
3214 $query = "preferred_language($args{preferred_language}) $query"
3215 if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3216 $query = "preferred_language_weight($args{preferred_language_weight}) $query"
3217 if ($args{preferred_language_weight} and !$base_plan->parse_tree->find_filter('preferred_language_weight') and !$base_plan->parse_tree->find_filter('preferred_language_multiplier'));
3221 if (!$base_plan->parse_tree->find_filter('badge_orgs')) {
3222 # supply a suitable badge_orgs filter unless user has
3223 # explicitly supplied one
3226 my @lg_id_list = (); # We must define the variable with a static value
3227 # because an idomatic my+set causes the previous
3228 # value is remembered via closure.
3230 @lg_id_list = @{$args{location_groups}} if (ref $args{location_groups});
3232 my ($lg_filter) = $base_plan->parse_tree->find_filter('location_groups');
3233 @lg_id_list = @{$lg_filter->args} if ($lg_filter && @{$lg_filter->args});
3237 for my $lg ( grep { /^\d+$/ } @lg_id_list ) {
3238 my $lg_obj = asset::copy_location_group->retrieve($lg);
3239 next unless $lg_obj;
3241 push(@borg_list, @{$U->get_org_ancestors(''.$lg_obj->owner)});
3243 $borgs = join(',', uniq @borg_list) if @borg_list;
3247 my ($site_filter) = $base_plan->parse_tree->find_filter('site');
3248 if ($site_filter && @{$site_filter->args}) {
3249 $site = $top_org if ($site_filter->args->[0] eq '-');
3250 $site = $top_org if ($site_filter->args->[0] eq $top_org->shortname);
3251 $site = actor::org_unit->search( { shortname => $site_filter->args->[0] })->next unless ($site);
3252 } elsif ($args{org_unit}) {
3253 $site = $top_org if ($args{org_unit} eq '-');
3254 $site = $top_org if ($args{org_unit} eq $top_org->shortname);
3255 $site = actor::org_unit->search( { shortname => $args{org_unit} })->next unless ($site);
3261 $borgs = $U->get_org_ancestors($site->id);
3262 $borgs = @$borgs ? join(',', @$borgs) : undef;
3267 # gather the limit or default to 10
3268 my $limit = delete($args{check_limit}) || $base_plan->superpage_size;
3269 if (my ($filter) = $base_plan->parse_tree->find_filter('limit')) {
3270 $limit = $filter->args->[0] if (@{$filter->args});
3272 if (my ($filter) = $base_plan->parse_tree->find_filter('check_limit')) {
3273 $limit = $filter->args->[0] if (@{$filter->args});
3276 # gather the offset or default to 0
3277 my $offset = delete($args{skip_check}) || delete($args{offset}) || 0;
3278 if (my ($filter) = $base_plan->parse_tree->find_filter('offset')) {
3279 $offset = $filter->args->[0] if (@{$filter->args});
3281 if (my ($filter) = $base_plan->parse_tree->find_filter('skip_check')) {
3282 $offset = $filter->args->[0] if (@{$filter->args});
3286 $query = "check_limit($limit) $query" if (defined $limit);
3287 $query = "skip_check($offset) $query" if (defined $offset);
3288 $query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
3289 $query = "badge_orgs($borgs) $query" if ($borgs);
3291 # XXX All of the following, down to the 'return' is basically dead code. someone higher up should handle it
3292 $query = "site($args{org_unit}) $query" if ($args{org_unit});
3293 $query = "lasso($args{lasso}) $query" if ($args{lasso});
3294 $query = "depth($args{depth}) $query" if (defined($args{depth}));
3295 $query = "sort($args{sort}) $query" if ($args{sort});
3296 $query = "core_limit($args{core_limit}) $query" if ($args{core_limit});
3297 # $query = "limit($args{limit}) $query" if ($args{limit});
3298 # $query = "skip_check($args{skip_check}) $query" if ($args{skip_check});
3299 $query = "superpage($args{superpage}) $query" if ($args{superpage});
3300 $query = "offset($args{offset}) $query" if ($args{offset});
3301 $query = "#metarecord $query" if ($self->api_name =~ /metabib/);
3302 $query = "from_metarecord($args{from_metarecord}) $query" if ($args{from_metarecord});
3303 $query = "#available $query" if ($args{available});
3304 $query = "#descending $query" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3305 $query = "#staff $query" if ($self->api_name =~ /staff/);
3306 $query = "before($args{before}) $query" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3307 $query = "after($args{after}) $query" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3308 $query = "during($args{during}) $query" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3309 $query = "between($args{between}[0],$args{between}[1]) $query"
3310 if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3313 my (@between,@statuses,@locations,@location_groups,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3315 # XXX legacy format and item type support
3316 if ($args{format}) {
3317 my ($t, $f) = split '-', $args{format};
3318 $args{item_type} = [ split '', $t ];
3319 $args{item_form} = [ split '', $f ];
3322 for my $filter ( qw/locations location_groups statuses audience language lit_form item_form item_type bib_level vr_format badges/ ) {
3323 if (my $s = $args{$filter}) {
3324 $s = [$s] if (!ref($s));
3326 my @filter_list = @$s;
3328 next if (@filter_list == 0);
3330 my $filter_string = join ',', @filter_list;
3331 $query = "$query $filter($filter_string)";
3335 $log->debug("Full QueryParser query: $query", DEBUG);
3337 return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan, return_query => $args{return_query} );
3339 __PACKAGE__->register_method(
3340 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
3342 method => 'query_parser_fts_wrapper',
3347 __PACKAGE__->register_method(
3348 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3350 method => 'query_parser_fts_wrapper',
3355 __PACKAGE__->register_method(
3356 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
3358 method => 'query_parser_fts_wrapper',
3363 __PACKAGE__->register_method(
3364 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3366 method => 'query_parser_fts_wrapper',