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 # Staff want to see all copies regardless of visibility
469 $copies_visible = '';
470 # When searching globally for staff avoid any copy filtering.
471 if ((defined $args{depth} && $args{depth} == 0)
472 || $args{org_unit} == $U->get_org_tree->id) {
478 my ($t_filter, $f_filter) = ('','');
479 my ($a_filter, $l_filter, $lf_filter) = ('','','');
482 if (my $a = $args{audience}) {
483 $a = [$a] if (!ref($a));
486 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
491 if (my $l = $args{language}) {
492 $l = [$l] if (!ref($l));
495 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
500 if (my $f = $args{lit_form}) {
501 $f = [$f] if (!ref($f));
504 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
505 push @binds, @lit_form;
509 if (my $f = $args{item_form}) {
510 $f = [$f] if (!ref($f));
513 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
518 if (my $t = $args{item_type}) {
519 $t = [$t] if (!ref($t));
522 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
529 my ($t, $f) = split '-', $args{format};
530 my @types = split '', $t;
531 my @forms = split '', $f;
533 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
538 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
541 push @binds, @types, @forms;
544 my $relevance = 'sum(f.sum)';
545 $relevance = 1 if (!$copies_visible);
547 my $string_default_sort = 'zzzz';
548 $string_default_sort = 'AAAA' if ($sort_dir =~ /^DESC$/i);
550 my $number_default_sort = '9999';
551 $number_default_sort = '0000' if ($sort_dir =~/^DESC$/i);
553 my $rank = $relevance;
554 if (lc($sort) eq 'pubdate') {
557 SELECT COALESCE(SUBSTRING(MAX(frp.value) FROM E'\\\\d{4}'), '$number_default_sort')::INT
558 FROM $metabib_full_rec frp
559 WHERE frp.record = f.record
561 AND frp.subfield = 'c'
565 } elsif (lc($sort) eq 'create_date') {
567 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
569 } elsif (lc($sort) eq 'edit_date') {
571 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
573 } elsif (lc($sort) =~ /^title/i) {
576 SELECT COALESCE(LTRIM(SUBSTR(MAX(frt.value), COALESCE(SUBSTRING(MAX(frt.ind2) FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
577 FROM $metabib_full_rec frt
578 WHERE frt.record = f.record
580 AND frt.subfield = 'a'
584 } elsif (lc($sort) =~ /^author/i) {
587 SELECT COALESCE(LTRIM(MAX(query.value)), '$string_default_sort')
590 FROM $metabib_full_rec fra
591 WHERE fra.record = f.record
592 AND fra.tag LIKE '1%'
593 AND fra.subfield = 'a'
594 ORDER BY fra.tag::text::int
603 my $rd_join = $use_rd ? "$metabib_record_descriptor rd," : '';
604 my $rd_filter = $use_rd ? 'AND rd.record = f.record' : '';
608 SELECT f.record, $relevance, count(DISTINCT cp.id), $rank
609 FROM $search_table f,
610 $asset_call_number_table cn,
611 $asset_copy_table cp,
617 WHERE br.id = f.record
618 AND cn.record = f.record
619 AND cp.status = cs.id
620 AND cp.location = cl.id
621 AND br.deleted IS FALSE
622 AND cn.deleted IS FALSE
623 AND cp.deleted IS FALSE
633 GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
634 ORDER BY 4 $sort_dir,3 DESC
638 SELECT f.record, 1, 1, $rank
639 FROM $search_table f,
642 WHERE br.id = f.record
643 AND br.deleted IS FALSE
656 $log->debug("Search SQL :: [$select]",DEBUG);
658 my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
659 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
662 $max = 1 if (!@$recs);
664 $max = $$_[1] if ($$_[1] > $max);
668 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
669 next unless ($$rec[0]);
670 my ($rid,$rank,$junk,$skip) = @$rec;
671 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
675 __PACKAGE__->register_method(
676 api_name => 'open-ils.storage.biblio.full_rec.multi_search',
678 method => 'biblio_multi_search_full_rec',
683 __PACKAGE__->register_method(
684 api_name => 'open-ils.storage.biblio.full_rec.multi_search.staff',
686 method => 'biblio_multi_search_full_rec',
692 sub search_full_rec {
698 my $term = $args{term};
699 my $limiters = $args{restrict};
701 my ($index_col) = metabib::full_rec->columns('FTS');
702 $index_col ||= 'value';
703 my $search_table = metabib::full_rec->table;
705 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
707 my $fts_where = $fts->sql_where_clause();
708 my @fts_ranks = $fts->fts_rank;
710 my $rank = join(' + ', @fts_ranks);
714 for my $limit (@$limiters) {
715 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
716 # MARC control field; mfr.subfield is NULL
717 push @wheres, "( tag = ? AND $fts_where )";
718 push @binds, $$limit{tag};
719 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
721 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
722 push @binds, $$limit{tag}, $$limit{subfield};
723 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
726 my $where = join(' OR ', @wheres);
728 my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
730 $log->debug("Search SQL :: [$select]",DEBUG);
732 my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
733 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
735 $client->respond($_) for (@$recs);
738 __PACKAGE__->register_method(
739 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
741 method => 'search_full_rec',
746 __PACKAGE__->register_method(
747 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
749 method => 'search_full_rec',
756 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
757 sub search_class_fts {
762 my $term = $args{term};
763 my $ou = $args{org_unit};
764 my $ou_type = $args{depth};
765 my $limit = $args{limit};
766 my $offset = $args{offset};
768 my $limit_clause = '';
769 my $offset_clause = '';
771 $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
772 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
775 my ($t_filter, $f_filter) = ('','');
778 my ($t, $f) = split '-', $args{format};
779 @types = split '', $t;
780 @forms = split '', $f;
782 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
786 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
792 my $descendants = defined($ou_type) ?
793 "actor.org_unit_descendants($ou, $ou_type)" :
794 "actor.org_unit_descendants($ou)";
796 my $class = $self->{cdbi};
797 my $search_table = $class->table;
799 my $metabib_record_descriptor = metabib::record_descriptor->table;
800 my $metabib_metarecord = metabib::metarecord->table;
801 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
802 my $asset_call_number_table = asset::call_number->table;
803 my $asset_copy_table = asset::copy->table;
804 my $cs_table = config::copy_status->table;
805 my $cl_table = asset::copy_location->table;
807 my ($index_col) = $class->columns('FTS');
808 $index_col ||= 'value';
810 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
811 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
813 my $fts_where = $fts->sql_where_clause;
814 my @fts_ranks = $fts->fts_rank;
816 my $rank = join(' + ', @fts_ranks);
818 my $has_vols = 'AND cn.owning_lib = d.id';
819 my $has_copies = 'AND cp.call_number = cn.id';
820 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';
822 my $visible_count = ', count(DISTINCT cp.id)';
823 my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
825 if ($self->api_name =~ /staff/o) {
826 $copies_visible = '';
827 $visible_count_test = '';
828 $has_copies = '' if ($ou_type == 0);
829 $has_vols = '' if ($ou_type == 0);
832 my $rank_calc = <<" RANK";
834 * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
835 * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
836 * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
837 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
840 $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
842 if ($copies_visible) {
844 SELECT m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
845 FROM $search_table f,
846 $metabib_metarecord_source_map_table m,
847 $asset_call_number_table cn,
848 $asset_copy_table cp,
851 $metabib_record_descriptor rd,
854 AND m.source = f.source
855 AND cn.record = m.source
856 AND rd.record = m.source
857 AND cp.status = cs.id
858 AND cp.location = cl.id
864 GROUP BY 1 $visible_count_test
866 $limit_clause $offset_clause
870 SELECT m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
871 FROM $search_table f,
872 $metabib_metarecord_source_map_table m,
873 $metabib_record_descriptor rd
875 AND m.source = f.source
876 AND rd.record = m.source
881 $limit_clause $offset_clause
885 $log->debug("Field Search SQL :: [$select]",DEBUG);
887 my $SQLstring = join('%',$fts->words);
888 my $REstring = join('\\s+',$fts->words);
889 my $first_word = ($fts->words)[0].'%';
890 my $recs = ($self->api_name =~ /unordered/o) ?
891 $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
892 $class->db_Main->selectall_arrayref($select, {},
893 '%'.lc($SQLstring).'%', # phrase order match
894 lc($first_word), # first word match
895 '^\\s*'.lc($REstring).'\\s*/?\s*$', # full exact match
899 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
901 $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
905 for my $class ( qw/title author subject keyword series identifier/ ) {
906 __PACKAGE__->register_method(
907 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord",
909 method => 'search_class_fts',
912 cdbi => "metabib::${class}_field_entry",
915 __PACKAGE__->register_method(
916 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
918 method => 'search_class_fts',
921 cdbi => "metabib::${class}_field_entry",
924 __PACKAGE__->register_method(
925 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
927 method => 'search_class_fts',
930 cdbi => "metabib::${class}_field_entry",
933 __PACKAGE__->register_method(
934 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
936 method => 'search_class_fts',
939 cdbi => "metabib::${class}_field_entry",
944 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
945 sub search_class_fts_count {
950 my $term = $args{term};
951 my $ou = $args{org_unit};
952 my $ou_type = $args{depth};
953 my $limit = $args{limit} || 100;
954 my $offset = $args{offset} || 0;
956 my $descendants = defined($ou_type) ?
957 "actor.org_unit_descendants($ou, $ou_type)" :
958 "actor.org_unit_descendants($ou)";
961 my ($t_filter, $f_filter) = ('','');
964 my ($t, $f) = split '-', $args{format};
965 @types = split '', $t;
966 @forms = split '', $f;
968 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
972 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
977 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
979 my $class = $self->{cdbi};
980 my $search_table = $class->table;
982 my $metabib_record_descriptor = metabib::record_descriptor->table;
983 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
984 my $asset_call_number_table = asset::call_number->table;
985 my $asset_copy_table = asset::copy->table;
986 my $cs_table = config::copy_status->table;
987 my $cl_table = asset::copy_location->table;
989 my ($index_col) = $class->columns('FTS');
990 $index_col ||= 'value';
992 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
994 my $fts_where = $fts->sql_where_clause;
996 my $has_vols = 'AND cn.owning_lib = d.id';
997 my $has_copies = 'AND cp.call_number = cn.id';
998 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';
999 if ($self->api_name =~ /staff/o) {
1000 $copies_visible = '';
1001 $has_vols = '' if ($ou_type == 0);
1002 $has_copies = '' if ($ou_type == 0);
1005 # XXX test an "EXISTS version of descendant checking...
1007 if ($copies_visible) {
1009 SELECT count(distinct m.metarecord)
1010 FROM $search_table f,
1011 $metabib_metarecord_source_map_table m,
1012 $metabib_metarecord_source_map_table mr,
1013 $asset_call_number_table cn,
1014 $asset_copy_table cp,
1017 $metabib_record_descriptor rd,
1020 AND mr.source = f.source
1021 AND mr.metarecord = m.metarecord
1022 AND cn.record = m.source
1023 AND rd.record = m.source
1024 AND cp.status = cs.id
1025 AND cp.location = cl.id
1034 SELECT count(distinct m.metarecord)
1035 FROM $search_table f,
1036 $metabib_metarecord_source_map_table m,
1037 $metabib_metarecord_source_map_table mr,
1038 $metabib_record_descriptor rd
1040 AND mr.source = f.source
1041 AND mr.metarecord = m.metarecord
1042 AND rd.record = m.source
1048 $log->debug("Field Search Count SQL :: [$select]",DEBUG);
1050 my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1052 $log->debug("Count Search yielded $recs results.",DEBUG);
1057 for my $class ( qw/title author subject keyword series identifier/ ) {
1058 __PACKAGE__->register_method(
1059 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
1061 method => 'search_class_fts_count',
1064 cdbi => "metabib::${class}_field_entry",
1067 __PACKAGE__->register_method(
1068 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
1070 method => 'search_class_fts_count',
1073 cdbi => "metabib::${class}_field_entry",
1079 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1080 sub postfilter_search_class_fts {
1085 my $term = $args{term};
1086 my $sort = $args{'sort'};
1087 my $sort_dir = $args{sort_dir} || 'DESC';
1088 my $ou = $args{org_unit};
1089 my $ou_type = $args{depth};
1090 my $limit = $args{limit} || 10;
1091 my $visibility_limit = $args{visibility_limit} || 5000;
1092 my $offset = $args{offset} || 0;
1094 my $outer_limit = 1000;
1096 my $limit_clause = '';
1097 my $offset_clause = '';
1099 $limit_clause = "LIMIT $outer_limit";
1100 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1102 my (@types,@forms,@lang,@aud,@lit_form);
1103 my ($t_filter, $f_filter) = ('','');
1104 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1105 my ($ot_filter, $of_filter) = ('','');
1106 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1108 if (my $a = $args{audience}) {
1109 $a = [$a] if (!ref($a));
1112 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1113 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1116 if (my $l = $args{language}) {
1117 $l = [$l] if (!ref($l));
1120 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1121 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1124 if (my $f = $args{lit_form}) {
1125 $f = [$f] if (!ref($f));
1128 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1129 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1132 if ($args{format}) {
1133 my ($t, $f) = split '-', $args{format};
1134 @types = split '', $t;
1135 @forms = split '', $f;
1137 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1138 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1142 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1143 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1148 my $descendants = defined($ou_type) ?
1149 "actor.org_unit_descendants($ou, $ou_type)" :
1150 "actor.org_unit_descendants($ou)";
1152 my $class = $self->{cdbi};
1153 my $search_table = $class->table;
1155 my $metabib_full_rec = metabib::full_rec->table;
1156 my $metabib_record_descriptor = metabib::record_descriptor->table;
1157 my $metabib_metarecord = metabib::metarecord->table;
1158 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1159 my $asset_call_number_table = asset::call_number->table;
1160 my $asset_copy_table = asset::copy->table;
1161 my $cs_table = config::copy_status->table;
1162 my $cl_table = asset::copy_location->table;
1163 my $br_table = biblio::record_entry->table;
1165 my ($index_col) = $class->columns('FTS');
1166 $index_col ||= 'value';
1168 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1170 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1172 my $SQLstring = join('%',map { lc($_) } $fts->words);
1173 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1174 my $first_word = lc(($fts->words)[0]).'%';
1176 my $fts_where = $fts->sql_where_clause;
1177 my @fts_ranks = $fts->fts_rank;
1180 $bonus{'metabib::identifier_field_entry'} =
1181 $bonus{'metabib::keyword_field_entry'} = [
1182 { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring }
1185 $bonus{'metabib::title_field_entry'} =
1186 $bonus{'metabib::series_field_entry'} = [
1187 { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1188 { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1189 @{ $bonus{'metabib::keyword_field_entry'} }
1192 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1193 $bonus_list ||= '1';
1195 my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1197 my $relevance = join(' + ', @fts_ranks);
1198 $relevance = <<" RANK";
1199 (SUM( ( $relevance ) * ( $bonus_list ) )/COUNT(m.source))
1202 my $string_default_sort = 'zzzz';
1203 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1205 my $number_default_sort = '9999';
1206 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1208 my $rank = $relevance;
1209 if (lc($sort) eq 'pubdate') {
1212 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1213 FROM $metabib_full_rec frp
1214 WHERE frp.record = mr.master_record
1216 AND frp.subfield = 'c'
1220 } elsif (lc($sort) eq 'create_date') {
1222 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1224 } elsif (lc($sort) eq 'edit_date') {
1226 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1228 } elsif (lc($sort) eq 'title') {
1231 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1232 FROM $metabib_full_rec frt
1233 WHERE frt.record = mr.master_record
1235 AND frt.subfield = 'a'
1239 } elsif (lc($sort) eq 'author') {
1242 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1243 FROM $metabib_full_rec fra
1244 WHERE fra.record = mr.master_record
1245 AND fra.tag LIKE '1%'
1246 AND fra.subfield = 'a'
1247 ORDER BY fra.tag::text::int
1255 my $select = <<" SQL";
1256 SELECT m.metarecord,
1258 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1260 FROM $search_table f,
1261 $metabib_metarecord_source_map_table m,
1262 $metabib_metarecord_source_map_table smrs,
1263 $metabib_metarecord mr,
1264 $metabib_record_descriptor rd
1266 AND smrs.metarecord = mr.id
1267 AND m.source = f.source
1268 AND m.metarecord = mr.id
1269 AND rd.record = smrs.source
1275 GROUP BY m.metarecord
1276 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1277 LIMIT $visibility_limit
1284 FROM $asset_call_number_table cn,
1285 $metabib_metarecord_source_map_table mrs,
1286 $asset_copy_table cp,
1291 $metabib_record_descriptor ord,
1293 WHERE mrs.metarecord = s.metarecord
1294 AND br.id = mrs.source
1295 AND cn.record = mrs.source
1296 AND cp.status = cs.id
1297 AND cp.location = cl.id
1298 AND cn.owning_lib = d.id
1299 AND cp.call_number = cn.id
1300 AND cp.opac_visible IS TRUE
1301 AND cs.opac_visible IS TRUE
1302 AND cl.opac_visible IS TRUE
1303 AND d.opac_visible IS TRUE
1304 AND br.active IS TRUE
1305 AND br.deleted IS FALSE
1306 AND ord.record = mrs.source
1312 ORDER BY 4 $sort_dir
1314 } elsif ($self->api_name !~ /staff/o) {
1321 FROM $asset_call_number_table cn,
1322 $metabib_metarecord_source_map_table mrs,
1323 $asset_copy_table cp,
1328 $metabib_record_descriptor ord
1330 WHERE mrs.metarecord = s.metarecord
1331 AND br.id = mrs.source
1332 AND cn.record = mrs.source
1333 AND cp.status = cs.id
1334 AND cp.location = cl.id
1335 AND cp.circ_lib = d.id
1336 AND cp.call_number = cn.id
1337 AND cp.opac_visible IS TRUE
1338 AND cs.opac_visible IS TRUE
1339 AND cl.opac_visible IS TRUE
1340 AND d.opac_visible IS TRUE
1341 AND br.active IS TRUE
1342 AND br.deleted IS FALSE
1343 AND ord.record = mrs.source
1351 ORDER BY 4 $sort_dir
1360 FROM $asset_call_number_table cn,
1361 $asset_copy_table cp,
1362 $metabib_metarecord_source_map_table mrs,
1365 $metabib_record_descriptor ord
1367 WHERE mrs.metarecord = s.metarecord
1368 AND br.id = mrs.source
1369 AND cn.record = mrs.source
1370 AND cn.id = cp.call_number
1371 AND br.deleted IS FALSE
1372 AND cn.deleted IS FALSE
1373 AND ord.record = mrs.source
1374 AND ( cn.owning_lib = d.id
1375 OR ( cp.circ_lib = d.id
1376 AND cp.deleted IS FALSE
1388 FROM $asset_call_number_table cn,
1389 $metabib_metarecord_source_map_table mrs,
1390 $metabib_record_descriptor ord
1391 WHERE mrs.metarecord = s.metarecord
1392 AND cn.record = mrs.source
1393 AND ord.record = mrs.source
1401 ORDER BY 4 $sort_dir
1406 $log->debug("Field Search SQL :: [$select]",DEBUG);
1408 my $recs = $class->db_Main->selectall_arrayref(
1410 (@bonus_values > 0 ? @bonus_values : () ),
1411 ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1412 @types, @forms, @aud, @lang, @lit_form,
1413 @types, @forms, @aud, @lang, @lit_form,
1414 ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1416 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1419 $max = 1 if (!@$recs);
1421 $max = $$_[1] if ($$_[1] > $max);
1424 my $count = scalar(@$recs);
1425 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1426 my ($mrid,$rank,$skip) = @$rec;
1427 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1432 for my $class ( qw/title author subject keyword series identifier/ ) {
1433 __PACKAGE__->register_method(
1434 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1436 method => 'postfilter_search_class_fts',
1439 cdbi => "metabib::${class}_field_entry",
1442 __PACKAGE__->register_method(
1443 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1445 method => 'postfilter_search_class_fts',
1448 cdbi => "metabib::${class}_field_entry",
1455 my $_cdbi = { title => "metabib::title_field_entry",
1456 author => "metabib::author_field_entry",
1457 subject => "metabib::subject_field_entry",
1458 keyword => "metabib::keyword_field_entry",
1459 series => "metabib::series_field_entry",
1460 identifier => "metabib::identifier_field_entry",
1463 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1464 sub postfilter_search_multi_class_fts {
1469 my $sort = $args{'sort'};
1470 my $sort_dir = $args{sort_dir} || 'DESC';
1471 my $ou = $args{org_unit};
1472 my $ou_type = $args{depth};
1473 my $limit = $args{limit} || 10;
1474 my $offset = $args{offset} || 0;
1475 my $visibility_limit = $args{visibility_limit} || 5000;
1478 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1481 if (!defined($args{org_unit})) {
1482 die "No target organizational unit passed to ".$self->api_name;
1485 if (! scalar( keys %{$args{searches}} )) {
1486 die "No search arguments were passed to ".$self->api_name;
1489 my $outer_limit = 1000;
1491 my $limit_clause = '';
1492 my $offset_clause = '';
1494 $limit_clause = "LIMIT $outer_limit";
1495 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1497 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1498 my ($t_filter, $f_filter, $v_filter) = ('','','');
1499 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1500 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1501 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1503 if ($args{available}) {
1504 $avail_filter = ' AND cp.status IN (0,7,12)';
1507 if (my $a = $args{audience}) {
1508 $a = [$a] if (!ref($a));
1511 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1512 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1515 if (my $l = $args{language}) {
1516 $l = [$l] if (!ref($l));
1519 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1520 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1523 if (my $f = $args{lit_form}) {
1524 $f = [$f] if (!ref($f));
1527 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1528 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1531 if (my $f = $args{item_form}) {
1532 $f = [$f] if (!ref($f));
1535 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1536 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1539 if (my $t = $args{item_type}) {
1540 $t = [$t] if (!ref($t));
1543 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1544 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1547 if (my $v = $args{vr_format}) {
1548 $v = [$v] if (!ref($v));
1551 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1552 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1556 # XXX legacy format and item type support
1557 if ($args{format}) {
1558 my ($t, $f) = split '-', $args{format};
1559 @types = split '', $t;
1560 @forms = split '', $f;
1562 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1563 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1567 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1568 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1574 my $descendants = defined($ou_type) ?
1575 "actor.org_unit_descendants($ou, $ou_type)" :
1576 "actor.org_unit_descendants($ou)";
1578 my $search_table_list = '';
1580 my $join_table_list = '';
1583 my $field_table = config::metabib_field->table;
1587 my $prev_search_group;
1588 my $curr_search_group;
1592 for my $search_group (sort keys %{$args{searches}}) {
1593 (my $search_group_name = $search_group) =~ s/\|/_/gso;
1594 ($search_class,$search_field) = split /\|/, $search_group;
1595 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1597 if ($search_field) {
1598 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1599 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1604 $prev_search_group = $curr_search_group if ($curr_search_group);
1606 $curr_search_group = $search_group_name;
1608 my $class = $_cdbi->{$search_class};
1609 my $search_table = $class->table;
1611 my ($index_col) = $class->columns('FTS');
1612 $index_col ||= 'value';
1615 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1617 my $fts_where = $fts->sql_where_clause;
1618 my @fts_ranks = $fts->fts_rank;
1620 my $SQLstring = join('%',map { lc($_) } $fts->words);
1621 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1622 my $first_word = lc(($fts->words)[0]).'%';
1624 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1625 my $rank = join(' + ', @fts_ranks);
1628 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1629 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1631 $bonus{'series'} = [
1632 { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1633 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1636 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1638 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1639 $bonus_list ||= '1';
1641 push @bonus_lists, $bonus_list;
1642 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1645 #---------------------
1647 $search_table_list .= "$search_table $search_group_name, ";
1648 push @rank_list,$rank;
1649 $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1651 if ($metabib_field) {
1652 $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1653 $metabib_field = undef;
1656 if ($prev_search_group) {
1657 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1661 my $metabib_record_descriptor = metabib::record_descriptor->table;
1662 my $metabib_full_rec = metabib::full_rec->table;
1663 my $metabib_metarecord = metabib::metarecord->table;
1664 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1665 my $asset_call_number_table = asset::call_number->table;
1666 my $asset_copy_table = asset::copy->table;
1667 my $cs_table = config::copy_status->table;
1668 my $cl_table = asset::copy_location->table;
1669 my $br_table = biblio::record_entry->table;
1670 my $source_table = config::bib_source->table;
1672 my $bonuses = join (' * ', @bonus_lists);
1673 my $relevance = join (' + ', @rank_list);
1674 $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1676 my $string_default_sort = 'zzzz';
1677 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1679 my $number_default_sort = '9999';
1680 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1684 my $secondary_sort = <<" SORT";
1686 SELECT COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1687 FROM $metabib_full_rec sfrt,
1688 $metabib_metarecord mr
1689 WHERE sfrt.record = mr.master_record
1690 AND sfrt.tag = '245'
1691 AND sfrt.subfield = 'a'
1696 my $rank = $relevance;
1697 if (lc($sort) eq 'pubdate') {
1700 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1701 FROM $metabib_full_rec frp
1702 WHERE frp.record = mr.master_record
1704 AND frp.subfield = 'c'
1708 } elsif (lc($sort) eq 'create_date') {
1710 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1712 } elsif (lc($sort) eq 'edit_date') {
1714 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1716 } elsif (lc($sort) eq 'title') {
1719 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1720 FROM $metabib_full_rec frt
1721 WHERE frt.record = mr.master_record
1723 AND frt.subfield = 'a'
1727 $secondary_sort = <<" SORT";
1729 SELECT COALESCE(SUBSTRING(sfrp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1730 FROM $metabib_full_rec sfrp
1731 WHERE sfrp.record = mr.master_record
1732 AND sfrp.tag = '260'
1733 AND sfrp.subfield = 'c'
1737 } elsif (lc($sort) eq 'author') {
1740 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1741 FROM $metabib_full_rec fra
1742 WHERE fra.record = mr.master_record
1743 AND fra.tag LIKE '1%'
1744 AND fra.subfield = 'a'
1745 ORDER BY fra.tag::text::int
1750 push @bonus_values, @bonus_values;
1755 my $select = <<" SQL";
1756 SELECT m.metarecord,
1758 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1761 FROM $search_table_list
1762 $metabib_metarecord mr,
1763 $metabib_metarecord_source_map_table m,
1764 $metabib_metarecord_source_map_table smrs
1765 WHERE m.metarecord = smrs.metarecord
1766 AND mr.id = m.metarecord
1769 GROUP BY m.metarecord
1770 -- ORDER BY 4 $sort_dir
1771 LIMIT $visibility_limit
1774 if ($self->api_name !~ /staff/o) {
1781 FROM $asset_call_number_table cn,
1782 $metabib_metarecord_source_map_table mrs,
1783 $asset_copy_table cp,
1788 $metabib_record_descriptor ord
1789 WHERE mrs.metarecord = s.metarecord
1790 AND br.id = mrs.source
1791 AND cn.record = mrs.source
1792 AND cp.status = cs.id
1793 AND cp.location = cl.id
1794 AND cp.circ_lib = d.id
1795 AND cp.call_number = cn.id
1796 AND cp.opac_visible IS TRUE
1797 AND cs.opac_visible IS TRUE
1798 AND cl.opac_visible IS TRUE
1799 AND d.opac_visible IS TRUE
1800 AND br.active IS TRUE
1801 AND br.deleted IS FALSE
1802 AND cp.deleted IS FALSE
1803 AND cn.deleted IS FALSE
1804 AND ord.record = mrs.source
1817 $metabib_metarecord_source_map_table mrs,
1818 $metabib_record_descriptor ord,
1820 WHERE mrs.metarecord = s.metarecord
1821 AND ord.record = mrs.source
1822 AND br.id = mrs.source
1823 AND br.source = src.id
1824 AND src.transcendant IS TRUE
1832 ORDER BY 4 $sort_dir, 5
1839 $metabib_metarecord_source_map_table omrs,
1840 $metabib_record_descriptor ord
1841 WHERE omrs.metarecord = s.metarecord
1842 AND ord.record = omrs.source
1845 FROM $asset_call_number_table cn,
1846 $asset_copy_table cp,
1849 WHERE br.id = omrs.source
1850 AND cn.record = omrs.source
1851 AND br.deleted IS FALSE
1852 AND cn.deleted IS FALSE
1853 AND cp.call_number = cn.id
1854 AND ( cn.owning_lib = d.id
1855 OR ( cp.circ_lib = d.id
1856 AND cp.deleted IS FALSE
1864 FROM $asset_call_number_table cn
1865 WHERE cn.record = omrs.source
1866 AND cn.deleted IS FALSE
1872 $metabib_metarecord_source_map_table mrs,
1873 $metabib_record_descriptor ord,
1875 WHERE mrs.metarecord = s.metarecord
1876 AND br.id = mrs.source
1877 AND br.source = src.id
1878 AND src.transcendant IS TRUE
1894 ORDER BY 4 $sort_dir, 5
1899 $log->debug("Field Search SQL :: [$select]",DEBUG);
1901 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1904 @types, @forms, @vformats, @aud, @lang, @lit_form,
1905 @types, @forms, @vformats, @aud, @lang, @lit_form,
1906 # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1909 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1912 $max = 1 if (!@$recs);
1914 $max = $$_[1] if ($$_[1] > $max);
1917 my $count = scalar(@$recs);
1918 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1919 next unless ($$rec[0]);
1920 my ($mrid,$rank,$skip) = @$rec;
1921 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1926 __PACKAGE__->register_method(
1927 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1929 method => 'postfilter_search_multi_class_fts',
1934 __PACKAGE__->register_method(
1935 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1937 method => 'postfilter_search_multi_class_fts',
1943 __PACKAGE__->register_method(
1944 api_name => "open-ils.storage.metabib.multiclass.search_fts",
1946 method => 'postfilter_search_multi_class_fts',
1951 __PACKAGE__->register_method(
1952 api_name => "open-ils.storage.metabib.multiclass.search_fts.staff",
1954 method => 'postfilter_search_multi_class_fts',
1960 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1961 sub biblio_search_multi_class_fts {
1966 my $sort = $args{'sort'};
1967 my $sort_dir = $args{sort_dir} || 'DESC';
1968 my $ou = $args{org_unit};
1969 my $ou_type = $args{depth};
1970 my $limit = $args{limit} || 10;
1971 my $offset = $args{offset} || 0;
1972 my $pref_lang = $args{preferred_language} || 'eng';
1973 my $visibility_limit = $args{visibility_limit} || 5000;
1976 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1979 if (! scalar( keys %{$args{searches}} )) {
1980 die "No search arguments were passed to ".$self->api_name;
1983 my $outer_limit = 1000;
1985 my $limit_clause = '';
1986 my $offset_clause = '';
1988 $limit_clause = "LIMIT $outer_limit";
1989 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1991 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1992 my ($t_filter, $f_filter, $v_filter) = ('','','');
1993 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1994 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1995 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1997 if ($args{available}) {
1998 $avail_filter = ' AND cp.status IN (0,7,12)';
2001 if (my $a = $args{audience}) {
2002 $a = [$a] if (!ref($a));
2005 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
2006 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
2009 if (my $l = $args{language}) {
2010 $l = [$l] if (!ref($l));
2013 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
2014 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
2017 if (my $f = $args{lit_form}) {
2018 $f = [$f] if (!ref($f));
2021 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2022 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2025 if (my $f = $args{item_form}) {
2026 $f = [$f] if (!ref($f));
2029 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2030 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2033 if (my $t = $args{item_type}) {
2034 $t = [$t] if (!ref($t));
2037 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2038 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2041 if (my $v = $args{vr_format}) {
2042 $v = [$v] if (!ref($v));
2045 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
2046 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
2049 # XXX legacy format and item type support
2050 if ($args{format}) {
2051 my ($t, $f) = split '-', $args{format};
2052 @types = split '', $t;
2053 @forms = split '', $f;
2055 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2056 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2060 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2061 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2066 my $descendants = defined($ou_type) ?
2067 "actor.org_unit_descendants($ou, $ou_type)" :
2068 "actor.org_unit_descendants($ou)";
2070 my $search_table_list = '';
2072 my $join_table_list = '';
2075 my $field_table = config::metabib_field->table;
2079 my $prev_search_group;
2080 my $curr_search_group;
2084 for my $search_group (sort keys %{$args{searches}}) {
2085 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2086 ($search_class,$search_field) = split /\|/, $search_group;
2087 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2089 if ($search_field) {
2090 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2091 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2096 $prev_search_group = $curr_search_group if ($curr_search_group);
2098 $curr_search_group = $search_group_name;
2100 my $class = $_cdbi->{$search_class};
2101 my $search_table = $class->table;
2103 my ($index_col) = $class->columns('FTS');
2104 $index_col ||= 'value';
2107 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2109 my $fts_where = $fts->sql_where_clause;
2110 my @fts_ranks = $fts->fts_rank;
2112 my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2113 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2114 my $first_word = lc(($fts->words)[0]).'%';
2116 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2117 my $rank = join(' + ', @fts_ranks);
2120 $bonus{'subject'} = [];
2121 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2123 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2125 $bonus{'series'} = [
2126 { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2127 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2130 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2133 push @{ $bonus{'title'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2134 push @{ $bonus{'author'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2135 push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2136 push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2137 push @{ $bonus{'series'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2140 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2141 $bonus_list ||= '1';
2143 push @bonus_lists, $bonus_list;
2144 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2146 #---------------------
2148 $search_table_list .= "$search_table $search_group_name, ";
2149 push @rank_list,$rank;
2150 $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2152 if ($metabib_field) {
2153 $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2154 $metabib_field = undef;
2157 if ($prev_search_group) {
2158 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2162 my $metabib_record_descriptor = metabib::record_descriptor->table;
2163 my $metabib_full_rec = metabib::full_rec->table;
2164 my $metabib_metarecord = metabib::metarecord->table;
2165 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2166 my $asset_call_number_table = asset::call_number->table;
2167 my $asset_copy_table = asset::copy->table;
2168 my $cs_table = config::copy_status->table;
2169 my $cl_table = asset::copy_location->table;
2170 my $br_table = biblio::record_entry->table;
2171 my $source_table = config::bib_source->table;
2174 my $bonuses = join (' * ', @bonus_lists);
2175 my $relevance = join (' + ', @rank_list);
2176 $relevance = "AVG( ($relevance) * ($bonuses) )";
2178 my $string_default_sort = 'zzzz';
2179 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2181 my $number_default_sort = '9999';
2182 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2184 my $rank = $relevance;
2185 if (lc($sort) eq 'pubdate') {
2188 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d{4}'),'$number_default_sort')::INT
2189 FROM $metabib_full_rec frp
2190 WHERE frp.record = b.id
2192 AND frp.subfield = 'c'
2196 } elsif (lc($sort) eq 'create_date') {
2198 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2200 } elsif (lc($sort) eq 'edit_date') {
2202 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2204 } elsif (lc($sort) eq 'title') {
2207 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2208 FROM $metabib_full_rec frt
2209 WHERE frt.record = b.id
2211 AND frt.subfield = 'a'
2215 } elsif (lc($sort) eq 'author') {
2218 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
2219 FROM $metabib_full_rec fra
2220 WHERE fra.record = b.id
2221 AND fra.tag LIKE '1%'
2222 AND fra.subfield = 'a'
2223 ORDER BY fra.tag::text::int
2228 push @bonus_values, @bonus_values;
2233 my $select = <<" SQL";
2238 FROM $search_table_list
2239 $metabib_record_descriptor rd,
2242 WHERE rd.record = b.id
2243 AND b.active IS TRUE
2244 AND b.deleted IS FALSE
2253 GROUP BY b.id, b.source
2254 ORDER BY 3 $sort_dir
2255 LIMIT $visibility_limit
2258 if ($self->api_name !~ /staff/o) {
2263 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2266 FROM $asset_call_number_table cn,
2267 $asset_copy_table cp,
2271 WHERE cn.record = s.id
2272 AND cp.status = cs.id
2273 AND cp.location = cl.id
2274 AND cp.call_number = cn.id
2275 AND cp.opac_visible IS TRUE
2276 AND cs.opac_visible IS TRUE
2277 AND cl.opac_visible IS TRUE
2278 AND d.opac_visible IS TRUE
2279 AND cp.deleted IS FALSE
2280 AND cn.deleted IS FALSE
2281 AND cp.circ_lib = d.id
2285 OR src.transcendant IS TRUE
2286 ORDER BY 3 $sort_dir
2293 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2296 FROM $asset_call_number_table cn,
2297 $asset_copy_table cp,
2299 WHERE cn.record = s.id
2300 AND cp.call_number = cn.id
2301 AND cn.deleted IS FALSE
2302 AND cp.circ_lib = d.id
2303 AND cp.deleted IS FALSE
2309 FROM $asset_call_number_table cn
2310 WHERE cn.record = s.id
2313 OR src.transcendant IS TRUE
2314 ORDER BY 3 $sort_dir
2319 $log->debug("Field Search SQL :: [$select]",DEBUG);
2321 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2323 @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2326 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2328 my $count = scalar(@$recs);
2329 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2330 next unless ($$rec[0]);
2331 my ($mrid,$rank) = @$rec;
2332 $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2337 __PACKAGE__->register_method(
2338 api_name => "open-ils.storage.biblio.multiclass.search_fts.record",
2340 method => 'biblio_search_multi_class_fts',
2345 __PACKAGE__->register_method(
2346 api_name => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2348 method => 'biblio_search_multi_class_fts',
2353 __PACKAGE__->register_method(
2354 api_name => "open-ils.storage.biblio.multiclass.search_fts",
2356 method => 'biblio_search_multi_class_fts',
2361 __PACKAGE__->register_method(
2362 api_name => "open-ils.storage.biblio.multiclass.search_fts.staff",
2364 method => 'biblio_search_multi_class_fts',
2372 my $default_preferred_language;
2373 my $default_preferred_language_weight;
2375 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2381 if (!$locale_map{COMPLETE}) {
2383 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2384 for my $locale ( @locales ) {
2385 $locale_map{lc($locale->code)} = $locale->marc_code;
2387 $locale_map{COMPLETE} = 1;
2391 my $config = OpenSRF::Utils::SettingsClient->new();
2393 if (!$default_preferred_language) {
2395 $default_preferred_language = $config->config_value(
2396 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2397 ) || $config->config_value(
2398 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2403 if (!$default_preferred_language_weight) {
2405 $default_preferred_language_weight = $config->config_value(
2406 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2407 ) || $config->config_value(
2408 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2412 # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2413 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2415 my $ou = $args{org_unit};
2416 my $limit = $args{limit} || 10;
2417 my $offset = $args{offset} || 0;
2420 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2423 if (! scalar( keys %{$args{searches}} )) {
2424 die "No search arguments were passed to ".$self->api_name;
2427 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2429 if (!defined($args{preferred_language})) {
2430 my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2431 $args{preferred_language} =
2432 $locale_map{ lc($ses_locale) } || 'eng';
2435 if (!defined($args{preferred_language_weight})) {
2436 $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2439 if ($args{available}) {
2440 @statuses = (0,7,12);
2443 if (my $s = $args{locations}) {
2444 $s = [$s] if (!ref($s));
2448 if (my $b = $args{between}) {
2449 if (ref($b) && @$b == 2) {
2454 if (my $s = $args{statuses}) {
2455 $s = [$s] if (!ref($s));
2459 if (my $a = $args{audience}) {
2460 $a = [$a] if (!ref($a));
2464 if (my $l = $args{language}) {
2465 $l = [$l] if (!ref($l));
2469 if (my $f = $args{lit_form}) {
2470 $f = [$f] if (!ref($f));
2474 if (my $f = $args{item_form}) {
2475 $f = [$f] if (!ref($f));
2479 if (my $t = $args{item_type}) {
2480 $t = [$t] if (!ref($t));
2484 if (my $b = $args{bib_level}) {
2485 $b = [$b] if (!ref($b));
2489 if (my $v = $args{vr_format}) {
2490 $v = [$v] if (!ref($v));
2494 # XXX legacy format and item type support
2495 if ($args{format}) {
2496 my ($t, $f) = split '-', $args{format};
2497 @types = split '', $t;
2498 @forms = split '', $f;
2501 my %stored_proc_search_args;
2502 for my $search_group (sort keys %{$args{searches}}) {
2503 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2504 my ($search_class,$search_field) = split /\|/, $search_group;
2505 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2507 if ($search_field) {
2508 unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2509 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2514 my $class = $_cdbi->{$search_class};
2515 my $search_table = $class->table;
2517 my ($index_col) = $class->columns('FTS');
2518 $index_col ||= 'value';
2521 my $fts = OpenILS::Application::Storage::FTS->compile(
2522 $search_class => $args{searches}{$search_group}{term},
2523 $search_group_name.'.value',
2524 "$search_group_name.$index_col"
2526 $fts->sql_where_clause; # this builds the ranks for us
2528 my @fts_ranks = $fts->fts_rank;
2529 my @fts_queries = $fts->fts_query;
2530 my @phrases = map { lc($_) } $fts->phrases;
2531 my @words = map { lc($_) } $fts->words;
2533 $stored_proc_search_args{$search_group} = {
2534 fts_rank => \@fts_ranks,
2535 fts_query => \@fts_queries,
2536 phrase => \@phrases,
2542 my $param_search_ou = $ou;
2543 my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2544 my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2545 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2546 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2547 my $param_audience = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud ) . '}$$';
2548 my $param_language = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang ) . '}$$';
2549 my $param_lit_form = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2550 my $param_types = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types ) . '}$$';
2551 my $param_forms = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms ) . '}$$';
2552 my $param_vformats = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2553 my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2554 my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2555 my $param_after = $args{after} ; $param_after = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2556 my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2557 my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2558 my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2559 my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2560 my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2561 my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2562 my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2563 my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2564 my $param_rel_limit = $args{core_limit}; $param_rel_limit ||= 'NULL';
2565 my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2566 my $param_skip_chk = $args{skip_check}; $param_skip_chk ||= 'NULL';
2568 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2570 FROM search.staged_fts(
2571 $param_search_ou\:\:INT,
2572 $param_depth\:\:INT,
2573 $param_searches\:\:TEXT,
2574 $param_statuses\:\:INT[],
2575 $param_locations\:\:INT[],
2576 $param_audience\:\:TEXT[],
2577 $param_language\:\:TEXT[],
2578 $param_lit_form\:\:TEXT[],
2579 $param_types\:\:TEXT[],
2580 $param_forms\:\:TEXT[],
2581 $param_vformats\:\:TEXT[],
2582 $param_bib_level\:\:TEXT[],
2583 $param_before\:\:TEXT,
2584 $param_after\:\:TEXT,
2585 $param_during\:\:TEXT,
2586 $param_between\:\:TEXT[],
2587 $param_pref_lang\:\:TEXT,
2588 $param_pref_lang_multiplier\:\:REAL,
2589 $param_sort\:\:TEXT,
2590 $param_sort_desc\:\:BOOL,
2591 $metarecord\:\:BOOL,
2593 $param_rel_limit\:\:INT,
2594 $param_chk_limit\:\:INT,
2595 $param_skip_chk\:\:INT
2601 my $recs = $sth->fetchall_arrayref({});
2602 my $summary_row = pop @$recs;
2604 my $total = $$summary_row{total};
2605 my $checked = $$summary_row{checked};
2606 my $visible = $$summary_row{visible};
2607 my $deleted = $$summary_row{deleted};
2608 my $excluded = $$summary_row{excluded};
2610 my $estimate = $visible;
2611 if ( $total > $checked && $checked ) {
2613 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2614 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2618 delete $$summary_row{id};
2619 delete $$summary_row{rel};
2620 delete $$summary_row{record};
2622 $client->respond( $summary_row );
2624 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2626 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2627 delete $$rec{checked};
2628 delete $$rec{visible};
2629 delete $$rec{excluded};
2630 delete $$rec{deleted};
2631 delete $$rec{total};
2632 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2634 $client->respond( $rec );
2638 __PACKAGE__->register_method(
2639 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
2641 method => 'staged_fts',
2646 __PACKAGE__->register_method(
2647 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2649 method => 'staged_fts',
2654 __PACKAGE__->register_method(
2655 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
2657 method => 'staged_fts',
2662 __PACKAGE__->register_method(
2663 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2665 method => 'staged_fts',
2671 sub FTS_paging_estimate {
2675 my $checked = shift;
2676 my $visible = shift;
2677 my $excluded = shift;
2678 my $deleted = shift;
2681 my $deleted_ratio = $deleted / $checked;
2682 my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2684 my $exclusion_ratio = $excluded / $checked;
2685 my $delete_adjusted_exclusion_ratio = $checked - $deleted ? $excluded / ($checked - $deleted) : 1;
2687 my $inclusion_ratio = $visible / $checked;
2688 my $delete_adjusted_inclusion_ratio = $checked - $deleted ? $visible / ($checked - $deleted) : 0;
2691 exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2692 inclusion => int($delete_adjusted_total * $inclusion_ratio),
2693 delete_adjusted_exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2694 delete_adjusted_inclusion => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2697 __PACKAGE__->register_method(
2698 api_name => "open-ils.storage.fts_paging_estimate",
2700 method => 'FTS_paging_estimate',
2706 Hash of estimation values based on four variant estimation strategies:
2707 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2708 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2709 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2710 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2713 Helper method used to determin the approximate number of
2714 hits for a search that spans multiple superpages. For
2715 sparse superpages, the inclusion estimate will likely be the
2716 best estimate. The exclusion strategy is the original, but
2717 inclusion is the default.
2720 { name => 'checked',
2721 desc => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2724 { name => 'visible',
2725 desc => 'Number of records visible to the search location on the current superpage.',
2728 { name => 'excluded',
2729 desc => 'Number of records excluded from the search location on the current superpage.',
2732 { name => 'deleted',
2733 desc => 'Number of deleted records on the current superpage.',
2737 desc => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2750 my $term = $$args{term};
2751 my $limit = $$args{max} || 1;
2752 my $min = $$args{min} || 1;
2753 my @classes = @{$$args{class}};
2755 $limit = $min if ($min > $limit);
2758 @classes = ( qw/ title author subject series keyword / );
2762 my $bre_table = biblio::record_entry->table;
2763 my $cn_table = asset::call_number->table;
2764 my $cp_table = asset::copy->table;
2766 for my $search_class ( @classes ) {
2768 my $class = $_cdbi->{$search_class};
2769 my $search_table = $class->table;
2771 my ($index_col) = $class->columns('FTS');
2772 $index_col ||= 'value';
2775 my $where = OpenILS::Application::Storage::FTS
2776 ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2780 SELECT COUNT(DISTINCT X.source)
2781 FROM (SELECT $search_class.source
2782 FROM $search_table $search_class
2783 JOIN $bre_table b ON (b.id = $search_class.source)
2788 HAVING COUNT(DISTINCT X.source) >= $min;
2791 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2792 $matches{$search_class} = $res ? $res->[0] : 0;
2797 __PACKAGE__->register_method(
2798 api_name => "open-ils.storage.search.xref",
2800 method => 'xref_count',
2804 # Takes an abstract query object and recursively turns it back into a string
2806 sub abstract_query2str {
2807 my ($self, $conn, $query) = @_;
2809 return QueryParser::Canonicalize::abstract_query2str_impl($query, 0, $OpenILS::Application::Storage::QParser);
2812 __PACKAGE__->register_method(
2813 api_name => "open-ils.storage.query_parser.abstract_query.canonicalize",
2815 method => "abstract_query2str",
2820 Abstract query parser object, with complete config data. For example input,
2821 see the 'abstract_query' part of the output of an API call like
2822 open-ils.search.biblio.multiclass.query, when called with the return_abstract
2826 return => { type => "string", desc => "String representation of abstract query object" }
2830 sub str2abstract_query {
2831 my ($self, $conn, $query, $qp_opts, $with_config) = @_;
2833 my %use_opts = ( # reasonable defaults? should these even be hardcoded here?
2835 superpage_size => 1000,
2836 core_limit => 25000,
2838 (ref $opts eq 'HASH' ? %$opts : ())
2843 # grab the query parser and initialize it
2844 my $parser = $OpenILS::Application::Storage::QParser;
2847 _initialize_parser($parser) unless $parser->initialization_complete;
2849 my $query = $parser->new(%use_opts)->parse;
2851 return $query->parse_tree->to_abstract_query(with_config => $with_config);
2854 __PACKAGE__->register_method(
2855 api_name => "open-ils.storage.query_parser.abstract_query.from_string",
2857 method => "str2abstract_query",
2861 {desc => "Query", type => "string"},
2862 {desc => q/Arguments for initializing QueryParser (optional)/,
2864 {desc => q/Flag enabling inclusion of QP config in returned object (optional, default false)/,
2867 return => { type => "object", desc => "abstract representation of query parser query" }
2871 my @available_statuses_cache;
2872 sub available_statuses {
2873 if (!scalar(@available_statuses_cache)) {
2874 @available_statuses_cache = map { $_->id } config::copy_status->search_where({is_available => 't'});
2876 return @available_statuses_cache;
2879 sub query_parser_fts {
2885 # grab the query parser and initialize it
2886 my $parser = $OpenILS::Application::Storage::QParser;
2889 _initialize_parser($parser) unless $parser->initialization_complete;
2891 # populate the locale/language map
2892 if (!$locale_map{COMPLETE}) {
2894 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2895 for my $locale ( @locales ) {
2896 $locale_map{lc($locale->code)} = $locale->marc_code;
2898 $locale_map{COMPLETE} = 1;
2902 # I hope we have a query!
2903 if (! $args{query} ) {
2904 die "No query was passed to ".$self->api_name;
2907 my $default_CD_modifiers = OpenSRF::Utils::SettingsClient->new->config_value(
2908 apps => 'open-ils.search' => app_settings => 'default_CD_modifiers'
2911 # Protect against empty / missing default_CD_modifiers setting
2912 if ($default_CD_modifiers and !ref($default_CD_modifiers)) {
2913 $args{query} = "$default_CD_modifiers $args{query}";
2916 my $simple_plan = $args{_simple_plan};
2917 # remove bad chunks of the %args hash
2918 for my $bad ( grep { /^_/ } keys(%args)) {
2919 delete($args{$bad});
2923 # parse the query and supply any query-level %arg-based defaults
2924 # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2925 my $query = $parser->new( %args )->parse;
2927 my $config = OpenSRF::Utils::SettingsClient->new();
2929 # set the locale-based default preferred location
2930 if (!$query->parse_tree->find_filter('preferred_language')) {
2931 $parser->default_preferred_language( $args{preferred_language} );
2933 if (!$parser->default_preferred_language) {
2934 my $ses_locale = $client->session ? $client->session->session_locale : '';
2935 $parser->default_preferred_language( $locale_map{ lc($ses_locale) } );
2938 if (!$parser->default_preferred_language) { # still nothing...
2939 my $tmp_dpl = $config->config_value(
2940 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2941 ) || $config->config_value(
2942 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2945 $parser->default_preferred_language( $tmp_dpl )
2950 # set the global default language multiplier
2951 if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2954 if ($tmp_dplw = $args{preferred_language_weight} || $args{preferred_language_multiplier} ) {
2955 $parser->default_preferred_language_multiplier($tmp_dplw);
2958 $tmp_dplw = $config->config_value(
2959 apps => 'open-ils.search' => app_settings => 'default_preferred_language_weight'
2960 ) || $config->config_value(
2961 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2964 $parser->default_preferred_language_multiplier( $tmp_dplw );
2968 # gather the site, if one is specified, defaulting to the in-query version
2969 my $ou = $args{org_unit};
2970 if (my ($filter) = $query->parse_tree->find_filter('site')) {
2971 $ou = $filter->args->[0] if (@{$filter->args});
2973 $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^(-)?\d+$/);
2975 # gather lasso, as with $ou
2976 my $lasso = $args{lasso};
2977 if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
2978 $lasso = $filter->args->[0] if (@{$filter->args});
2980 $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
2981 $lasso = -$lasso if ($lasso);
2984 # # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
2985 # # gather user lasso, as with $ou and lasso
2986 # my $mylasso = $args{my_lasso};
2987 # if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
2988 # $mylasso = $filter->args->[0] if (@{$filter->args});
2990 # $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
2993 # if we have a lasso, go with that, otherwise ... ou
2994 $ou = $lasso if ($lasso);
2996 # gather the preferred OU, if one is specified, as with $ou
2997 my $pref_ou = $args{pref_ou};
2998 $log->info("pref_ou = $pref_ou");
2999 if (my ($filter) = $query->parse_tree->find_filter('pref_ou')) {
3000 $pref_ou = $filter->args->[0] if (@{$filter->args});
3002 $pref_ou = actor::org_unit->search( { shortname => $pref_ou } )->next->id if ($pref_ou and $pref_ou !~ /^(-)?\d+$/);
3004 # get the default $ou if we have nothing
3005 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
3008 # 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
3009 # gather the depth, if one is specified, defaulting to the in-query version
3010 my $depth = $args{depth};
3011 if (my ($filter) = $query->parse_tree->find_filter('depth')) {
3012 $depth = $filter->args->[0] if (@{$filter->args});
3014 $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
3017 # gather the limit or default to 10
3018 my $limit = $args{check_limit};
3019 if (my ($filter) = $query->parse_tree->find_filter('limit')) {
3020 $limit = $filter->args->[0] if (@{$filter->args});
3022 if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
3023 $limit = $filter->args->[0] if (@{$filter->args});
3027 # gather the offset or default to 0
3028 my $offset = $args{skip_check} || $args{offset};
3029 if (my ($filter) = $query->parse_tree->find_filter('offset')) {
3030 $offset = $filter->args->[0] if (@{$filter->args});
3032 if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
3033 $offset = $filter->args->[0] if (@{$filter->args});
3037 # gather the estimation strategy or default to inclusion
3038 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
3039 if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
3040 $estimation_strategy = $filter->args->[0] if (@{$filter->args});
3044 # gather the estimation strategy or default to inclusion
3045 my $core_limit = $args{core_limit};
3046 if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
3047 $core_limit = $filter->args->[0] if (@{$filter->args});
3051 # gather statuses, and then forget those if we have an #available modifier
3053 if ($query->parse_tree->find_modifier('available')) {
3054 @statuses = available_statuses();
3055 } elsif (my ($filter) = $query->parse_tree->find_filter('statuses')) {
3056 @statuses = @{$filter->args} if (@{$filter->args});
3062 if (my ($filter) = $query->parse_tree->find_filter('locations')) {
3063 @location = @{$filter->args} if (@{$filter->args});
3066 # gather location_groups
3067 if (my ($filter) = $query->parse_tree->find_filter('location_groups')) {
3068 my @loc_groups = ();
3069 @loc_groups = @{$filter->args} if (@{$filter->args});
3071 # collect the mapped locations and add them to the locations() filter
3074 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3075 my $maps = $cstore->request(
3076 'open-ils.cstore.direct.asset.copy_location_group_map.search.atomic',
3077 {lgroup => \@loc_groups})->gather(1);
3079 push(@location, $_->location) for @$maps;
3084 my $param_check = $limit || $query->superpage_size || 'NULL';
3085 my $param_offset = $offset || 'NULL';
3086 my $param_limit = $core_limit || 'NULL';
3088 my $sp = $query->superpage || 1;
3090 $param_offset = ($sp - 1) * $sp_size;
3093 my $param_search_ou = $ou;
3094 my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
3095 # my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
3096 my $param_core_query = $query->parse_tree->toSQL;
3097 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
3098 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
3099 my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
3100 my $deleted_search = ($query->parse_tree->find_modifier('deleted')) ? "'t'" : "'f'";
3101 my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
3102 my $param_pref_ou = $pref_ou || 'NULL';
3104 # my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3105 # SELECT * -- bib search: $args{query}
3106 # FROM search.query_parser_fts(
3107 # $param_search_ou\:\:INT,
3108 # $param_depth\:\:INT,
3109 # $param_core_query\:\:TEXT,
3110 # $param_statuses\:\:INT[],
3111 # $param_locations\:\:INT[],
3112 # $param_offset\:\:INT,
3113 # $param_check\:\:INT,
3114 # $param_limit\:\:INT,
3115 # $metarecord\:\:BOOL,
3117 # $deleted_search\:\:BOOL,
3118 # $param_pref_ou\:\:INT
3122 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3123 -- bib search: $args{query}
3129 my $recs = $sth->fetchall_arrayref({});
3130 my $summary_row = pop @$recs;
3132 my $total = $$summary_row{total};
3133 my $checked = $$summary_row{checked};
3134 my $visible = $$summary_row{visible};
3135 my $deleted = $$summary_row{deleted};
3136 my $excluded = $$summary_row{excluded};
3138 delete $$summary_row{id};
3139 delete $$summary_row{rel};
3140 delete $$summary_row{record};
3141 delete $$summary_row{badges};
3142 delete $$summary_row{popularity};
3144 if (defined($simple_plan)) {
3145 $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3147 $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3150 if ($args{return_query}) {
3151 $$summary_row{query_struct} = $query->parse_tree->to_abstract_query();
3152 $$summary_row{canonicalized_query} = QueryParser::Canonicalize::abstract_query2str_impl($$summary_row{query_struct}, 0, $parser);
3155 $client->respond( $summary_row );
3157 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $visible.",DEBUG);
3159 for my $rec (@$recs) {
3160 delete $$rec{checked};
3161 delete $$rec{visible};
3162 delete $$rec{excluded};
3163 delete $$rec{deleted};
3164 delete $$rec{total};
3165 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3167 $client->respond( $rec );
3171 __PACKAGE__->register_method(
3172 api_name => "open-ils.storage.query_parser_search",
3174 method => 'query_parser_fts',
3182 sub query_parser_fts_wrapper {
3187 $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3188 # grab the query parser and initialize it
3189 my $parser = $OpenILS::Application::Storage::QParser;
3192 _initialize_parser($parser) unless $parser->initialization_complete;
3194 $args{searches} ||= {};
3195 if (!scalar(keys(%{$args{searches}})) && !$args{query}) {
3196 die "No search arguments were passed to ".$self->api_name;
3199 $top_org ||= actor::org_unit->search( { parent_ou => undef } )->next;
3201 my $base_query = $args{query} || '';
3202 if (scalar(keys(%{$args{searches}}))) {
3203 $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3204 for my $sclass ( keys %{$args{searches}} ) {
3205 $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3206 $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3210 my $query = $base_query;
3211 $log->debug("Full base query: $base_query", DEBUG);
3213 $query = "$args{facets} $query" if ($args{facets});
3215 if (!$locale_map{COMPLETE}) {
3217 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3218 for my $locale ( @locales ) {
3219 $locale_map{lc($locale->code)} = $locale->marc_code;
3221 $locale_map{COMPLETE} = 1;
3225 my $base_plan = $parser->new( query => $base_query )->parse;
3227 $query = "preferred_language($args{preferred_language}) $query"
3228 if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3229 $query = "preferred_language_weight($args{preferred_language_weight}) $query"
3230 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'));
3234 if (!$base_plan->parse_tree->find_filter('badge_orgs')) {
3235 # supply a suitable badge_orgs filter unless user has
3236 # explicitly supplied one
3239 my @lg_id_list = (); # We must define the variable with a static value
3240 # because an idomatic my+set causes the previous
3241 # value is remembered via closure.
3243 @lg_id_list = @{$args{location_groups}} if (ref $args{location_groups});
3245 my ($lg_filter) = $base_plan->parse_tree->find_filter('location_groups');
3246 @lg_id_list = @{$lg_filter->args} if ($lg_filter && @{$lg_filter->args});
3250 for my $lg ( grep { /^\d+$/ } @lg_id_list ) {
3251 my $lg_obj = asset::copy_location_group->retrieve($lg);
3252 next unless $lg_obj;
3254 push(@borg_list, @{$U->get_org_ancestors(''.$lg_obj->owner)});
3256 $borgs = join(',', uniq @borg_list) if @borg_list;
3260 my ($site_filter) = $base_plan->parse_tree->find_filter('site');
3261 if ($site_filter && @{$site_filter->args}) {
3262 $site = $top_org if ($site_filter->args->[0] eq '-');
3263 $site = $top_org if ($site_filter->args->[0] eq $top_org->shortname);
3264 $site = actor::org_unit->search( { shortname => $site_filter->args->[0] })->next unless ($site);
3265 } elsif ($args{org_unit}) {
3266 $site = $top_org if ($args{org_unit} eq '-');
3267 $site = $top_org if ($args{org_unit} eq $top_org->shortname);
3268 $site = actor::org_unit->search( { shortname => $args{org_unit} })->next unless ($site);
3274 $borgs = $U->get_org_ancestors($site->id);
3275 $borgs = @$borgs ? join(',', @$borgs) : undef;
3280 # gather the limit or default to 10
3281 my $limit = delete($args{check_limit}) || $base_plan->superpage_size;
3282 if (my ($filter) = $base_plan->parse_tree->find_filter('limit')) {
3283 $limit = $filter->args->[0] if (@{$filter->args});
3285 if (my ($filter) = $base_plan->parse_tree->find_filter('check_limit')) {
3286 $limit = $filter->args->[0] if (@{$filter->args});
3289 # gather the offset or default to 0
3290 my $offset = delete($args{skip_check}) || delete($args{offset}) || 0;
3291 if (my ($filter) = $base_plan->parse_tree->find_filter('offset')) {
3292 $offset = $filter->args->[0] if (@{$filter->args});
3294 if (my ($filter) = $base_plan->parse_tree->find_filter('skip_check')) {
3295 $offset = $filter->args->[0] if (@{$filter->args});
3299 $query = "check_limit($limit) $query" if (defined $limit);
3300 $query = "skip_check($offset) $query" if (defined $offset);
3301 $query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
3302 $query = "badge_orgs($borgs) $query" if ($borgs);
3304 # XXX All of the following, down to the 'return' is basically dead code. someone higher up should handle it
3305 $query = "site($args{org_unit}) $query" if ($args{org_unit});
3306 $query = "depth($args{depth}) $query" if (defined($args{depth}));
3307 $query = "sort($args{sort}) $query" if ($args{sort});
3308 $query = "core_limit($args{core_limit}) $query" if ($args{core_limit});
3309 # $query = "limit($args{limit}) $query" if ($args{limit});
3310 # $query = "skip_check($args{skip_check}) $query" if ($args{skip_check});
3311 $query = "superpage($args{superpage}) $query" if ($args{superpage});
3312 $query = "offset($args{offset}) $query" if ($args{offset});
3313 $query = "#metarecord $query" if ($self->api_name =~ /metabib/);
3314 $query = "from_metarecord($args{from_metarecord}) $query" if ($args{from_metarecord});
3315 $query = "#available $query" if ($args{available});
3316 $query = "#descending $query" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3317 $query = "#staff $query" if ($self->api_name =~ /staff/);
3318 $query = "before($args{before}) $query" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3319 $query = "after($args{after}) $query" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3320 $query = "during($args{during}) $query" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3321 $query = "between($args{between}[0],$args{between}[1]) $query"
3322 if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3325 my (@between,@statuses,@locations,@location_groups,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3327 # XXX legacy format and item type support
3328 if ($args{format}) {
3329 my ($t, $f) = split '-', $args{format};
3330 $args{item_type} = [ split '', $t ];
3331 $args{item_form} = [ split '', $f ];
3334 for my $filter ( qw/locations location_groups statuses audience language lit_form item_form item_type bib_level vr_format badges/ ) {
3335 if (my $s = $args{$filter}) {
3336 $s = [$s] if (!ref($s));
3338 my @filter_list = @$s;
3340 next if (@filter_list == 0);
3342 my $filter_string = join ',', @filter_list;
3343 $query = "$query $filter($filter_string)";
3347 $log->debug("Full QueryParser query: $query", DEBUG);
3349 return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan, return_query => $args{return_query} );
3351 __PACKAGE__->register_method(
3352 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
3354 method => 'query_parser_fts_wrapper',
3359 __PACKAGE__->register_method(
3360 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3362 method => 'query_parser_fts_wrapper',
3367 __PACKAGE__->register_method(
3368 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
3370 method => 'query_parser_fts_wrapper',
3375 __PACKAGE__->register_method(
3376 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3378 method => 'query_parser_fts_wrapper',