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;
12 use Digest::MD5 qw/md5_hex/;
14 use OpenILS::Application::Storage::QueryParser;
16 my $U = 'OpenILS::Application::AppUtils';
18 my $log = 'OpenSRF::Utils::Logger';
22 sub _initialize_parser {
25 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
27 config_record_attr_index_norm_map =>
29 'open-ils.cstore.direct.config.record_attr_index_norm_map.search.atomic',
30 { id => { "!=" => undef } },
31 { flesh => 1, flesh_fields => { crainm => [qw/norm/] }, order_by => [{ class => "crainm", field => "pos" }] }
33 search_relevance_adjustment =>
35 'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
36 { id => { "!=" => undef } }
38 config_metabib_field =>
40 'open-ils.cstore.direct.config.metabib_field.search.atomic',
41 { id => { "!=" => undef } }
43 config_metabib_search_alias =>
45 'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
46 { alias => { "!=" => undef } }
48 config_metabib_field_index_norm_map =>
50 'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
51 { id => { "!=" => undef } },
52 { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
54 config_record_attr_definition =>
56 'open-ils.cstore.direct.config.record_attr_definition.search.atomic',
57 { name => { "!=" => undef } }
59 config_metabib_class_ts_map =>
61 'open-ils.cstore.direct.config.metabib_class_ts_map.search.atomic',
64 config_metabib_field_ts_map =>
66 'open-ils.cstore.direct.config.metabib_field_ts_map.search.atomic',
69 config_metabib_class =>
71 'open-ils.cstore.direct.config.metabib_class.search.atomic',
72 { name => { "!=" => undef } }
77 my $cgf = $cstore->request(
78 'open-ils.cstore.direct.config.global_flag.retrieve',
79 'search.max_popularity_importance_multiplier'
81 $max_mult = $cgf->value if $cgf && $U->is_true($cgf->enabled);
83 $max_mult = 2.0 unless $max_mult =~ /^-?(?:\d+\.?|\.\d)\d*\z/; # just in case
84 $parser->max_popularity_importance_multiplier($max_mult);
87 die("Cannot initialize $parser!") unless ($parser->initialization_complete);
90 sub ordered_records_from_metarecord { # XXX Replace with QP-based search-within-MR
94 my $formats = shift; # dead
98 my $copies_visible = 'LEFT JOIN asset.copy_attr_vis_cache vc ON (br.id = vc.record '.
99 'AND vc.vis_attr_vector @@ (SELECT c_attrs::query_int FROM asset.patron_default_visibility_mask() LIMIT 1))';
100 $copies_visible = '' if ($self->api_name =~ /staff/o);
102 my $copies_visible_count = ',COUNT(vc.id)';
103 $copies_visible_count = '' if ($self->api_name =~ /staff/o);
105 my $descendants = '';
107 $descendants = defined($depth) ?
108 ",actor.org_unit_descendants($org, $depth) d" :
109 ",actor.org_unit_descendants($org) d" ;
116 $copies_visible_count
117 FROM metabib.metarecord_source_map sm
118 JOIN biblio.record_entry br ON (sm.source = br.id AND NOT br.deleted)
119 LEFT JOIN metabib.record_sorter s ON (s.source = br.id AND s.attr = 'titlesort')
120 LEFT JOIN config.bib_source bs ON (br.source = bs.id)
123 WHERE sm.metarecord = ?
127 if ($copies_visible) {
128 $sql .= 'AND (bs.transcendant OR ';
130 $sql .= 'vc.circ_lib = d.id)';
132 $sql .= 'vc.id IS NOT NULL)'
134 $having = 'HAVING COUNT(vc.id) > 0';
142 s.value ASC NULLS LAST
145 my $ids = metabib::metarecord_source_map->db_Main->selectcol_arrayref($sql, {}, "$mr");
146 return $ids if ($self->api_name =~ /atomic$/o);
148 $client->respond( $_ ) for ( @$ids );
152 __PACKAGE__->register_method(
153 api_name => 'open-ils.storage.ordered.metabib.metarecord.records',
155 method => 'ordered_records_from_metarecord',
159 __PACKAGE__->register_method(
160 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff',
162 method => 'ordered_records_from_metarecord',
167 __PACKAGE__->register_method(
168 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.atomic',
170 method => 'ordered_records_from_metarecord',
174 __PACKAGE__->register_method(
175 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic',
177 method => 'ordered_records_from_metarecord',
182 # XXX: this subroutine and its two registered methods are marked for
183 # deprecation, as they do not work properly in 2.x (these tags are no longer
184 # normalized in mfr) and are not in known use
188 my $isxn = lc(shift());
192 $isxn =~ s/-//o if ($self->api_name =~ /isbn/o);
194 my $tag = ($self->api_name =~ /isbn/o) ? "'020' OR f.tag = '024'" : "'022'";
196 my $fr_table = metabib::full_rec->table;
197 my $bib_table = biblio::record_entry->table;
200 SELECT DISTINCT f.record
202 JOIN $bib_table b ON (b.id = f.record)
205 AND b.deleted IS FALSE
208 my $list = metabib::full_rec->db_Main->selectcol_arrayref($sql, {}, "$isxn%");
209 $client->respond($_) for (@$list);
212 __PACKAGE__->register_method(
213 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
215 method => 'isxn_search',
219 __PACKAGE__->register_method(
220 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.issn',
222 method => 'isxn_search',
227 sub metarecord_copy_count {
233 my $sm_table = metabib::metarecord_source_map->table;
234 my $rd_table = metabib::record_descriptor->table;
235 my $cn_table = asset::call_number->table;
236 my $cp_table = asset::copy->table;
237 my $br_table = biblio::record_entry->table;
238 my $src_table = config::bib_source->table;
239 my $cl_table = asset::copy_location->table;
240 my $cs_table = config::copy_status->table;
241 my $out_table = actor::org_unit_type->table;
243 my $descendants = "actor.org_unit_descendants(u.id)";
244 my $ancestors = "actor.org_unit_ancestors(?) u JOIN $out_table t ON (u.ou_type = t.id)";
246 if ($args{org_unit} < 0) {
247 $args{org_unit} *= -1;
248 $ancestors = "(select org_unit as id from actor.org_lasso_map where lasso = ?) u CROSS JOIN (SELECT -1 AS depth) t";
251 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';
252 $copies_visible = '' if ($self->api_name =~ /staff/o);
254 my (@types,@forms,@blvl);
255 my ($t_filter, $f_filter, $b_filter) = ('','','');
258 my ($t, $f, $b) = split '-', $args{format};
259 @types = split '', $t;
260 @forms = split '', $f;
261 @blvl = split '', $b;
264 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
268 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
272 $b_filter .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
282 JOIN $cn_table cn ON (cn.record = r.source)
283 JOIN $rd_table rd ON (cn.record = rd.record)
284 JOIN $cp_table cp ON (cn.id = cp.call_number)
285 JOIN $cs_table cs ON (cp.status = cs.id)
286 JOIN $cl_table cl ON (cp.location = cl.id)
287 JOIN $descendants a ON (cp.circ_lib = a.id)
288 WHERE r.metarecord = ?
289 AND cn.deleted IS FALSE
290 AND cp.deleted IS FALSE
300 JOIN $cn_table cn ON (cn.record = r.source)
301 JOIN $rd_table rd ON (cn.record = rd.record)
302 JOIN $cp_table cp ON (cn.id = cp.call_number)
303 JOIN $cs_table cs ON (cp.status = cs.id)
304 JOIN $cl_table cl ON (cp.location = cl.id)
305 JOIN $descendants a ON (cp.circ_lib = a.id)
306 WHERE r.metarecord = ?
307 AND cp.status IN (0,7,12)
308 AND cn.deleted IS FALSE
309 AND cp.deleted IS FALSE
319 JOIN $cn_table cn ON (cn.record = r.source)
320 JOIN $rd_table rd ON (cn.record = rd.record)
321 JOIN $cp_table cp ON (cn.id = cp.call_number)
322 JOIN $cs_table cs ON (cp.status = cs.id)
323 JOIN $cl_table cl ON (cp.location = cl.id)
324 WHERE r.metarecord = ?
325 AND cn.deleted IS FALSE
326 AND cp.deleted IS FALSE
327 AND cp.opac_visible IS TRUE
328 AND cs.opac_visible IS TRUE
329 AND cl.opac_visible IS TRUE
338 JOIN $br_table br ON (br.id = r.source)
339 JOIN $src_table src ON (src.id = br.source)
340 WHERE r.metarecord = ?
341 AND src.transcendant IS TRUE
349 my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
350 $sth->execute( ''.$args{metarecord},
354 ''.$args{metarecord},
358 ''.$args{metarecord},
362 ''.$args{metarecord},
366 while ( my $row = $sth->fetchrow_hashref ) {
367 $client->respond( $row );
371 __PACKAGE__->register_method(
372 api_name => 'open-ils.storage.metabib.metarecord.copy_count',
374 method => 'metarecord_copy_count',
379 __PACKAGE__->register_method(
380 api_name => 'open-ils.storage.metabib.metarecord.copy_count.staff',
382 method => 'metarecord_copy_count',
388 sub biblio_multi_search_full_rec {
393 my $class_join = $args{class_join} || 'AND';
394 my $limit = $args{limit} || 100;
395 my $offset = $args{offset} || 0;
396 my $sort = $args{'sort'};
397 my $sort_dir = $args{sort_dir} || 'DESC';
402 for my $arg (@{ $args{searches} }) {
403 my $term = $$arg{term};
404 my $limiters = $$arg{restrict};
406 my ($index_col) = metabib::full_rec->columns('FTS');
407 $index_col ||= 'value';
408 my $search_table = metabib::full_rec->table;
410 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
412 my $fts_where = $fts->sql_where_clause();
413 my @fts_ranks = $fts->fts_rank;
415 my $rank = join(' + ', @fts_ranks);
418 for my $limit (@$limiters) {
419 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
420 # MARC control field; mfr.subfield is NULL
421 push @wheres, "( tag = ? AND $fts_where )";
422 push @binds, $$limit{tag};
423 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
425 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
426 push @binds, $$limit{tag}, $$limit{subfield};
427 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
430 my $where = join(' OR ', @wheres);
432 push @selects, "SELECT record, AVG($rank) as sum FROM $search_table WHERE $where GROUP BY record";
436 my $descendants = defined($args{depth}) ?
437 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
438 "actor.org_unit_descendants($args{org_unit})" ;
441 my $metabib_record_descriptor = metabib::record_descriptor->table;
442 my $metabib_full_rec = metabib::full_rec->table;
443 my $asset_call_number_table = asset::call_number->table;
444 my $asset_copy_table = asset::copy->table;
445 my $cs_table = config::copy_status->table;
446 my $cl_table = asset::copy_location->table;
447 my $br_table = biblio::record_entry->table;
449 my $cj = 'HAVING COUNT(x.record) = ' . scalar(@selects) if ($class_join eq 'AND');
451 '(SELECT x.record, sum(x.sum) FROM (('.
452 join(') UNION ALL (', @selects).
453 ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
455 my $has_vols = 'AND cn.owning_lib = d.id';
456 my $has_copies = 'AND cp.call_number = cn.id';
457 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';
459 if ($self->api_name =~ /staff/o) {
460 $copies_visible = '';
461 $has_copies = '' if ($ou_type == 0);
462 $has_vols = '' if ($ou_type == 0);
465 my ($t_filter, $f_filter) = ('','');
466 my ($a_filter, $l_filter, $lf_filter) = ('','','');
469 if (my $a = $args{audience}) {
470 $a = [$a] if (!ref($a));
473 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
478 if (my $l = $args{language}) {
479 $l = [$l] if (!ref($l));
482 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
487 if (my $f = $args{lit_form}) {
488 $f = [$f] if (!ref($f));
491 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
492 push @binds, @lit_form;
496 if (my $f = $args{item_form}) {
497 $f = [$f] if (!ref($f));
500 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
505 if (my $t = $args{item_type}) {
506 $t = [$t] if (!ref($t));
509 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
516 my ($t, $f) = split '-', $args{format};
517 my @types = split '', $t;
518 my @forms = split '', $f;
520 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
525 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
528 push @binds, @types, @forms;
531 my $relevance = 'sum(f.sum)';
532 $relevance = 1 if (!$copies_visible);
534 my $string_default_sort = 'zzzz';
535 $string_default_sort = 'AAAA' if ($sort_dir =~ /^DESC$/i);
537 my $number_default_sort = '9999';
538 $number_default_sort = '0000' if ($sort_dir =~/^DESC$/i);
540 my $rank = $relevance;
541 if (lc($sort) eq 'pubdate') {
544 SELECT COALESCE(SUBSTRING(MAX(frp.value) FROM E'\\\\d{4}'), '$number_default_sort')::INT
545 FROM $metabib_full_rec frp
546 WHERE frp.record = f.record
548 AND frp.subfield = 'c'
552 } elsif (lc($sort) eq 'create_date') {
554 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
556 } elsif (lc($sort) eq 'edit_date') {
558 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
560 } elsif (lc($sort) =~ /^title/i) {
563 SELECT COALESCE(LTRIM(SUBSTR(MAX(frt.value), COALESCE(SUBSTRING(MAX(frt.ind2) FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
564 FROM $metabib_full_rec frt
565 WHERE frt.record = f.record
567 AND frt.subfield = 'a'
571 } elsif (lc($sort) =~ /^author/i) {
574 SELECT COALESCE(LTRIM(MAX(query.value)), '$string_default_sort')
577 FROM $metabib_full_rec fra
578 WHERE fra.record = f.record
579 AND fra.tag LIKE '1%'
580 AND fra.subfield = 'a'
581 ORDER BY fra.tag::text::int
590 my $rd_join = $use_rd ? "$metabib_record_descriptor rd," : '';
591 my $rd_filter = $use_rd ? 'AND rd.record = f.record' : '';
593 if ($copies_visible) {
595 SELECT f.record, $relevance, count(DISTINCT cp.id), $rank
596 FROM $search_table f,
597 $asset_call_number_table cn,
598 $asset_copy_table cp,
604 WHERE br.id = f.record
605 AND cn.record = f.record
606 AND cp.status = cs.id
607 AND cp.location = cl.id
608 AND br.deleted IS FALSE
609 AND cn.deleted IS FALSE
610 AND cp.deleted IS FALSE
620 GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
621 ORDER BY 4 $sort_dir,3 DESC
625 SELECT f.record, 1, 1, $rank
626 FROM $search_table f,
629 WHERE br.id = f.record
630 AND br.deleted IS FALSE
643 $log->debug("Search SQL :: [$select]",DEBUG);
645 my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
646 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
649 $max = 1 if (!@$recs);
651 $max = $$_[1] if ($$_[1] > $max);
655 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
656 next unless ($$rec[0]);
657 my ($rid,$rank,$junk,$skip) = @$rec;
658 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
662 __PACKAGE__->register_method(
663 api_name => 'open-ils.storage.biblio.full_rec.multi_search',
665 method => 'biblio_multi_search_full_rec',
670 __PACKAGE__->register_method(
671 api_name => 'open-ils.storage.biblio.full_rec.multi_search.staff',
673 method => 'biblio_multi_search_full_rec',
679 sub search_full_rec {
685 my $term = $args{term};
686 my $limiters = $args{restrict};
688 my ($index_col) = metabib::full_rec->columns('FTS');
689 $index_col ||= 'value';
690 my $search_table = metabib::full_rec->table;
692 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
694 my $fts_where = $fts->sql_where_clause();
695 my @fts_ranks = $fts->fts_rank;
697 my $rank = join(' + ', @fts_ranks);
701 for my $limit (@$limiters) {
702 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
703 # MARC control field; mfr.subfield is NULL
704 push @wheres, "( tag = ? AND $fts_where )";
705 push @binds, $$limit{tag};
706 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
708 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
709 push @binds, $$limit{tag}, $$limit{subfield};
710 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
713 my $where = join(' OR ', @wheres);
715 my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
717 $log->debug("Search SQL :: [$select]",DEBUG);
719 my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
720 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
722 $client->respond($_) for (@$recs);
725 __PACKAGE__->register_method(
726 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
728 method => 'search_full_rec',
733 __PACKAGE__->register_method(
734 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
736 method => 'search_full_rec',
743 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
744 sub search_class_fts {
749 my $term = $args{term};
750 my $ou = $args{org_unit};
751 my $ou_type = $args{depth};
752 my $limit = $args{limit};
753 my $offset = $args{offset};
755 my $limit_clause = '';
756 my $offset_clause = '';
758 $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
759 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
762 my ($t_filter, $f_filter) = ('','');
765 my ($t, $f) = split '-', $args{format};
766 @types = split '', $t;
767 @forms = split '', $f;
769 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
773 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
779 my $descendants = defined($ou_type) ?
780 "actor.org_unit_descendants($ou, $ou_type)" :
781 "actor.org_unit_descendants($ou)";
783 my $class = $self->{cdbi};
784 my $search_table = $class->table;
786 my $metabib_record_descriptor = metabib::record_descriptor->table;
787 my $metabib_metarecord = metabib::metarecord->table;
788 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
789 my $asset_call_number_table = asset::call_number->table;
790 my $asset_copy_table = asset::copy->table;
791 my $cs_table = config::copy_status->table;
792 my $cl_table = asset::copy_location->table;
794 my ($index_col) = $class->columns('FTS');
795 $index_col ||= 'value';
797 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
798 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
800 my $fts_where = $fts->sql_where_clause;
801 my @fts_ranks = $fts->fts_rank;
803 my $rank = join(' + ', @fts_ranks);
805 my $has_vols = 'AND cn.owning_lib = d.id';
806 my $has_copies = 'AND cp.call_number = cn.id';
807 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';
809 my $visible_count = ', count(DISTINCT cp.id)';
810 my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
812 if ($self->api_name =~ /staff/o) {
813 $copies_visible = '';
814 $visible_count_test = '';
815 $has_copies = '' if ($ou_type == 0);
816 $has_vols = '' if ($ou_type == 0);
819 my $rank_calc = <<" RANK";
821 * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
822 * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
823 * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
824 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
827 $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
829 if ($copies_visible) {
831 SELECT m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
832 FROM $search_table f,
833 $metabib_metarecord_source_map_table m,
834 $asset_call_number_table cn,
835 $asset_copy_table cp,
838 $metabib_record_descriptor rd,
841 AND m.source = f.source
842 AND cn.record = m.source
843 AND rd.record = m.source
844 AND cp.status = cs.id
845 AND cp.location = cl.id
851 GROUP BY 1 $visible_count_test
853 $limit_clause $offset_clause
857 SELECT m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
858 FROM $search_table f,
859 $metabib_metarecord_source_map_table m,
860 $metabib_record_descriptor rd
862 AND m.source = f.source
863 AND rd.record = m.source
868 $limit_clause $offset_clause
872 $log->debug("Field Search SQL :: [$select]",DEBUG);
874 my $SQLstring = join('%',$fts->words);
875 my $REstring = join('\\s+',$fts->words);
876 my $first_word = ($fts->words)[0].'%';
877 my $recs = ($self->api_name =~ /unordered/o) ?
878 $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
879 $class->db_Main->selectall_arrayref($select, {},
880 '%'.lc($SQLstring).'%', # phrase order match
881 lc($first_word), # first word match
882 '^\\s*'.lc($REstring).'\\s*/?\s*$', # full exact match
886 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
888 $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
892 for my $class ( qw/title author subject keyword series identifier/ ) {
893 __PACKAGE__->register_method(
894 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord",
896 method => 'search_class_fts',
899 cdbi => "metabib::${class}_field_entry",
902 __PACKAGE__->register_method(
903 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
905 method => 'search_class_fts',
908 cdbi => "metabib::${class}_field_entry",
911 __PACKAGE__->register_method(
912 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
914 method => 'search_class_fts',
917 cdbi => "metabib::${class}_field_entry",
920 __PACKAGE__->register_method(
921 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
923 method => 'search_class_fts',
926 cdbi => "metabib::${class}_field_entry",
931 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
932 sub search_class_fts_count {
937 my $term = $args{term};
938 my $ou = $args{org_unit};
939 my $ou_type = $args{depth};
940 my $limit = $args{limit} || 100;
941 my $offset = $args{offset} || 0;
943 my $descendants = defined($ou_type) ?
944 "actor.org_unit_descendants($ou, $ou_type)" :
945 "actor.org_unit_descendants($ou)";
948 my ($t_filter, $f_filter) = ('','');
951 my ($t, $f) = split '-', $args{format};
952 @types = split '', $t;
953 @forms = split '', $f;
955 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
959 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
964 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
966 my $class = $self->{cdbi};
967 my $search_table = $class->table;
969 my $metabib_record_descriptor = metabib::record_descriptor->table;
970 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
971 my $asset_call_number_table = asset::call_number->table;
972 my $asset_copy_table = asset::copy->table;
973 my $cs_table = config::copy_status->table;
974 my $cl_table = asset::copy_location->table;
976 my ($index_col) = $class->columns('FTS');
977 $index_col ||= 'value';
979 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
981 my $fts_where = $fts->sql_where_clause;
983 my $has_vols = 'AND cn.owning_lib = d.id';
984 my $has_copies = 'AND cp.call_number = cn.id';
985 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';
986 if ($self->api_name =~ /staff/o) {
987 $copies_visible = '';
988 $has_vols = '' if ($ou_type == 0);
989 $has_copies = '' if ($ou_type == 0);
992 # XXX test an "EXISTS version of descendant checking...
994 if ($copies_visible) {
996 SELECT count(distinct m.metarecord)
997 FROM $search_table f,
998 $metabib_metarecord_source_map_table m,
999 $metabib_metarecord_source_map_table mr,
1000 $asset_call_number_table cn,
1001 $asset_copy_table cp,
1004 $metabib_record_descriptor rd,
1007 AND mr.source = f.source
1008 AND mr.metarecord = m.metarecord
1009 AND cn.record = m.source
1010 AND rd.record = m.source
1011 AND cp.status = cs.id
1012 AND cp.location = cl.id
1021 SELECT count(distinct m.metarecord)
1022 FROM $search_table f,
1023 $metabib_metarecord_source_map_table m,
1024 $metabib_metarecord_source_map_table mr,
1025 $metabib_record_descriptor rd
1027 AND mr.source = f.source
1028 AND mr.metarecord = m.metarecord
1029 AND rd.record = m.source
1035 $log->debug("Field Search Count SQL :: [$select]",DEBUG);
1037 my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1039 $log->debug("Count Search yielded $recs results.",DEBUG);
1044 for my $class ( qw/title author subject keyword series identifier/ ) {
1045 __PACKAGE__->register_method(
1046 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
1048 method => 'search_class_fts_count',
1051 cdbi => "metabib::${class}_field_entry",
1054 __PACKAGE__->register_method(
1055 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
1057 method => 'search_class_fts_count',
1060 cdbi => "metabib::${class}_field_entry",
1066 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1067 sub postfilter_search_class_fts {
1072 my $term = $args{term};
1073 my $sort = $args{'sort'};
1074 my $sort_dir = $args{sort_dir} || 'DESC';
1075 my $ou = $args{org_unit};
1076 my $ou_type = $args{depth};
1077 my $limit = $args{limit} || 10;
1078 my $visibility_limit = $args{visibility_limit} || 5000;
1079 my $offset = $args{offset} || 0;
1081 my $outer_limit = 1000;
1083 my $limit_clause = '';
1084 my $offset_clause = '';
1086 $limit_clause = "LIMIT $outer_limit";
1087 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1089 my (@types,@forms,@lang,@aud,@lit_form);
1090 my ($t_filter, $f_filter) = ('','');
1091 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1092 my ($ot_filter, $of_filter) = ('','');
1093 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1095 if (my $a = $args{audience}) {
1096 $a = [$a] if (!ref($a));
1099 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1100 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1103 if (my $l = $args{language}) {
1104 $l = [$l] if (!ref($l));
1107 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1108 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1111 if (my $f = $args{lit_form}) {
1112 $f = [$f] if (!ref($f));
1115 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1116 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1119 if ($args{format}) {
1120 my ($t, $f) = split '-', $args{format};
1121 @types = split '', $t;
1122 @forms = split '', $f;
1124 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1125 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1129 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1130 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1135 my $descendants = defined($ou_type) ?
1136 "actor.org_unit_descendants($ou, $ou_type)" :
1137 "actor.org_unit_descendants($ou)";
1139 my $class = $self->{cdbi};
1140 my $search_table = $class->table;
1142 my $metabib_full_rec = metabib::full_rec->table;
1143 my $metabib_record_descriptor = metabib::record_descriptor->table;
1144 my $metabib_metarecord = metabib::metarecord->table;
1145 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1146 my $asset_call_number_table = asset::call_number->table;
1147 my $asset_copy_table = asset::copy->table;
1148 my $cs_table = config::copy_status->table;
1149 my $cl_table = asset::copy_location->table;
1150 my $br_table = biblio::record_entry->table;
1152 my ($index_col) = $class->columns('FTS');
1153 $index_col ||= 'value';
1155 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1157 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1159 my $SQLstring = join('%',map { lc($_) } $fts->words);
1160 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1161 my $first_word = lc(($fts->words)[0]).'%';
1163 my $fts_where = $fts->sql_where_clause;
1164 my @fts_ranks = $fts->fts_rank;
1167 $bonus{'metabib::identifier_field_entry'} =
1168 $bonus{'metabib::keyword_field_entry'} = [
1169 { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring }
1172 $bonus{'metabib::title_field_entry'} =
1173 $bonus{'metabib::series_field_entry'} = [
1174 { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1175 { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1176 @{ $bonus{'metabib::keyword_field_entry'} }
1179 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1180 $bonus_list ||= '1';
1182 my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1184 my $relevance = join(' + ', @fts_ranks);
1185 $relevance = <<" RANK";
1186 (SUM( ( $relevance ) * ( $bonus_list ) )/COUNT(m.source))
1189 my $string_default_sort = 'zzzz';
1190 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1192 my $number_default_sort = '9999';
1193 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1195 my $rank = $relevance;
1196 if (lc($sort) eq 'pubdate') {
1199 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1200 FROM $metabib_full_rec frp
1201 WHERE frp.record = mr.master_record
1203 AND frp.subfield = 'c'
1207 } elsif (lc($sort) eq 'create_date') {
1209 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1211 } elsif (lc($sort) eq 'edit_date') {
1213 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1215 } elsif (lc($sort) eq 'title') {
1218 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1219 FROM $metabib_full_rec frt
1220 WHERE frt.record = mr.master_record
1222 AND frt.subfield = 'a'
1226 } elsif (lc($sort) eq 'author') {
1229 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1230 FROM $metabib_full_rec fra
1231 WHERE fra.record = mr.master_record
1232 AND fra.tag LIKE '1%'
1233 AND fra.subfield = 'a'
1234 ORDER BY fra.tag::text::int
1242 my $select = <<" SQL";
1243 SELECT m.metarecord,
1245 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1247 FROM $search_table f,
1248 $metabib_metarecord_source_map_table m,
1249 $metabib_metarecord_source_map_table smrs,
1250 $metabib_metarecord mr,
1251 $metabib_record_descriptor rd
1253 AND smrs.metarecord = mr.id
1254 AND m.source = f.source
1255 AND m.metarecord = mr.id
1256 AND rd.record = smrs.source
1262 GROUP BY m.metarecord
1263 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1264 LIMIT $visibility_limit
1271 FROM $asset_call_number_table cn,
1272 $metabib_metarecord_source_map_table mrs,
1273 $asset_copy_table cp,
1278 $metabib_record_descriptor ord,
1280 WHERE mrs.metarecord = s.metarecord
1281 AND br.id = mrs.source
1282 AND cn.record = mrs.source
1283 AND cp.status = cs.id
1284 AND cp.location = cl.id
1285 AND cn.owning_lib = d.id
1286 AND cp.call_number = cn.id
1287 AND cp.opac_visible IS TRUE
1288 AND cs.opac_visible IS TRUE
1289 AND cl.opac_visible IS TRUE
1290 AND d.opac_visible IS TRUE
1291 AND br.active IS TRUE
1292 AND br.deleted IS FALSE
1293 AND ord.record = mrs.source
1299 ORDER BY 4 $sort_dir
1301 } elsif ($self->api_name !~ /staff/o) {
1308 FROM $asset_call_number_table cn,
1309 $metabib_metarecord_source_map_table mrs,
1310 $asset_copy_table cp,
1315 $metabib_record_descriptor ord
1317 WHERE mrs.metarecord = s.metarecord
1318 AND br.id = mrs.source
1319 AND cn.record = mrs.source
1320 AND cp.status = cs.id
1321 AND cp.location = cl.id
1322 AND cp.circ_lib = d.id
1323 AND cp.call_number = cn.id
1324 AND cp.opac_visible IS TRUE
1325 AND cs.opac_visible IS TRUE
1326 AND cl.opac_visible IS TRUE
1327 AND d.opac_visible IS TRUE
1328 AND br.active IS TRUE
1329 AND br.deleted IS FALSE
1330 AND ord.record = mrs.source
1338 ORDER BY 4 $sort_dir
1347 FROM $asset_call_number_table cn,
1348 $asset_copy_table cp,
1349 $metabib_metarecord_source_map_table mrs,
1352 $metabib_record_descriptor ord
1354 WHERE mrs.metarecord = s.metarecord
1355 AND br.id = mrs.source
1356 AND cn.record = mrs.source
1357 AND cn.id = cp.call_number
1358 AND br.deleted IS FALSE
1359 AND cn.deleted IS FALSE
1360 AND ord.record = mrs.source
1361 AND ( cn.owning_lib = d.id
1362 OR ( cp.circ_lib = d.id
1363 AND cp.deleted IS FALSE
1375 FROM $asset_call_number_table cn,
1376 $metabib_metarecord_source_map_table mrs,
1377 $metabib_record_descriptor ord
1378 WHERE mrs.metarecord = s.metarecord
1379 AND cn.record = mrs.source
1380 AND ord.record = mrs.source
1388 ORDER BY 4 $sort_dir
1393 $log->debug("Field Search SQL :: [$select]",DEBUG);
1395 my $recs = $class->db_Main->selectall_arrayref(
1397 (@bonus_values > 0 ? @bonus_values : () ),
1398 ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1399 @types, @forms, @aud, @lang, @lit_form,
1400 @types, @forms, @aud, @lang, @lit_form,
1401 ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1403 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1406 $max = 1 if (!@$recs);
1408 $max = $$_[1] if ($$_[1] > $max);
1411 my $count = scalar(@$recs);
1412 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1413 my ($mrid,$rank,$skip) = @$rec;
1414 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1419 for my $class ( qw/title author subject keyword series identifier/ ) {
1420 __PACKAGE__->register_method(
1421 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1423 method => 'postfilter_search_class_fts',
1426 cdbi => "metabib::${class}_field_entry",
1429 __PACKAGE__->register_method(
1430 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1432 method => 'postfilter_search_class_fts',
1435 cdbi => "metabib::${class}_field_entry",
1442 my $_cdbi = { title => "metabib::title_field_entry",
1443 author => "metabib::author_field_entry",
1444 subject => "metabib::subject_field_entry",
1445 keyword => "metabib::keyword_field_entry",
1446 series => "metabib::series_field_entry",
1447 identifier => "metabib::identifier_field_entry",
1450 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1451 sub postfilter_search_multi_class_fts {
1456 my $sort = $args{'sort'};
1457 my $sort_dir = $args{sort_dir} || 'DESC';
1458 my $ou = $args{org_unit};
1459 my $ou_type = $args{depth};
1460 my $limit = $args{limit} || 10;
1461 my $offset = $args{offset} || 0;
1462 my $visibility_limit = $args{visibility_limit} || 5000;
1465 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1468 if (!defined($args{org_unit})) {
1469 die "No target organizational unit passed to ".$self->api_name;
1472 if (! scalar( keys %{$args{searches}} )) {
1473 die "No search arguments were passed to ".$self->api_name;
1476 my $outer_limit = 1000;
1478 my $limit_clause = '';
1479 my $offset_clause = '';
1481 $limit_clause = "LIMIT $outer_limit";
1482 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1484 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1485 my ($t_filter, $f_filter, $v_filter) = ('','','');
1486 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1487 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1488 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1490 if ($args{available}) {
1491 $avail_filter = ' AND cp.status IN (0,7,12)';
1494 if (my $a = $args{audience}) {
1495 $a = [$a] if (!ref($a));
1498 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1499 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1502 if (my $l = $args{language}) {
1503 $l = [$l] if (!ref($l));
1506 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1507 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1510 if (my $f = $args{lit_form}) {
1511 $f = [$f] if (!ref($f));
1514 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1515 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1518 if (my $f = $args{item_form}) {
1519 $f = [$f] if (!ref($f));
1522 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1523 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1526 if (my $t = $args{item_type}) {
1527 $t = [$t] if (!ref($t));
1530 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1531 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1534 if (my $v = $args{vr_format}) {
1535 $v = [$v] if (!ref($v));
1538 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1539 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1543 # XXX legacy format and item type support
1544 if ($args{format}) {
1545 my ($t, $f) = split '-', $args{format};
1546 @types = split '', $t;
1547 @forms = split '', $f;
1549 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1550 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1554 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1555 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1561 my $descendants = defined($ou_type) ?
1562 "actor.org_unit_descendants($ou, $ou_type)" :
1563 "actor.org_unit_descendants($ou)";
1565 my $search_table_list = '';
1567 my $join_table_list = '';
1570 my $field_table = config::metabib_field->table;
1574 my $prev_search_group;
1575 my $curr_search_group;
1579 for my $search_group (sort keys %{$args{searches}}) {
1580 (my $search_group_name = $search_group) =~ s/\|/_/gso;
1581 ($search_class,$search_field) = split /\|/, $search_group;
1582 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1584 if ($search_field) {
1585 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1586 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1591 $prev_search_group = $curr_search_group if ($curr_search_group);
1593 $curr_search_group = $search_group_name;
1595 my $class = $_cdbi->{$search_class};
1596 my $search_table = $class->table;
1598 my ($index_col) = $class->columns('FTS');
1599 $index_col ||= 'value';
1602 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1604 my $fts_where = $fts->sql_where_clause;
1605 my @fts_ranks = $fts->fts_rank;
1607 my $SQLstring = join('%',map { lc($_) } $fts->words);
1608 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1609 my $first_word = lc(($fts->words)[0]).'%';
1611 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1612 my $rank = join(' + ', @fts_ranks);
1615 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1616 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1618 $bonus{'series'} = [
1619 { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1620 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1623 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1625 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1626 $bonus_list ||= '1';
1628 push @bonus_lists, $bonus_list;
1629 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1632 #---------------------
1634 $search_table_list .= "$search_table $search_group_name, ";
1635 push @rank_list,$rank;
1636 $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1638 if ($metabib_field) {
1639 $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1640 $metabib_field = undef;
1643 if ($prev_search_group) {
1644 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1648 my $metabib_record_descriptor = metabib::record_descriptor->table;
1649 my $metabib_full_rec = metabib::full_rec->table;
1650 my $metabib_metarecord = metabib::metarecord->table;
1651 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1652 my $asset_call_number_table = asset::call_number->table;
1653 my $asset_copy_table = asset::copy->table;
1654 my $cs_table = config::copy_status->table;
1655 my $cl_table = asset::copy_location->table;
1656 my $br_table = biblio::record_entry->table;
1657 my $source_table = config::bib_source->table;
1659 my $bonuses = join (' * ', @bonus_lists);
1660 my $relevance = join (' + ', @rank_list);
1661 $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1663 my $string_default_sort = 'zzzz';
1664 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1666 my $number_default_sort = '9999';
1667 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1671 my $secondary_sort = <<" SORT";
1673 SELECT COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1674 FROM $metabib_full_rec sfrt,
1675 $metabib_metarecord mr
1676 WHERE sfrt.record = mr.master_record
1677 AND sfrt.tag = '245'
1678 AND sfrt.subfield = 'a'
1683 my $rank = $relevance;
1684 if (lc($sort) eq 'pubdate') {
1687 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1688 FROM $metabib_full_rec frp
1689 WHERE frp.record = mr.master_record
1691 AND frp.subfield = 'c'
1695 } elsif (lc($sort) eq 'create_date') {
1697 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1699 } elsif (lc($sort) eq 'edit_date') {
1701 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1703 } elsif (lc($sort) eq 'title') {
1706 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1707 FROM $metabib_full_rec frt
1708 WHERE frt.record = mr.master_record
1710 AND frt.subfield = 'a'
1714 $secondary_sort = <<" SORT";
1716 SELECT COALESCE(SUBSTRING(sfrp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1717 FROM $metabib_full_rec sfrp
1718 WHERE sfrp.record = mr.master_record
1719 AND sfrp.tag = '260'
1720 AND sfrp.subfield = 'c'
1724 } elsif (lc($sort) eq 'author') {
1727 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1728 FROM $metabib_full_rec fra
1729 WHERE fra.record = mr.master_record
1730 AND fra.tag LIKE '1%'
1731 AND fra.subfield = 'a'
1732 ORDER BY fra.tag::text::int
1737 push @bonus_values, @bonus_values;
1742 my $select = <<" SQL";
1743 SELECT m.metarecord,
1745 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1748 FROM $search_table_list
1749 $metabib_metarecord mr,
1750 $metabib_metarecord_source_map_table m,
1751 $metabib_metarecord_source_map_table smrs
1752 WHERE m.metarecord = smrs.metarecord
1753 AND mr.id = m.metarecord
1756 GROUP BY m.metarecord
1757 -- ORDER BY 4 $sort_dir
1758 LIMIT $visibility_limit
1761 if ($self->api_name !~ /staff/o) {
1768 FROM $asset_call_number_table cn,
1769 $metabib_metarecord_source_map_table mrs,
1770 $asset_copy_table cp,
1775 $metabib_record_descriptor ord
1776 WHERE mrs.metarecord = s.metarecord
1777 AND br.id = mrs.source
1778 AND cn.record = mrs.source
1779 AND cp.status = cs.id
1780 AND cp.location = cl.id
1781 AND cp.circ_lib = d.id
1782 AND cp.call_number = cn.id
1783 AND cp.opac_visible IS TRUE
1784 AND cs.opac_visible IS TRUE
1785 AND cl.opac_visible IS TRUE
1786 AND d.opac_visible IS TRUE
1787 AND br.active IS TRUE
1788 AND br.deleted IS FALSE
1789 AND cp.deleted IS FALSE
1790 AND cn.deleted IS FALSE
1791 AND ord.record = mrs.source
1804 $metabib_metarecord_source_map_table mrs,
1805 $metabib_record_descriptor ord,
1807 WHERE mrs.metarecord = s.metarecord
1808 AND ord.record = mrs.source
1809 AND br.id = mrs.source
1810 AND br.source = src.id
1811 AND src.transcendant IS TRUE
1819 ORDER BY 4 $sort_dir, 5
1826 $metabib_metarecord_source_map_table omrs,
1827 $metabib_record_descriptor ord
1828 WHERE omrs.metarecord = s.metarecord
1829 AND ord.record = omrs.source
1832 FROM $asset_call_number_table cn,
1833 $asset_copy_table cp,
1836 WHERE br.id = omrs.source
1837 AND cn.record = omrs.source
1838 AND br.deleted IS FALSE
1839 AND cn.deleted IS FALSE
1840 AND cp.call_number = cn.id
1841 AND ( cn.owning_lib = d.id
1842 OR ( cp.circ_lib = d.id
1843 AND cp.deleted IS FALSE
1851 FROM $asset_call_number_table cn
1852 WHERE cn.record = omrs.source
1853 AND cn.deleted IS FALSE
1859 $metabib_metarecord_source_map_table mrs,
1860 $metabib_record_descriptor ord,
1862 WHERE mrs.metarecord = s.metarecord
1863 AND br.id = mrs.source
1864 AND br.source = src.id
1865 AND src.transcendant IS TRUE
1881 ORDER BY 4 $sort_dir, 5
1886 $log->debug("Field Search SQL :: [$select]",DEBUG);
1888 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1891 @types, @forms, @vformats, @aud, @lang, @lit_form,
1892 @types, @forms, @vformats, @aud, @lang, @lit_form,
1893 # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1896 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1899 $max = 1 if (!@$recs);
1901 $max = $$_[1] if ($$_[1] > $max);
1904 my $count = scalar(@$recs);
1905 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1906 next unless ($$rec[0]);
1907 my ($mrid,$rank,$skip) = @$rec;
1908 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1913 __PACKAGE__->register_method(
1914 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1916 method => 'postfilter_search_multi_class_fts',
1921 __PACKAGE__->register_method(
1922 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1924 method => 'postfilter_search_multi_class_fts',
1930 __PACKAGE__->register_method(
1931 api_name => "open-ils.storage.metabib.multiclass.search_fts",
1933 method => 'postfilter_search_multi_class_fts',
1938 __PACKAGE__->register_method(
1939 api_name => "open-ils.storage.metabib.multiclass.search_fts.staff",
1941 method => 'postfilter_search_multi_class_fts',
1947 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1948 sub biblio_search_multi_class_fts {
1953 my $sort = $args{'sort'};
1954 my $sort_dir = $args{sort_dir} || 'DESC';
1955 my $ou = $args{org_unit};
1956 my $ou_type = $args{depth};
1957 my $limit = $args{limit} || 10;
1958 my $offset = $args{offset} || 0;
1959 my $pref_lang = $args{preferred_language} || 'eng';
1960 my $visibility_limit = $args{visibility_limit} || 5000;
1963 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1966 if (! scalar( keys %{$args{searches}} )) {
1967 die "No search arguments were passed to ".$self->api_name;
1970 my $outer_limit = 1000;
1972 my $limit_clause = '';
1973 my $offset_clause = '';
1975 $limit_clause = "LIMIT $outer_limit";
1976 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1978 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1979 my ($t_filter, $f_filter, $v_filter) = ('','','');
1980 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1981 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1982 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1984 if ($args{available}) {
1985 $avail_filter = ' AND cp.status IN (0,7,12)';
1988 if (my $a = $args{audience}) {
1989 $a = [$a] if (!ref($a));
1992 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1993 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1996 if (my $l = $args{language}) {
1997 $l = [$l] if (!ref($l));
2000 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
2001 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
2004 if (my $f = $args{lit_form}) {
2005 $f = [$f] if (!ref($f));
2008 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2009 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2012 if (my $f = $args{item_form}) {
2013 $f = [$f] if (!ref($f));
2016 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2017 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2020 if (my $t = $args{item_type}) {
2021 $t = [$t] if (!ref($t));
2024 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2025 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2028 if (my $v = $args{vr_format}) {
2029 $v = [$v] if (!ref($v));
2032 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
2033 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
2036 # XXX legacy format and item type support
2037 if ($args{format}) {
2038 my ($t, $f) = split '-', $args{format};
2039 @types = split '', $t;
2040 @forms = split '', $f;
2042 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2043 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2047 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2048 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2053 my $descendants = defined($ou_type) ?
2054 "actor.org_unit_descendants($ou, $ou_type)" :
2055 "actor.org_unit_descendants($ou)";
2057 my $search_table_list = '';
2059 my $join_table_list = '';
2062 my $field_table = config::metabib_field->table;
2066 my $prev_search_group;
2067 my $curr_search_group;
2071 for my $search_group (sort keys %{$args{searches}}) {
2072 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2073 ($search_class,$search_field) = split /\|/, $search_group;
2074 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2076 if ($search_field) {
2077 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2078 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2083 $prev_search_group = $curr_search_group if ($curr_search_group);
2085 $curr_search_group = $search_group_name;
2087 my $class = $_cdbi->{$search_class};
2088 my $search_table = $class->table;
2090 my ($index_col) = $class->columns('FTS');
2091 $index_col ||= 'value';
2094 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2096 my $fts_where = $fts->sql_where_clause;
2097 my @fts_ranks = $fts->fts_rank;
2099 my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2100 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2101 my $first_word = lc(($fts->words)[0]).'%';
2103 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2104 my $rank = join(' + ', @fts_ranks);
2107 $bonus{'subject'} = [];
2108 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2110 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2112 $bonus{'series'} = [
2113 { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2114 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2117 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2120 push @{ $bonus{'title'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2121 push @{ $bonus{'author'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2122 push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2123 push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2124 push @{ $bonus{'series'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2127 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2128 $bonus_list ||= '1';
2130 push @bonus_lists, $bonus_list;
2131 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2133 #---------------------
2135 $search_table_list .= "$search_table $search_group_name, ";
2136 push @rank_list,$rank;
2137 $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2139 if ($metabib_field) {
2140 $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2141 $metabib_field = undef;
2144 if ($prev_search_group) {
2145 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2149 my $metabib_record_descriptor = metabib::record_descriptor->table;
2150 my $metabib_full_rec = metabib::full_rec->table;
2151 my $metabib_metarecord = metabib::metarecord->table;
2152 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2153 my $asset_call_number_table = asset::call_number->table;
2154 my $asset_copy_table = asset::copy->table;
2155 my $cs_table = config::copy_status->table;
2156 my $cl_table = asset::copy_location->table;
2157 my $br_table = biblio::record_entry->table;
2158 my $source_table = config::bib_source->table;
2161 my $bonuses = join (' * ', @bonus_lists);
2162 my $relevance = join (' + ', @rank_list);
2163 $relevance = "AVG( ($relevance) * ($bonuses) )";
2165 my $string_default_sort = 'zzzz';
2166 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2168 my $number_default_sort = '9999';
2169 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2171 my $rank = $relevance;
2172 if (lc($sort) eq 'pubdate') {
2175 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d{4}'),'$number_default_sort')::INT
2176 FROM $metabib_full_rec frp
2177 WHERE frp.record = b.id
2179 AND frp.subfield = 'c'
2183 } elsif (lc($sort) eq 'create_date') {
2185 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2187 } elsif (lc($sort) eq 'edit_date') {
2189 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2191 } elsif (lc($sort) eq 'title') {
2194 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2195 FROM $metabib_full_rec frt
2196 WHERE frt.record = b.id
2198 AND frt.subfield = 'a'
2202 } elsif (lc($sort) eq 'author') {
2205 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
2206 FROM $metabib_full_rec fra
2207 WHERE fra.record = b.id
2208 AND fra.tag LIKE '1%'
2209 AND fra.subfield = 'a'
2210 ORDER BY fra.tag::text::int
2215 push @bonus_values, @bonus_values;
2220 my $select = <<" SQL";
2225 FROM $search_table_list
2226 $metabib_record_descriptor rd,
2229 WHERE rd.record = b.id
2230 AND b.active IS TRUE
2231 AND b.deleted IS FALSE
2240 GROUP BY b.id, b.source
2241 ORDER BY 3 $sort_dir
2242 LIMIT $visibility_limit
2245 if ($self->api_name !~ /staff/o) {
2250 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2253 FROM $asset_call_number_table cn,
2254 $asset_copy_table cp,
2258 WHERE cn.record = s.id
2259 AND cp.status = cs.id
2260 AND cp.location = cl.id
2261 AND cp.call_number = cn.id
2262 AND cp.opac_visible IS TRUE
2263 AND cs.opac_visible IS TRUE
2264 AND cl.opac_visible IS TRUE
2265 AND d.opac_visible IS TRUE
2266 AND cp.deleted IS FALSE
2267 AND cn.deleted IS FALSE
2268 AND cp.circ_lib = d.id
2272 OR src.transcendant IS TRUE
2273 ORDER BY 3 $sort_dir
2280 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2283 FROM $asset_call_number_table cn,
2284 $asset_copy_table cp,
2286 WHERE cn.record = s.id
2287 AND cp.call_number = cn.id
2288 AND cn.deleted IS FALSE
2289 AND cp.circ_lib = d.id
2290 AND cp.deleted IS FALSE
2296 FROM $asset_call_number_table cn
2297 WHERE cn.record = s.id
2300 OR src.transcendant IS TRUE
2301 ORDER BY 3 $sort_dir
2306 $log->debug("Field Search SQL :: [$select]",DEBUG);
2308 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2310 @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2313 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2315 my $count = scalar(@$recs);
2316 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2317 next unless ($$rec[0]);
2318 my ($mrid,$rank) = @$rec;
2319 $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2324 __PACKAGE__->register_method(
2325 api_name => "open-ils.storage.biblio.multiclass.search_fts.record",
2327 method => 'biblio_search_multi_class_fts',
2332 __PACKAGE__->register_method(
2333 api_name => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2335 method => 'biblio_search_multi_class_fts',
2340 __PACKAGE__->register_method(
2341 api_name => "open-ils.storage.biblio.multiclass.search_fts",
2343 method => 'biblio_search_multi_class_fts',
2348 __PACKAGE__->register_method(
2349 api_name => "open-ils.storage.biblio.multiclass.search_fts.staff",
2351 method => 'biblio_search_multi_class_fts',
2359 my $default_preferred_language;
2360 my $default_preferred_language_weight;
2362 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2368 if (!$locale_map{COMPLETE}) {
2370 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2371 for my $locale ( @locales ) {
2372 $locale_map{lc($locale->code)} = $locale->marc_code;
2374 $locale_map{COMPLETE} = 1;
2378 my $config = OpenSRF::Utils::SettingsClient->new();
2380 if (!$default_preferred_language) {
2382 $default_preferred_language = $config->config_value(
2383 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2384 ) || $config->config_value(
2385 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2390 if (!$default_preferred_language_weight) {
2392 $default_preferred_language_weight = $config->config_value(
2393 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2394 ) || $config->config_value(
2395 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2399 # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2400 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2402 my $ou = $args{org_unit};
2403 my $limit = $args{limit} || 10;
2404 my $offset = $args{offset} || 0;
2407 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2410 if (! scalar( keys %{$args{searches}} )) {
2411 die "No search arguments were passed to ".$self->api_name;
2414 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2416 if (!defined($args{preferred_language})) {
2417 my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2418 $args{preferred_language} =
2419 $locale_map{ lc($ses_locale) } || 'eng';
2422 if (!defined($args{preferred_language_weight})) {
2423 $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2426 if ($args{available}) {
2427 @statuses = (0,7,12);
2430 if (my $s = $args{locations}) {
2431 $s = [$s] if (!ref($s));
2435 if (my $b = $args{between}) {
2436 if (ref($b) && @$b == 2) {
2441 if (my $s = $args{statuses}) {
2442 $s = [$s] if (!ref($s));
2446 if (my $a = $args{audience}) {
2447 $a = [$a] if (!ref($a));
2451 if (my $l = $args{language}) {
2452 $l = [$l] if (!ref($l));
2456 if (my $f = $args{lit_form}) {
2457 $f = [$f] if (!ref($f));
2461 if (my $f = $args{item_form}) {
2462 $f = [$f] if (!ref($f));
2466 if (my $t = $args{item_type}) {
2467 $t = [$t] if (!ref($t));
2471 if (my $b = $args{bib_level}) {
2472 $b = [$b] if (!ref($b));
2476 if (my $v = $args{vr_format}) {
2477 $v = [$v] if (!ref($v));
2481 # XXX legacy format and item type support
2482 if ($args{format}) {
2483 my ($t, $f) = split '-', $args{format};
2484 @types = split '', $t;
2485 @forms = split '', $f;
2488 my %stored_proc_search_args;
2489 for my $search_group (sort keys %{$args{searches}}) {
2490 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2491 my ($search_class,$search_field) = split /\|/, $search_group;
2492 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2494 if ($search_field) {
2495 unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2496 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2501 my $class = $_cdbi->{$search_class};
2502 my $search_table = $class->table;
2504 my ($index_col) = $class->columns('FTS');
2505 $index_col ||= 'value';
2508 my $fts = OpenILS::Application::Storage::FTS->compile(
2509 $search_class => $args{searches}{$search_group}{term},
2510 $search_group_name.'.value',
2511 "$search_group_name.$index_col"
2513 $fts->sql_where_clause; # this builds the ranks for us
2515 my @fts_ranks = $fts->fts_rank;
2516 my @fts_queries = $fts->fts_query;
2517 my @phrases = map { lc($_) } $fts->phrases;
2518 my @words = map { lc($_) } $fts->words;
2520 $stored_proc_search_args{$search_group} = {
2521 fts_rank => \@fts_ranks,
2522 fts_query => \@fts_queries,
2523 phrase => \@phrases,
2529 my $param_search_ou = $ou;
2530 my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2531 my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2532 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2533 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2534 my $param_audience = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud ) . '}$$';
2535 my $param_language = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang ) . '}$$';
2536 my $param_lit_form = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2537 my $param_types = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types ) . '}$$';
2538 my $param_forms = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms ) . '}$$';
2539 my $param_vformats = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2540 my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2541 my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2542 my $param_after = $args{after} ; $param_after = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2543 my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2544 my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2545 my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2546 my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2547 my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2548 my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2549 my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2550 my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2551 my $param_rel_limit = $args{core_limit}; $param_rel_limit ||= 'NULL';
2552 my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2553 my $param_skip_chk = $args{skip_check}; $param_skip_chk ||= 'NULL';
2555 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2557 FROM search.staged_fts(
2558 $param_search_ou\:\:INT,
2559 $param_depth\:\:INT,
2560 $param_searches\:\:TEXT,
2561 $param_statuses\:\:INT[],
2562 $param_locations\:\:INT[],
2563 $param_audience\:\:TEXT[],
2564 $param_language\:\:TEXT[],
2565 $param_lit_form\:\:TEXT[],
2566 $param_types\:\:TEXT[],
2567 $param_forms\:\:TEXT[],
2568 $param_vformats\:\:TEXT[],
2569 $param_bib_level\:\:TEXT[],
2570 $param_before\:\:TEXT,
2571 $param_after\:\:TEXT,
2572 $param_during\:\:TEXT,
2573 $param_between\:\:TEXT[],
2574 $param_pref_lang\:\:TEXT,
2575 $param_pref_lang_multiplier\:\:REAL,
2576 $param_sort\:\:TEXT,
2577 $param_sort_desc\:\:BOOL,
2578 $metarecord\:\:BOOL,
2580 $param_rel_limit\:\:INT,
2581 $param_chk_limit\:\:INT,
2582 $param_skip_chk\:\:INT
2588 my $recs = $sth->fetchall_arrayref({});
2589 my $summary_row = pop @$recs;
2591 my $total = $$summary_row{total};
2592 my $checked = $$summary_row{checked};
2593 my $visible = $$summary_row{visible};
2594 my $deleted = $$summary_row{deleted};
2595 my $excluded = $$summary_row{excluded};
2597 my $estimate = $visible;
2598 if ( $total > $checked && $checked ) {
2600 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2601 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2605 delete $$summary_row{id};
2606 delete $$summary_row{rel};
2607 delete $$summary_row{record};
2609 $client->respond( $summary_row );
2611 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2613 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2614 delete $$rec{checked};
2615 delete $$rec{visible};
2616 delete $$rec{excluded};
2617 delete $$rec{deleted};
2618 delete $$rec{total};
2619 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2621 $client->respond( $rec );
2625 __PACKAGE__->register_method(
2626 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
2628 method => 'staged_fts',
2633 __PACKAGE__->register_method(
2634 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2636 method => 'staged_fts',
2641 __PACKAGE__->register_method(
2642 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
2644 method => 'staged_fts',
2649 __PACKAGE__->register_method(
2650 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2652 method => 'staged_fts',
2658 sub FTS_paging_estimate {
2662 my $checked = shift;
2663 my $visible = shift;
2664 my $excluded = shift;
2665 my $deleted = shift;
2668 my $deleted_ratio = $deleted / $checked;
2669 my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2671 my $exclusion_ratio = $excluded / $checked;
2672 my $delete_adjusted_exclusion_ratio = $checked - $deleted ? $excluded / ($checked - $deleted) : 1;
2674 my $inclusion_ratio = $visible / $checked;
2675 my $delete_adjusted_inclusion_ratio = $checked - $deleted ? $visible / ($checked - $deleted) : 0;
2678 exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2679 inclusion => int($delete_adjusted_total * $inclusion_ratio),
2680 delete_adjusted_exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2681 delete_adjusted_inclusion => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2684 __PACKAGE__->register_method(
2685 api_name => "open-ils.storage.fts_paging_estimate",
2687 method => 'FTS_paging_estimate',
2693 Hash of estimation values based on four variant estimation strategies:
2694 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2695 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2696 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2697 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2700 Helper method used to determin the approximate number of
2701 hits for a search that spans multiple superpages. For
2702 sparse superpages, the inclusion estimate will likely be the
2703 best estimate. The exclusion strategy is the original, but
2704 inclusion is the default.
2707 { name => 'checked',
2708 desc => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2711 { name => 'visible',
2712 desc => 'Number of records visible to the search location on the current superpage.',
2715 { name => 'excluded',
2716 desc => 'Number of records excluded from the search location on the current superpage.',
2719 { name => 'deleted',
2720 desc => 'Number of deleted records on the current superpage.',
2724 desc => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2737 my $term = $$args{term};
2738 my $limit = $$args{max} || 1;
2739 my $min = $$args{min} || 1;
2740 my @classes = @{$$args{class}};
2742 $limit = $min if ($min > $limit);
2745 @classes = ( qw/ title author subject series keyword / );
2749 my $bre_table = biblio::record_entry->table;
2750 my $cn_table = asset::call_number->table;
2751 my $cp_table = asset::copy->table;
2753 for my $search_class ( @classes ) {
2755 my $class = $_cdbi->{$search_class};
2756 my $search_table = $class->table;
2758 my ($index_col) = $class->columns('FTS');
2759 $index_col ||= 'value';
2762 my $where = OpenILS::Application::Storage::FTS
2763 ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2767 SELECT COUNT(DISTINCT X.source)
2768 FROM (SELECT $search_class.source
2769 FROM $search_table $search_class
2770 JOIN $bre_table b ON (b.id = $search_class.source)
2775 HAVING COUNT(DISTINCT X.source) >= $min;
2778 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2779 $matches{$search_class} = $res ? $res->[0] : 0;
2784 __PACKAGE__->register_method(
2785 api_name => "open-ils.storage.search.xref",
2787 method => 'xref_count',
2791 # Takes an abstract query object and recursively turns it back into a string
2793 sub abstract_query2str {
2794 my ($self, $conn, $query) = @_;
2796 return QueryParser::Canonicalize::abstract_query2str_impl($query, 0, $OpenILS::Application::Storage::QParser);
2799 __PACKAGE__->register_method(
2800 api_name => "open-ils.storage.query_parser.abstract_query.canonicalize",
2802 method => "abstract_query2str",
2807 Abstract query parser object, with complete config data. For example input,
2808 see the 'abstract_query' part of the output of an API call like
2809 open-ils.search.biblio.multiclass.query, when called with the return_abstract
2813 return => { type => "string", desc => "String representation of abstract query object" }
2817 sub str2abstract_query {
2818 my ($self, $conn, $query, $qp_opts, $with_config) = @_;
2820 my %use_opts = ( # reasonable defaults? should these even be hardcoded here?
2822 superpage_size => 1000,
2823 core_limit => 25000,
2825 (ref $opts eq 'HASH' ? %$opts : ())
2830 # grab the query parser and initialize it
2831 my $parser = $OpenILS::Application::Storage::QParser;
2834 _initialize_parser($parser) unless $parser->initialization_complete;
2836 my $query = $parser->new(%use_opts)->parse;
2838 return $query->parse_tree->to_abstract_query(with_config => $with_config);
2841 __PACKAGE__->register_method(
2842 api_name => "open-ils.storage.query_parser.abstract_query.from_string",
2844 method => "str2abstract_query",
2848 {desc => "Query", type => "string"},
2849 {desc => q/Arguments for initializing QueryParser (optional)/,
2851 {desc => q/Flag enabling inclusion of QP config in returned object (optional, default false)/,
2854 return => { type => "object", desc => "abstract representation of query parser query" }
2858 my @available_statuses_cache;
2859 sub available_statuses {
2860 if (!scalar(@available_statuses_cache)) {
2861 @available_statuses_cache = map { $_->id } config::copy_status->search_where({is_available => 't'});
2863 return @available_statuses_cache;
2866 sub query_parser_fts {
2872 # grab the query parser and initialize it
2873 my $parser = $OpenILS::Application::Storage::QParser;
2876 _initialize_parser($parser) unless $parser->initialization_complete;
2878 # populate the locale/language map
2879 if (!$locale_map{COMPLETE}) {
2881 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2882 for my $locale ( @locales ) {
2883 $locale_map{lc($locale->code)} = $locale->marc_code;
2885 $locale_map{COMPLETE} = 1;
2889 # I hope we have a query!
2890 if (! $args{query} ) {
2891 die "No query was passed to ".$self->api_name;
2894 my $default_CD_modifiers = OpenSRF::Utils::SettingsClient->new->config_value(
2895 apps => 'open-ils.search' => app_settings => 'default_CD_modifiers'
2898 # Protect against empty / missing default_CD_modifiers setting
2899 if ($default_CD_modifiers and !ref($default_CD_modifiers)) {
2900 $args{query} = "$default_CD_modifiers $args{query}";
2903 my $simple_plan = $args{_simple_plan};
2904 # remove bad chunks of the %args hash
2905 for my $bad ( grep { /^_/ } keys(%args)) {
2906 delete($args{$bad});
2910 # parse the query and supply any query-level %arg-based defaults
2911 # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2912 my $query = $parser->new( %args )->parse;
2914 my $config = OpenSRF::Utils::SettingsClient->new();
2916 # set the locale-based default preferred location
2917 if (!$query->parse_tree->find_filter('preferred_language')) {
2918 $parser->default_preferred_language( $args{preferred_language} );
2920 if (!$parser->default_preferred_language) {
2921 my $ses_locale = $client->session ? $client->session->session_locale : '';
2922 $parser->default_preferred_language( $locale_map{ lc($ses_locale) } );
2925 if (!$parser->default_preferred_language) { # still nothing...
2926 my $tmp_dpl = $config->config_value(
2927 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2928 ) || $config->config_value(
2929 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2932 $parser->default_preferred_language( $tmp_dpl )
2937 # set the global default language multiplier
2938 if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2941 if ($tmp_dplw = $args{preferred_language_weight} || $args{preferred_language_multiplier} ) {
2942 $parser->default_preferred_language_multiplier($tmp_dplw);
2945 $tmp_dplw = $config->config_value(
2946 apps => 'open-ils.search' => app_settings => 'default_preferred_language_weight'
2947 ) || $config->config_value(
2948 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2951 $parser->default_preferred_language_multiplier( $tmp_dplw );
2955 # gather the site, if one is specified, defaulting to the in-query version
2956 my $ou = $args{org_unit};
2957 if (my ($filter) = $query->parse_tree->find_filter('site')) {
2958 $ou = $filter->args->[0] if (@{$filter->args});
2960 $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^(-)?\d+$/);
2962 # gather lasso, as with $ou
2963 my $lasso = $args{lasso};
2964 if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
2965 $lasso = $filter->args->[0] if (@{$filter->args});
2967 $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
2968 $lasso = -$lasso if ($lasso);
2971 # # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
2972 # # gather user lasso, as with $ou and lasso
2973 # my $mylasso = $args{my_lasso};
2974 # if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
2975 # $mylasso = $filter->args->[0] if (@{$filter->args});
2977 # $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
2980 # if we have a lasso, go with that, otherwise ... ou
2981 $ou = $lasso if ($lasso);
2983 # gather the preferred OU, if one is specified, as with $ou
2984 my $pref_ou = $args{pref_ou};
2985 $log->info("pref_ou = $pref_ou");
2986 if (my ($filter) = $query->parse_tree->find_filter('pref_ou')) {
2987 $pref_ou = $filter->args->[0] if (@{$filter->args});
2989 $pref_ou = actor::org_unit->search( { shortname => $pref_ou } )->next->id if ($pref_ou and $pref_ou !~ /^(-)?\d+$/);
2991 # get the default $ou if we have nothing
2992 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
2995 # 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
2996 # gather the depth, if one is specified, defaulting to the in-query version
2997 my $depth = $args{depth};
2998 if (my ($filter) = $query->parse_tree->find_filter('depth')) {
2999 $depth = $filter->args->[0] if (@{$filter->args});
3001 $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
3004 # gather the limit or default to 10
3005 my $limit = $args{check_limit};
3006 if (my ($filter) = $query->parse_tree->find_filter('limit')) {
3007 $limit = $filter->args->[0] if (@{$filter->args});
3009 if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
3010 $limit = $filter->args->[0] if (@{$filter->args});
3014 # gather the offset or default to 0
3015 my $offset = $args{skip_check} || $args{offset};
3016 if (my ($filter) = $query->parse_tree->find_filter('offset')) {
3017 $offset = $filter->args->[0] if (@{$filter->args});
3019 if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
3020 $offset = $filter->args->[0] if (@{$filter->args});
3024 # gather the estimation strategy or default to inclusion
3025 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
3026 if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
3027 $estimation_strategy = $filter->args->[0] if (@{$filter->args});
3031 # gather the estimation strategy or default to inclusion
3032 my $core_limit = $args{core_limit};
3033 if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
3034 $core_limit = $filter->args->[0] if (@{$filter->args});
3038 # gather statuses, and then forget those if we have an #available modifier
3040 if ($query->parse_tree->find_modifier('available')) {
3041 @statuses = available_statuses();
3042 } elsif (my ($filter) = $query->parse_tree->find_filter('statuses')) {
3043 @statuses = @{$filter->args} if (@{$filter->args});
3049 if (my ($filter) = $query->parse_tree->find_filter('locations')) {
3050 @location = @{$filter->args} if (@{$filter->args});
3053 # gather location_groups
3054 if (my ($filter) = $query->parse_tree->find_filter('location_groups')) {
3055 my @loc_groups = @{$filter->args} if (@{$filter->args});
3057 # collect the mapped locations and add them to the locations() filter
3060 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3061 my $maps = $cstore->request(
3062 'open-ils.cstore.direct.asset.copy_location_group_map.search.atomic',
3063 {lgroup => \@loc_groups})->gather(1);
3065 push(@location, $_->location) for @$maps;
3070 my $param_check = $limit || $query->superpage_size || 'NULL';
3071 my $param_offset = $offset || 'NULL';
3072 my $param_limit = $core_limit || 'NULL';
3074 my $sp = $query->superpage || 1;
3076 $param_offset = ($sp - 1) * $sp_size;
3079 my $param_search_ou = $ou;
3080 my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
3081 # my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
3082 my $param_core_query = $query->parse_tree->toSQL;
3083 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
3084 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
3085 my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
3086 my $deleted_search = ($query->parse_tree->find_modifier('deleted')) ? "'t'" : "'f'";
3087 my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
3088 my $param_pref_ou = $pref_ou || 'NULL';
3090 # my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3091 # SELECT * -- bib search: $args{query}
3092 # FROM search.query_parser_fts(
3093 # $param_search_ou\:\:INT,
3094 # $param_depth\:\:INT,
3095 # $param_core_query\:\:TEXT,
3096 # $param_statuses\:\:INT[],
3097 # $param_locations\:\:INT[],
3098 # $param_offset\:\:INT,
3099 # $param_check\:\:INT,
3100 # $param_limit\:\:INT,
3101 # $metarecord\:\:BOOL,
3103 # $deleted_search\:\:BOOL,
3104 # $param_pref_ou\:\:INT
3108 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3109 -- bib search: $args{query}
3115 my $recs = $sth->fetchall_arrayref({});
3116 my $summary_row = pop @$recs;
3118 my $total = $$summary_row{total};
3119 my $checked = $$summary_row{checked};
3120 my $visible = $$summary_row{visible};
3121 my $deleted = $$summary_row{deleted};
3122 my $excluded = $$summary_row{excluded};
3124 delete $$summary_row{id};
3125 delete $$summary_row{rel};
3126 delete $$summary_row{record};
3127 delete $$summary_row{badges};
3128 delete $$summary_row{popularity};
3130 if (defined($simple_plan)) {
3131 $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3133 $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3136 if ($args{return_query}) {
3137 $$summary_row{query_struct} = $query->parse_tree->to_abstract_query();
3138 $$summary_row{canonicalized_query} = QueryParser::Canonicalize::abstract_query2str_impl($$summary_row{query_struct}, 0, $parser);
3141 $client->respond( $summary_row );
3143 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $visible.",DEBUG);
3145 for my $rec (@$recs) {
3146 delete $$rec{checked};
3147 delete $$rec{visible};
3148 delete $$rec{excluded};
3149 delete $$rec{deleted};
3150 delete $$rec{total};
3151 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3153 $client->respond( $rec );
3157 __PACKAGE__->register_method(
3158 api_name => "open-ils.storage.query_parser_search",
3160 method => 'query_parser_fts',
3168 sub query_parser_fts_wrapper {
3173 $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3174 # grab the query parser and initialize it
3175 my $parser = $OpenILS::Application::Storage::QParser;
3178 _initialize_parser($parser) unless $parser->initialization_complete;
3180 $args{searches} ||= {};
3181 if (!scalar(keys(%{$args{searches}})) && !$args{query}) {
3182 die "No search arguments were passed to ".$self->api_name;
3185 $top_org ||= actor::org_unit->search( { parent_ou => undef } )->next;
3187 my $base_query = $args{query} || '';
3188 if (scalar(keys(%{$args{searches}}))) {
3189 $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3190 for my $sclass ( keys %{$args{searches}} ) {
3191 $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3192 $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3196 my $query = $base_query;
3197 $log->debug("Full base query: $base_query", DEBUG);
3199 $query = "$args{facets} $query" if ($args{facets});
3201 if (!$locale_map{COMPLETE}) {
3203 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3204 for my $locale ( @locales ) {
3205 $locale_map{lc($locale->code)} = $locale->marc_code;
3207 $locale_map{COMPLETE} = 1;
3211 my $base_plan = $parser->new( query => $base_query )->parse;
3213 $query = "preferred_language($args{preferred_language}) $query"
3214 if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3215 $query = "preferred_language_weight($args{preferred_language_weight}) $query"
3216 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'));
3220 if (!$base_plan->parse_tree->find_filter('badge_orgs')) {
3221 # supply a suitable badge_orgs filter unless user has
3222 # explicitly supplied one
3225 my @lg_id_list = @{$args{location_groups}} if (ref $args{location_groups});
3227 my ($lg_filter) = $base_plan->parse_tree->find_filter('location_groups');
3228 @lg_id_list = @{$lg_filter->args} if ($lg_filter && @{$lg_filter->args});
3232 for my $lg ( grep { /^\d+$/ } @lg_id_list ) {
3233 my $lg_obj = asset::copy_location_group->retrieve($lg);
3234 next unless $lg_obj;
3236 push(@borg_list, ''.$lg_obj->owner);
3238 $borgs = join(',', @borg_list) if @borg_list;
3242 my ($site_filter) = $base_plan->parse_tree->find_filter('site');
3243 if ($site_filter && @{$site_filter->args}) {
3244 $site = $top_org if ($site_filter->args->[0] eq '-');
3245 $site = $top_org if ($site_filter->args->[0] eq $top_org->shortname);
3246 $site = actor::org_unit->search( { shortname => $site_filter->args->[0] })->next unless ($site);
3247 } elsif ($args{org_unit}) {
3248 $site = $top_org if ($args{org_unit} eq '-');
3249 $site = $top_org if ($args{org_unit} eq $top_org->shortname);
3250 $site = actor::org_unit->search( { shortname => $args{org_unit} })->next unless ($site);
3256 $borgs = OpenSRF::AppSession->create( 'open-ils.cstore' )->request(
3257 'open-ils.cstore.json_query.atomic',
3258 { from => [ 'actor.org_unit_ancestors', $site->id ] }
3261 if (ref $borgs && @$borgs) {
3262 $borgs = join(',', map { $_->{'id'} } @$borgs);
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',