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_search_alias =>
46 'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
47 { alias => { "!=" => undef } }
49 config_metabib_field_index_norm_map =>
51 'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
52 { id => { "!=" => undef } },
53 { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
55 config_record_attr_definition =>
57 'open-ils.cstore.direct.config.record_attr_definition.search.atomic',
58 { name => { "!=" => undef } }
60 config_metabib_class_ts_map =>
62 'open-ils.cstore.direct.config.metabib_class_ts_map.search.atomic',
65 config_metabib_field_ts_map =>
67 'open-ils.cstore.direct.config.metabib_field_ts_map.search.atomic',
70 config_metabib_class =>
72 'open-ils.cstore.direct.config.metabib_class.search.atomic',
73 { name => { "!=" => undef } }
78 my $cgf = $cstore->request(
79 'open-ils.cstore.direct.config.global_flag.retrieve',
80 'search.max_popularity_importance_multiplier'
82 $max_mult = $cgf->value if $cgf && $U->is_true($cgf->enabled);
84 $max_mult = 2.0 unless $max_mult =~ /^-?(?:\d+\.?|\.\d)\d*\z/; # just in case
85 $parser->max_popularity_importance_multiplier($max_mult);
88 die("Cannot initialize $parser!") unless ($parser->initialization_complete);
91 sub ordered_records_from_metarecord { # XXX Replace with QP-based search-within-MR
95 my $formats = shift; # dead
99 my $copies_visible = 'LEFT JOIN asset.copy_vis_attr_cache vc ON (br.id = vc.record '.
100 'AND vc.vis_attr_vector @@ (SELECT c_attrs::query_int FROM asset.patron_default_visibility_mask() LIMIT 1))';
101 $copies_visible = '' if ($self->api_name =~ /staff/o);
103 my $copies_visible_count = ',COUNT(vc.id)';
104 $copies_visible_count = '' if ($self->api_name =~ /staff/o);
106 my $descendants = '';
108 $descendants = defined($depth) ?
109 ",actor.org_unit_descendants($org, $depth) d" :
110 ",actor.org_unit_descendants($org) d" ;
117 $copies_visible_count
118 FROM metabib.metarecord_source_map sm
119 JOIN biblio.record_entry br ON (sm.source = br.id AND NOT br.deleted)
120 LEFT JOIN metabib.record_sorter s ON (s.source = br.id AND s.attr = 'titlesort')
121 LEFT JOIN config.bib_source bs ON (br.source = bs.id)
124 WHERE sm.metarecord = ?
128 if ($copies_visible) {
129 $sql .= 'AND (bs.transcendant OR ';
131 $sql .= 'vc.circ_lib = d.id)';
133 $sql .= 'vc.id IS NOT NULL)'
135 $having = 'HAVING COUNT(vc.id) > 0';
143 s.value ASC NULLS LAST
146 my $ids = metabib::metarecord_source_map->db_Main->selectcol_arrayref($sql, {}, "$mr");
147 return $ids if ($self->api_name =~ /atomic$/o);
149 $client->respond( $_ ) for ( @$ids );
153 __PACKAGE__->register_method(
154 api_name => 'open-ils.storage.ordered.metabib.metarecord.records',
156 method => 'ordered_records_from_metarecord',
160 __PACKAGE__->register_method(
161 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff',
163 method => 'ordered_records_from_metarecord',
168 __PACKAGE__->register_method(
169 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.atomic',
171 method => 'ordered_records_from_metarecord',
175 __PACKAGE__->register_method(
176 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic',
178 method => 'ordered_records_from_metarecord',
183 # XXX: this subroutine and its two registered methods are marked for
184 # deprecation, as they do not work properly in 2.x (these tags are no longer
185 # normalized in mfr) and are not in known use
189 my $isxn = lc(shift());
193 $isxn =~ s/-//o if ($self->api_name =~ /isbn/o);
195 my $tag = ($self->api_name =~ /isbn/o) ? "'020' OR f.tag = '024'" : "'022'";
197 my $fr_table = metabib::full_rec->table;
198 my $bib_table = biblio::record_entry->table;
201 SELECT DISTINCT f.record
203 JOIN $bib_table b ON (b.id = f.record)
206 AND b.deleted IS FALSE
209 my $list = metabib::full_rec->db_Main->selectcol_arrayref($sql, {}, "$isxn%");
210 $client->respond($_) for (@$list);
213 __PACKAGE__->register_method(
214 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
216 method => 'isxn_search',
220 __PACKAGE__->register_method(
221 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.issn',
223 method => 'isxn_search',
228 sub metarecord_copy_count {
234 my $sm_table = metabib::metarecord_source_map->table;
235 my $rd_table = metabib::record_descriptor->table;
236 my $cn_table = asset::call_number->table;
237 my $cp_table = asset::copy->table;
238 my $br_table = biblio::record_entry->table;
239 my $src_table = config::bib_source->table;
240 my $cl_table = asset::copy_location->table;
241 my $cs_table = config::copy_status->table;
242 my $out_table = actor::org_unit_type->table;
244 my $descendants = "actor.org_unit_descendants(u.id)";
245 my $ancestors = "actor.org_unit_ancestors(?) u JOIN $out_table t ON (u.ou_type = t.id)";
247 if ($args{org_unit} < 0) {
248 $args{org_unit} *= -1;
249 $ancestors = "(select org_unit as id from actor.org_lasso_map where lasso = ?) u CROSS JOIN (SELECT -1 AS depth) t";
252 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';
253 $copies_visible = '' if ($self->api_name =~ /staff/o);
255 my (@types,@forms,@blvl);
256 my ($t_filter, $f_filter, $b_filter) = ('','','');
259 my ($t, $f, $b) = split '-', $args{format};
260 @types = split '', $t;
261 @forms = split '', $f;
262 @blvl = split '', $b;
265 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
269 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
273 $b_filter .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
283 JOIN $cn_table cn ON (cn.record = r.source)
284 JOIN $rd_table rd ON (cn.record = rd.record)
285 JOIN $cp_table cp ON (cn.id = cp.call_number)
286 JOIN $cs_table cs ON (cp.status = cs.id)
287 JOIN $cl_table cl ON (cp.location = cl.id)
288 JOIN $descendants a ON (cp.circ_lib = a.id)
289 WHERE r.metarecord = ?
290 AND cn.deleted IS FALSE
291 AND cp.deleted IS FALSE
301 JOIN $cn_table cn ON (cn.record = r.source)
302 JOIN $rd_table rd ON (cn.record = rd.record)
303 JOIN $cp_table cp ON (cn.id = cp.call_number)
304 JOIN $cs_table cs ON (cp.status = cs.id)
305 JOIN $cl_table cl ON (cp.location = cl.id)
306 JOIN $descendants a ON (cp.circ_lib = a.id)
307 WHERE r.metarecord = ?
308 AND cp.status IN (0,7,12)
309 AND cn.deleted IS FALSE
310 AND cp.deleted IS FALSE
320 JOIN $cn_table cn ON (cn.record = r.source)
321 JOIN $rd_table rd ON (cn.record = rd.record)
322 JOIN $cp_table cp ON (cn.id = cp.call_number)
323 JOIN $cs_table cs ON (cp.status = cs.id)
324 JOIN $cl_table cl ON (cp.location = cl.id)
325 WHERE r.metarecord = ?
326 AND cn.deleted IS FALSE
327 AND cp.deleted IS FALSE
328 AND cp.opac_visible IS TRUE
329 AND cs.opac_visible IS TRUE
330 AND cl.opac_visible IS TRUE
339 JOIN $br_table br ON (br.id = r.source)
340 JOIN $src_table src ON (src.id = br.source)
341 WHERE r.metarecord = ?
342 AND src.transcendant IS TRUE
350 my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
351 $sth->execute( ''.$args{metarecord},
355 ''.$args{metarecord},
359 ''.$args{metarecord},
363 ''.$args{metarecord},
367 while ( my $row = $sth->fetchrow_hashref ) {
368 $client->respond( $row );
372 __PACKAGE__->register_method(
373 api_name => 'open-ils.storage.metabib.metarecord.copy_count',
375 method => 'metarecord_copy_count',
380 __PACKAGE__->register_method(
381 api_name => 'open-ils.storage.metabib.metarecord.copy_count.staff',
383 method => 'metarecord_copy_count',
389 sub biblio_multi_search_full_rec {
394 my $class_join = $args{class_join} || 'AND';
395 my $limit = $args{limit} || 100;
396 my $offset = $args{offset} || 0;
397 my $sort = $args{'sort'};
398 my $sort_dir = $args{sort_dir} || 'DESC';
403 for my $arg (@{ $args{searches} }) {
404 my $term = $$arg{term};
405 my $limiters = $$arg{restrict};
407 my ($index_col) = metabib::full_rec->columns('FTS');
408 $index_col ||= 'value';
409 my $search_table = metabib::full_rec->table;
411 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
413 my $fts_where = $fts->sql_where_clause();
414 my @fts_ranks = $fts->fts_rank;
416 my $rank = join(' + ', @fts_ranks);
419 for my $limit (@$limiters) {
420 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
421 # MARC control field; mfr.subfield is NULL
422 push @wheres, "( tag = ? AND $fts_where )";
423 push @binds, $$limit{tag};
424 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
426 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
427 push @binds, $$limit{tag}, $$limit{subfield};
428 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
431 my $where = join(' OR ', @wheres);
433 push @selects, "SELECT record, AVG($rank) as sum FROM $search_table WHERE $where GROUP BY record";
437 my $descendants = defined($args{depth}) ?
438 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
439 "actor.org_unit_descendants($args{org_unit})" ;
442 my $metabib_record_descriptor = metabib::record_descriptor->table;
443 my $metabib_full_rec = metabib::full_rec->table;
444 my $asset_call_number_table = asset::call_number->table;
445 my $asset_copy_table = asset::copy->table;
446 my $cs_table = config::copy_status->table;
447 my $cl_table = asset::copy_location->table;
448 my $br_table = biblio::record_entry->table;
451 $cj = 'HAVING COUNT(x.record) = ' . scalar(@selects) if ($class_join eq 'AND');
454 '(SELECT x.record, sum(x.sum) FROM (('.
455 join(') UNION ALL (', @selects).
456 ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
458 my $has_vols = 'AND cn.owning_lib = d.id';
459 my $has_copies = 'AND cp.call_number = cn.id';
460 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';
462 if ($self->api_name =~ /staff/o) {
463 $copies_visible = '';
464 $has_copies = '' if ($ou_type == 0);
465 $has_vols = '' if ($ou_type == 0);
468 my ($t_filter, $f_filter) = ('','');
469 my ($a_filter, $l_filter, $lf_filter) = ('','','');
472 if (my $a = $args{audience}) {
473 $a = [$a] if (!ref($a));
476 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
481 if (my $l = $args{language}) {
482 $l = [$l] if (!ref($l));
485 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
490 if (my $f = $args{lit_form}) {
491 $f = [$f] if (!ref($f));
494 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
495 push @binds, @lit_form;
499 if (my $f = $args{item_form}) {
500 $f = [$f] if (!ref($f));
503 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
508 if (my $t = $args{item_type}) {
509 $t = [$t] if (!ref($t));
512 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
519 my ($t, $f) = split '-', $args{format};
520 my @types = split '', $t;
521 my @forms = split '', $f;
523 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
528 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
531 push @binds, @types, @forms;
534 my $relevance = 'sum(f.sum)';
535 $relevance = 1 if (!$copies_visible);
537 my $string_default_sort = 'zzzz';
538 $string_default_sort = 'AAAA' if ($sort_dir =~ /^DESC$/i);
540 my $number_default_sort = '9999';
541 $number_default_sort = '0000' if ($sort_dir =~/^DESC$/i);
543 my $rank = $relevance;
544 if (lc($sort) eq 'pubdate') {
547 SELECT COALESCE(SUBSTRING(MAX(frp.value) FROM E'\\\\d{4}'), '$number_default_sort')::INT
548 FROM $metabib_full_rec frp
549 WHERE frp.record = f.record
551 AND frp.subfield = 'c'
555 } elsif (lc($sort) eq 'create_date') {
557 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
559 } elsif (lc($sort) eq 'edit_date') {
561 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
563 } elsif (lc($sort) =~ /^title/i) {
566 SELECT COALESCE(LTRIM(SUBSTR(MAX(frt.value), COALESCE(SUBSTRING(MAX(frt.ind2) FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
567 FROM $metabib_full_rec frt
568 WHERE frt.record = f.record
570 AND frt.subfield = 'a'
574 } elsif (lc($sort) =~ /^author/i) {
577 SELECT COALESCE(LTRIM(MAX(query.value)), '$string_default_sort')
580 FROM $metabib_full_rec fra
581 WHERE fra.record = f.record
582 AND fra.tag LIKE '1%'
583 AND fra.subfield = 'a'
584 ORDER BY fra.tag::text::int
593 my $rd_join = $use_rd ? "$metabib_record_descriptor rd," : '';
594 my $rd_filter = $use_rd ? 'AND rd.record = f.record' : '';
596 if ($copies_visible) {
598 SELECT f.record, $relevance, count(DISTINCT cp.id), $rank
599 FROM $search_table f,
600 $asset_call_number_table cn,
601 $asset_copy_table cp,
607 WHERE br.id = f.record
608 AND cn.record = f.record
609 AND cp.status = cs.id
610 AND cp.location = cl.id
611 AND br.deleted IS FALSE
612 AND cn.deleted IS FALSE
613 AND cp.deleted IS FALSE
623 GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
624 ORDER BY 4 $sort_dir,3 DESC
628 SELECT f.record, 1, 1, $rank
629 FROM $search_table f,
632 WHERE br.id = f.record
633 AND br.deleted IS FALSE
646 $log->debug("Search SQL :: [$select]",DEBUG);
648 my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
649 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
652 $max = 1 if (!@$recs);
654 $max = $$_[1] if ($$_[1] > $max);
658 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
659 next unless ($$rec[0]);
660 my ($rid,$rank,$junk,$skip) = @$rec;
661 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
665 __PACKAGE__->register_method(
666 api_name => 'open-ils.storage.biblio.full_rec.multi_search',
668 method => 'biblio_multi_search_full_rec',
673 __PACKAGE__->register_method(
674 api_name => 'open-ils.storage.biblio.full_rec.multi_search.staff',
676 method => 'biblio_multi_search_full_rec',
682 sub search_full_rec {
688 my $term = $args{term};
689 my $limiters = $args{restrict};
691 my ($index_col) = metabib::full_rec->columns('FTS');
692 $index_col ||= 'value';
693 my $search_table = metabib::full_rec->table;
695 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
697 my $fts_where = $fts->sql_where_clause();
698 my @fts_ranks = $fts->fts_rank;
700 my $rank = join(' + ', @fts_ranks);
704 for my $limit (@$limiters) {
705 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
706 # MARC control field; mfr.subfield is NULL
707 push @wheres, "( tag = ? AND $fts_where )";
708 push @binds, $$limit{tag};
709 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
711 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
712 push @binds, $$limit{tag}, $$limit{subfield};
713 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
716 my $where = join(' OR ', @wheres);
718 my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
720 $log->debug("Search SQL :: [$select]",DEBUG);
722 my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
723 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
725 $client->respond($_) for (@$recs);
728 __PACKAGE__->register_method(
729 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
731 method => 'search_full_rec',
736 __PACKAGE__->register_method(
737 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
739 method => 'search_full_rec',
746 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
747 sub search_class_fts {
752 my $term = $args{term};
753 my $ou = $args{org_unit};
754 my $ou_type = $args{depth};
755 my $limit = $args{limit};
756 my $offset = $args{offset};
758 my $limit_clause = '';
759 my $offset_clause = '';
761 $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
762 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
765 my ($t_filter, $f_filter) = ('','');
768 my ($t, $f) = split '-', $args{format};
769 @types = split '', $t;
770 @forms = split '', $f;
772 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
776 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
782 my $descendants = defined($ou_type) ?
783 "actor.org_unit_descendants($ou, $ou_type)" :
784 "actor.org_unit_descendants($ou)";
786 my $class = $self->{cdbi};
787 my $search_table = $class->table;
789 my $metabib_record_descriptor = metabib::record_descriptor->table;
790 my $metabib_metarecord = metabib::metarecord->table;
791 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
792 my $asset_call_number_table = asset::call_number->table;
793 my $asset_copy_table = asset::copy->table;
794 my $cs_table = config::copy_status->table;
795 my $cl_table = asset::copy_location->table;
797 my ($index_col) = $class->columns('FTS');
798 $index_col ||= 'value';
800 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
801 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
803 my $fts_where = $fts->sql_where_clause;
804 my @fts_ranks = $fts->fts_rank;
806 my $rank = join(' + ', @fts_ranks);
808 my $has_vols = 'AND cn.owning_lib = d.id';
809 my $has_copies = 'AND cp.call_number = cn.id';
810 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';
812 my $visible_count = ', count(DISTINCT cp.id)';
813 my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
815 if ($self->api_name =~ /staff/o) {
816 $copies_visible = '';
817 $visible_count_test = '';
818 $has_copies = '' if ($ou_type == 0);
819 $has_vols = '' if ($ou_type == 0);
822 my $rank_calc = <<" RANK";
824 * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
825 * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
826 * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
827 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
830 $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
832 if ($copies_visible) {
834 SELECT m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
835 FROM $search_table f,
836 $metabib_metarecord_source_map_table m,
837 $asset_call_number_table cn,
838 $asset_copy_table cp,
841 $metabib_record_descriptor rd,
844 AND m.source = f.source
845 AND cn.record = m.source
846 AND rd.record = m.source
847 AND cp.status = cs.id
848 AND cp.location = cl.id
854 GROUP BY 1 $visible_count_test
856 $limit_clause $offset_clause
860 SELECT m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
861 FROM $search_table f,
862 $metabib_metarecord_source_map_table m,
863 $metabib_record_descriptor rd
865 AND m.source = f.source
866 AND rd.record = m.source
871 $limit_clause $offset_clause
875 $log->debug("Field Search SQL :: [$select]",DEBUG);
877 my $SQLstring = join('%',$fts->words);
878 my $REstring = join('\\s+',$fts->words);
879 my $first_word = ($fts->words)[0].'%';
880 my $recs = ($self->api_name =~ /unordered/o) ?
881 $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
882 $class->db_Main->selectall_arrayref($select, {},
883 '%'.lc($SQLstring).'%', # phrase order match
884 lc($first_word), # first word match
885 '^\\s*'.lc($REstring).'\\s*/?\s*$', # full exact match
889 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
891 $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
895 for my $class ( qw/title author subject keyword series identifier/ ) {
896 __PACKAGE__->register_method(
897 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord",
899 method => 'search_class_fts',
902 cdbi => "metabib::${class}_field_entry",
905 __PACKAGE__->register_method(
906 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
908 method => 'search_class_fts',
911 cdbi => "metabib::${class}_field_entry",
914 __PACKAGE__->register_method(
915 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
917 method => 'search_class_fts',
920 cdbi => "metabib::${class}_field_entry",
923 __PACKAGE__->register_method(
924 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
926 method => 'search_class_fts',
929 cdbi => "metabib::${class}_field_entry",
934 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
935 sub search_class_fts_count {
940 my $term = $args{term};
941 my $ou = $args{org_unit};
942 my $ou_type = $args{depth};
943 my $limit = $args{limit} || 100;
944 my $offset = $args{offset} || 0;
946 my $descendants = defined($ou_type) ?
947 "actor.org_unit_descendants($ou, $ou_type)" :
948 "actor.org_unit_descendants($ou)";
951 my ($t_filter, $f_filter) = ('','');
954 my ($t, $f) = split '-', $args{format};
955 @types = split '', $t;
956 @forms = split '', $f;
958 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
962 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
967 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
969 my $class = $self->{cdbi};
970 my $search_table = $class->table;
972 my $metabib_record_descriptor = metabib::record_descriptor->table;
973 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
974 my $asset_call_number_table = asset::call_number->table;
975 my $asset_copy_table = asset::copy->table;
976 my $cs_table = config::copy_status->table;
977 my $cl_table = asset::copy_location->table;
979 my ($index_col) = $class->columns('FTS');
980 $index_col ||= 'value';
982 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
984 my $fts_where = $fts->sql_where_clause;
986 my $has_vols = 'AND cn.owning_lib = d.id';
987 my $has_copies = 'AND cp.call_number = cn.id';
988 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';
989 if ($self->api_name =~ /staff/o) {
990 $copies_visible = '';
991 $has_vols = '' if ($ou_type == 0);
992 $has_copies = '' if ($ou_type == 0);
995 # XXX test an "EXISTS version of descendant checking...
997 if ($copies_visible) {
999 SELECT count(distinct m.metarecord)
1000 FROM $search_table f,
1001 $metabib_metarecord_source_map_table m,
1002 $metabib_metarecord_source_map_table mr,
1003 $asset_call_number_table cn,
1004 $asset_copy_table cp,
1007 $metabib_record_descriptor rd,
1010 AND mr.source = f.source
1011 AND mr.metarecord = m.metarecord
1012 AND cn.record = m.source
1013 AND rd.record = m.source
1014 AND cp.status = cs.id
1015 AND cp.location = cl.id
1024 SELECT count(distinct m.metarecord)
1025 FROM $search_table f,
1026 $metabib_metarecord_source_map_table m,
1027 $metabib_metarecord_source_map_table mr,
1028 $metabib_record_descriptor rd
1030 AND mr.source = f.source
1031 AND mr.metarecord = m.metarecord
1032 AND rd.record = m.source
1038 $log->debug("Field Search Count SQL :: [$select]",DEBUG);
1040 my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1042 $log->debug("Count Search yielded $recs results.",DEBUG);
1047 for my $class ( qw/title author subject keyword series identifier/ ) {
1048 __PACKAGE__->register_method(
1049 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
1051 method => 'search_class_fts_count',
1054 cdbi => "metabib::${class}_field_entry",
1057 __PACKAGE__->register_method(
1058 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
1060 method => 'search_class_fts_count',
1063 cdbi => "metabib::${class}_field_entry",
1069 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1070 sub postfilter_search_class_fts {
1075 my $term = $args{term};
1076 my $sort = $args{'sort'};
1077 my $sort_dir = $args{sort_dir} || 'DESC';
1078 my $ou = $args{org_unit};
1079 my $ou_type = $args{depth};
1080 my $limit = $args{limit} || 10;
1081 my $visibility_limit = $args{visibility_limit} || 5000;
1082 my $offset = $args{offset} || 0;
1084 my $outer_limit = 1000;
1086 my $limit_clause = '';
1087 my $offset_clause = '';
1089 $limit_clause = "LIMIT $outer_limit";
1090 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1092 my (@types,@forms,@lang,@aud,@lit_form);
1093 my ($t_filter, $f_filter) = ('','');
1094 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1095 my ($ot_filter, $of_filter) = ('','');
1096 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1098 if (my $a = $args{audience}) {
1099 $a = [$a] if (!ref($a));
1102 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1103 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1106 if (my $l = $args{language}) {
1107 $l = [$l] if (!ref($l));
1110 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1111 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1114 if (my $f = $args{lit_form}) {
1115 $f = [$f] if (!ref($f));
1118 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1119 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1122 if ($args{format}) {
1123 my ($t, $f) = split '-', $args{format};
1124 @types = split '', $t;
1125 @forms = split '', $f;
1127 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1128 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1132 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1133 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1138 my $descendants = defined($ou_type) ?
1139 "actor.org_unit_descendants($ou, $ou_type)" :
1140 "actor.org_unit_descendants($ou)";
1142 my $class = $self->{cdbi};
1143 my $search_table = $class->table;
1145 my $metabib_full_rec = metabib::full_rec->table;
1146 my $metabib_record_descriptor = metabib::record_descriptor->table;
1147 my $metabib_metarecord = metabib::metarecord->table;
1148 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1149 my $asset_call_number_table = asset::call_number->table;
1150 my $asset_copy_table = asset::copy->table;
1151 my $cs_table = config::copy_status->table;
1152 my $cl_table = asset::copy_location->table;
1153 my $br_table = biblio::record_entry->table;
1155 my ($index_col) = $class->columns('FTS');
1156 $index_col ||= 'value';
1158 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1160 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1162 my $SQLstring = join('%',map { lc($_) } $fts->words);
1163 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1164 my $first_word = lc(($fts->words)[0]).'%';
1166 my $fts_where = $fts->sql_where_clause;
1167 my @fts_ranks = $fts->fts_rank;
1170 $bonus{'metabib::identifier_field_entry'} =
1171 $bonus{'metabib::keyword_field_entry'} = [
1172 { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring }
1175 $bonus{'metabib::title_field_entry'} =
1176 $bonus{'metabib::series_field_entry'} = [
1177 { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1178 { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1179 @{ $bonus{'metabib::keyword_field_entry'} }
1182 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1183 $bonus_list ||= '1';
1185 my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1187 my $relevance = join(' + ', @fts_ranks);
1188 $relevance = <<" RANK";
1189 (SUM( ( $relevance ) * ( $bonus_list ) )/COUNT(m.source))
1192 my $string_default_sort = 'zzzz';
1193 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1195 my $number_default_sort = '9999';
1196 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1198 my $rank = $relevance;
1199 if (lc($sort) eq 'pubdate') {
1202 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1203 FROM $metabib_full_rec frp
1204 WHERE frp.record = mr.master_record
1206 AND frp.subfield = 'c'
1210 } elsif (lc($sort) eq 'create_date') {
1212 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1214 } elsif (lc($sort) eq 'edit_date') {
1216 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1218 } elsif (lc($sort) eq 'title') {
1221 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1222 FROM $metabib_full_rec frt
1223 WHERE frt.record = mr.master_record
1225 AND frt.subfield = 'a'
1229 } elsif (lc($sort) eq 'author') {
1232 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1233 FROM $metabib_full_rec fra
1234 WHERE fra.record = mr.master_record
1235 AND fra.tag LIKE '1%'
1236 AND fra.subfield = 'a'
1237 ORDER BY fra.tag::text::int
1245 my $select = <<" SQL";
1246 SELECT m.metarecord,
1248 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1250 FROM $search_table f,
1251 $metabib_metarecord_source_map_table m,
1252 $metabib_metarecord_source_map_table smrs,
1253 $metabib_metarecord mr,
1254 $metabib_record_descriptor rd
1256 AND smrs.metarecord = mr.id
1257 AND m.source = f.source
1258 AND m.metarecord = mr.id
1259 AND rd.record = smrs.source
1265 GROUP BY m.metarecord
1266 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1267 LIMIT $visibility_limit
1274 FROM $asset_call_number_table cn,
1275 $metabib_metarecord_source_map_table mrs,
1276 $asset_copy_table cp,
1281 $metabib_record_descriptor ord,
1283 WHERE mrs.metarecord = s.metarecord
1284 AND br.id = mrs.source
1285 AND cn.record = mrs.source
1286 AND cp.status = cs.id
1287 AND cp.location = cl.id
1288 AND cn.owning_lib = d.id
1289 AND cp.call_number = cn.id
1290 AND cp.opac_visible IS TRUE
1291 AND cs.opac_visible IS TRUE
1292 AND cl.opac_visible IS TRUE
1293 AND d.opac_visible IS TRUE
1294 AND br.active IS TRUE
1295 AND br.deleted IS FALSE
1296 AND ord.record = mrs.source
1302 ORDER BY 4 $sort_dir
1304 } elsif ($self->api_name !~ /staff/o) {
1311 FROM $asset_call_number_table cn,
1312 $metabib_metarecord_source_map_table mrs,
1313 $asset_copy_table cp,
1318 $metabib_record_descriptor ord
1320 WHERE mrs.metarecord = s.metarecord
1321 AND br.id = mrs.source
1322 AND cn.record = mrs.source
1323 AND cp.status = cs.id
1324 AND cp.location = cl.id
1325 AND cp.circ_lib = d.id
1326 AND cp.call_number = cn.id
1327 AND cp.opac_visible IS TRUE
1328 AND cs.opac_visible IS TRUE
1329 AND cl.opac_visible IS TRUE
1330 AND d.opac_visible IS TRUE
1331 AND br.active IS TRUE
1332 AND br.deleted IS FALSE
1333 AND ord.record = mrs.source
1341 ORDER BY 4 $sort_dir
1350 FROM $asset_call_number_table cn,
1351 $asset_copy_table cp,
1352 $metabib_metarecord_source_map_table mrs,
1355 $metabib_record_descriptor ord
1357 WHERE mrs.metarecord = s.metarecord
1358 AND br.id = mrs.source
1359 AND cn.record = mrs.source
1360 AND cn.id = cp.call_number
1361 AND br.deleted IS FALSE
1362 AND cn.deleted IS FALSE
1363 AND ord.record = mrs.source
1364 AND ( cn.owning_lib = d.id
1365 OR ( cp.circ_lib = d.id
1366 AND cp.deleted IS FALSE
1378 FROM $asset_call_number_table cn,
1379 $metabib_metarecord_source_map_table mrs,
1380 $metabib_record_descriptor ord
1381 WHERE mrs.metarecord = s.metarecord
1382 AND cn.record = mrs.source
1383 AND ord.record = mrs.source
1391 ORDER BY 4 $sort_dir
1396 $log->debug("Field Search SQL :: [$select]",DEBUG);
1398 my $recs = $class->db_Main->selectall_arrayref(
1400 (@bonus_values > 0 ? @bonus_values : () ),
1401 ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1402 @types, @forms, @aud, @lang, @lit_form,
1403 @types, @forms, @aud, @lang, @lit_form,
1404 ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1406 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1409 $max = 1 if (!@$recs);
1411 $max = $$_[1] if ($$_[1] > $max);
1414 my $count = scalar(@$recs);
1415 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1416 my ($mrid,$rank,$skip) = @$rec;
1417 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1422 for my $class ( qw/title author subject keyword series identifier/ ) {
1423 __PACKAGE__->register_method(
1424 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1426 method => 'postfilter_search_class_fts',
1429 cdbi => "metabib::${class}_field_entry",
1432 __PACKAGE__->register_method(
1433 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1435 method => 'postfilter_search_class_fts',
1438 cdbi => "metabib::${class}_field_entry",
1445 my $_cdbi = { title => "metabib::title_field_entry",
1446 author => "metabib::author_field_entry",
1447 subject => "metabib::subject_field_entry",
1448 keyword => "metabib::keyword_field_entry",
1449 series => "metabib::series_field_entry",
1450 identifier => "metabib::identifier_field_entry",
1453 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1454 sub postfilter_search_multi_class_fts {
1459 my $sort = $args{'sort'};
1460 my $sort_dir = $args{sort_dir} || 'DESC';
1461 my $ou = $args{org_unit};
1462 my $ou_type = $args{depth};
1463 my $limit = $args{limit} || 10;
1464 my $offset = $args{offset} || 0;
1465 my $visibility_limit = $args{visibility_limit} || 5000;
1468 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1471 if (!defined($args{org_unit})) {
1472 die "No target organizational unit passed to ".$self->api_name;
1475 if (! scalar( keys %{$args{searches}} )) {
1476 die "No search arguments were passed to ".$self->api_name;
1479 my $outer_limit = 1000;
1481 my $limit_clause = '';
1482 my $offset_clause = '';
1484 $limit_clause = "LIMIT $outer_limit";
1485 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1487 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1488 my ($t_filter, $f_filter, $v_filter) = ('','','');
1489 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1490 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1491 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1493 if ($args{available}) {
1494 $avail_filter = ' AND cp.status IN (0,7,12)';
1497 if (my $a = $args{audience}) {
1498 $a = [$a] if (!ref($a));
1501 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1502 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1505 if (my $l = $args{language}) {
1506 $l = [$l] if (!ref($l));
1509 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1510 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1513 if (my $f = $args{lit_form}) {
1514 $f = [$f] if (!ref($f));
1517 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1518 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1521 if (my $f = $args{item_form}) {
1522 $f = [$f] if (!ref($f));
1525 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1526 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1529 if (my $t = $args{item_type}) {
1530 $t = [$t] if (!ref($t));
1533 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1534 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1537 if (my $v = $args{vr_format}) {
1538 $v = [$v] if (!ref($v));
1541 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1542 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1546 # XXX legacy format and item type support
1547 if ($args{format}) {
1548 my ($t, $f) = split '-', $args{format};
1549 @types = split '', $t;
1550 @forms = split '', $f;
1552 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1553 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1557 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1558 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1564 my $descendants = defined($ou_type) ?
1565 "actor.org_unit_descendants($ou, $ou_type)" :
1566 "actor.org_unit_descendants($ou)";
1568 my $search_table_list = '';
1570 my $join_table_list = '';
1573 my $field_table = config::metabib_field->table;
1577 my $prev_search_group;
1578 my $curr_search_group;
1582 for my $search_group (sort keys %{$args{searches}}) {
1583 (my $search_group_name = $search_group) =~ s/\|/_/gso;
1584 ($search_class,$search_field) = split /\|/, $search_group;
1585 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1587 if ($search_field) {
1588 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1589 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1594 $prev_search_group = $curr_search_group if ($curr_search_group);
1596 $curr_search_group = $search_group_name;
1598 my $class = $_cdbi->{$search_class};
1599 my $search_table = $class->table;
1601 my ($index_col) = $class->columns('FTS');
1602 $index_col ||= 'value';
1605 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1607 my $fts_where = $fts->sql_where_clause;
1608 my @fts_ranks = $fts->fts_rank;
1610 my $SQLstring = join('%',map { lc($_) } $fts->words);
1611 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1612 my $first_word = lc(($fts->words)[0]).'%';
1614 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1615 my $rank = join(' + ', @fts_ranks);
1618 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1619 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1621 $bonus{'series'} = [
1622 { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1623 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1626 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1628 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1629 $bonus_list ||= '1';
1631 push @bonus_lists, $bonus_list;
1632 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1635 #---------------------
1637 $search_table_list .= "$search_table $search_group_name, ";
1638 push @rank_list,$rank;
1639 $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1641 if ($metabib_field) {
1642 $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1643 $metabib_field = undef;
1646 if ($prev_search_group) {
1647 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1651 my $metabib_record_descriptor = metabib::record_descriptor->table;
1652 my $metabib_full_rec = metabib::full_rec->table;
1653 my $metabib_metarecord = metabib::metarecord->table;
1654 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1655 my $asset_call_number_table = asset::call_number->table;
1656 my $asset_copy_table = asset::copy->table;
1657 my $cs_table = config::copy_status->table;
1658 my $cl_table = asset::copy_location->table;
1659 my $br_table = biblio::record_entry->table;
1660 my $source_table = config::bib_source->table;
1662 my $bonuses = join (' * ', @bonus_lists);
1663 my $relevance = join (' + ', @rank_list);
1664 $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1666 my $string_default_sort = 'zzzz';
1667 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1669 my $number_default_sort = '9999';
1670 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1674 my $secondary_sort = <<" SORT";
1676 SELECT COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1677 FROM $metabib_full_rec sfrt,
1678 $metabib_metarecord mr
1679 WHERE sfrt.record = mr.master_record
1680 AND sfrt.tag = '245'
1681 AND sfrt.subfield = 'a'
1686 my $rank = $relevance;
1687 if (lc($sort) eq 'pubdate') {
1690 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1691 FROM $metabib_full_rec frp
1692 WHERE frp.record = mr.master_record
1694 AND frp.subfield = 'c'
1698 } elsif (lc($sort) eq 'create_date') {
1700 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1702 } elsif (lc($sort) eq 'edit_date') {
1704 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1706 } elsif (lc($sort) eq 'title') {
1709 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1710 FROM $metabib_full_rec frt
1711 WHERE frt.record = mr.master_record
1713 AND frt.subfield = 'a'
1717 $secondary_sort = <<" SORT";
1719 SELECT COALESCE(SUBSTRING(sfrp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1720 FROM $metabib_full_rec sfrp
1721 WHERE sfrp.record = mr.master_record
1722 AND sfrp.tag = '260'
1723 AND sfrp.subfield = 'c'
1727 } elsif (lc($sort) eq 'author') {
1730 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1731 FROM $metabib_full_rec fra
1732 WHERE fra.record = mr.master_record
1733 AND fra.tag LIKE '1%'
1734 AND fra.subfield = 'a'
1735 ORDER BY fra.tag::text::int
1740 push @bonus_values, @bonus_values;
1745 my $select = <<" SQL";
1746 SELECT m.metarecord,
1748 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1751 FROM $search_table_list
1752 $metabib_metarecord mr,
1753 $metabib_metarecord_source_map_table m,
1754 $metabib_metarecord_source_map_table smrs
1755 WHERE m.metarecord = smrs.metarecord
1756 AND mr.id = m.metarecord
1759 GROUP BY m.metarecord
1760 -- ORDER BY 4 $sort_dir
1761 LIMIT $visibility_limit
1764 if ($self->api_name !~ /staff/o) {
1771 FROM $asset_call_number_table cn,
1772 $metabib_metarecord_source_map_table mrs,
1773 $asset_copy_table cp,
1778 $metabib_record_descriptor ord
1779 WHERE mrs.metarecord = s.metarecord
1780 AND br.id = mrs.source
1781 AND cn.record = mrs.source
1782 AND cp.status = cs.id
1783 AND cp.location = cl.id
1784 AND cp.circ_lib = d.id
1785 AND cp.call_number = cn.id
1786 AND cp.opac_visible IS TRUE
1787 AND cs.opac_visible IS TRUE
1788 AND cl.opac_visible IS TRUE
1789 AND d.opac_visible IS TRUE
1790 AND br.active IS TRUE
1791 AND br.deleted IS FALSE
1792 AND cp.deleted IS FALSE
1793 AND cn.deleted IS FALSE
1794 AND ord.record = mrs.source
1807 $metabib_metarecord_source_map_table mrs,
1808 $metabib_record_descriptor ord,
1810 WHERE mrs.metarecord = s.metarecord
1811 AND ord.record = mrs.source
1812 AND br.id = mrs.source
1813 AND br.source = src.id
1814 AND src.transcendant IS TRUE
1822 ORDER BY 4 $sort_dir, 5
1829 $metabib_metarecord_source_map_table omrs,
1830 $metabib_record_descriptor ord
1831 WHERE omrs.metarecord = s.metarecord
1832 AND ord.record = omrs.source
1835 FROM $asset_call_number_table cn,
1836 $asset_copy_table cp,
1839 WHERE br.id = omrs.source
1840 AND cn.record = omrs.source
1841 AND br.deleted IS FALSE
1842 AND cn.deleted IS FALSE
1843 AND cp.call_number = cn.id
1844 AND ( cn.owning_lib = d.id
1845 OR ( cp.circ_lib = d.id
1846 AND cp.deleted IS FALSE
1854 FROM $asset_call_number_table cn
1855 WHERE cn.record = omrs.source
1856 AND cn.deleted IS FALSE
1862 $metabib_metarecord_source_map_table mrs,
1863 $metabib_record_descriptor ord,
1865 WHERE mrs.metarecord = s.metarecord
1866 AND br.id = mrs.source
1867 AND br.source = src.id
1868 AND src.transcendant IS TRUE
1884 ORDER BY 4 $sort_dir, 5
1889 $log->debug("Field Search SQL :: [$select]",DEBUG);
1891 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1894 @types, @forms, @vformats, @aud, @lang, @lit_form,
1895 @types, @forms, @vformats, @aud, @lang, @lit_form,
1896 # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1899 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1902 $max = 1 if (!@$recs);
1904 $max = $$_[1] if ($$_[1] > $max);
1907 my $count = scalar(@$recs);
1908 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1909 next unless ($$rec[0]);
1910 my ($mrid,$rank,$skip) = @$rec;
1911 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1916 __PACKAGE__->register_method(
1917 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1919 method => 'postfilter_search_multi_class_fts',
1924 __PACKAGE__->register_method(
1925 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1927 method => 'postfilter_search_multi_class_fts',
1933 __PACKAGE__->register_method(
1934 api_name => "open-ils.storage.metabib.multiclass.search_fts",
1936 method => 'postfilter_search_multi_class_fts',
1941 __PACKAGE__->register_method(
1942 api_name => "open-ils.storage.metabib.multiclass.search_fts.staff",
1944 method => 'postfilter_search_multi_class_fts',
1950 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1951 sub biblio_search_multi_class_fts {
1956 my $sort = $args{'sort'};
1957 my $sort_dir = $args{sort_dir} || 'DESC';
1958 my $ou = $args{org_unit};
1959 my $ou_type = $args{depth};
1960 my $limit = $args{limit} || 10;
1961 my $offset = $args{offset} || 0;
1962 my $pref_lang = $args{preferred_language} || 'eng';
1963 my $visibility_limit = $args{visibility_limit} || 5000;
1966 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1969 if (! scalar( keys %{$args{searches}} )) {
1970 die "No search arguments were passed to ".$self->api_name;
1973 my $outer_limit = 1000;
1975 my $limit_clause = '';
1976 my $offset_clause = '';
1978 $limit_clause = "LIMIT $outer_limit";
1979 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1981 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1982 my ($t_filter, $f_filter, $v_filter) = ('','','');
1983 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1984 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1985 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1987 if ($args{available}) {
1988 $avail_filter = ' AND cp.status IN (0,7,12)';
1991 if (my $a = $args{audience}) {
1992 $a = [$a] if (!ref($a));
1995 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1996 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1999 if (my $l = $args{language}) {
2000 $l = [$l] if (!ref($l));
2003 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
2004 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
2007 if (my $f = $args{lit_form}) {
2008 $f = [$f] if (!ref($f));
2011 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2012 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2015 if (my $f = $args{item_form}) {
2016 $f = [$f] if (!ref($f));
2019 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2020 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2023 if (my $t = $args{item_type}) {
2024 $t = [$t] if (!ref($t));
2027 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2028 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2031 if (my $v = $args{vr_format}) {
2032 $v = [$v] if (!ref($v));
2035 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
2036 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
2039 # XXX legacy format and item type support
2040 if ($args{format}) {
2041 my ($t, $f) = split '-', $args{format};
2042 @types = split '', $t;
2043 @forms = split '', $f;
2045 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2046 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2050 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2051 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2056 my $descendants = defined($ou_type) ?
2057 "actor.org_unit_descendants($ou, $ou_type)" :
2058 "actor.org_unit_descendants($ou)";
2060 my $search_table_list = '';
2062 my $join_table_list = '';
2065 my $field_table = config::metabib_field->table;
2069 my $prev_search_group;
2070 my $curr_search_group;
2074 for my $search_group (sort keys %{$args{searches}}) {
2075 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2076 ($search_class,$search_field) = split /\|/, $search_group;
2077 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2079 if ($search_field) {
2080 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2081 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2086 $prev_search_group = $curr_search_group if ($curr_search_group);
2088 $curr_search_group = $search_group_name;
2090 my $class = $_cdbi->{$search_class};
2091 my $search_table = $class->table;
2093 my ($index_col) = $class->columns('FTS');
2094 $index_col ||= 'value';
2097 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2099 my $fts_where = $fts->sql_where_clause;
2100 my @fts_ranks = $fts->fts_rank;
2102 my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2103 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2104 my $first_word = lc(($fts->words)[0]).'%';
2106 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2107 my $rank = join(' + ', @fts_ranks);
2110 $bonus{'subject'} = [];
2111 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2113 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2115 $bonus{'series'} = [
2116 { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2117 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2120 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2123 push @{ $bonus{'title'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2124 push @{ $bonus{'author'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2125 push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2126 push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2127 push @{ $bonus{'series'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2130 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2131 $bonus_list ||= '1';
2133 push @bonus_lists, $bonus_list;
2134 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2136 #---------------------
2138 $search_table_list .= "$search_table $search_group_name, ";
2139 push @rank_list,$rank;
2140 $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2142 if ($metabib_field) {
2143 $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2144 $metabib_field = undef;
2147 if ($prev_search_group) {
2148 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2152 my $metabib_record_descriptor = metabib::record_descriptor->table;
2153 my $metabib_full_rec = metabib::full_rec->table;
2154 my $metabib_metarecord = metabib::metarecord->table;
2155 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2156 my $asset_call_number_table = asset::call_number->table;
2157 my $asset_copy_table = asset::copy->table;
2158 my $cs_table = config::copy_status->table;
2159 my $cl_table = asset::copy_location->table;
2160 my $br_table = biblio::record_entry->table;
2161 my $source_table = config::bib_source->table;
2164 my $bonuses = join (' * ', @bonus_lists);
2165 my $relevance = join (' + ', @rank_list);
2166 $relevance = "AVG( ($relevance) * ($bonuses) )";
2168 my $string_default_sort = 'zzzz';
2169 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2171 my $number_default_sort = '9999';
2172 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2174 my $rank = $relevance;
2175 if (lc($sort) eq 'pubdate') {
2178 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d{4}'),'$number_default_sort')::INT
2179 FROM $metabib_full_rec frp
2180 WHERE frp.record = b.id
2182 AND frp.subfield = 'c'
2186 } elsif (lc($sort) eq 'create_date') {
2188 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2190 } elsif (lc($sort) eq 'edit_date') {
2192 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2194 } elsif (lc($sort) eq 'title') {
2197 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2198 FROM $metabib_full_rec frt
2199 WHERE frt.record = b.id
2201 AND frt.subfield = 'a'
2205 } elsif (lc($sort) eq 'author') {
2208 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
2209 FROM $metabib_full_rec fra
2210 WHERE fra.record = b.id
2211 AND fra.tag LIKE '1%'
2212 AND fra.subfield = 'a'
2213 ORDER BY fra.tag::text::int
2218 push @bonus_values, @bonus_values;
2223 my $select = <<" SQL";
2228 FROM $search_table_list
2229 $metabib_record_descriptor rd,
2232 WHERE rd.record = b.id
2233 AND b.active IS TRUE
2234 AND b.deleted IS FALSE
2243 GROUP BY b.id, b.source
2244 ORDER BY 3 $sort_dir
2245 LIMIT $visibility_limit
2248 if ($self->api_name !~ /staff/o) {
2253 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2256 FROM $asset_call_number_table cn,
2257 $asset_copy_table cp,
2261 WHERE cn.record = s.id
2262 AND cp.status = cs.id
2263 AND cp.location = cl.id
2264 AND cp.call_number = cn.id
2265 AND cp.opac_visible IS TRUE
2266 AND cs.opac_visible IS TRUE
2267 AND cl.opac_visible IS TRUE
2268 AND d.opac_visible IS TRUE
2269 AND cp.deleted IS FALSE
2270 AND cn.deleted IS FALSE
2271 AND cp.circ_lib = d.id
2275 OR src.transcendant IS TRUE
2276 ORDER BY 3 $sort_dir
2283 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2286 FROM $asset_call_number_table cn,
2287 $asset_copy_table cp,
2289 WHERE cn.record = s.id
2290 AND cp.call_number = cn.id
2291 AND cn.deleted IS FALSE
2292 AND cp.circ_lib = d.id
2293 AND cp.deleted IS FALSE
2299 FROM $asset_call_number_table cn
2300 WHERE cn.record = s.id
2303 OR src.transcendant IS TRUE
2304 ORDER BY 3 $sort_dir
2309 $log->debug("Field Search SQL :: [$select]",DEBUG);
2311 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2313 @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2316 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2318 my $count = scalar(@$recs);
2319 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2320 next unless ($$rec[0]);
2321 my ($mrid,$rank) = @$rec;
2322 $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2327 __PACKAGE__->register_method(
2328 api_name => "open-ils.storage.biblio.multiclass.search_fts.record",
2330 method => 'biblio_search_multi_class_fts',
2335 __PACKAGE__->register_method(
2336 api_name => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2338 method => 'biblio_search_multi_class_fts',
2343 __PACKAGE__->register_method(
2344 api_name => "open-ils.storage.biblio.multiclass.search_fts",
2346 method => 'biblio_search_multi_class_fts',
2351 __PACKAGE__->register_method(
2352 api_name => "open-ils.storage.biblio.multiclass.search_fts.staff",
2354 method => 'biblio_search_multi_class_fts',
2362 my $default_preferred_language;
2363 my $default_preferred_language_weight;
2365 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2371 if (!$locale_map{COMPLETE}) {
2373 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2374 for my $locale ( @locales ) {
2375 $locale_map{lc($locale->code)} = $locale->marc_code;
2377 $locale_map{COMPLETE} = 1;
2381 my $config = OpenSRF::Utils::SettingsClient->new();
2383 if (!$default_preferred_language) {
2385 $default_preferred_language = $config->config_value(
2386 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2387 ) || $config->config_value(
2388 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2393 if (!$default_preferred_language_weight) {
2395 $default_preferred_language_weight = $config->config_value(
2396 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2397 ) || $config->config_value(
2398 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2402 # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2403 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2405 my $ou = $args{org_unit};
2406 my $limit = $args{limit} || 10;
2407 my $offset = $args{offset} || 0;
2410 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2413 if (! scalar( keys %{$args{searches}} )) {
2414 die "No search arguments were passed to ".$self->api_name;
2417 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2419 if (!defined($args{preferred_language})) {
2420 my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2421 $args{preferred_language} =
2422 $locale_map{ lc($ses_locale) } || 'eng';
2425 if (!defined($args{preferred_language_weight})) {
2426 $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2429 if ($args{available}) {
2430 @statuses = (0,7,12);
2433 if (my $s = $args{locations}) {
2434 $s = [$s] if (!ref($s));
2438 if (my $b = $args{between}) {
2439 if (ref($b) && @$b == 2) {
2444 if (my $s = $args{statuses}) {
2445 $s = [$s] if (!ref($s));
2449 if (my $a = $args{audience}) {
2450 $a = [$a] if (!ref($a));
2454 if (my $l = $args{language}) {
2455 $l = [$l] if (!ref($l));
2459 if (my $f = $args{lit_form}) {
2460 $f = [$f] if (!ref($f));
2464 if (my $f = $args{item_form}) {
2465 $f = [$f] if (!ref($f));
2469 if (my $t = $args{item_type}) {
2470 $t = [$t] if (!ref($t));
2474 if (my $b = $args{bib_level}) {
2475 $b = [$b] if (!ref($b));
2479 if (my $v = $args{vr_format}) {
2480 $v = [$v] if (!ref($v));
2484 # XXX legacy format and item type support
2485 if ($args{format}) {
2486 my ($t, $f) = split '-', $args{format};
2487 @types = split '', $t;
2488 @forms = split '', $f;
2491 my %stored_proc_search_args;
2492 for my $search_group (sort keys %{$args{searches}}) {
2493 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2494 my ($search_class,$search_field) = split /\|/, $search_group;
2495 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2497 if ($search_field) {
2498 unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2499 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2504 my $class = $_cdbi->{$search_class};
2505 my $search_table = $class->table;
2507 my ($index_col) = $class->columns('FTS');
2508 $index_col ||= 'value';
2511 my $fts = OpenILS::Application::Storage::FTS->compile(
2512 $search_class => $args{searches}{$search_group}{term},
2513 $search_group_name.'.value',
2514 "$search_group_name.$index_col"
2516 $fts->sql_where_clause; # this builds the ranks for us
2518 my @fts_ranks = $fts->fts_rank;
2519 my @fts_queries = $fts->fts_query;
2520 my @phrases = map { lc($_) } $fts->phrases;
2521 my @words = map { lc($_) } $fts->words;
2523 $stored_proc_search_args{$search_group} = {
2524 fts_rank => \@fts_ranks,
2525 fts_query => \@fts_queries,
2526 phrase => \@phrases,
2532 my $param_search_ou = $ou;
2533 my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2534 my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2535 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2536 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2537 my $param_audience = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud ) . '}$$';
2538 my $param_language = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang ) . '}$$';
2539 my $param_lit_form = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2540 my $param_types = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types ) . '}$$';
2541 my $param_forms = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms ) . '}$$';
2542 my $param_vformats = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2543 my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2544 my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2545 my $param_after = $args{after} ; $param_after = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2546 my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2547 my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2548 my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2549 my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2550 my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2551 my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2552 my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2553 my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2554 my $param_rel_limit = $args{core_limit}; $param_rel_limit ||= 'NULL';
2555 my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2556 my $param_skip_chk = $args{skip_check}; $param_skip_chk ||= 'NULL';
2558 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2560 FROM search.staged_fts(
2561 $param_search_ou\:\:INT,
2562 $param_depth\:\:INT,
2563 $param_searches\:\:TEXT,
2564 $param_statuses\:\:INT[],
2565 $param_locations\:\:INT[],
2566 $param_audience\:\:TEXT[],
2567 $param_language\:\:TEXT[],
2568 $param_lit_form\:\:TEXT[],
2569 $param_types\:\:TEXT[],
2570 $param_forms\:\:TEXT[],
2571 $param_vformats\:\:TEXT[],
2572 $param_bib_level\:\:TEXT[],
2573 $param_before\:\:TEXT,
2574 $param_after\:\:TEXT,
2575 $param_during\:\:TEXT,
2576 $param_between\:\:TEXT[],
2577 $param_pref_lang\:\:TEXT,
2578 $param_pref_lang_multiplier\:\:REAL,
2579 $param_sort\:\:TEXT,
2580 $param_sort_desc\:\:BOOL,
2581 $metarecord\:\:BOOL,
2583 $param_rel_limit\:\:INT,
2584 $param_chk_limit\:\:INT,
2585 $param_skip_chk\:\:INT
2591 my $recs = $sth->fetchall_arrayref({});
2592 my $summary_row = pop @$recs;
2594 my $total = $$summary_row{total};
2595 my $checked = $$summary_row{checked};
2596 my $visible = $$summary_row{visible};
2597 my $deleted = $$summary_row{deleted};
2598 my $excluded = $$summary_row{excluded};
2600 my $estimate = $visible;
2601 if ( $total > $checked && $checked ) {
2603 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2604 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2608 delete $$summary_row{id};
2609 delete $$summary_row{rel};
2610 delete $$summary_row{record};
2612 $client->respond( $summary_row );
2614 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2616 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2617 delete $$rec{checked};
2618 delete $$rec{visible};
2619 delete $$rec{excluded};
2620 delete $$rec{deleted};
2621 delete $$rec{total};
2622 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2624 $client->respond( $rec );
2628 __PACKAGE__->register_method(
2629 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
2631 method => 'staged_fts',
2636 __PACKAGE__->register_method(
2637 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2639 method => 'staged_fts',
2644 __PACKAGE__->register_method(
2645 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
2647 method => 'staged_fts',
2652 __PACKAGE__->register_method(
2653 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2655 method => 'staged_fts',
2661 sub FTS_paging_estimate {
2665 my $checked = shift;
2666 my $visible = shift;
2667 my $excluded = shift;
2668 my $deleted = shift;
2671 my $deleted_ratio = $deleted / $checked;
2672 my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2674 my $exclusion_ratio = $excluded / $checked;
2675 my $delete_adjusted_exclusion_ratio = $checked - $deleted ? $excluded / ($checked - $deleted) : 1;
2677 my $inclusion_ratio = $visible / $checked;
2678 my $delete_adjusted_inclusion_ratio = $checked - $deleted ? $visible / ($checked - $deleted) : 0;
2681 exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2682 inclusion => int($delete_adjusted_total * $inclusion_ratio),
2683 delete_adjusted_exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2684 delete_adjusted_inclusion => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2687 __PACKAGE__->register_method(
2688 api_name => "open-ils.storage.fts_paging_estimate",
2690 method => 'FTS_paging_estimate',
2696 Hash of estimation values based on four variant estimation strategies:
2697 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2698 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2699 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2700 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2703 Helper method used to determin the approximate number of
2704 hits for a search that spans multiple superpages. For
2705 sparse superpages, the inclusion estimate will likely be the
2706 best estimate. The exclusion strategy is the original, but
2707 inclusion is the default.
2710 { name => 'checked',
2711 desc => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2714 { name => 'visible',
2715 desc => 'Number of records visible to the search location on the current superpage.',
2718 { name => 'excluded',
2719 desc => 'Number of records excluded from the search location on the current superpage.',
2722 { name => 'deleted',
2723 desc => 'Number of deleted records on the current superpage.',
2727 desc => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2740 my $term = $$args{term};
2741 my $limit = $$args{max} || 1;
2742 my $min = $$args{min} || 1;
2743 my @classes = @{$$args{class}};
2745 $limit = $min if ($min > $limit);
2748 @classes = ( qw/ title author subject series keyword / );
2752 my $bre_table = biblio::record_entry->table;
2753 my $cn_table = asset::call_number->table;
2754 my $cp_table = asset::copy->table;
2756 for my $search_class ( @classes ) {
2758 my $class = $_cdbi->{$search_class};
2759 my $search_table = $class->table;
2761 my ($index_col) = $class->columns('FTS');
2762 $index_col ||= 'value';
2765 my $where = OpenILS::Application::Storage::FTS
2766 ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2770 SELECT COUNT(DISTINCT X.source)
2771 FROM (SELECT $search_class.source
2772 FROM $search_table $search_class
2773 JOIN $bre_table b ON (b.id = $search_class.source)
2778 HAVING COUNT(DISTINCT X.source) >= $min;
2781 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2782 $matches{$search_class} = $res ? $res->[0] : 0;
2787 __PACKAGE__->register_method(
2788 api_name => "open-ils.storage.search.xref",
2790 method => 'xref_count',
2794 # Takes an abstract query object and recursively turns it back into a string
2796 sub abstract_query2str {
2797 my ($self, $conn, $query) = @_;
2799 return QueryParser::Canonicalize::abstract_query2str_impl($query, 0, $OpenILS::Application::Storage::QParser);
2802 __PACKAGE__->register_method(
2803 api_name => "open-ils.storage.query_parser.abstract_query.canonicalize",
2805 method => "abstract_query2str",
2810 Abstract query parser object, with complete config data. For example input,
2811 see the 'abstract_query' part of the output of an API call like
2812 open-ils.search.biblio.multiclass.query, when called with the return_abstract
2816 return => { type => "string", desc => "String representation of abstract query object" }
2820 sub str2abstract_query {
2821 my ($self, $conn, $query, $qp_opts, $with_config) = @_;
2823 my %use_opts = ( # reasonable defaults? should these even be hardcoded here?
2825 superpage_size => 1000,
2826 core_limit => 25000,
2828 (ref $opts eq 'HASH' ? %$opts : ())
2833 # grab the query parser and initialize it
2834 my $parser = $OpenILS::Application::Storage::QParser;
2837 _initialize_parser($parser) unless $parser->initialization_complete;
2839 my $query = $parser->new(%use_opts)->parse;
2841 return $query->parse_tree->to_abstract_query(with_config => $with_config);
2844 __PACKAGE__->register_method(
2845 api_name => "open-ils.storage.query_parser.abstract_query.from_string",
2847 method => "str2abstract_query",
2851 {desc => "Query", type => "string"},
2852 {desc => q/Arguments for initializing QueryParser (optional)/,
2854 {desc => q/Flag enabling inclusion of QP config in returned object (optional, default false)/,
2857 return => { type => "object", desc => "abstract representation of query parser query" }
2861 my @available_statuses_cache;
2862 sub available_statuses {
2863 if (!scalar(@available_statuses_cache)) {
2864 @available_statuses_cache = map { $_->id } config::copy_status->search_where({is_available => 't'});
2866 return @available_statuses_cache;
2869 sub query_parser_fts {
2875 # grab the query parser and initialize it
2876 my $parser = $OpenILS::Application::Storage::QParser;
2879 _initialize_parser($parser) unless $parser->initialization_complete;
2881 # populate the locale/language map
2882 if (!$locale_map{COMPLETE}) {
2884 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2885 for my $locale ( @locales ) {
2886 $locale_map{lc($locale->code)} = $locale->marc_code;
2888 $locale_map{COMPLETE} = 1;
2892 # I hope we have a query!
2893 if (! $args{query} ) {
2894 die "No query was passed to ".$self->api_name;
2897 my $default_CD_modifiers = OpenSRF::Utils::SettingsClient->new->config_value(
2898 apps => 'open-ils.search' => app_settings => 'default_CD_modifiers'
2901 # Protect against empty / missing default_CD_modifiers setting
2902 if ($default_CD_modifiers and !ref($default_CD_modifiers)) {
2903 $args{query} = "$default_CD_modifiers $args{query}";
2906 my $simple_plan = $args{_simple_plan};
2907 # remove bad chunks of the %args hash
2908 for my $bad ( grep { /^_/ } keys(%args)) {
2909 delete($args{$bad});
2913 # parse the query and supply any query-level %arg-based defaults
2914 # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2915 my $query = $parser->new( %args )->parse;
2917 my $config = OpenSRF::Utils::SettingsClient->new();
2919 # set the locale-based default preferred location
2920 if (!$query->parse_tree->find_filter('preferred_language')) {
2921 $parser->default_preferred_language( $args{preferred_language} );
2923 if (!$parser->default_preferred_language) {
2924 my $ses_locale = $client->session ? $client->session->session_locale : '';
2925 $parser->default_preferred_language( $locale_map{ lc($ses_locale) } );
2928 if (!$parser->default_preferred_language) { # still nothing...
2929 my $tmp_dpl = $config->config_value(
2930 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2931 ) || $config->config_value(
2932 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2935 $parser->default_preferred_language( $tmp_dpl )
2940 # set the global default language multiplier
2941 if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2944 if ($tmp_dplw = $args{preferred_language_weight} || $args{preferred_language_multiplier} ) {
2945 $parser->default_preferred_language_multiplier($tmp_dplw);
2948 $tmp_dplw = $config->config_value(
2949 apps => 'open-ils.search' => app_settings => 'default_preferred_language_weight'
2950 ) || $config->config_value(
2951 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2954 $parser->default_preferred_language_multiplier( $tmp_dplw );
2958 # gather the site, if one is specified, defaulting to the in-query version
2959 my $ou = $args{org_unit};
2960 if (my ($filter) = $query->parse_tree->find_filter('site')) {
2961 $ou = $filter->args->[0] if (@{$filter->args});
2963 $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^(-)?\d+$/);
2965 # gather lasso, as with $ou
2966 my $lasso = $args{lasso};
2967 if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
2968 $lasso = $filter->args->[0] if (@{$filter->args});
2970 $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
2971 $lasso = -$lasso if ($lasso);
2974 # # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
2975 # # gather user lasso, as with $ou and lasso
2976 # my $mylasso = $args{my_lasso};
2977 # if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
2978 # $mylasso = $filter->args->[0] if (@{$filter->args});
2980 # $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
2983 # if we have a lasso, go with that, otherwise ... ou
2984 $ou = $lasso if ($lasso);
2986 # gather the preferred OU, if one is specified, as with $ou
2987 my $pref_ou = $args{pref_ou};
2988 $log->info("pref_ou = $pref_ou");
2989 if (my ($filter) = $query->parse_tree->find_filter('pref_ou')) {
2990 $pref_ou = $filter->args->[0] if (@{$filter->args});
2992 $pref_ou = actor::org_unit->search( { shortname => $pref_ou } )->next->id if ($pref_ou and $pref_ou !~ /^(-)?\d+$/);
2994 # get the default $ou if we have nothing
2995 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
2998 # 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
2999 # gather the depth, if one is specified, defaulting to the in-query version
3000 my $depth = $args{depth};
3001 if (my ($filter) = $query->parse_tree->find_filter('depth')) {
3002 $depth = $filter->args->[0] if (@{$filter->args});
3004 $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
3007 # gather the limit or default to 10
3008 my $limit = $args{check_limit};
3009 if (my ($filter) = $query->parse_tree->find_filter('limit')) {
3010 $limit = $filter->args->[0] if (@{$filter->args});
3012 if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
3013 $limit = $filter->args->[0] if (@{$filter->args});
3017 # gather the offset or default to 0
3018 my $offset = $args{skip_check} || $args{offset};
3019 if (my ($filter) = $query->parse_tree->find_filter('offset')) {
3020 $offset = $filter->args->[0] if (@{$filter->args});
3022 if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
3023 $offset = $filter->args->[0] if (@{$filter->args});
3027 # gather the estimation strategy or default to inclusion
3028 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
3029 if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
3030 $estimation_strategy = $filter->args->[0] if (@{$filter->args});
3034 # gather the estimation strategy or default to inclusion
3035 my $core_limit = $args{core_limit};
3036 if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
3037 $core_limit = $filter->args->[0] if (@{$filter->args});
3041 # gather statuses, and then forget those if we have an #available modifier
3043 if ($query->parse_tree->find_modifier('available')) {
3044 @statuses = available_statuses();
3045 } elsif (my ($filter) = $query->parse_tree->find_filter('statuses')) {
3046 @statuses = @{$filter->args} if (@{$filter->args});
3052 if (my ($filter) = $query->parse_tree->find_filter('locations')) {
3053 @location = @{$filter->args} if (@{$filter->args});
3056 # gather location_groups
3057 if (my ($filter) = $query->parse_tree->find_filter('location_groups')) {
3058 my @loc_groups = ();
3059 @loc_groups = @{$filter->args} if (@{$filter->args});
3061 # collect the mapped locations and add them to the locations() filter
3064 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3065 my $maps = $cstore->request(
3066 'open-ils.cstore.direct.asset.copy_location_group_map.search.atomic',
3067 {lgroup => \@loc_groups})->gather(1);
3069 push(@location, $_->location) for @$maps;
3074 my $param_check = $limit || $query->superpage_size || 'NULL';
3075 my $param_offset = $offset || 'NULL';
3076 my $param_limit = $core_limit || 'NULL';
3078 my $sp = $query->superpage || 1;
3080 $param_offset = ($sp - 1) * $sp_size;
3083 my $param_search_ou = $ou;
3084 my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
3085 # my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
3086 my $param_core_query = $query->parse_tree->toSQL;
3087 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
3088 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
3089 my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
3090 my $deleted_search = ($query->parse_tree->find_modifier('deleted')) ? "'t'" : "'f'";
3091 my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
3092 my $param_pref_ou = $pref_ou || 'NULL';
3094 # my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3095 # SELECT * -- bib search: $args{query}
3096 # FROM search.query_parser_fts(
3097 # $param_search_ou\:\:INT,
3098 # $param_depth\:\:INT,
3099 # $param_core_query\:\:TEXT,
3100 # $param_statuses\:\:INT[],
3101 # $param_locations\:\:INT[],
3102 # $param_offset\:\:INT,
3103 # $param_check\:\:INT,
3104 # $param_limit\:\:INT,
3105 # $metarecord\:\:BOOL,
3107 # $deleted_search\:\:BOOL,
3108 # $param_pref_ou\:\:INT
3112 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3113 -- bib search: $args{query}
3119 my $recs = $sth->fetchall_arrayref({});
3120 my $summary_row = pop @$recs;
3122 my $total = $$summary_row{total};
3123 my $checked = $$summary_row{checked};
3124 my $visible = $$summary_row{visible};
3125 my $deleted = $$summary_row{deleted};
3126 my $excluded = $$summary_row{excluded};
3128 delete $$summary_row{id};
3129 delete $$summary_row{rel};
3130 delete $$summary_row{record};
3131 delete $$summary_row{badges};
3132 delete $$summary_row{popularity};
3134 if (defined($simple_plan)) {
3135 $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3137 $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3140 if ($args{return_query}) {
3141 $$summary_row{query_struct} = $query->parse_tree->to_abstract_query();
3142 $$summary_row{canonicalized_query} = QueryParser::Canonicalize::abstract_query2str_impl($$summary_row{query_struct}, 0, $parser);
3145 $client->respond( $summary_row );
3147 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $visible.",DEBUG);
3149 for my $rec (@$recs) {
3150 delete $$rec{checked};
3151 delete $$rec{visible};
3152 delete $$rec{excluded};
3153 delete $$rec{deleted};
3154 delete $$rec{total};
3155 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3157 $client->respond( $rec );
3161 __PACKAGE__->register_method(
3162 api_name => "open-ils.storage.query_parser_search",
3164 method => 'query_parser_fts',
3172 sub query_parser_fts_wrapper {
3177 $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3178 # grab the query parser and initialize it
3179 my $parser = $OpenILS::Application::Storage::QParser;
3182 _initialize_parser($parser) unless $parser->initialization_complete;
3184 $args{searches} ||= {};
3185 if (!scalar(keys(%{$args{searches}})) && !$args{query}) {
3186 die "No search arguments were passed to ".$self->api_name;
3189 $top_org ||= actor::org_unit->search( { parent_ou => undef } )->next;
3191 my $base_query = $args{query} || '';
3192 if (scalar(keys(%{$args{searches}}))) {
3193 $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3194 for my $sclass ( keys %{$args{searches}} ) {
3195 $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3196 $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3200 my $query = $base_query;
3201 $log->debug("Full base query: $base_query", DEBUG);
3203 $query = "$args{facets} $query" if ($args{facets});
3205 if (!$locale_map{COMPLETE}) {
3207 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3208 for my $locale ( @locales ) {
3209 $locale_map{lc($locale->code)} = $locale->marc_code;
3211 $locale_map{COMPLETE} = 1;
3215 my $base_plan = $parser->new( query => $base_query )->parse;
3217 $query = "preferred_language($args{preferred_language}) $query"
3218 if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3219 $query = "preferred_language_weight($args{preferred_language_weight}) $query"
3220 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'));
3224 if (!$base_plan->parse_tree->find_filter('badge_orgs')) {
3225 # supply a suitable badge_orgs filter unless user has
3226 # explicitly supplied one
3229 my @lg_id_list = (); # We must define the variable with a static value
3230 # because an idomatic my+set causes the previous
3231 # value is remembered via closure.
3233 @lg_id_list = @{$args{location_groups}} if (ref $args{location_groups});
3235 my ($lg_filter) = $base_plan->parse_tree->find_filter('location_groups');
3236 @lg_id_list = @{$lg_filter->args} if ($lg_filter && @{$lg_filter->args});
3240 for my $lg ( grep { /^\d+$/ } @lg_id_list ) {
3241 my $lg_obj = asset::copy_location_group->retrieve($lg);
3242 next unless $lg_obj;
3244 push(@borg_list, @{$U->get_org_ancestors(''.$lg_obj->owner)});
3246 $borgs = join(',', uniq @borg_list) if @borg_list;
3250 my ($site_filter) = $base_plan->parse_tree->find_filter('site');
3251 if ($site_filter && @{$site_filter->args}) {
3252 $site = $top_org if ($site_filter->args->[0] eq '-');
3253 $site = $top_org if ($site_filter->args->[0] eq $top_org->shortname);
3254 $site = actor::org_unit->search( { shortname => $site_filter->args->[0] })->next unless ($site);
3255 } elsif ($args{org_unit}) {
3256 $site = $top_org if ($args{org_unit} eq '-');
3257 $site = $top_org if ($args{org_unit} eq $top_org->shortname);
3258 $site = actor::org_unit->search( { shortname => $args{org_unit} })->next unless ($site);
3264 $borgs = $U->get_org_ancestors($site->id);
3265 $borgs = @$borgs ? join(',', @$borgs) : undef;
3270 # gather the limit or default to 10
3271 my $limit = delete($args{check_limit}) || $base_plan->superpage_size;
3272 if (my ($filter) = $base_plan->parse_tree->find_filter('limit')) {
3273 $limit = $filter->args->[0] if (@{$filter->args});
3275 if (my ($filter) = $base_plan->parse_tree->find_filter('check_limit')) {
3276 $limit = $filter->args->[0] if (@{$filter->args});
3279 # gather the offset or default to 0
3280 my $offset = delete($args{skip_check}) || delete($args{offset}) || 0;
3281 if (my ($filter) = $base_plan->parse_tree->find_filter('offset')) {
3282 $offset = $filter->args->[0] if (@{$filter->args});
3284 if (my ($filter) = $base_plan->parse_tree->find_filter('skip_check')) {
3285 $offset = $filter->args->[0] if (@{$filter->args});
3289 $query = "check_limit($limit) $query" if (defined $limit);
3290 $query = "skip_check($offset) $query" if (defined $offset);
3291 $query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
3292 $query = "badge_orgs($borgs) $query" if ($borgs);
3294 # XXX All of the following, down to the 'return' is basically dead code. someone higher up should handle it
3295 $query = "site($args{org_unit}) $query" if ($args{org_unit});
3296 $query = "depth($args{depth}) $query" if (defined($args{depth}));
3297 $query = "sort($args{sort}) $query" if ($args{sort});
3298 $query = "core_limit($args{core_limit}) $query" if ($args{core_limit});
3299 # $query = "limit($args{limit}) $query" if ($args{limit});
3300 # $query = "skip_check($args{skip_check}) $query" if ($args{skip_check});
3301 $query = "superpage($args{superpage}) $query" if ($args{superpage});
3302 $query = "offset($args{offset}) $query" if ($args{offset});
3303 $query = "#metarecord $query" if ($self->api_name =~ /metabib/);
3304 $query = "from_metarecord($args{from_metarecord}) $query" if ($args{from_metarecord});
3305 $query = "#available $query" if ($args{available});
3306 $query = "#descending $query" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3307 $query = "#staff $query" if ($self->api_name =~ /staff/);
3308 $query = "before($args{before}) $query" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3309 $query = "after($args{after}) $query" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3310 $query = "during($args{during}) $query" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3311 $query = "between($args{between}[0],$args{between}[1]) $query"
3312 if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3315 my (@between,@statuses,@locations,@location_groups,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3317 # XXX legacy format and item type support
3318 if ($args{format}) {
3319 my ($t, $f) = split '-', $args{format};
3320 $args{item_type} = [ split '', $t ];
3321 $args{item_form} = [ split '', $f ];
3324 for my $filter ( qw/locations location_groups statuses audience language lit_form item_form item_type bib_level vr_format badges/ ) {
3325 if (my $s = $args{$filter}) {
3326 $s = [$s] if (!ref($s));
3328 my @filter_list = @$s;
3330 next if (@filter_list == 0);
3332 my $filter_string = join ',', @filter_list;
3333 $query = "$query $filter($filter_string)";
3337 $log->debug("Full QueryParser query: $query", DEBUG);
3339 return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan, return_query => $args{return_query} );
3341 __PACKAGE__->register_method(
3342 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
3344 method => 'query_parser_fts_wrapper',
3349 __PACKAGE__->register_method(
3350 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3352 method => 'query_parser_fts_wrapper',
3357 __PACKAGE__->register_method(
3358 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
3360 method => 'query_parser_fts_wrapper',
3365 __PACKAGE__->register_method(
3366 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3368 method => 'query_parser_fts_wrapper',