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);
93 die("Cannot initialize $parser!") unless ($parser->initialization_complete);
96 sub ordered_records_from_metarecord { # XXX Replace with QP-based search-within-MR
100 my $formats = shift; # dead
104 my $copies_visible = 'LEFT JOIN asset.copy_vis_attr_cache vc ON (br.id = vc.record '.
105 'AND vc.vis_attr_vector @@ (SELECT c_attrs::query_int FROM asset.patron_default_visibility_mask() LIMIT 1))';
106 $copies_visible = '' if ($self->api_name =~ /staff/o);
108 my $copies_visible_count = ',COUNT(vc.id)';
109 $copies_visible_count = '' if ($self->api_name =~ /staff/o);
111 my $descendants = '';
113 $descendants = defined($depth) ?
114 ",actor.org_unit_descendants($org, $depth) d" :
115 ",actor.org_unit_descendants($org) d" ;
122 $copies_visible_count
123 FROM metabib.metarecord_source_map sm
124 JOIN biblio.record_entry br ON (sm.source = br.id AND NOT br.deleted)
125 LEFT JOIN metabib.record_sorter s ON (s.source = br.id AND s.attr = 'titlesort')
126 LEFT JOIN config.bib_source bs ON (br.source = bs.id)
129 WHERE sm.metarecord = ?
133 if ($copies_visible) {
134 $sql .= 'AND (bs.transcendant OR ';
136 $sql .= 'vc.circ_lib = d.id)';
138 $sql .= 'vc.id IS NOT NULL)'
140 $having = 'HAVING COUNT(vc.id) > 0';
148 s.value ASC NULLS LAST
151 my $ids = metabib::metarecord_source_map->db_Main->selectcol_arrayref($sql, {}, "$mr");
152 return $ids if ($self->api_name =~ /atomic$/o);
154 $client->respond( $_ ) for ( @$ids );
158 __PACKAGE__->register_method(
159 api_name => 'open-ils.storage.ordered.metabib.metarecord.records',
161 method => 'ordered_records_from_metarecord',
165 __PACKAGE__->register_method(
166 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff',
168 method => 'ordered_records_from_metarecord',
173 __PACKAGE__->register_method(
174 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.atomic',
176 method => 'ordered_records_from_metarecord',
180 __PACKAGE__->register_method(
181 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic',
183 method => 'ordered_records_from_metarecord',
188 # XXX: this subroutine and its two registered methods are marked for
189 # deprecation, as they do not work properly in 2.x (these tags are no longer
190 # normalized in mfr) and are not in known use
194 my $isxn = lc(shift());
198 $isxn =~ s/-//o if ($self->api_name =~ /isbn/o);
200 my $tag = ($self->api_name =~ /isbn/o) ? "'020' OR f.tag = '024'" : "'022'";
202 my $fr_table = metabib::full_rec->table;
203 my $bib_table = biblio::record_entry->table;
206 SELECT DISTINCT f.record
208 JOIN $bib_table b ON (b.id = f.record)
211 AND b.deleted IS FALSE
214 my $list = metabib::full_rec->db_Main->selectcol_arrayref($sql, {}, "$isxn%");
215 $client->respond($_) for (@$list);
218 __PACKAGE__->register_method(
219 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
221 method => 'isxn_search',
225 __PACKAGE__->register_method(
226 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.issn',
228 method => 'isxn_search',
233 sub metarecord_copy_count {
239 my $sm_table = metabib::metarecord_source_map->table;
240 my $rd_table = metabib::record_descriptor->table;
241 my $cn_table = asset::call_number->table;
242 my $cp_table = asset::copy->table;
243 my $br_table = biblio::record_entry->table;
244 my $src_table = config::bib_source->table;
245 my $cl_table = asset::copy_location->table;
246 my $cs_table = config::copy_status->table;
247 my $out_table = actor::org_unit_type->table;
249 my $descendants = "actor.org_unit_descendants(u.id)";
250 my $ancestors = "actor.org_unit_ancestors(?) u JOIN $out_table t ON (u.ou_type = t.id)";
252 if ($args{org_unit} < 0) {
253 $args{org_unit} *= -1;
254 $ancestors = "(select org_unit as id from actor.org_lasso_map where lasso = ?) u CROSS JOIN (SELECT -1 AS depth) t";
257 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';
258 $copies_visible = '' if ($self->api_name =~ /staff/o);
260 my (@types,@forms,@blvl);
261 my ($t_filter, $f_filter, $b_filter) = ('','','');
264 my ($t, $f, $b) = split '-', $args{format};
265 @types = split '', $t;
266 @forms = split '', $f;
267 @blvl = split '', $b;
270 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
274 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
278 $b_filter .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
288 JOIN $cn_table cn ON (cn.record = r.source)
289 JOIN $rd_table rd ON (cn.record = rd.record)
290 JOIN $cp_table cp ON (cn.id = cp.call_number)
291 JOIN $cs_table cs ON (cp.status = cs.id)
292 JOIN $cl_table cl ON (cp.location = cl.id)
293 JOIN $descendants a ON (cp.circ_lib = a.id)
294 WHERE r.metarecord = ?
295 AND cn.deleted IS FALSE
296 AND cp.deleted IS FALSE
306 JOIN $cn_table cn ON (cn.record = r.source)
307 JOIN $rd_table rd ON (cn.record = rd.record)
308 JOIN $cp_table cp ON (cn.id = cp.call_number)
309 JOIN $cs_table cs ON (cp.status = cs.id)
310 JOIN $cl_table cl ON (cp.location = cl.id)
311 JOIN $descendants a ON (cp.circ_lib = a.id)
312 WHERE r.metarecord = ?
313 AND cp.status IN (0,7,12)
314 AND cn.deleted IS FALSE
315 AND cp.deleted IS FALSE
325 JOIN $cn_table cn ON (cn.record = r.source)
326 JOIN $rd_table rd ON (cn.record = rd.record)
327 JOIN $cp_table cp ON (cn.id = cp.call_number)
328 JOIN $cs_table cs ON (cp.status = cs.id)
329 JOIN $cl_table cl ON (cp.location = cl.id)
330 WHERE r.metarecord = ?
331 AND cn.deleted IS FALSE
332 AND cp.deleted IS FALSE
333 AND cp.opac_visible IS TRUE
334 AND cs.opac_visible IS TRUE
335 AND cl.opac_visible IS TRUE
344 JOIN $br_table br ON (br.id = r.source)
345 JOIN $src_table src ON (src.id = br.source)
346 WHERE r.metarecord = ?
347 AND src.transcendant IS TRUE
355 my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
356 $sth->execute( ''.$args{metarecord},
360 ''.$args{metarecord},
364 ''.$args{metarecord},
368 ''.$args{metarecord},
372 while ( my $row = $sth->fetchrow_hashref ) {
373 $client->respond( $row );
377 __PACKAGE__->register_method(
378 api_name => 'open-ils.storage.metabib.metarecord.copy_count',
380 method => 'metarecord_copy_count',
385 __PACKAGE__->register_method(
386 api_name => 'open-ils.storage.metabib.metarecord.copy_count.staff',
388 method => 'metarecord_copy_count',
394 sub biblio_multi_search_full_rec {
399 my $class_join = $args{class_join} || 'AND';
400 my $limit = $args{limit} || 100;
401 my $offset = $args{offset} || 0;
402 my $sort = $args{'sort'};
403 my $sort_dir = $args{sort_dir} || 'DESC';
408 for my $arg (@{ $args{searches} }) {
409 my $term = $$arg{term};
410 my $limiters = $$arg{restrict};
412 my ($index_col) = metabib::full_rec->columns('FTS');
413 $index_col ||= 'value';
414 my $search_table = metabib::full_rec->table;
416 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
418 my $fts_where = $fts->sql_where_clause();
419 my @fts_ranks = $fts->fts_rank;
421 my $rank = join(' + ', @fts_ranks);
424 for my $limit (@$limiters) {
425 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
426 # MARC control field; mfr.subfield is NULL
427 push @wheres, "( tag = ? AND $fts_where )";
428 push @binds, $$limit{tag};
429 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
431 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
432 push @binds, $$limit{tag}, $$limit{subfield};
433 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
436 my $where = join(' OR ', @wheres);
438 push @selects, "SELECT record, AVG($rank) as sum FROM $search_table WHERE $where GROUP BY record";
442 my $descendants = defined($args{depth}) ?
443 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
444 "actor.org_unit_descendants($args{org_unit})" ;
447 my $metabib_record_descriptor = metabib::record_descriptor->table;
448 my $metabib_full_rec = metabib::full_rec->table;
449 my $asset_call_number_table = asset::call_number->table;
450 my $asset_copy_table = asset::copy->table;
451 my $cs_table = config::copy_status->table;
452 my $cl_table = asset::copy_location->table;
453 my $br_table = biblio::record_entry->table;
456 $cj = 'HAVING COUNT(x.record) = ' . scalar(@selects) if ($class_join eq 'AND');
459 '(SELECT x.record, sum(x.sum) FROM (('.
460 join(') UNION ALL (', @selects).
461 ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
463 my $has_vols = 'AND cn.owning_lib = d.id';
464 my $has_copies = 'AND cp.call_number = cn.id';
465 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';
467 if ($self->api_name =~ /staff/o) {
468 $copies_visible = '';
469 $has_copies = '' if ($ou_type == 0);
470 $has_vols = '' if ($ou_type == 0);
473 my ($t_filter, $f_filter) = ('','');
474 my ($a_filter, $l_filter, $lf_filter) = ('','','');
477 if (my $a = $args{audience}) {
478 $a = [$a] if (!ref($a));
481 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
486 if (my $l = $args{language}) {
487 $l = [$l] if (!ref($l));
490 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
495 if (my $f = $args{lit_form}) {
496 $f = [$f] if (!ref($f));
499 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
500 push @binds, @lit_form;
504 if (my $f = $args{item_form}) {
505 $f = [$f] if (!ref($f));
508 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
513 if (my $t = $args{item_type}) {
514 $t = [$t] if (!ref($t));
517 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
524 my ($t, $f) = split '-', $args{format};
525 my @types = split '', $t;
526 my @forms = split '', $f;
528 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
533 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
536 push @binds, @types, @forms;
539 my $relevance = 'sum(f.sum)';
540 $relevance = 1 if (!$copies_visible);
542 my $string_default_sort = 'zzzz';
543 $string_default_sort = 'AAAA' if ($sort_dir =~ /^DESC$/i);
545 my $number_default_sort = '9999';
546 $number_default_sort = '0000' if ($sort_dir =~/^DESC$/i);
548 my $rank = $relevance;
549 if (lc($sort) eq 'pubdate') {
552 SELECT COALESCE(SUBSTRING(MAX(frp.value) FROM E'\\\\d{4}'), '$number_default_sort')::INT
553 FROM $metabib_full_rec frp
554 WHERE frp.record = f.record
556 AND frp.subfield = 'c'
560 } elsif (lc($sort) eq 'create_date') {
562 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
564 } elsif (lc($sort) eq 'edit_date') {
566 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
568 } elsif (lc($sort) =~ /^title/i) {
571 SELECT COALESCE(LTRIM(SUBSTR(MAX(frt.value), COALESCE(SUBSTRING(MAX(frt.ind2) FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
572 FROM $metabib_full_rec frt
573 WHERE frt.record = f.record
575 AND frt.subfield = 'a'
579 } elsif (lc($sort) =~ /^author/i) {
582 SELECT COALESCE(LTRIM(MAX(query.value)), '$string_default_sort')
585 FROM $metabib_full_rec fra
586 WHERE fra.record = f.record
587 AND fra.tag LIKE '1%'
588 AND fra.subfield = 'a'
589 ORDER BY fra.tag::text::int
598 my $rd_join = $use_rd ? "$metabib_record_descriptor rd," : '';
599 my $rd_filter = $use_rd ? 'AND rd.record = f.record' : '';
601 if ($copies_visible) {
603 SELECT f.record, $relevance, count(DISTINCT cp.id), $rank
604 FROM $search_table f,
605 $asset_call_number_table cn,
606 $asset_copy_table cp,
612 WHERE br.id = f.record
613 AND cn.record = f.record
614 AND cp.status = cs.id
615 AND cp.location = cl.id
616 AND br.deleted IS FALSE
617 AND cn.deleted IS FALSE
618 AND cp.deleted IS FALSE
628 GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
629 ORDER BY 4 $sort_dir,3 DESC
633 SELECT f.record, 1, 1, $rank
634 FROM $search_table f,
637 WHERE br.id = f.record
638 AND br.deleted IS FALSE
651 $log->debug("Search SQL :: [$select]",DEBUG);
653 my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
654 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
657 $max = 1 if (!@$recs);
659 $max = $$_[1] if ($$_[1] > $max);
663 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
664 next unless ($$rec[0]);
665 my ($rid,$rank,$junk,$skip) = @$rec;
666 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
670 __PACKAGE__->register_method(
671 api_name => 'open-ils.storage.biblio.full_rec.multi_search',
673 method => 'biblio_multi_search_full_rec',
678 __PACKAGE__->register_method(
679 api_name => 'open-ils.storage.biblio.full_rec.multi_search.staff',
681 method => 'biblio_multi_search_full_rec',
687 sub search_full_rec {
693 my $term = $args{term};
694 my $limiters = $args{restrict};
696 my ($index_col) = metabib::full_rec->columns('FTS');
697 $index_col ||= 'value';
698 my $search_table = metabib::full_rec->table;
700 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
702 my $fts_where = $fts->sql_where_clause();
703 my @fts_ranks = $fts->fts_rank;
705 my $rank = join(' + ', @fts_ranks);
709 for my $limit (@$limiters) {
710 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
711 # MARC control field; mfr.subfield is NULL
712 push @wheres, "( tag = ? AND $fts_where )";
713 push @binds, $$limit{tag};
714 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
716 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
717 push @binds, $$limit{tag}, $$limit{subfield};
718 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
721 my $where = join(' OR ', @wheres);
723 my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
725 $log->debug("Search SQL :: [$select]",DEBUG);
727 my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
728 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
730 $client->respond($_) for (@$recs);
733 __PACKAGE__->register_method(
734 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
736 method => 'search_full_rec',
741 __PACKAGE__->register_method(
742 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
744 method => 'search_full_rec',
751 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
752 sub search_class_fts {
757 my $term = $args{term};
758 my $ou = $args{org_unit};
759 my $ou_type = $args{depth};
760 my $limit = $args{limit};
761 my $offset = $args{offset};
763 my $limit_clause = '';
764 my $offset_clause = '';
766 $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
767 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
770 my ($t_filter, $f_filter) = ('','');
773 my ($t, $f) = split '-', $args{format};
774 @types = split '', $t;
775 @forms = split '', $f;
777 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
781 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
787 my $descendants = defined($ou_type) ?
788 "actor.org_unit_descendants($ou, $ou_type)" :
789 "actor.org_unit_descendants($ou)";
791 my $class = $self->{cdbi};
792 my $search_table = $class->table;
794 my $metabib_record_descriptor = metabib::record_descriptor->table;
795 my $metabib_metarecord = metabib::metarecord->table;
796 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
797 my $asset_call_number_table = asset::call_number->table;
798 my $asset_copy_table = asset::copy->table;
799 my $cs_table = config::copy_status->table;
800 my $cl_table = asset::copy_location->table;
802 my ($index_col) = $class->columns('FTS');
803 $index_col ||= 'value';
805 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
806 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
808 my $fts_where = $fts->sql_where_clause;
809 my @fts_ranks = $fts->fts_rank;
811 my $rank = join(' + ', @fts_ranks);
813 my $has_vols = 'AND cn.owning_lib = d.id';
814 my $has_copies = 'AND cp.call_number = cn.id';
815 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';
817 my $visible_count = ', count(DISTINCT cp.id)';
818 my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
820 if ($self->api_name =~ /staff/o) {
821 $copies_visible = '';
822 $visible_count_test = '';
823 $has_copies = '' if ($ou_type == 0);
824 $has_vols = '' if ($ou_type == 0);
827 my $rank_calc = <<" RANK";
829 * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
830 * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
831 * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
832 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
835 $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
837 if ($copies_visible) {
839 SELECT m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
840 FROM $search_table f,
841 $metabib_metarecord_source_map_table m,
842 $asset_call_number_table cn,
843 $asset_copy_table cp,
846 $metabib_record_descriptor rd,
849 AND m.source = f.source
850 AND cn.record = m.source
851 AND rd.record = m.source
852 AND cp.status = cs.id
853 AND cp.location = cl.id
859 GROUP BY 1 $visible_count_test
861 $limit_clause $offset_clause
865 SELECT m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
866 FROM $search_table f,
867 $metabib_metarecord_source_map_table m,
868 $metabib_record_descriptor rd
870 AND m.source = f.source
871 AND rd.record = m.source
876 $limit_clause $offset_clause
880 $log->debug("Field Search SQL :: [$select]",DEBUG);
882 my $SQLstring = join('%',$fts->words);
883 my $REstring = join('\\s+',$fts->words);
884 my $first_word = ($fts->words)[0].'%';
885 my $recs = ($self->api_name =~ /unordered/o) ?
886 $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
887 $class->db_Main->selectall_arrayref($select, {},
888 '%'.lc($SQLstring).'%', # phrase order match
889 lc($first_word), # first word match
890 '^\\s*'.lc($REstring).'\\s*/?\s*$', # full exact match
894 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
896 $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
900 for my $class ( qw/title author subject keyword series identifier/ ) {
901 __PACKAGE__->register_method(
902 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord",
904 method => 'search_class_fts',
907 cdbi => "metabib::${class}_field_entry",
910 __PACKAGE__->register_method(
911 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
913 method => 'search_class_fts',
916 cdbi => "metabib::${class}_field_entry",
919 __PACKAGE__->register_method(
920 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
922 method => 'search_class_fts',
925 cdbi => "metabib::${class}_field_entry",
928 __PACKAGE__->register_method(
929 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
931 method => 'search_class_fts',
934 cdbi => "metabib::${class}_field_entry",
939 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
940 sub search_class_fts_count {
945 my $term = $args{term};
946 my $ou = $args{org_unit};
947 my $ou_type = $args{depth};
948 my $limit = $args{limit} || 100;
949 my $offset = $args{offset} || 0;
951 my $descendants = defined($ou_type) ?
952 "actor.org_unit_descendants($ou, $ou_type)" :
953 "actor.org_unit_descendants($ou)";
956 my ($t_filter, $f_filter) = ('','');
959 my ($t, $f) = split '-', $args{format};
960 @types = split '', $t;
961 @forms = split '', $f;
963 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
967 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
972 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
974 my $class = $self->{cdbi};
975 my $search_table = $class->table;
977 my $metabib_record_descriptor = metabib::record_descriptor->table;
978 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
979 my $asset_call_number_table = asset::call_number->table;
980 my $asset_copy_table = asset::copy->table;
981 my $cs_table = config::copy_status->table;
982 my $cl_table = asset::copy_location->table;
984 my ($index_col) = $class->columns('FTS');
985 $index_col ||= 'value';
987 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
989 my $fts_where = $fts->sql_where_clause;
991 my $has_vols = 'AND cn.owning_lib = d.id';
992 my $has_copies = 'AND cp.call_number = cn.id';
993 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';
994 if ($self->api_name =~ /staff/o) {
995 $copies_visible = '';
996 $has_vols = '' if ($ou_type == 0);
997 $has_copies = '' if ($ou_type == 0);
1000 # XXX test an "EXISTS version of descendant checking...
1002 if ($copies_visible) {
1004 SELECT count(distinct m.metarecord)
1005 FROM $search_table f,
1006 $metabib_metarecord_source_map_table m,
1007 $metabib_metarecord_source_map_table mr,
1008 $asset_call_number_table cn,
1009 $asset_copy_table cp,
1012 $metabib_record_descriptor rd,
1015 AND mr.source = f.source
1016 AND mr.metarecord = m.metarecord
1017 AND cn.record = m.source
1018 AND rd.record = m.source
1019 AND cp.status = cs.id
1020 AND cp.location = cl.id
1029 SELECT count(distinct m.metarecord)
1030 FROM $search_table f,
1031 $metabib_metarecord_source_map_table m,
1032 $metabib_metarecord_source_map_table mr,
1033 $metabib_record_descriptor rd
1035 AND mr.source = f.source
1036 AND mr.metarecord = m.metarecord
1037 AND rd.record = m.source
1043 $log->debug("Field Search Count SQL :: [$select]",DEBUG);
1045 my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1047 $log->debug("Count Search yielded $recs results.",DEBUG);
1052 for my $class ( qw/title author subject keyword series identifier/ ) {
1053 __PACKAGE__->register_method(
1054 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
1056 method => 'search_class_fts_count',
1059 cdbi => "metabib::${class}_field_entry",
1062 __PACKAGE__->register_method(
1063 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
1065 method => 'search_class_fts_count',
1068 cdbi => "metabib::${class}_field_entry",
1074 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1075 sub postfilter_search_class_fts {
1080 my $term = $args{term};
1081 my $sort = $args{'sort'};
1082 my $sort_dir = $args{sort_dir} || 'DESC';
1083 my $ou = $args{org_unit};
1084 my $ou_type = $args{depth};
1085 my $limit = $args{limit} || 10;
1086 my $visibility_limit = $args{visibility_limit} || 5000;
1087 my $offset = $args{offset} || 0;
1089 my $outer_limit = 1000;
1091 my $limit_clause = '';
1092 my $offset_clause = '';
1094 $limit_clause = "LIMIT $outer_limit";
1095 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1097 my (@types,@forms,@lang,@aud,@lit_form);
1098 my ($t_filter, $f_filter) = ('','');
1099 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1100 my ($ot_filter, $of_filter) = ('','');
1101 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1103 if (my $a = $args{audience}) {
1104 $a = [$a] if (!ref($a));
1107 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1108 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1111 if (my $l = $args{language}) {
1112 $l = [$l] if (!ref($l));
1115 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1116 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1119 if (my $f = $args{lit_form}) {
1120 $f = [$f] if (!ref($f));
1123 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1124 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1127 if ($args{format}) {
1128 my ($t, $f) = split '-', $args{format};
1129 @types = split '', $t;
1130 @forms = split '', $f;
1132 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1133 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1137 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1138 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1143 my $descendants = defined($ou_type) ?
1144 "actor.org_unit_descendants($ou, $ou_type)" :
1145 "actor.org_unit_descendants($ou)";
1147 my $class = $self->{cdbi};
1148 my $search_table = $class->table;
1150 my $metabib_full_rec = metabib::full_rec->table;
1151 my $metabib_record_descriptor = metabib::record_descriptor->table;
1152 my $metabib_metarecord = metabib::metarecord->table;
1153 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1154 my $asset_call_number_table = asset::call_number->table;
1155 my $asset_copy_table = asset::copy->table;
1156 my $cs_table = config::copy_status->table;
1157 my $cl_table = asset::copy_location->table;
1158 my $br_table = biblio::record_entry->table;
1160 my ($index_col) = $class->columns('FTS');
1161 $index_col ||= 'value';
1163 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1165 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1167 my $SQLstring = join('%',map { lc($_) } $fts->words);
1168 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1169 my $first_word = lc(($fts->words)[0]).'%';
1171 my $fts_where = $fts->sql_where_clause;
1172 my @fts_ranks = $fts->fts_rank;
1175 $bonus{'metabib::identifier_field_entry'} =
1176 $bonus{'metabib::keyword_field_entry'} = [
1177 { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring }
1180 $bonus{'metabib::title_field_entry'} =
1181 $bonus{'metabib::series_field_entry'} = [
1182 { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1183 { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1184 @{ $bonus{'metabib::keyword_field_entry'} }
1187 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1188 $bonus_list ||= '1';
1190 my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1192 my $relevance = join(' + ', @fts_ranks);
1193 $relevance = <<" RANK";
1194 (SUM( ( $relevance ) * ( $bonus_list ) )/COUNT(m.source))
1197 my $string_default_sort = 'zzzz';
1198 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1200 my $number_default_sort = '9999';
1201 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1203 my $rank = $relevance;
1204 if (lc($sort) eq 'pubdate') {
1207 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1208 FROM $metabib_full_rec frp
1209 WHERE frp.record = mr.master_record
1211 AND frp.subfield = 'c'
1215 } elsif (lc($sort) eq 'create_date') {
1217 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1219 } elsif (lc($sort) eq 'edit_date') {
1221 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1223 } elsif (lc($sort) eq 'title') {
1226 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1227 FROM $metabib_full_rec frt
1228 WHERE frt.record = mr.master_record
1230 AND frt.subfield = 'a'
1234 } elsif (lc($sort) eq 'author') {
1237 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1238 FROM $metabib_full_rec fra
1239 WHERE fra.record = mr.master_record
1240 AND fra.tag LIKE '1%'
1241 AND fra.subfield = 'a'
1242 ORDER BY fra.tag::text::int
1250 my $select = <<" SQL";
1251 SELECT m.metarecord,
1253 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1255 FROM $search_table f,
1256 $metabib_metarecord_source_map_table m,
1257 $metabib_metarecord_source_map_table smrs,
1258 $metabib_metarecord mr,
1259 $metabib_record_descriptor rd
1261 AND smrs.metarecord = mr.id
1262 AND m.source = f.source
1263 AND m.metarecord = mr.id
1264 AND rd.record = smrs.source
1270 GROUP BY m.metarecord
1271 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1272 LIMIT $visibility_limit
1279 FROM $asset_call_number_table cn,
1280 $metabib_metarecord_source_map_table mrs,
1281 $asset_copy_table cp,
1286 $metabib_record_descriptor ord,
1288 WHERE mrs.metarecord = s.metarecord
1289 AND br.id = mrs.source
1290 AND cn.record = mrs.source
1291 AND cp.status = cs.id
1292 AND cp.location = cl.id
1293 AND cn.owning_lib = d.id
1294 AND cp.call_number = cn.id
1295 AND cp.opac_visible IS TRUE
1296 AND cs.opac_visible IS TRUE
1297 AND cl.opac_visible IS TRUE
1298 AND d.opac_visible IS TRUE
1299 AND br.active IS TRUE
1300 AND br.deleted IS FALSE
1301 AND ord.record = mrs.source
1307 ORDER BY 4 $sort_dir
1309 } elsif ($self->api_name !~ /staff/o) {
1316 FROM $asset_call_number_table cn,
1317 $metabib_metarecord_source_map_table mrs,
1318 $asset_copy_table cp,
1323 $metabib_record_descriptor ord
1325 WHERE mrs.metarecord = s.metarecord
1326 AND br.id = mrs.source
1327 AND cn.record = mrs.source
1328 AND cp.status = cs.id
1329 AND cp.location = cl.id
1330 AND cp.circ_lib = d.id
1331 AND cp.call_number = cn.id
1332 AND cp.opac_visible IS TRUE
1333 AND cs.opac_visible IS TRUE
1334 AND cl.opac_visible IS TRUE
1335 AND d.opac_visible IS TRUE
1336 AND br.active IS TRUE
1337 AND br.deleted IS FALSE
1338 AND ord.record = mrs.source
1346 ORDER BY 4 $sort_dir
1355 FROM $asset_call_number_table cn,
1356 $asset_copy_table cp,
1357 $metabib_metarecord_source_map_table mrs,
1360 $metabib_record_descriptor ord
1362 WHERE mrs.metarecord = s.metarecord
1363 AND br.id = mrs.source
1364 AND cn.record = mrs.source
1365 AND cn.id = cp.call_number
1366 AND br.deleted IS FALSE
1367 AND cn.deleted IS FALSE
1368 AND ord.record = mrs.source
1369 AND ( cn.owning_lib = d.id
1370 OR ( cp.circ_lib = d.id
1371 AND cp.deleted IS FALSE
1383 FROM $asset_call_number_table cn,
1384 $metabib_metarecord_source_map_table mrs,
1385 $metabib_record_descriptor ord
1386 WHERE mrs.metarecord = s.metarecord
1387 AND cn.record = mrs.source
1388 AND ord.record = mrs.source
1396 ORDER BY 4 $sort_dir
1401 $log->debug("Field Search SQL :: [$select]",DEBUG);
1403 my $recs = $class->db_Main->selectall_arrayref(
1405 (@bonus_values > 0 ? @bonus_values : () ),
1406 ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1407 @types, @forms, @aud, @lang, @lit_form,
1408 @types, @forms, @aud, @lang, @lit_form,
1409 ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1411 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1414 $max = 1 if (!@$recs);
1416 $max = $$_[1] if ($$_[1] > $max);
1419 my $count = scalar(@$recs);
1420 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1421 my ($mrid,$rank,$skip) = @$rec;
1422 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1427 for my $class ( qw/title author subject keyword series identifier/ ) {
1428 __PACKAGE__->register_method(
1429 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1431 method => 'postfilter_search_class_fts',
1434 cdbi => "metabib::${class}_field_entry",
1437 __PACKAGE__->register_method(
1438 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1440 method => 'postfilter_search_class_fts',
1443 cdbi => "metabib::${class}_field_entry",
1450 my $_cdbi = { title => "metabib::title_field_entry",
1451 author => "metabib::author_field_entry",
1452 subject => "metabib::subject_field_entry",
1453 keyword => "metabib::keyword_field_entry",
1454 series => "metabib::series_field_entry",
1455 identifier => "metabib::identifier_field_entry",
1458 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1459 sub postfilter_search_multi_class_fts {
1464 my $sort = $args{'sort'};
1465 my $sort_dir = $args{sort_dir} || 'DESC';
1466 my $ou = $args{org_unit};
1467 my $ou_type = $args{depth};
1468 my $limit = $args{limit} || 10;
1469 my $offset = $args{offset} || 0;
1470 my $visibility_limit = $args{visibility_limit} || 5000;
1473 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1476 if (!defined($args{org_unit})) {
1477 die "No target organizational unit passed to ".$self->api_name;
1480 if (! scalar( keys %{$args{searches}} )) {
1481 die "No search arguments were passed to ".$self->api_name;
1484 my $outer_limit = 1000;
1486 my $limit_clause = '';
1487 my $offset_clause = '';
1489 $limit_clause = "LIMIT $outer_limit";
1490 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1492 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1493 my ($t_filter, $f_filter, $v_filter) = ('','','');
1494 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1495 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1496 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1498 if ($args{available}) {
1499 $avail_filter = ' AND cp.status IN (0,7,12)';
1502 if (my $a = $args{audience}) {
1503 $a = [$a] if (!ref($a));
1506 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1507 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1510 if (my $l = $args{language}) {
1511 $l = [$l] if (!ref($l));
1514 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1515 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1518 if (my $f = $args{lit_form}) {
1519 $f = [$f] if (!ref($f));
1522 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1523 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1526 if (my $f = $args{item_form}) {
1527 $f = [$f] if (!ref($f));
1530 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1531 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1534 if (my $t = $args{item_type}) {
1535 $t = [$t] if (!ref($t));
1538 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1539 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1542 if (my $v = $args{vr_format}) {
1543 $v = [$v] if (!ref($v));
1546 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1547 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1551 # XXX legacy format and item type support
1552 if ($args{format}) {
1553 my ($t, $f) = split '-', $args{format};
1554 @types = split '', $t;
1555 @forms = split '', $f;
1557 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1558 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1562 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1563 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1569 my $descendants = defined($ou_type) ?
1570 "actor.org_unit_descendants($ou, $ou_type)" :
1571 "actor.org_unit_descendants($ou)";
1573 my $search_table_list = '';
1575 my $join_table_list = '';
1578 my $field_table = config::metabib_field->table;
1582 my $prev_search_group;
1583 my $curr_search_group;
1587 for my $search_group (sort keys %{$args{searches}}) {
1588 (my $search_group_name = $search_group) =~ s/\|/_/gso;
1589 ($search_class,$search_field) = split /\|/, $search_group;
1590 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1592 if ($search_field) {
1593 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1594 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1599 $prev_search_group = $curr_search_group if ($curr_search_group);
1601 $curr_search_group = $search_group_name;
1603 my $class = $_cdbi->{$search_class};
1604 my $search_table = $class->table;
1606 my ($index_col) = $class->columns('FTS');
1607 $index_col ||= 'value';
1610 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1612 my $fts_where = $fts->sql_where_clause;
1613 my @fts_ranks = $fts->fts_rank;
1615 my $SQLstring = join('%',map { lc($_) } $fts->words);
1616 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1617 my $first_word = lc(($fts->words)[0]).'%';
1619 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1620 my $rank = join(' + ', @fts_ranks);
1623 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1624 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1626 $bonus{'series'} = [
1627 { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1628 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1631 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1633 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1634 $bonus_list ||= '1';
1636 push @bonus_lists, $bonus_list;
1637 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1640 #---------------------
1642 $search_table_list .= "$search_table $search_group_name, ";
1643 push @rank_list,$rank;
1644 $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1646 if ($metabib_field) {
1647 $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1648 $metabib_field = undef;
1651 if ($prev_search_group) {
1652 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1656 my $metabib_record_descriptor = metabib::record_descriptor->table;
1657 my $metabib_full_rec = metabib::full_rec->table;
1658 my $metabib_metarecord = metabib::metarecord->table;
1659 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1660 my $asset_call_number_table = asset::call_number->table;
1661 my $asset_copy_table = asset::copy->table;
1662 my $cs_table = config::copy_status->table;
1663 my $cl_table = asset::copy_location->table;
1664 my $br_table = biblio::record_entry->table;
1665 my $source_table = config::bib_source->table;
1667 my $bonuses = join (' * ', @bonus_lists);
1668 my $relevance = join (' + ', @rank_list);
1669 $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1671 my $string_default_sort = 'zzzz';
1672 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1674 my $number_default_sort = '9999';
1675 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1679 my $secondary_sort = <<" SORT";
1681 SELECT COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1682 FROM $metabib_full_rec sfrt,
1683 $metabib_metarecord mr
1684 WHERE sfrt.record = mr.master_record
1685 AND sfrt.tag = '245'
1686 AND sfrt.subfield = 'a'
1691 my $rank = $relevance;
1692 if (lc($sort) eq 'pubdate') {
1695 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1696 FROM $metabib_full_rec frp
1697 WHERE frp.record = mr.master_record
1699 AND frp.subfield = 'c'
1703 } elsif (lc($sort) eq 'create_date') {
1705 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1707 } elsif (lc($sort) eq 'edit_date') {
1709 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1711 } elsif (lc($sort) eq 'title') {
1714 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1715 FROM $metabib_full_rec frt
1716 WHERE frt.record = mr.master_record
1718 AND frt.subfield = 'a'
1722 $secondary_sort = <<" SORT";
1724 SELECT COALESCE(SUBSTRING(sfrp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1725 FROM $metabib_full_rec sfrp
1726 WHERE sfrp.record = mr.master_record
1727 AND sfrp.tag = '260'
1728 AND sfrp.subfield = 'c'
1732 } elsif (lc($sort) eq 'author') {
1735 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1736 FROM $metabib_full_rec fra
1737 WHERE fra.record = mr.master_record
1738 AND fra.tag LIKE '1%'
1739 AND fra.subfield = 'a'
1740 ORDER BY fra.tag::text::int
1745 push @bonus_values, @bonus_values;
1750 my $select = <<" SQL";
1751 SELECT m.metarecord,
1753 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1756 FROM $search_table_list
1757 $metabib_metarecord mr,
1758 $metabib_metarecord_source_map_table m,
1759 $metabib_metarecord_source_map_table smrs
1760 WHERE m.metarecord = smrs.metarecord
1761 AND mr.id = m.metarecord
1764 GROUP BY m.metarecord
1765 -- ORDER BY 4 $sort_dir
1766 LIMIT $visibility_limit
1769 if ($self->api_name !~ /staff/o) {
1776 FROM $asset_call_number_table cn,
1777 $metabib_metarecord_source_map_table mrs,
1778 $asset_copy_table cp,
1783 $metabib_record_descriptor ord
1784 WHERE mrs.metarecord = s.metarecord
1785 AND br.id = mrs.source
1786 AND cn.record = mrs.source
1787 AND cp.status = cs.id
1788 AND cp.location = cl.id
1789 AND cp.circ_lib = d.id
1790 AND cp.call_number = cn.id
1791 AND cp.opac_visible IS TRUE
1792 AND cs.opac_visible IS TRUE
1793 AND cl.opac_visible IS TRUE
1794 AND d.opac_visible IS TRUE
1795 AND br.active IS TRUE
1796 AND br.deleted IS FALSE
1797 AND cp.deleted IS FALSE
1798 AND cn.deleted IS FALSE
1799 AND ord.record = mrs.source
1812 $metabib_metarecord_source_map_table mrs,
1813 $metabib_record_descriptor ord,
1815 WHERE mrs.metarecord = s.metarecord
1816 AND ord.record = mrs.source
1817 AND br.id = mrs.source
1818 AND br.source = src.id
1819 AND src.transcendant IS TRUE
1827 ORDER BY 4 $sort_dir, 5
1834 $metabib_metarecord_source_map_table omrs,
1835 $metabib_record_descriptor ord
1836 WHERE omrs.metarecord = s.metarecord
1837 AND ord.record = omrs.source
1840 FROM $asset_call_number_table cn,
1841 $asset_copy_table cp,
1844 WHERE br.id = omrs.source
1845 AND cn.record = omrs.source
1846 AND br.deleted IS FALSE
1847 AND cn.deleted IS FALSE
1848 AND cp.call_number = cn.id
1849 AND ( cn.owning_lib = d.id
1850 OR ( cp.circ_lib = d.id
1851 AND cp.deleted IS FALSE
1859 FROM $asset_call_number_table cn
1860 WHERE cn.record = omrs.source
1861 AND cn.deleted IS FALSE
1867 $metabib_metarecord_source_map_table mrs,
1868 $metabib_record_descriptor ord,
1870 WHERE mrs.metarecord = s.metarecord
1871 AND br.id = mrs.source
1872 AND br.source = src.id
1873 AND src.transcendant IS TRUE
1889 ORDER BY 4 $sort_dir, 5
1894 $log->debug("Field Search SQL :: [$select]",DEBUG);
1896 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1899 @types, @forms, @vformats, @aud, @lang, @lit_form,
1900 @types, @forms, @vformats, @aud, @lang, @lit_form,
1901 # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1904 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1907 $max = 1 if (!@$recs);
1909 $max = $$_[1] if ($$_[1] > $max);
1912 my $count = scalar(@$recs);
1913 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1914 next unless ($$rec[0]);
1915 my ($mrid,$rank,$skip) = @$rec;
1916 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1921 __PACKAGE__->register_method(
1922 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1924 method => 'postfilter_search_multi_class_fts',
1929 __PACKAGE__->register_method(
1930 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1932 method => 'postfilter_search_multi_class_fts',
1938 __PACKAGE__->register_method(
1939 api_name => "open-ils.storage.metabib.multiclass.search_fts",
1941 method => 'postfilter_search_multi_class_fts',
1946 __PACKAGE__->register_method(
1947 api_name => "open-ils.storage.metabib.multiclass.search_fts.staff",
1949 method => 'postfilter_search_multi_class_fts',
1955 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1956 sub biblio_search_multi_class_fts {
1961 my $sort = $args{'sort'};
1962 my $sort_dir = $args{sort_dir} || 'DESC';
1963 my $ou = $args{org_unit};
1964 my $ou_type = $args{depth};
1965 my $limit = $args{limit} || 10;
1966 my $offset = $args{offset} || 0;
1967 my $pref_lang = $args{preferred_language} || 'eng';
1968 my $visibility_limit = $args{visibility_limit} || 5000;
1971 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1974 if (! scalar( keys %{$args{searches}} )) {
1975 die "No search arguments were passed to ".$self->api_name;
1978 my $outer_limit = 1000;
1980 my $limit_clause = '';
1981 my $offset_clause = '';
1983 $limit_clause = "LIMIT $outer_limit";
1984 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1986 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1987 my ($t_filter, $f_filter, $v_filter) = ('','','');
1988 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1989 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1990 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1992 if ($args{available}) {
1993 $avail_filter = ' AND cp.status IN (0,7,12)';
1996 if (my $a = $args{audience}) {
1997 $a = [$a] if (!ref($a));
2000 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
2001 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
2004 if (my $l = $args{language}) {
2005 $l = [$l] if (!ref($l));
2008 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
2009 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
2012 if (my $f = $args{lit_form}) {
2013 $f = [$f] if (!ref($f));
2016 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2017 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2020 if (my $f = $args{item_form}) {
2021 $f = [$f] if (!ref($f));
2024 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2025 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2028 if (my $t = $args{item_type}) {
2029 $t = [$t] if (!ref($t));
2032 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2033 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2036 if (my $v = $args{vr_format}) {
2037 $v = [$v] if (!ref($v));
2040 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
2041 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
2044 # XXX legacy format and item type support
2045 if ($args{format}) {
2046 my ($t, $f) = split '-', $args{format};
2047 @types = split '', $t;
2048 @forms = split '', $f;
2050 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2051 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2055 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2056 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2061 my $descendants = defined($ou_type) ?
2062 "actor.org_unit_descendants($ou, $ou_type)" :
2063 "actor.org_unit_descendants($ou)";
2065 my $search_table_list = '';
2067 my $join_table_list = '';
2070 my $field_table = config::metabib_field->table;
2074 my $prev_search_group;
2075 my $curr_search_group;
2079 for my $search_group (sort keys %{$args{searches}}) {
2080 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2081 ($search_class,$search_field) = split /\|/, $search_group;
2082 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2084 if ($search_field) {
2085 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2086 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2091 $prev_search_group = $curr_search_group if ($curr_search_group);
2093 $curr_search_group = $search_group_name;
2095 my $class = $_cdbi->{$search_class};
2096 my $search_table = $class->table;
2098 my ($index_col) = $class->columns('FTS');
2099 $index_col ||= 'value';
2102 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2104 my $fts_where = $fts->sql_where_clause;
2105 my @fts_ranks = $fts->fts_rank;
2107 my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2108 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2109 my $first_word = lc(($fts->words)[0]).'%';
2111 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2112 my $rank = join(' + ', @fts_ranks);
2115 $bonus{'subject'} = [];
2116 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2118 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2120 $bonus{'series'} = [
2121 { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2122 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2125 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2128 push @{ $bonus{'title'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2129 push @{ $bonus{'author'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2130 push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2131 push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2132 push @{ $bonus{'series'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2135 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2136 $bonus_list ||= '1';
2138 push @bonus_lists, $bonus_list;
2139 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2141 #---------------------
2143 $search_table_list .= "$search_table $search_group_name, ";
2144 push @rank_list,$rank;
2145 $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2147 if ($metabib_field) {
2148 $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2149 $metabib_field = undef;
2152 if ($prev_search_group) {
2153 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2157 my $metabib_record_descriptor = metabib::record_descriptor->table;
2158 my $metabib_full_rec = metabib::full_rec->table;
2159 my $metabib_metarecord = metabib::metarecord->table;
2160 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2161 my $asset_call_number_table = asset::call_number->table;
2162 my $asset_copy_table = asset::copy->table;
2163 my $cs_table = config::copy_status->table;
2164 my $cl_table = asset::copy_location->table;
2165 my $br_table = biblio::record_entry->table;
2166 my $source_table = config::bib_source->table;
2169 my $bonuses = join (' * ', @bonus_lists);
2170 my $relevance = join (' + ', @rank_list);
2171 $relevance = "AVG( ($relevance) * ($bonuses) )";
2173 my $string_default_sort = 'zzzz';
2174 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2176 my $number_default_sort = '9999';
2177 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2179 my $rank = $relevance;
2180 if (lc($sort) eq 'pubdate') {
2183 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d{4}'),'$number_default_sort')::INT
2184 FROM $metabib_full_rec frp
2185 WHERE frp.record = b.id
2187 AND frp.subfield = 'c'
2191 } elsif (lc($sort) eq 'create_date') {
2193 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2195 } elsif (lc($sort) eq 'edit_date') {
2197 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2199 } elsif (lc($sort) eq 'title') {
2202 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2203 FROM $metabib_full_rec frt
2204 WHERE frt.record = b.id
2206 AND frt.subfield = 'a'
2210 } elsif (lc($sort) eq 'author') {
2213 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
2214 FROM $metabib_full_rec fra
2215 WHERE fra.record = b.id
2216 AND fra.tag LIKE '1%'
2217 AND fra.subfield = 'a'
2218 ORDER BY fra.tag::text::int
2223 push @bonus_values, @bonus_values;
2228 my $select = <<" SQL";
2233 FROM $search_table_list
2234 $metabib_record_descriptor rd,
2237 WHERE rd.record = b.id
2238 AND b.active IS TRUE
2239 AND b.deleted IS FALSE
2248 GROUP BY b.id, b.source
2249 ORDER BY 3 $sort_dir
2250 LIMIT $visibility_limit
2253 if ($self->api_name !~ /staff/o) {
2258 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2261 FROM $asset_call_number_table cn,
2262 $asset_copy_table cp,
2266 WHERE cn.record = s.id
2267 AND cp.status = cs.id
2268 AND cp.location = cl.id
2269 AND cp.call_number = cn.id
2270 AND cp.opac_visible IS TRUE
2271 AND cs.opac_visible IS TRUE
2272 AND cl.opac_visible IS TRUE
2273 AND d.opac_visible IS TRUE
2274 AND cp.deleted IS FALSE
2275 AND cn.deleted IS FALSE
2276 AND cp.circ_lib = d.id
2280 OR src.transcendant IS TRUE
2281 ORDER BY 3 $sort_dir
2288 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2291 FROM $asset_call_number_table cn,
2292 $asset_copy_table cp,
2294 WHERE cn.record = s.id
2295 AND cp.call_number = cn.id
2296 AND cn.deleted IS FALSE
2297 AND cp.circ_lib = d.id
2298 AND cp.deleted IS FALSE
2304 FROM $asset_call_number_table cn
2305 WHERE cn.record = s.id
2308 OR src.transcendant IS TRUE
2309 ORDER BY 3 $sort_dir
2314 $log->debug("Field Search SQL :: [$select]",DEBUG);
2316 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2318 @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2321 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2323 my $count = scalar(@$recs);
2324 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2325 next unless ($$rec[0]);
2326 my ($mrid,$rank) = @$rec;
2327 $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2332 __PACKAGE__->register_method(
2333 api_name => "open-ils.storage.biblio.multiclass.search_fts.record",
2335 method => 'biblio_search_multi_class_fts',
2340 __PACKAGE__->register_method(
2341 api_name => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2343 method => 'biblio_search_multi_class_fts',
2348 __PACKAGE__->register_method(
2349 api_name => "open-ils.storage.biblio.multiclass.search_fts",
2351 method => 'biblio_search_multi_class_fts',
2356 __PACKAGE__->register_method(
2357 api_name => "open-ils.storage.biblio.multiclass.search_fts.staff",
2359 method => 'biblio_search_multi_class_fts',
2367 my $default_preferred_language;
2368 my $default_preferred_language_weight;
2370 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2376 if (!$locale_map{COMPLETE}) {
2378 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2379 for my $locale ( @locales ) {
2380 $locale_map{lc($locale->code)} = $locale->marc_code;
2382 $locale_map{COMPLETE} = 1;
2386 my $config = OpenSRF::Utils::SettingsClient->new();
2388 if (!$default_preferred_language) {
2390 $default_preferred_language = $config->config_value(
2391 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2392 ) || $config->config_value(
2393 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2398 if (!$default_preferred_language_weight) {
2400 $default_preferred_language_weight = $config->config_value(
2401 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2402 ) || $config->config_value(
2403 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2407 # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2408 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2410 my $ou = $args{org_unit};
2411 my $limit = $args{limit} || 10;
2412 my $offset = $args{offset} || 0;
2415 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2418 if (! scalar( keys %{$args{searches}} )) {
2419 die "No search arguments were passed to ".$self->api_name;
2422 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2424 if (!defined($args{preferred_language})) {
2425 my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2426 $args{preferred_language} =
2427 $locale_map{ lc($ses_locale) } || 'eng';
2430 if (!defined($args{preferred_language_weight})) {
2431 $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2434 if ($args{available}) {
2435 @statuses = (0,7,12);
2438 if (my $s = $args{locations}) {
2439 $s = [$s] if (!ref($s));
2443 if (my $b = $args{between}) {
2444 if (ref($b) && @$b == 2) {
2449 if (my $s = $args{statuses}) {
2450 $s = [$s] if (!ref($s));
2454 if (my $a = $args{audience}) {
2455 $a = [$a] if (!ref($a));
2459 if (my $l = $args{language}) {
2460 $l = [$l] if (!ref($l));
2464 if (my $f = $args{lit_form}) {
2465 $f = [$f] if (!ref($f));
2469 if (my $f = $args{item_form}) {
2470 $f = [$f] if (!ref($f));
2474 if (my $t = $args{item_type}) {
2475 $t = [$t] if (!ref($t));
2479 if (my $b = $args{bib_level}) {
2480 $b = [$b] if (!ref($b));
2484 if (my $v = $args{vr_format}) {
2485 $v = [$v] if (!ref($v));
2489 # XXX legacy format and item type support
2490 if ($args{format}) {
2491 my ($t, $f) = split '-', $args{format};
2492 @types = split '', $t;
2493 @forms = split '', $f;
2496 my %stored_proc_search_args;
2497 for my $search_group (sort keys %{$args{searches}}) {
2498 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2499 my ($search_class,$search_field) = split /\|/, $search_group;
2500 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2502 if ($search_field) {
2503 unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2504 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2509 my $class = $_cdbi->{$search_class};
2510 my $search_table = $class->table;
2512 my ($index_col) = $class->columns('FTS');
2513 $index_col ||= 'value';
2516 my $fts = OpenILS::Application::Storage::FTS->compile(
2517 $search_class => $args{searches}{$search_group}{term},
2518 $search_group_name.'.value',
2519 "$search_group_name.$index_col"
2521 $fts->sql_where_clause; # this builds the ranks for us
2523 my @fts_ranks = $fts->fts_rank;
2524 my @fts_queries = $fts->fts_query;
2525 my @phrases = map { lc($_) } $fts->phrases;
2526 my @words = map { lc($_) } $fts->words;
2528 $stored_proc_search_args{$search_group} = {
2529 fts_rank => \@fts_ranks,
2530 fts_query => \@fts_queries,
2531 phrase => \@phrases,
2537 my $param_search_ou = $ou;
2538 my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2539 my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2540 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2541 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2542 my $param_audience = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud ) . '}$$';
2543 my $param_language = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang ) . '}$$';
2544 my $param_lit_form = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2545 my $param_types = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types ) . '}$$';
2546 my $param_forms = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms ) . '}$$';
2547 my $param_vformats = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2548 my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2549 my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2550 my $param_after = $args{after} ; $param_after = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2551 my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2552 my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2553 my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2554 my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2555 my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2556 my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2557 my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2558 my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2559 my $param_rel_limit = $args{core_limit}; $param_rel_limit ||= 'NULL';
2560 my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2561 my $param_skip_chk = $args{skip_check}; $param_skip_chk ||= 'NULL';
2563 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2565 FROM search.staged_fts(
2566 $param_search_ou\:\:INT,
2567 $param_depth\:\:INT,
2568 $param_searches\:\:TEXT,
2569 $param_statuses\:\:INT[],
2570 $param_locations\:\:INT[],
2571 $param_audience\:\:TEXT[],
2572 $param_language\:\:TEXT[],
2573 $param_lit_form\:\:TEXT[],
2574 $param_types\:\:TEXT[],
2575 $param_forms\:\:TEXT[],
2576 $param_vformats\:\:TEXT[],
2577 $param_bib_level\:\:TEXT[],
2578 $param_before\:\:TEXT,
2579 $param_after\:\:TEXT,
2580 $param_during\:\:TEXT,
2581 $param_between\:\:TEXT[],
2582 $param_pref_lang\:\:TEXT,
2583 $param_pref_lang_multiplier\:\:REAL,
2584 $param_sort\:\:TEXT,
2585 $param_sort_desc\:\:BOOL,
2586 $metarecord\:\:BOOL,
2588 $param_rel_limit\:\:INT,
2589 $param_chk_limit\:\:INT,
2590 $param_skip_chk\:\:INT
2596 my $recs = $sth->fetchall_arrayref({});
2597 my $summary_row = pop @$recs;
2599 my $total = $$summary_row{total};
2600 my $checked = $$summary_row{checked};
2601 my $visible = $$summary_row{visible};
2602 my $deleted = $$summary_row{deleted};
2603 my $excluded = $$summary_row{excluded};
2605 my $estimate = $visible;
2606 if ( $total > $checked && $checked ) {
2608 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2609 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2613 delete $$summary_row{id};
2614 delete $$summary_row{rel};
2615 delete $$summary_row{record};
2617 $client->respond( $summary_row );
2619 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2621 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2622 delete $$rec{checked};
2623 delete $$rec{visible};
2624 delete $$rec{excluded};
2625 delete $$rec{deleted};
2626 delete $$rec{total};
2627 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2629 $client->respond( $rec );
2633 __PACKAGE__->register_method(
2634 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
2636 method => 'staged_fts',
2641 __PACKAGE__->register_method(
2642 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2644 method => 'staged_fts',
2649 __PACKAGE__->register_method(
2650 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
2652 method => 'staged_fts',
2657 __PACKAGE__->register_method(
2658 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2660 method => 'staged_fts',
2666 sub FTS_paging_estimate {
2670 my $checked = shift;
2671 my $visible = shift;
2672 my $excluded = shift;
2673 my $deleted = shift;
2676 my $deleted_ratio = $deleted / $checked;
2677 my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2679 my $exclusion_ratio = $excluded / $checked;
2680 my $delete_adjusted_exclusion_ratio = $checked - $deleted ? $excluded / ($checked - $deleted) : 1;
2682 my $inclusion_ratio = $visible / $checked;
2683 my $delete_adjusted_inclusion_ratio = $checked - $deleted ? $visible / ($checked - $deleted) : 0;
2686 exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2687 inclusion => int($delete_adjusted_total * $inclusion_ratio),
2688 delete_adjusted_exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2689 delete_adjusted_inclusion => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2692 __PACKAGE__->register_method(
2693 api_name => "open-ils.storage.fts_paging_estimate",
2695 method => 'FTS_paging_estimate',
2701 Hash of estimation values based on four variant estimation strategies:
2702 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2703 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2704 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2705 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2708 Helper method used to determin the approximate number of
2709 hits for a search that spans multiple superpages. For
2710 sparse superpages, the inclusion estimate will likely be the
2711 best estimate. The exclusion strategy is the original, but
2712 inclusion is the default.
2715 { name => 'checked',
2716 desc => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2719 { name => 'visible',
2720 desc => 'Number of records visible to the search location on the current superpage.',
2723 { name => 'excluded',
2724 desc => 'Number of records excluded from the search location on the current superpage.',
2727 { name => 'deleted',
2728 desc => 'Number of deleted records on the current superpage.',
2732 desc => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2745 my $term = $$args{term};
2746 my $limit = $$args{max} || 1;
2747 my $min = $$args{min} || 1;
2748 my @classes = @{$$args{class}};
2750 $limit = $min if ($min > $limit);
2753 @classes = ( qw/ title author subject series keyword / );
2757 my $bre_table = biblio::record_entry->table;
2758 my $cn_table = asset::call_number->table;
2759 my $cp_table = asset::copy->table;
2761 for my $search_class ( @classes ) {
2763 my $class = $_cdbi->{$search_class};
2764 my $search_table = $class->table;
2766 my ($index_col) = $class->columns('FTS');
2767 $index_col ||= 'value';
2770 my $where = OpenILS::Application::Storage::FTS
2771 ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2775 SELECT COUNT(DISTINCT X.source)
2776 FROM (SELECT $search_class.source
2777 FROM $search_table $search_class
2778 JOIN $bre_table b ON (b.id = $search_class.source)
2783 HAVING COUNT(DISTINCT X.source) >= $min;
2786 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2787 $matches{$search_class} = $res ? $res->[0] : 0;
2792 __PACKAGE__->register_method(
2793 api_name => "open-ils.storage.search.xref",
2795 method => 'xref_count',
2799 # Takes an abstract query object and recursively turns it back into a string
2801 sub abstract_query2str {
2802 my ($self, $conn, $query) = @_;
2804 return QueryParser::Canonicalize::abstract_query2str_impl($query, 0, $OpenILS::Application::Storage::QParser);
2807 __PACKAGE__->register_method(
2808 api_name => "open-ils.storage.query_parser.abstract_query.canonicalize",
2810 method => "abstract_query2str",
2815 Abstract query parser object, with complete config data. For example input,
2816 see the 'abstract_query' part of the output of an API call like
2817 open-ils.search.biblio.multiclass.query, when called with the return_abstract
2821 return => { type => "string", desc => "String representation of abstract query object" }
2825 sub str2abstract_query {
2826 my ($self, $conn, $query, $qp_opts, $with_config) = @_;
2828 my %use_opts = ( # reasonable defaults? should these even be hardcoded here?
2830 superpage_size => 1000,
2831 core_limit => 25000,
2833 (ref $opts eq 'HASH' ? %$opts : ())
2838 # grab the query parser and initialize it
2839 my $parser = $OpenILS::Application::Storage::QParser;
2842 _initialize_parser($parser) unless $parser->initialization_complete;
2844 my $query = $parser->new(%use_opts)->parse;
2846 return $query->parse_tree->to_abstract_query(with_config => $with_config);
2849 __PACKAGE__->register_method(
2850 api_name => "open-ils.storage.query_parser.abstract_query.from_string",
2852 method => "str2abstract_query",
2856 {desc => "Query", type => "string"},
2857 {desc => q/Arguments for initializing QueryParser (optional)/,
2859 {desc => q/Flag enabling inclusion of QP config in returned object (optional, default false)/,
2862 return => { type => "object", desc => "abstract representation of query parser query" }
2866 my @available_statuses_cache;
2867 sub available_statuses {
2868 if (!scalar(@available_statuses_cache)) {
2869 @available_statuses_cache = map { $_->id } config::copy_status->search_where({is_available => 't'});
2871 return @available_statuses_cache;
2874 sub query_parser_fts {
2880 # grab the query parser and initialize it
2881 my $parser = $OpenILS::Application::Storage::QParser;
2884 _initialize_parser($parser) unless $parser->initialization_complete;
2886 # populate the locale/language map
2887 if (!$locale_map{COMPLETE}) {
2889 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2890 for my $locale ( @locales ) {
2891 $locale_map{lc($locale->code)} = $locale->marc_code;
2893 $locale_map{COMPLETE} = 1;
2897 # I hope we have a query!
2898 if (! $args{query} ) {
2899 die "No query was passed to ".$self->api_name;
2902 my $default_CD_modifiers = OpenSRF::Utils::SettingsClient->new->config_value(
2903 apps => 'open-ils.search' => app_settings => 'default_CD_modifiers'
2906 # Protect against empty / missing default_CD_modifiers setting
2907 if ($default_CD_modifiers and !ref($default_CD_modifiers)) {
2908 $args{query} = "$default_CD_modifiers $args{query}";
2911 my $simple_plan = $args{_simple_plan};
2912 # remove bad chunks of the %args hash
2913 for my $bad ( grep { /^_/ } keys(%args)) {
2914 delete($args{$bad});
2918 # parse the query and supply any query-level %arg-based defaults
2919 # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2920 my $query = $parser->new( %args )->parse;
2922 my $config = OpenSRF::Utils::SettingsClient->new();
2924 # set the locale-based default preferred location
2925 if (!$query->parse_tree->find_filter('preferred_language')) {
2926 $parser->default_preferred_language( $args{preferred_language} );
2928 if (!$parser->default_preferred_language) {
2929 my $ses_locale = $client->session ? $client->session->session_locale : '';
2930 $parser->default_preferred_language( $locale_map{ lc($ses_locale) } );
2933 if (!$parser->default_preferred_language) { # still nothing...
2934 my $tmp_dpl = $config->config_value(
2935 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2936 ) || $config->config_value(
2937 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2940 $parser->default_preferred_language( $tmp_dpl )
2945 # set the global default language multiplier
2946 if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2949 if ($tmp_dplw = $args{preferred_language_weight} || $args{preferred_language_multiplier} ) {
2950 $parser->default_preferred_language_multiplier($tmp_dplw);
2953 $tmp_dplw = $config->config_value(
2954 apps => 'open-ils.search' => app_settings => 'default_preferred_language_weight'
2955 ) || $config->config_value(
2956 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2959 $parser->default_preferred_language_multiplier( $tmp_dplw );
2963 # gather the site, if one is specified, defaulting to the in-query version
2964 my $ou = $args{org_unit};
2965 if (my ($filter) = $query->parse_tree->find_filter('site')) {
2966 $ou = $filter->args->[0] if (@{$filter->args});
2968 $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^(-)?\d+$/);
2970 # gather lasso, as with $ou
2971 my $lasso = $args{lasso};
2972 if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
2973 $lasso = $filter->args->[0] if (@{$filter->args});
2975 $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
2976 $lasso = -$lasso if ($lasso);
2979 # # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
2980 # # gather user lasso, as with $ou and lasso
2981 # my $mylasso = $args{my_lasso};
2982 # if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
2983 # $mylasso = $filter->args->[0] if (@{$filter->args});
2985 # $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
2988 # if we have a lasso, go with that, otherwise ... ou
2989 $ou = $lasso if ($lasso);
2991 # gather the preferred OU, if one is specified, as with $ou
2992 my $pref_ou = $args{pref_ou};
2993 $log->info("pref_ou = $pref_ou");
2994 if (my ($filter) = $query->parse_tree->find_filter('pref_ou')) {
2995 $pref_ou = $filter->args->[0] if (@{$filter->args});
2997 $pref_ou = actor::org_unit->search( { shortname => $pref_ou } )->next->id if ($pref_ou and $pref_ou !~ /^(-)?\d+$/);
2999 # get the default $ou if we have nothing
3000 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
3003 # 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
3004 # gather the depth, if one is specified, defaulting to the in-query version
3005 my $depth = $args{depth};
3006 if (my ($filter) = $query->parse_tree->find_filter('depth')) {
3007 $depth = $filter->args->[0] if (@{$filter->args});
3009 $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
3012 # gather the limit or default to 10
3013 my $limit = $args{check_limit};
3014 if (my ($filter) = $query->parse_tree->find_filter('limit')) {
3015 $limit = $filter->args->[0] if (@{$filter->args});
3017 if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
3018 $limit = $filter->args->[0] if (@{$filter->args});
3022 # gather the offset or default to 0
3023 my $offset = $args{skip_check} || $args{offset};
3024 if (my ($filter) = $query->parse_tree->find_filter('offset')) {
3025 $offset = $filter->args->[0] if (@{$filter->args});
3027 if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
3028 $offset = $filter->args->[0] if (@{$filter->args});
3032 # gather the estimation strategy or default to inclusion
3033 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
3034 if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
3035 $estimation_strategy = $filter->args->[0] if (@{$filter->args});
3039 # gather the estimation strategy or default to inclusion
3040 my $core_limit = $args{core_limit};
3041 if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
3042 $core_limit = $filter->args->[0] if (@{$filter->args});
3046 # gather statuses, and then forget those if we have an #available modifier
3048 if ($query->parse_tree->find_modifier('available')) {
3049 @statuses = available_statuses();
3050 } elsif (my ($filter) = $query->parse_tree->find_filter('statuses')) {
3051 @statuses = @{$filter->args} if (@{$filter->args});
3057 if (my ($filter) = $query->parse_tree->find_filter('locations')) {
3058 @location = @{$filter->args} if (@{$filter->args});
3061 # gather location_groups
3062 if (my ($filter) = $query->parse_tree->find_filter('location_groups')) {
3063 my @loc_groups = ();
3064 @loc_groups = @{$filter->args} if (@{$filter->args});
3066 # collect the mapped locations and add them to the locations() filter
3069 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3070 my $maps = $cstore->request(
3071 'open-ils.cstore.direct.asset.copy_location_group_map.search.atomic',
3072 {lgroup => \@loc_groups})->gather(1);
3074 push(@location, $_->location) for @$maps;
3079 my $param_check = $limit || $query->superpage_size || 'NULL';
3080 my $param_offset = $offset || 'NULL';
3081 my $param_limit = $core_limit || 'NULL';
3083 my $sp = $query->superpage || 1;
3085 $param_offset = ($sp - 1) * $sp_size;
3088 my $param_search_ou = $ou;
3089 my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
3090 # my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
3091 my $param_core_query = $query->parse_tree->toSQL;
3092 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
3093 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
3094 my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
3095 my $deleted_search = ($query->parse_tree->find_modifier('deleted')) ? "'t'" : "'f'";
3096 my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
3097 my $param_pref_ou = $pref_ou || 'NULL';
3099 # my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3100 # SELECT * -- bib search: $args{query}
3101 # FROM search.query_parser_fts(
3102 # $param_search_ou\:\:INT,
3103 # $param_depth\:\:INT,
3104 # $param_core_query\:\:TEXT,
3105 # $param_statuses\:\:INT[],
3106 # $param_locations\:\:INT[],
3107 # $param_offset\:\:INT,
3108 # $param_check\:\:INT,
3109 # $param_limit\:\:INT,
3110 # $metarecord\:\:BOOL,
3112 # $deleted_search\:\:BOOL,
3113 # $param_pref_ou\:\:INT
3117 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3118 -- bib search: $args{query}
3124 my $recs = $sth->fetchall_arrayref({});
3125 my $summary_row = pop @$recs;
3127 my $total = $$summary_row{total};
3128 my $checked = $$summary_row{checked};
3129 my $visible = $$summary_row{visible};
3130 my $deleted = $$summary_row{deleted};
3131 my $excluded = $$summary_row{excluded};
3133 delete $$summary_row{id};
3134 delete $$summary_row{rel};
3135 delete $$summary_row{record};
3136 delete $$summary_row{badges};
3137 delete $$summary_row{popularity};
3139 if (defined($simple_plan)) {
3140 $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3142 $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3145 if ($args{return_query}) {
3146 $$summary_row{query_struct} = $query->parse_tree->to_abstract_query();
3147 $$summary_row{canonicalized_query} = QueryParser::Canonicalize::abstract_query2str_impl($$summary_row{query_struct}, 0, $parser);
3150 $client->respond( $summary_row );
3152 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $visible.",DEBUG);
3154 for my $rec (@$recs) {
3155 delete $$rec{checked};
3156 delete $$rec{visible};
3157 delete $$rec{excluded};
3158 delete $$rec{deleted};
3159 delete $$rec{total};
3160 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3162 $client->respond( $rec );
3166 __PACKAGE__->register_method(
3167 api_name => "open-ils.storage.query_parser_search",
3169 method => 'query_parser_fts',
3177 sub query_parser_fts_wrapper {
3182 $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3183 # grab the query parser and initialize it
3184 my $parser = $OpenILS::Application::Storage::QParser;
3187 _initialize_parser($parser) unless $parser->initialization_complete;
3189 $args{searches} ||= {};
3190 if (!scalar(keys(%{$args{searches}})) && !$args{query}) {
3191 die "No search arguments were passed to ".$self->api_name;
3194 $top_org ||= actor::org_unit->search( { parent_ou => undef } )->next;
3196 my $base_query = $args{query} || '';
3197 if (scalar(keys(%{$args{searches}}))) {
3198 $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3199 for my $sclass ( keys %{$args{searches}} ) {
3200 $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3201 $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3205 my $query = $base_query;
3206 $log->debug("Full base query: $base_query", DEBUG);
3208 $query = "$args{facets} $query" if ($args{facets});
3210 if (!$locale_map{COMPLETE}) {
3212 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3213 for my $locale ( @locales ) {
3214 $locale_map{lc($locale->code)} = $locale->marc_code;
3216 $locale_map{COMPLETE} = 1;
3220 my $base_plan = $parser->new( query => $base_query )->parse;
3222 $query = "preferred_language($args{preferred_language}) $query"
3223 if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3224 $query = "preferred_language_weight($args{preferred_language_weight}) $query"
3225 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'));
3229 if (!$base_plan->parse_tree->find_filter('badge_orgs')) {
3230 # supply a suitable badge_orgs filter unless user has
3231 # explicitly supplied one
3234 my @lg_id_list = (); # We must define the variable with a static value
3235 # because an idomatic my+set causes the previous
3236 # value is remembered via closure.
3238 @lg_id_list = @{$args{location_groups}} if (ref $args{location_groups});
3240 my ($lg_filter) = $base_plan->parse_tree->find_filter('location_groups');
3241 @lg_id_list = @{$lg_filter->args} if ($lg_filter && @{$lg_filter->args});
3245 for my $lg ( grep { /^\d+$/ } @lg_id_list ) {
3246 my $lg_obj = asset::copy_location_group->retrieve($lg);
3247 next unless $lg_obj;
3249 push(@borg_list, @{$U->get_org_ancestors(''.$lg_obj->owner)});
3251 $borgs = join(',', uniq @borg_list) if @borg_list;
3255 my ($site_filter) = $base_plan->parse_tree->find_filter('site');
3256 if ($site_filter && @{$site_filter->args}) {
3257 $site = $top_org if ($site_filter->args->[0] eq '-');
3258 $site = $top_org if ($site_filter->args->[0] eq $top_org->shortname);
3259 $site = actor::org_unit->search( { shortname => $site_filter->args->[0] })->next unless ($site);
3260 } elsif ($args{org_unit}) {
3261 $site = $top_org if ($args{org_unit} eq '-');
3262 $site = $top_org if ($args{org_unit} eq $top_org->shortname);
3263 $site = actor::org_unit->search( { shortname => $args{org_unit} })->next unless ($site);
3269 $borgs = $U->get_org_ancestors($site->id);
3270 $borgs = @$borgs ? join(',', @$borgs) : undef;
3275 # gather the limit or default to 10
3276 my $limit = delete($args{check_limit}) || $base_plan->superpage_size;
3277 if (my ($filter) = $base_plan->parse_tree->find_filter('limit')) {
3278 $limit = $filter->args->[0] if (@{$filter->args});
3280 if (my ($filter) = $base_plan->parse_tree->find_filter('check_limit')) {
3281 $limit = $filter->args->[0] if (@{$filter->args});
3284 # gather the offset or default to 0
3285 my $offset = delete($args{skip_check}) || delete($args{offset}) || 0;
3286 if (my ($filter) = $base_plan->parse_tree->find_filter('offset')) {
3287 $offset = $filter->args->[0] if (@{$filter->args});
3289 if (my ($filter) = $base_plan->parse_tree->find_filter('skip_check')) {
3290 $offset = $filter->args->[0] if (@{$filter->args});
3294 $query = "check_limit($limit) $query" if (defined $limit);
3295 $query = "skip_check($offset) $query" if (defined $offset);
3296 $query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
3297 $query = "badge_orgs($borgs) $query" if ($borgs);
3299 # XXX All of the following, down to the 'return' is basically dead code. someone higher up should handle it
3300 $query = "site($args{org_unit}) $query" if ($args{org_unit});
3301 $query = "depth($args{depth}) $query" if (defined($args{depth}));
3302 $query = "sort($args{sort}) $query" if ($args{sort});
3303 $query = "core_limit($args{core_limit}) $query" if ($args{core_limit});
3304 # $query = "limit($args{limit}) $query" if ($args{limit});
3305 # $query = "skip_check($args{skip_check}) $query" if ($args{skip_check});
3306 $query = "superpage($args{superpage}) $query" if ($args{superpage});
3307 $query = "offset($args{offset}) $query" if ($args{offset});
3308 $query = "#metarecord $query" if ($self->api_name =~ /metabib/);
3309 $query = "from_metarecord($args{from_metarecord}) $query" if ($args{from_metarecord});
3310 $query = "#available $query" if ($args{available});
3311 $query = "#descending $query" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3312 $query = "#staff $query" if ($self->api_name =~ /staff/);
3313 $query = "before($args{before}) $query" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3314 $query = "after($args{after}) $query" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3315 $query = "during($args{during}) $query" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3316 $query = "between($args{between}[0],$args{between}[1]) $query"
3317 if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3320 my (@between,@statuses,@locations,@location_groups,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3322 # XXX legacy format and item type support
3323 if ($args{format}) {
3324 my ($t, $f) = split '-', $args{format};
3325 $args{item_type} = [ split '', $t ];
3326 $args{item_form} = [ split '', $f ];
3329 for my $filter ( qw/locations location_groups statuses audience language lit_form item_form item_type bib_level vr_format badges/ ) {
3330 if (my $s = $args{$filter}) {
3331 $s = [$s] if (!ref($s));
3333 my @filter_list = @$s;
3335 next if (@filter_list == 0);
3337 my $filter_string = join ',', @filter_list;
3338 $query = "$query $filter($filter_string)";
3342 $log->debug("Full QueryParser query: $query", DEBUG);
3344 return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan, return_query => $args{return_query} );
3346 __PACKAGE__->register_method(
3347 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
3349 method => 'query_parser_fts_wrapper',
3354 __PACKAGE__->register_method(
3355 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3357 method => 'query_parser_fts_wrapper',
3362 __PACKAGE__->register_method(
3363 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
3365 method => 'query_parser_fts_wrapper',
3370 __PACKAGE__->register_method(
3371 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3373 method => 'query_parser_fts_wrapper',