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_field_virtual_map =>
45 'open-ils.cstore.direct.config.metabib_field_virtual_map.search.atomic',
46 { id => { "!=" => undef } }
48 config_metabib_search_alias =>
50 'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
51 { alias => { "!=" => undef } }
53 config_metabib_field_index_norm_map =>
55 'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
56 { id => { "!=" => undef } },
57 { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
59 config_record_attr_definition =>
61 'open-ils.cstore.direct.config.record_attr_definition.search.atomic',
62 { name => { "!=" => undef } }
64 config_metabib_class_ts_map =>
66 'open-ils.cstore.direct.config.metabib_class_ts_map.search.atomic',
69 config_metabib_field_ts_map =>
71 'open-ils.cstore.direct.config.metabib_field_ts_map.search.atomic',
74 config_metabib_class =>
76 'open-ils.cstore.direct.config.metabib_class.search.atomic',
77 { name => { "!=" => undef } }
82 my $cgf = $cstore->request(
83 'open-ils.cstore.direct.config.global_flag.retrieve',
84 'search.max_popularity_importance_multiplier'
86 $max_mult = $cgf->value if $cgf && $U->is_true($cgf->enabled);
88 $max_mult = 2.0 unless $max_mult =~ /^-?(?:\d+\.?|\.\d)\d*\z/; # just in case
89 $parser->max_popularity_importance_multiplier($max_mult);
92 die("Cannot initialize $parser!") unless ($parser->initialization_complete);
95 sub ordered_records_from_metarecord { # XXX Replace with QP-based search-within-MR
99 my $formats = shift; # dead
103 my $copies_visible = 'LEFT JOIN asset.copy_vis_attr_cache vc ON (br.id = vc.record '.
104 'AND vc.vis_attr_vector @@ (SELECT c_attrs::query_int FROM asset.patron_default_visibility_mask() LIMIT 1))';
105 $copies_visible = '' if ($self->api_name =~ /staff/o);
107 my $copies_visible_count = ',COUNT(vc.id)';
108 $copies_visible_count = '' if ($self->api_name =~ /staff/o);
110 my $descendants = '';
112 $descendants = defined($depth) ?
113 ",actor.org_unit_descendants($org, $depth) d" :
114 ",actor.org_unit_descendants($org) d" ;
121 $copies_visible_count
122 FROM metabib.metarecord_source_map sm
123 JOIN biblio.record_entry br ON (sm.source = br.id AND NOT br.deleted)
124 LEFT JOIN metabib.record_sorter s ON (s.source = br.id AND s.attr = 'titlesort')
125 LEFT JOIN config.bib_source bs ON (br.source = bs.id)
128 WHERE sm.metarecord = ?
132 if ($copies_visible) {
133 $sql .= 'AND (bs.transcendant OR ';
135 $sql .= 'vc.circ_lib = d.id)';
137 $sql .= 'vc.id IS NOT NULL)'
139 $having = 'HAVING COUNT(vc.id) > 0';
147 s.value ASC NULLS LAST
150 my $ids = metabib::metarecord_source_map->db_Main->selectcol_arrayref($sql, {}, "$mr");
151 return $ids if ($self->api_name =~ /atomic$/o);
153 $client->respond( $_ ) for ( @$ids );
157 __PACKAGE__->register_method(
158 api_name => 'open-ils.storage.ordered.metabib.metarecord.records',
160 method => 'ordered_records_from_metarecord',
164 __PACKAGE__->register_method(
165 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff',
167 method => 'ordered_records_from_metarecord',
172 __PACKAGE__->register_method(
173 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.atomic',
175 method => 'ordered_records_from_metarecord',
179 __PACKAGE__->register_method(
180 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic',
182 method => 'ordered_records_from_metarecord',
187 # XXX: this subroutine and its two registered methods are marked for
188 # deprecation, as they do not work properly in 2.x (these tags are no longer
189 # normalized in mfr) and are not in known use
193 my $isxn = lc(shift());
197 $isxn =~ s/-//o if ($self->api_name =~ /isbn/o);
199 my $tag = ($self->api_name =~ /isbn/o) ? "'020' OR f.tag = '024'" : "'022'";
201 my $fr_table = metabib::full_rec->table;
202 my $bib_table = biblio::record_entry->table;
205 SELECT DISTINCT f.record
207 JOIN $bib_table b ON (b.id = f.record)
210 AND b.deleted IS FALSE
213 my $list = metabib::full_rec->db_Main->selectcol_arrayref($sql, {}, "$isxn%");
214 $client->respond($_) for (@$list);
217 __PACKAGE__->register_method(
218 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
220 method => 'isxn_search',
224 __PACKAGE__->register_method(
225 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.issn',
227 method => 'isxn_search',
232 sub metarecord_copy_count {
238 my $sm_table = metabib::metarecord_source_map->table;
239 my $rd_table = metabib::record_descriptor->table;
240 my $cn_table = asset::call_number->table;
241 my $cp_table = asset::copy->table;
242 my $br_table = biblio::record_entry->table;
243 my $src_table = config::bib_source->table;
244 my $cl_table = asset::copy_location->table;
245 my $cs_table = config::copy_status->table;
246 my $out_table = actor::org_unit_type->table;
248 my $descendants = "actor.org_unit_descendants(u.id)";
249 my $ancestors = "actor.org_unit_ancestors(?) u JOIN $out_table t ON (u.ou_type = t.id)";
251 if ($args{org_unit} < 0) {
252 $args{org_unit} *= -1;
253 $ancestors = "(select org_unit as id from actor.org_lasso_map where lasso = ?) u CROSS JOIN (SELECT -1 AS depth) t";
256 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';
257 $copies_visible = '' if ($self->api_name =~ /staff/o);
259 my (@types,@forms,@blvl);
260 my ($t_filter, $f_filter, $b_filter) = ('','','');
263 my ($t, $f, $b) = split '-', $args{format};
264 @types = split '', $t;
265 @forms = split '', $f;
266 @blvl = split '', $b;
269 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
273 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
277 $b_filter .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
287 JOIN $cn_table cn ON (cn.record = r.source)
288 JOIN $rd_table rd ON (cn.record = rd.record)
289 JOIN $cp_table cp ON (cn.id = cp.call_number)
290 JOIN $cs_table cs ON (cp.status = cs.id)
291 JOIN $cl_table cl ON (cp.location = cl.id)
292 JOIN $descendants a ON (cp.circ_lib = a.id)
293 WHERE r.metarecord = ?
294 AND cn.deleted IS FALSE
295 AND cp.deleted IS FALSE
305 JOIN $cn_table cn ON (cn.record = r.source)
306 JOIN $rd_table rd ON (cn.record = rd.record)
307 JOIN $cp_table cp ON (cn.id = cp.call_number)
308 JOIN $cs_table cs ON (cp.status = cs.id)
309 JOIN $cl_table cl ON (cp.location = cl.id)
310 JOIN $descendants a ON (cp.circ_lib = a.id)
311 WHERE r.metarecord = ?
312 AND cp.status IN (0,7,12)
313 AND cn.deleted IS FALSE
314 AND cp.deleted IS FALSE
324 JOIN $cn_table cn ON (cn.record = r.source)
325 JOIN $rd_table rd ON (cn.record = rd.record)
326 JOIN $cp_table cp ON (cn.id = cp.call_number)
327 JOIN $cs_table cs ON (cp.status = cs.id)
328 JOIN $cl_table cl ON (cp.location = cl.id)
329 WHERE r.metarecord = ?
330 AND cn.deleted IS FALSE
331 AND cp.deleted IS FALSE
332 AND cp.opac_visible IS TRUE
333 AND cs.opac_visible IS TRUE
334 AND cl.opac_visible IS TRUE
343 JOIN $br_table br ON (br.id = r.source)
344 JOIN $src_table src ON (src.id = br.source)
345 WHERE r.metarecord = ?
346 AND src.transcendant IS TRUE
354 my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
355 $sth->execute( ''.$args{metarecord},
359 ''.$args{metarecord},
363 ''.$args{metarecord},
367 ''.$args{metarecord},
371 while ( my $row = $sth->fetchrow_hashref ) {
372 $client->respond( $row );
376 __PACKAGE__->register_method(
377 api_name => 'open-ils.storage.metabib.metarecord.copy_count',
379 method => 'metarecord_copy_count',
384 __PACKAGE__->register_method(
385 api_name => 'open-ils.storage.metabib.metarecord.copy_count.staff',
387 method => 'metarecord_copy_count',
393 sub biblio_multi_search_full_rec {
398 my $class_join = $args{class_join} || 'AND';
399 my $limit = $args{limit} || 100;
400 my $offset = $args{offset} || 0;
401 my $sort = $args{'sort'};
402 my $sort_dir = $args{sort_dir} || 'DESC';
407 for my $arg (@{ $args{searches} }) {
408 my $term = $$arg{term};
409 my $limiters = $$arg{restrict};
411 my ($index_col) = metabib::full_rec->columns('FTS');
412 $index_col ||= 'value';
413 my $search_table = metabib::full_rec->table;
415 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
417 my $fts_where = $fts->sql_where_clause();
418 my @fts_ranks = $fts->fts_rank;
420 my $rank = join(' + ', @fts_ranks);
423 for my $limit (@$limiters) {
424 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
425 # MARC control field; mfr.subfield is NULL
426 push @wheres, "( tag = ? AND $fts_where )";
427 push @binds, $$limit{tag};
428 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
430 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
431 push @binds, $$limit{tag}, $$limit{subfield};
432 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
435 my $where = join(' OR ', @wheres);
437 push @selects, "SELECT record, AVG($rank) as sum FROM $search_table WHERE $where GROUP BY record";
441 my $descendants = defined($args{depth}) ?
442 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
443 "actor.org_unit_descendants($args{org_unit})" ;
446 my $metabib_record_descriptor = metabib::record_descriptor->table;
447 my $metabib_full_rec = metabib::full_rec->table;
448 my $asset_call_number_table = asset::call_number->table;
449 my $asset_copy_table = asset::copy->table;
450 my $cs_table = config::copy_status->table;
451 my $cl_table = asset::copy_location->table;
452 my $br_table = biblio::record_entry->table;
455 $cj = 'HAVING COUNT(x.record) = ' . scalar(@selects) if ($class_join eq 'AND');
458 '(SELECT x.record, sum(x.sum) FROM (('.
459 join(') UNION ALL (', @selects).
460 ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
462 my $has_vols = 'AND cn.owning_lib = d.id';
463 my $has_copies = 'AND cp.call_number = cn.id';
464 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';
466 if ($self->api_name =~ /staff/o) {
467 $copies_visible = '';
468 $has_copies = '' if ($ou_type == 0);
469 $has_vols = '' if ($ou_type == 0);
472 my ($t_filter, $f_filter) = ('','');
473 my ($a_filter, $l_filter, $lf_filter) = ('','','');
476 if (my $a = $args{audience}) {
477 $a = [$a] if (!ref($a));
480 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
485 if (my $l = $args{language}) {
486 $l = [$l] if (!ref($l));
489 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
494 if (my $f = $args{lit_form}) {
495 $f = [$f] if (!ref($f));
498 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
499 push @binds, @lit_form;
503 if (my $f = $args{item_form}) {
504 $f = [$f] if (!ref($f));
507 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
512 if (my $t = $args{item_type}) {
513 $t = [$t] if (!ref($t));
516 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
523 my ($t, $f) = split '-', $args{format};
524 my @types = split '', $t;
525 my @forms = split '', $f;
527 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
532 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
535 push @binds, @types, @forms;
538 my $relevance = 'sum(f.sum)';
539 $relevance = 1 if (!$copies_visible);
541 my $string_default_sort = 'zzzz';
542 $string_default_sort = 'AAAA' if ($sort_dir =~ /^DESC$/i);
544 my $number_default_sort = '9999';
545 $number_default_sort = '0000' if ($sort_dir =~/^DESC$/i);
547 my $rank = $relevance;
548 if (lc($sort) eq 'pubdate') {
551 SELECT COALESCE(SUBSTRING(MAX(frp.value) FROM E'\\\\d{4}'), '$number_default_sort')::INT
552 FROM $metabib_full_rec frp
553 WHERE frp.record = f.record
555 AND frp.subfield = 'c'
559 } elsif (lc($sort) eq 'create_date') {
561 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
563 } elsif (lc($sort) eq 'edit_date') {
565 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
567 } elsif (lc($sort) =~ /^title/i) {
570 SELECT COALESCE(LTRIM(SUBSTR(MAX(frt.value), COALESCE(SUBSTRING(MAX(frt.ind2) FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
571 FROM $metabib_full_rec frt
572 WHERE frt.record = f.record
574 AND frt.subfield = 'a'
578 } elsif (lc($sort) =~ /^author/i) {
581 SELECT COALESCE(LTRIM(MAX(query.value)), '$string_default_sort')
584 FROM $metabib_full_rec fra
585 WHERE fra.record = f.record
586 AND fra.tag LIKE '1%'
587 AND fra.subfield = 'a'
588 ORDER BY fra.tag::text::int
597 my $rd_join = $use_rd ? "$metabib_record_descriptor rd," : '';
598 my $rd_filter = $use_rd ? 'AND rd.record = f.record' : '';
600 if ($copies_visible) {
602 SELECT f.record, $relevance, count(DISTINCT cp.id), $rank
603 FROM $search_table f,
604 $asset_call_number_table cn,
605 $asset_copy_table cp,
611 WHERE br.id = f.record
612 AND cn.record = f.record
613 AND cp.status = cs.id
614 AND cp.location = cl.id
615 AND br.deleted IS FALSE
616 AND cn.deleted IS FALSE
617 AND cp.deleted IS FALSE
627 GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
628 ORDER BY 4 $sort_dir,3 DESC
632 SELECT f.record, 1, 1, $rank
633 FROM $search_table f,
636 WHERE br.id = f.record
637 AND br.deleted IS FALSE
650 $log->debug("Search SQL :: [$select]",DEBUG);
652 my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
653 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
656 $max = 1 if (!@$recs);
658 $max = $$_[1] if ($$_[1] > $max);
662 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
663 next unless ($$rec[0]);
664 my ($rid,$rank,$junk,$skip) = @$rec;
665 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
669 __PACKAGE__->register_method(
670 api_name => 'open-ils.storage.biblio.full_rec.multi_search',
672 method => 'biblio_multi_search_full_rec',
677 __PACKAGE__->register_method(
678 api_name => 'open-ils.storage.biblio.full_rec.multi_search.staff',
680 method => 'biblio_multi_search_full_rec',
686 sub search_full_rec {
692 my $term = $args{term};
693 my $limiters = $args{restrict};
695 my ($index_col) = metabib::full_rec->columns('FTS');
696 $index_col ||= 'value';
697 my $search_table = metabib::full_rec->table;
699 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
701 my $fts_where = $fts->sql_where_clause();
702 my @fts_ranks = $fts->fts_rank;
704 my $rank = join(' + ', @fts_ranks);
708 for my $limit (@$limiters) {
709 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
710 # MARC control field; mfr.subfield is NULL
711 push @wheres, "( tag = ? AND $fts_where )";
712 push @binds, $$limit{tag};
713 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
715 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
716 push @binds, $$limit{tag}, $$limit{subfield};
717 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
720 my $where = join(' OR ', @wheres);
722 my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
724 $log->debug("Search SQL :: [$select]",DEBUG);
726 my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
727 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
729 $client->respond($_) for (@$recs);
732 __PACKAGE__->register_method(
733 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
735 method => 'search_full_rec',
740 __PACKAGE__->register_method(
741 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
743 method => 'search_full_rec',
750 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
751 sub search_class_fts {
756 my $term = $args{term};
757 my $ou = $args{org_unit};
758 my $ou_type = $args{depth};
759 my $limit = $args{limit};
760 my $offset = $args{offset};
762 my $limit_clause = '';
763 my $offset_clause = '';
765 $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
766 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
769 my ($t_filter, $f_filter) = ('','');
772 my ($t, $f) = split '-', $args{format};
773 @types = split '', $t;
774 @forms = split '', $f;
776 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
780 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
786 my $descendants = defined($ou_type) ?
787 "actor.org_unit_descendants($ou, $ou_type)" :
788 "actor.org_unit_descendants($ou)";
790 my $class = $self->{cdbi};
791 my $search_table = $class->table;
793 my $metabib_record_descriptor = metabib::record_descriptor->table;
794 my $metabib_metarecord = metabib::metarecord->table;
795 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
796 my $asset_call_number_table = asset::call_number->table;
797 my $asset_copy_table = asset::copy->table;
798 my $cs_table = config::copy_status->table;
799 my $cl_table = asset::copy_location->table;
801 my ($index_col) = $class->columns('FTS');
802 $index_col ||= 'value';
804 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
805 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
807 my $fts_where = $fts->sql_where_clause;
808 my @fts_ranks = $fts->fts_rank;
810 my $rank = join(' + ', @fts_ranks);
812 my $has_vols = 'AND cn.owning_lib = d.id';
813 my $has_copies = 'AND cp.call_number = cn.id';
814 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';
816 my $visible_count = ', count(DISTINCT cp.id)';
817 my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
819 if ($self->api_name =~ /staff/o) {
820 $copies_visible = '';
821 $visible_count_test = '';
822 $has_copies = '' if ($ou_type == 0);
823 $has_vols = '' if ($ou_type == 0);
826 my $rank_calc = <<" RANK";
828 * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
829 * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
830 * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
831 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
834 $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
836 if ($copies_visible) {
838 SELECT m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
839 FROM $search_table f,
840 $metabib_metarecord_source_map_table m,
841 $asset_call_number_table cn,
842 $asset_copy_table cp,
845 $metabib_record_descriptor rd,
848 AND m.source = f.source
849 AND cn.record = m.source
850 AND rd.record = m.source
851 AND cp.status = cs.id
852 AND cp.location = cl.id
858 GROUP BY 1 $visible_count_test
860 $limit_clause $offset_clause
864 SELECT m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
865 FROM $search_table f,
866 $metabib_metarecord_source_map_table m,
867 $metabib_record_descriptor rd
869 AND m.source = f.source
870 AND rd.record = m.source
875 $limit_clause $offset_clause
879 $log->debug("Field Search SQL :: [$select]",DEBUG);
881 my $SQLstring = join('%',$fts->words);
882 my $REstring = join('\\s+',$fts->words);
883 my $first_word = ($fts->words)[0].'%';
884 my $recs = ($self->api_name =~ /unordered/o) ?
885 $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
886 $class->db_Main->selectall_arrayref($select, {},
887 '%'.lc($SQLstring).'%', # phrase order match
888 lc($first_word), # first word match
889 '^\\s*'.lc($REstring).'\\s*/?\s*$', # full exact match
893 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
895 $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
899 for my $class ( qw/title author subject keyword series identifier/ ) {
900 __PACKAGE__->register_method(
901 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord",
903 method => 'search_class_fts',
906 cdbi => "metabib::${class}_field_entry",
909 __PACKAGE__->register_method(
910 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
912 method => 'search_class_fts',
915 cdbi => "metabib::${class}_field_entry",
918 __PACKAGE__->register_method(
919 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
921 method => 'search_class_fts',
924 cdbi => "metabib::${class}_field_entry",
927 __PACKAGE__->register_method(
928 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
930 method => 'search_class_fts',
933 cdbi => "metabib::${class}_field_entry",
938 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
939 sub search_class_fts_count {
944 my $term = $args{term};
945 my $ou = $args{org_unit};
946 my $ou_type = $args{depth};
947 my $limit = $args{limit} || 100;
948 my $offset = $args{offset} || 0;
950 my $descendants = defined($ou_type) ?
951 "actor.org_unit_descendants($ou, $ou_type)" :
952 "actor.org_unit_descendants($ou)";
955 my ($t_filter, $f_filter) = ('','');
958 my ($t, $f) = split '-', $args{format};
959 @types = split '', $t;
960 @forms = split '', $f;
962 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
966 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
971 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
973 my $class = $self->{cdbi};
974 my $search_table = $class->table;
976 my $metabib_record_descriptor = metabib::record_descriptor->table;
977 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
978 my $asset_call_number_table = asset::call_number->table;
979 my $asset_copy_table = asset::copy->table;
980 my $cs_table = config::copy_status->table;
981 my $cl_table = asset::copy_location->table;
983 my ($index_col) = $class->columns('FTS');
984 $index_col ||= 'value';
986 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
988 my $fts_where = $fts->sql_where_clause;
990 my $has_vols = 'AND cn.owning_lib = d.id';
991 my $has_copies = 'AND cp.call_number = cn.id';
992 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';
993 if ($self->api_name =~ /staff/o) {
994 $copies_visible = '';
995 $has_vols = '' if ($ou_type == 0);
996 $has_copies = '' if ($ou_type == 0);
999 # XXX test an "EXISTS version of descendant checking...
1001 if ($copies_visible) {
1003 SELECT count(distinct m.metarecord)
1004 FROM $search_table f,
1005 $metabib_metarecord_source_map_table m,
1006 $metabib_metarecord_source_map_table mr,
1007 $asset_call_number_table cn,
1008 $asset_copy_table cp,
1011 $metabib_record_descriptor rd,
1014 AND mr.source = f.source
1015 AND mr.metarecord = m.metarecord
1016 AND cn.record = m.source
1017 AND rd.record = m.source
1018 AND cp.status = cs.id
1019 AND cp.location = cl.id
1028 SELECT count(distinct m.metarecord)
1029 FROM $search_table f,
1030 $metabib_metarecord_source_map_table m,
1031 $metabib_metarecord_source_map_table mr,
1032 $metabib_record_descriptor rd
1034 AND mr.source = f.source
1035 AND mr.metarecord = m.metarecord
1036 AND rd.record = m.source
1042 $log->debug("Field Search Count SQL :: [$select]",DEBUG);
1044 my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1046 $log->debug("Count Search yielded $recs results.",DEBUG);
1051 for my $class ( qw/title author subject keyword series identifier/ ) {
1052 __PACKAGE__->register_method(
1053 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
1055 method => 'search_class_fts_count',
1058 cdbi => "metabib::${class}_field_entry",
1061 __PACKAGE__->register_method(
1062 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
1064 method => 'search_class_fts_count',
1067 cdbi => "metabib::${class}_field_entry",
1073 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1074 sub postfilter_search_class_fts {
1079 my $term = $args{term};
1080 my $sort = $args{'sort'};
1081 my $sort_dir = $args{sort_dir} || 'DESC';
1082 my $ou = $args{org_unit};
1083 my $ou_type = $args{depth};
1084 my $limit = $args{limit} || 10;
1085 my $visibility_limit = $args{visibility_limit} || 5000;
1086 my $offset = $args{offset} || 0;
1088 my $outer_limit = 1000;
1090 my $limit_clause = '';
1091 my $offset_clause = '';
1093 $limit_clause = "LIMIT $outer_limit";
1094 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1096 my (@types,@forms,@lang,@aud,@lit_form);
1097 my ($t_filter, $f_filter) = ('','');
1098 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1099 my ($ot_filter, $of_filter) = ('','');
1100 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1102 if (my $a = $args{audience}) {
1103 $a = [$a] if (!ref($a));
1106 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1107 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1110 if (my $l = $args{language}) {
1111 $l = [$l] if (!ref($l));
1114 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1115 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1118 if (my $f = $args{lit_form}) {
1119 $f = [$f] if (!ref($f));
1122 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1123 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1126 if ($args{format}) {
1127 my ($t, $f) = split '-', $args{format};
1128 @types = split '', $t;
1129 @forms = split '', $f;
1131 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1132 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1136 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1137 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1142 my $descendants = defined($ou_type) ?
1143 "actor.org_unit_descendants($ou, $ou_type)" :
1144 "actor.org_unit_descendants($ou)";
1146 my $class = $self->{cdbi};
1147 my $search_table = $class->table;
1149 my $metabib_full_rec = metabib::full_rec->table;
1150 my $metabib_record_descriptor = metabib::record_descriptor->table;
1151 my $metabib_metarecord = metabib::metarecord->table;
1152 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1153 my $asset_call_number_table = asset::call_number->table;
1154 my $asset_copy_table = asset::copy->table;
1155 my $cs_table = config::copy_status->table;
1156 my $cl_table = asset::copy_location->table;
1157 my $br_table = biblio::record_entry->table;
1159 my ($index_col) = $class->columns('FTS');
1160 $index_col ||= 'value';
1162 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1164 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1166 my $SQLstring = join('%',map { lc($_) } $fts->words);
1167 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1168 my $first_word = lc(($fts->words)[0]).'%';
1170 my $fts_where = $fts->sql_where_clause;
1171 my @fts_ranks = $fts->fts_rank;
1174 $bonus{'metabib::identifier_field_entry'} =
1175 $bonus{'metabib::keyword_field_entry'} = [
1176 { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring }
1179 $bonus{'metabib::title_field_entry'} =
1180 $bonus{'metabib::series_field_entry'} = [
1181 { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1182 { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1183 @{ $bonus{'metabib::keyword_field_entry'} }
1186 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1187 $bonus_list ||= '1';
1189 my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1191 my $relevance = join(' + ', @fts_ranks);
1192 $relevance = <<" RANK";
1193 (SUM( ( $relevance ) * ( $bonus_list ) )/COUNT(m.source))
1196 my $string_default_sort = 'zzzz';
1197 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1199 my $number_default_sort = '9999';
1200 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1202 my $rank = $relevance;
1203 if (lc($sort) eq 'pubdate') {
1206 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1207 FROM $metabib_full_rec frp
1208 WHERE frp.record = mr.master_record
1210 AND frp.subfield = 'c'
1214 } elsif (lc($sort) eq 'create_date') {
1216 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1218 } elsif (lc($sort) eq 'edit_date') {
1220 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1222 } elsif (lc($sort) eq 'title') {
1225 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1226 FROM $metabib_full_rec frt
1227 WHERE frt.record = mr.master_record
1229 AND frt.subfield = 'a'
1233 } elsif (lc($sort) eq 'author') {
1236 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1237 FROM $metabib_full_rec fra
1238 WHERE fra.record = mr.master_record
1239 AND fra.tag LIKE '1%'
1240 AND fra.subfield = 'a'
1241 ORDER BY fra.tag::text::int
1249 my $select = <<" SQL";
1250 SELECT m.metarecord,
1252 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1254 FROM $search_table f,
1255 $metabib_metarecord_source_map_table m,
1256 $metabib_metarecord_source_map_table smrs,
1257 $metabib_metarecord mr,
1258 $metabib_record_descriptor rd
1260 AND smrs.metarecord = mr.id
1261 AND m.source = f.source
1262 AND m.metarecord = mr.id
1263 AND rd.record = smrs.source
1269 GROUP BY m.metarecord
1270 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1271 LIMIT $visibility_limit
1278 FROM $asset_call_number_table cn,
1279 $metabib_metarecord_source_map_table mrs,
1280 $asset_copy_table cp,
1285 $metabib_record_descriptor ord,
1287 WHERE mrs.metarecord = s.metarecord
1288 AND br.id = mrs.source
1289 AND cn.record = mrs.source
1290 AND cp.status = cs.id
1291 AND cp.location = cl.id
1292 AND cn.owning_lib = d.id
1293 AND cp.call_number = cn.id
1294 AND cp.opac_visible IS TRUE
1295 AND cs.opac_visible IS TRUE
1296 AND cl.opac_visible IS TRUE
1297 AND d.opac_visible IS TRUE
1298 AND br.active IS TRUE
1299 AND br.deleted IS FALSE
1300 AND ord.record = mrs.source
1306 ORDER BY 4 $sort_dir
1308 } elsif ($self->api_name !~ /staff/o) {
1315 FROM $asset_call_number_table cn,
1316 $metabib_metarecord_source_map_table mrs,
1317 $asset_copy_table cp,
1322 $metabib_record_descriptor ord
1324 WHERE mrs.metarecord = s.metarecord
1325 AND br.id = mrs.source
1326 AND cn.record = mrs.source
1327 AND cp.status = cs.id
1328 AND cp.location = cl.id
1329 AND cp.circ_lib = d.id
1330 AND cp.call_number = cn.id
1331 AND cp.opac_visible IS TRUE
1332 AND cs.opac_visible IS TRUE
1333 AND cl.opac_visible IS TRUE
1334 AND d.opac_visible IS TRUE
1335 AND br.active IS TRUE
1336 AND br.deleted IS FALSE
1337 AND ord.record = mrs.source
1345 ORDER BY 4 $sort_dir
1354 FROM $asset_call_number_table cn,
1355 $asset_copy_table cp,
1356 $metabib_metarecord_source_map_table mrs,
1359 $metabib_record_descriptor ord
1361 WHERE mrs.metarecord = s.metarecord
1362 AND br.id = mrs.source
1363 AND cn.record = mrs.source
1364 AND cn.id = cp.call_number
1365 AND br.deleted IS FALSE
1366 AND cn.deleted IS FALSE
1367 AND ord.record = mrs.source
1368 AND ( cn.owning_lib = d.id
1369 OR ( cp.circ_lib = d.id
1370 AND cp.deleted IS FALSE
1382 FROM $asset_call_number_table cn,
1383 $metabib_metarecord_source_map_table mrs,
1384 $metabib_record_descriptor ord
1385 WHERE mrs.metarecord = s.metarecord
1386 AND cn.record = mrs.source
1387 AND ord.record = mrs.source
1395 ORDER BY 4 $sort_dir
1400 $log->debug("Field Search SQL :: [$select]",DEBUG);
1402 my $recs = $class->db_Main->selectall_arrayref(
1404 (@bonus_values > 0 ? @bonus_values : () ),
1405 ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1406 @types, @forms, @aud, @lang, @lit_form,
1407 @types, @forms, @aud, @lang, @lit_form,
1408 ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1410 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1413 $max = 1 if (!@$recs);
1415 $max = $$_[1] if ($$_[1] > $max);
1418 my $count = scalar(@$recs);
1419 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1420 my ($mrid,$rank,$skip) = @$rec;
1421 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1426 for my $class ( qw/title author subject keyword series identifier/ ) {
1427 __PACKAGE__->register_method(
1428 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1430 method => 'postfilter_search_class_fts',
1433 cdbi => "metabib::${class}_field_entry",
1436 __PACKAGE__->register_method(
1437 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1439 method => 'postfilter_search_class_fts',
1442 cdbi => "metabib::${class}_field_entry",
1449 my $_cdbi = { title => "metabib::title_field_entry",
1450 author => "metabib::author_field_entry",
1451 subject => "metabib::subject_field_entry",
1452 keyword => "metabib::keyword_field_entry",
1453 series => "metabib::series_field_entry",
1454 identifier => "metabib::identifier_field_entry",
1457 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1458 sub postfilter_search_multi_class_fts {
1463 my $sort = $args{'sort'};
1464 my $sort_dir = $args{sort_dir} || 'DESC';
1465 my $ou = $args{org_unit};
1466 my $ou_type = $args{depth};
1467 my $limit = $args{limit} || 10;
1468 my $offset = $args{offset} || 0;
1469 my $visibility_limit = $args{visibility_limit} || 5000;
1472 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1475 if (!defined($args{org_unit})) {
1476 die "No target organizational unit passed to ".$self->api_name;
1479 if (! scalar( keys %{$args{searches}} )) {
1480 die "No search arguments were passed to ".$self->api_name;
1483 my $outer_limit = 1000;
1485 my $limit_clause = '';
1486 my $offset_clause = '';
1488 $limit_clause = "LIMIT $outer_limit";
1489 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1491 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1492 my ($t_filter, $f_filter, $v_filter) = ('','','');
1493 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1494 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1495 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1497 if ($args{available}) {
1498 $avail_filter = ' AND cp.status IN (0,7,12)';
1501 if (my $a = $args{audience}) {
1502 $a = [$a] if (!ref($a));
1505 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1506 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1509 if (my $l = $args{language}) {
1510 $l = [$l] if (!ref($l));
1513 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1514 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1517 if (my $f = $args{lit_form}) {
1518 $f = [$f] if (!ref($f));
1521 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1522 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1525 if (my $f = $args{item_form}) {
1526 $f = [$f] if (!ref($f));
1529 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1530 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1533 if (my $t = $args{item_type}) {
1534 $t = [$t] if (!ref($t));
1537 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1538 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1541 if (my $v = $args{vr_format}) {
1542 $v = [$v] if (!ref($v));
1545 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1546 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1550 # XXX legacy format and item type support
1551 if ($args{format}) {
1552 my ($t, $f) = split '-', $args{format};
1553 @types = split '', $t;
1554 @forms = split '', $f;
1556 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1557 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1561 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1562 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1568 my $descendants = defined($ou_type) ?
1569 "actor.org_unit_descendants($ou, $ou_type)" :
1570 "actor.org_unit_descendants($ou)";
1572 my $search_table_list = '';
1574 my $join_table_list = '';
1577 my $field_table = config::metabib_field->table;
1581 my $prev_search_group;
1582 my $curr_search_group;
1586 for my $search_group (sort keys %{$args{searches}}) {
1587 (my $search_group_name = $search_group) =~ s/\|/_/gso;
1588 ($search_class,$search_field) = split /\|/, $search_group;
1589 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1591 if ($search_field) {
1592 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1593 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1598 $prev_search_group = $curr_search_group if ($curr_search_group);
1600 $curr_search_group = $search_group_name;
1602 my $class = $_cdbi->{$search_class};
1603 my $search_table = $class->table;
1605 my ($index_col) = $class->columns('FTS');
1606 $index_col ||= 'value';
1609 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1611 my $fts_where = $fts->sql_where_clause;
1612 my @fts_ranks = $fts->fts_rank;
1614 my $SQLstring = join('%',map { lc($_) } $fts->words);
1615 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1616 my $first_word = lc(($fts->words)[0]).'%';
1618 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1619 my $rank = join(' + ', @fts_ranks);
1622 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1623 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1625 $bonus{'series'} = [
1626 { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1627 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1630 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1632 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1633 $bonus_list ||= '1';
1635 push @bonus_lists, $bonus_list;
1636 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1639 #---------------------
1641 $search_table_list .= "$search_table $search_group_name, ";
1642 push @rank_list,$rank;
1643 $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1645 if ($metabib_field) {
1646 $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1647 $metabib_field = undef;
1650 if ($prev_search_group) {
1651 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1655 my $metabib_record_descriptor = metabib::record_descriptor->table;
1656 my $metabib_full_rec = metabib::full_rec->table;
1657 my $metabib_metarecord = metabib::metarecord->table;
1658 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1659 my $asset_call_number_table = asset::call_number->table;
1660 my $asset_copy_table = asset::copy->table;
1661 my $cs_table = config::copy_status->table;
1662 my $cl_table = asset::copy_location->table;
1663 my $br_table = biblio::record_entry->table;
1664 my $source_table = config::bib_source->table;
1666 my $bonuses = join (' * ', @bonus_lists);
1667 my $relevance = join (' + ', @rank_list);
1668 $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1670 my $string_default_sort = 'zzzz';
1671 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1673 my $number_default_sort = '9999';
1674 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1678 my $secondary_sort = <<" SORT";
1680 SELECT COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1681 FROM $metabib_full_rec sfrt,
1682 $metabib_metarecord mr
1683 WHERE sfrt.record = mr.master_record
1684 AND sfrt.tag = '245'
1685 AND sfrt.subfield = 'a'
1690 my $rank = $relevance;
1691 if (lc($sort) eq 'pubdate') {
1694 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1695 FROM $metabib_full_rec frp
1696 WHERE frp.record = mr.master_record
1698 AND frp.subfield = 'c'
1702 } elsif (lc($sort) eq 'create_date') {
1704 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1706 } elsif (lc($sort) eq 'edit_date') {
1708 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1710 } elsif (lc($sort) eq 'title') {
1713 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1714 FROM $metabib_full_rec frt
1715 WHERE frt.record = mr.master_record
1717 AND frt.subfield = 'a'
1721 $secondary_sort = <<" SORT";
1723 SELECT COALESCE(SUBSTRING(sfrp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1724 FROM $metabib_full_rec sfrp
1725 WHERE sfrp.record = mr.master_record
1726 AND sfrp.tag = '260'
1727 AND sfrp.subfield = 'c'
1731 } elsif (lc($sort) eq 'author') {
1734 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1735 FROM $metabib_full_rec fra
1736 WHERE fra.record = mr.master_record
1737 AND fra.tag LIKE '1%'
1738 AND fra.subfield = 'a'
1739 ORDER BY fra.tag::text::int
1744 push @bonus_values, @bonus_values;
1749 my $select = <<" SQL";
1750 SELECT m.metarecord,
1752 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1755 FROM $search_table_list
1756 $metabib_metarecord mr,
1757 $metabib_metarecord_source_map_table m,
1758 $metabib_metarecord_source_map_table smrs
1759 WHERE m.metarecord = smrs.metarecord
1760 AND mr.id = m.metarecord
1763 GROUP BY m.metarecord
1764 -- ORDER BY 4 $sort_dir
1765 LIMIT $visibility_limit
1768 if ($self->api_name !~ /staff/o) {
1775 FROM $asset_call_number_table cn,
1776 $metabib_metarecord_source_map_table mrs,
1777 $asset_copy_table cp,
1782 $metabib_record_descriptor ord
1783 WHERE mrs.metarecord = s.metarecord
1784 AND br.id = mrs.source
1785 AND cn.record = mrs.source
1786 AND cp.status = cs.id
1787 AND cp.location = cl.id
1788 AND cp.circ_lib = d.id
1789 AND cp.call_number = cn.id
1790 AND cp.opac_visible IS TRUE
1791 AND cs.opac_visible IS TRUE
1792 AND cl.opac_visible IS TRUE
1793 AND d.opac_visible IS TRUE
1794 AND br.active IS TRUE
1795 AND br.deleted IS FALSE
1796 AND cp.deleted IS FALSE
1797 AND cn.deleted IS FALSE
1798 AND ord.record = mrs.source
1811 $metabib_metarecord_source_map_table mrs,
1812 $metabib_record_descriptor ord,
1814 WHERE mrs.metarecord = s.metarecord
1815 AND ord.record = mrs.source
1816 AND br.id = mrs.source
1817 AND br.source = src.id
1818 AND src.transcendant IS TRUE
1826 ORDER BY 4 $sort_dir, 5
1833 $metabib_metarecord_source_map_table omrs,
1834 $metabib_record_descriptor ord
1835 WHERE omrs.metarecord = s.metarecord
1836 AND ord.record = omrs.source
1839 FROM $asset_call_number_table cn,
1840 $asset_copy_table cp,
1843 WHERE br.id = omrs.source
1844 AND cn.record = omrs.source
1845 AND br.deleted IS FALSE
1846 AND cn.deleted IS FALSE
1847 AND cp.call_number = cn.id
1848 AND ( cn.owning_lib = d.id
1849 OR ( cp.circ_lib = d.id
1850 AND cp.deleted IS FALSE
1858 FROM $asset_call_number_table cn
1859 WHERE cn.record = omrs.source
1860 AND cn.deleted IS FALSE
1866 $metabib_metarecord_source_map_table mrs,
1867 $metabib_record_descriptor ord,
1869 WHERE mrs.metarecord = s.metarecord
1870 AND br.id = mrs.source
1871 AND br.source = src.id
1872 AND src.transcendant IS TRUE
1888 ORDER BY 4 $sort_dir, 5
1893 $log->debug("Field Search SQL :: [$select]",DEBUG);
1895 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1898 @types, @forms, @vformats, @aud, @lang, @lit_form,
1899 @types, @forms, @vformats, @aud, @lang, @lit_form,
1900 # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1903 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1906 $max = 1 if (!@$recs);
1908 $max = $$_[1] if ($$_[1] > $max);
1911 my $count = scalar(@$recs);
1912 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1913 next unless ($$rec[0]);
1914 my ($mrid,$rank,$skip) = @$rec;
1915 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1920 __PACKAGE__->register_method(
1921 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1923 method => 'postfilter_search_multi_class_fts',
1928 __PACKAGE__->register_method(
1929 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1931 method => 'postfilter_search_multi_class_fts',
1937 __PACKAGE__->register_method(
1938 api_name => "open-ils.storage.metabib.multiclass.search_fts",
1940 method => 'postfilter_search_multi_class_fts',
1945 __PACKAGE__->register_method(
1946 api_name => "open-ils.storage.metabib.multiclass.search_fts.staff",
1948 method => 'postfilter_search_multi_class_fts',
1954 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1955 sub biblio_search_multi_class_fts {
1960 my $sort = $args{'sort'};
1961 my $sort_dir = $args{sort_dir} || 'DESC';
1962 my $ou = $args{org_unit};
1963 my $ou_type = $args{depth};
1964 my $limit = $args{limit} || 10;
1965 my $offset = $args{offset} || 0;
1966 my $pref_lang = $args{preferred_language} || 'eng';
1967 my $visibility_limit = $args{visibility_limit} || 5000;
1970 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1973 if (! scalar( keys %{$args{searches}} )) {
1974 die "No search arguments were passed to ".$self->api_name;
1977 my $outer_limit = 1000;
1979 my $limit_clause = '';
1980 my $offset_clause = '';
1982 $limit_clause = "LIMIT $outer_limit";
1983 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1985 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1986 my ($t_filter, $f_filter, $v_filter) = ('','','');
1987 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1988 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1989 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1991 if ($args{available}) {
1992 $avail_filter = ' AND cp.status IN (0,7,12)';
1995 if (my $a = $args{audience}) {
1996 $a = [$a] if (!ref($a));
1999 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
2000 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
2003 if (my $l = $args{language}) {
2004 $l = [$l] if (!ref($l));
2007 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
2008 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
2011 if (my $f = $args{lit_form}) {
2012 $f = [$f] if (!ref($f));
2015 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2016 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2019 if (my $f = $args{item_form}) {
2020 $f = [$f] if (!ref($f));
2023 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2024 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2027 if (my $t = $args{item_type}) {
2028 $t = [$t] if (!ref($t));
2031 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2032 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2035 if (my $v = $args{vr_format}) {
2036 $v = [$v] if (!ref($v));
2039 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
2040 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
2043 # XXX legacy format and item type support
2044 if ($args{format}) {
2045 my ($t, $f) = split '-', $args{format};
2046 @types = split '', $t;
2047 @forms = split '', $f;
2049 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2050 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2054 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2055 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2060 my $descendants = defined($ou_type) ?
2061 "actor.org_unit_descendants($ou, $ou_type)" :
2062 "actor.org_unit_descendants($ou)";
2064 my $search_table_list = '';
2066 my $join_table_list = '';
2069 my $field_table = config::metabib_field->table;
2073 my $prev_search_group;
2074 my $curr_search_group;
2078 for my $search_group (sort keys %{$args{searches}}) {
2079 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2080 ($search_class,$search_field) = split /\|/, $search_group;
2081 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2083 if ($search_field) {
2084 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2085 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2090 $prev_search_group = $curr_search_group if ($curr_search_group);
2092 $curr_search_group = $search_group_name;
2094 my $class = $_cdbi->{$search_class};
2095 my $search_table = $class->table;
2097 my ($index_col) = $class->columns('FTS');
2098 $index_col ||= 'value';
2101 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2103 my $fts_where = $fts->sql_where_clause;
2104 my @fts_ranks = $fts->fts_rank;
2106 my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2107 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2108 my $first_word = lc(($fts->words)[0]).'%';
2110 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2111 my $rank = join(' + ', @fts_ranks);
2114 $bonus{'subject'} = [];
2115 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2117 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2119 $bonus{'series'} = [
2120 { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2121 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2124 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2127 push @{ $bonus{'title'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2128 push @{ $bonus{'author'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2129 push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2130 push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2131 push @{ $bonus{'series'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2134 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2135 $bonus_list ||= '1';
2137 push @bonus_lists, $bonus_list;
2138 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2140 #---------------------
2142 $search_table_list .= "$search_table $search_group_name, ";
2143 push @rank_list,$rank;
2144 $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2146 if ($metabib_field) {
2147 $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2148 $metabib_field = undef;
2151 if ($prev_search_group) {
2152 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2156 my $metabib_record_descriptor = metabib::record_descriptor->table;
2157 my $metabib_full_rec = metabib::full_rec->table;
2158 my $metabib_metarecord = metabib::metarecord->table;
2159 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2160 my $asset_call_number_table = asset::call_number->table;
2161 my $asset_copy_table = asset::copy->table;
2162 my $cs_table = config::copy_status->table;
2163 my $cl_table = asset::copy_location->table;
2164 my $br_table = biblio::record_entry->table;
2165 my $source_table = config::bib_source->table;
2168 my $bonuses = join (' * ', @bonus_lists);
2169 my $relevance = join (' + ', @rank_list);
2170 $relevance = "AVG( ($relevance) * ($bonuses) )";
2172 my $string_default_sort = 'zzzz';
2173 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2175 my $number_default_sort = '9999';
2176 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2178 my $rank = $relevance;
2179 if (lc($sort) eq 'pubdate') {
2182 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d{4}'),'$number_default_sort')::INT
2183 FROM $metabib_full_rec frp
2184 WHERE frp.record = b.id
2186 AND frp.subfield = 'c'
2190 } elsif (lc($sort) eq 'create_date') {
2192 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2194 } elsif (lc($sort) eq 'edit_date') {
2196 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2198 } elsif (lc($sort) eq 'title') {
2201 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2202 FROM $metabib_full_rec frt
2203 WHERE frt.record = b.id
2205 AND frt.subfield = 'a'
2209 } elsif (lc($sort) eq 'author') {
2212 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
2213 FROM $metabib_full_rec fra
2214 WHERE fra.record = b.id
2215 AND fra.tag LIKE '1%'
2216 AND fra.subfield = 'a'
2217 ORDER BY fra.tag::text::int
2222 push @bonus_values, @bonus_values;
2227 my $select = <<" SQL";
2232 FROM $search_table_list
2233 $metabib_record_descriptor rd,
2236 WHERE rd.record = b.id
2237 AND b.active IS TRUE
2238 AND b.deleted IS FALSE
2247 GROUP BY b.id, b.source
2248 ORDER BY 3 $sort_dir
2249 LIMIT $visibility_limit
2252 if ($self->api_name !~ /staff/o) {
2257 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2260 FROM $asset_call_number_table cn,
2261 $asset_copy_table cp,
2265 WHERE cn.record = s.id
2266 AND cp.status = cs.id
2267 AND cp.location = cl.id
2268 AND cp.call_number = cn.id
2269 AND cp.opac_visible IS TRUE
2270 AND cs.opac_visible IS TRUE
2271 AND cl.opac_visible IS TRUE
2272 AND d.opac_visible IS TRUE
2273 AND cp.deleted IS FALSE
2274 AND cn.deleted IS FALSE
2275 AND cp.circ_lib = d.id
2279 OR src.transcendant IS TRUE
2280 ORDER BY 3 $sort_dir
2287 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2290 FROM $asset_call_number_table cn,
2291 $asset_copy_table cp,
2293 WHERE cn.record = s.id
2294 AND cp.call_number = cn.id
2295 AND cn.deleted IS FALSE
2296 AND cp.circ_lib = d.id
2297 AND cp.deleted IS FALSE
2303 FROM $asset_call_number_table cn
2304 WHERE cn.record = s.id
2307 OR src.transcendant IS TRUE
2308 ORDER BY 3 $sort_dir
2313 $log->debug("Field Search SQL :: [$select]",DEBUG);
2315 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2317 @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2320 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2322 my $count = scalar(@$recs);
2323 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2324 next unless ($$rec[0]);
2325 my ($mrid,$rank) = @$rec;
2326 $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2331 __PACKAGE__->register_method(
2332 api_name => "open-ils.storage.biblio.multiclass.search_fts.record",
2334 method => 'biblio_search_multi_class_fts',
2339 __PACKAGE__->register_method(
2340 api_name => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2342 method => 'biblio_search_multi_class_fts',
2347 __PACKAGE__->register_method(
2348 api_name => "open-ils.storage.biblio.multiclass.search_fts",
2350 method => 'biblio_search_multi_class_fts',
2355 __PACKAGE__->register_method(
2356 api_name => "open-ils.storage.biblio.multiclass.search_fts.staff",
2358 method => 'biblio_search_multi_class_fts',
2366 my $default_preferred_language;
2367 my $default_preferred_language_weight;
2369 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2375 if (!$locale_map{COMPLETE}) {
2377 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2378 for my $locale ( @locales ) {
2379 $locale_map{lc($locale->code)} = $locale->marc_code;
2381 $locale_map{COMPLETE} = 1;
2385 my $config = OpenSRF::Utils::SettingsClient->new();
2387 if (!$default_preferred_language) {
2389 $default_preferred_language = $config->config_value(
2390 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2391 ) || $config->config_value(
2392 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2397 if (!$default_preferred_language_weight) {
2399 $default_preferred_language_weight = $config->config_value(
2400 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2401 ) || $config->config_value(
2402 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2406 # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2407 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2409 my $ou = $args{org_unit};
2410 my $limit = $args{limit} || 10;
2411 my $offset = $args{offset} || 0;
2414 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2417 if (! scalar( keys %{$args{searches}} )) {
2418 die "No search arguments were passed to ".$self->api_name;
2421 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2423 if (!defined($args{preferred_language})) {
2424 my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2425 $args{preferred_language} =
2426 $locale_map{ lc($ses_locale) } || 'eng';
2429 if (!defined($args{preferred_language_weight})) {
2430 $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2433 if ($args{available}) {
2434 @statuses = (0,7,12);
2437 if (my $s = $args{locations}) {
2438 $s = [$s] if (!ref($s));
2442 if (my $b = $args{between}) {
2443 if (ref($b) && @$b == 2) {
2448 if (my $s = $args{statuses}) {
2449 $s = [$s] if (!ref($s));
2453 if (my $a = $args{audience}) {
2454 $a = [$a] if (!ref($a));
2458 if (my $l = $args{language}) {
2459 $l = [$l] if (!ref($l));
2463 if (my $f = $args{lit_form}) {
2464 $f = [$f] if (!ref($f));
2468 if (my $f = $args{item_form}) {
2469 $f = [$f] if (!ref($f));
2473 if (my $t = $args{item_type}) {
2474 $t = [$t] if (!ref($t));
2478 if (my $b = $args{bib_level}) {
2479 $b = [$b] if (!ref($b));
2483 if (my $v = $args{vr_format}) {
2484 $v = [$v] if (!ref($v));
2488 # XXX legacy format and item type support
2489 if ($args{format}) {
2490 my ($t, $f) = split '-', $args{format};
2491 @types = split '', $t;
2492 @forms = split '', $f;
2495 my %stored_proc_search_args;
2496 for my $search_group (sort keys %{$args{searches}}) {
2497 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2498 my ($search_class,$search_field) = split /\|/, $search_group;
2499 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2501 if ($search_field) {
2502 unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2503 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2508 my $class = $_cdbi->{$search_class};
2509 my $search_table = $class->table;
2511 my ($index_col) = $class->columns('FTS');
2512 $index_col ||= 'value';
2515 my $fts = OpenILS::Application::Storage::FTS->compile(
2516 $search_class => $args{searches}{$search_group}{term},
2517 $search_group_name.'.value',
2518 "$search_group_name.$index_col"
2520 $fts->sql_where_clause; # this builds the ranks for us
2522 my @fts_ranks = $fts->fts_rank;
2523 my @fts_queries = $fts->fts_query;
2524 my @phrases = map { lc($_) } $fts->phrases;
2525 my @words = map { lc($_) } $fts->words;
2527 $stored_proc_search_args{$search_group} = {
2528 fts_rank => \@fts_ranks,
2529 fts_query => \@fts_queries,
2530 phrase => \@phrases,
2536 my $param_search_ou = $ou;
2537 my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2538 my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2539 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2540 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2541 my $param_audience = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud ) . '}$$';
2542 my $param_language = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang ) . '}$$';
2543 my $param_lit_form = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2544 my $param_types = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types ) . '}$$';
2545 my $param_forms = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms ) . '}$$';
2546 my $param_vformats = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2547 my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2548 my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2549 my $param_after = $args{after} ; $param_after = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2550 my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2551 my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2552 my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2553 my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2554 my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2555 my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2556 my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2557 my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2558 my $param_rel_limit = $args{core_limit}; $param_rel_limit ||= 'NULL';
2559 my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2560 my $param_skip_chk = $args{skip_check}; $param_skip_chk ||= 'NULL';
2562 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2564 FROM search.staged_fts(
2565 $param_search_ou\:\:INT,
2566 $param_depth\:\:INT,
2567 $param_searches\:\:TEXT,
2568 $param_statuses\:\:INT[],
2569 $param_locations\:\:INT[],
2570 $param_audience\:\:TEXT[],
2571 $param_language\:\:TEXT[],
2572 $param_lit_form\:\:TEXT[],
2573 $param_types\:\:TEXT[],
2574 $param_forms\:\:TEXT[],
2575 $param_vformats\:\:TEXT[],
2576 $param_bib_level\:\:TEXT[],
2577 $param_before\:\:TEXT,
2578 $param_after\:\:TEXT,
2579 $param_during\:\:TEXT,
2580 $param_between\:\:TEXT[],
2581 $param_pref_lang\:\:TEXT,
2582 $param_pref_lang_multiplier\:\:REAL,
2583 $param_sort\:\:TEXT,
2584 $param_sort_desc\:\:BOOL,
2585 $metarecord\:\:BOOL,
2587 $param_rel_limit\:\:INT,
2588 $param_chk_limit\:\:INT,
2589 $param_skip_chk\:\:INT
2595 my $recs = $sth->fetchall_arrayref({});
2596 my $summary_row = pop @$recs;
2598 my $total = $$summary_row{total};
2599 my $checked = $$summary_row{checked};
2600 my $visible = $$summary_row{visible};
2601 my $deleted = $$summary_row{deleted};
2602 my $excluded = $$summary_row{excluded};
2604 my $estimate = $visible;
2605 if ( $total > $checked && $checked ) {
2607 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2608 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2612 delete $$summary_row{id};
2613 delete $$summary_row{rel};
2614 delete $$summary_row{record};
2616 $client->respond( $summary_row );
2618 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2620 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2621 delete $$rec{checked};
2622 delete $$rec{visible};
2623 delete $$rec{excluded};
2624 delete $$rec{deleted};
2625 delete $$rec{total};
2626 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2628 $client->respond( $rec );
2632 __PACKAGE__->register_method(
2633 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
2635 method => 'staged_fts',
2640 __PACKAGE__->register_method(
2641 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2643 method => 'staged_fts',
2648 __PACKAGE__->register_method(
2649 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
2651 method => 'staged_fts',
2656 __PACKAGE__->register_method(
2657 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2659 method => 'staged_fts',
2665 sub FTS_paging_estimate {
2669 my $checked = shift;
2670 my $visible = shift;
2671 my $excluded = shift;
2672 my $deleted = shift;
2675 my $deleted_ratio = $deleted / $checked;
2676 my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2678 my $exclusion_ratio = $excluded / $checked;
2679 my $delete_adjusted_exclusion_ratio = $checked - $deleted ? $excluded / ($checked - $deleted) : 1;
2681 my $inclusion_ratio = $visible / $checked;
2682 my $delete_adjusted_inclusion_ratio = $checked - $deleted ? $visible / ($checked - $deleted) : 0;
2685 exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2686 inclusion => int($delete_adjusted_total * $inclusion_ratio),
2687 delete_adjusted_exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2688 delete_adjusted_inclusion => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2691 __PACKAGE__->register_method(
2692 api_name => "open-ils.storage.fts_paging_estimate",
2694 method => 'FTS_paging_estimate',
2700 Hash of estimation values based on four variant estimation strategies:
2701 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2702 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2703 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2704 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2707 Helper method used to determin the approximate number of
2708 hits for a search that spans multiple superpages. For
2709 sparse superpages, the inclusion estimate will likely be the
2710 best estimate. The exclusion strategy is the original, but
2711 inclusion is the default.
2714 { name => 'checked',
2715 desc => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2718 { name => 'visible',
2719 desc => 'Number of records visible to the search location on the current superpage.',
2722 { name => 'excluded',
2723 desc => 'Number of records excluded from the search location on the current superpage.',
2726 { name => 'deleted',
2727 desc => 'Number of deleted records on the current superpage.',
2731 desc => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2744 my $term = $$args{term};
2745 my $limit = $$args{max} || 1;
2746 my $min = $$args{min} || 1;
2747 my @classes = @{$$args{class}};
2749 $limit = $min if ($min > $limit);
2752 @classes = ( qw/ title author subject series keyword / );
2756 my $bre_table = biblio::record_entry->table;
2757 my $cn_table = asset::call_number->table;
2758 my $cp_table = asset::copy->table;
2760 for my $search_class ( @classes ) {
2762 my $class = $_cdbi->{$search_class};
2763 my $search_table = $class->table;
2765 my ($index_col) = $class->columns('FTS');
2766 $index_col ||= 'value';
2769 my $where = OpenILS::Application::Storage::FTS
2770 ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2774 SELECT COUNT(DISTINCT X.source)
2775 FROM (SELECT $search_class.source
2776 FROM $search_table $search_class
2777 JOIN $bre_table b ON (b.id = $search_class.source)
2782 HAVING COUNT(DISTINCT X.source) >= $min;
2785 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2786 $matches{$search_class} = $res ? $res->[0] : 0;
2791 __PACKAGE__->register_method(
2792 api_name => "open-ils.storage.search.xref",
2794 method => 'xref_count',
2798 # Takes an abstract query object and recursively turns it back into a string
2800 sub abstract_query2str {
2801 my ($self, $conn, $query) = @_;
2803 return QueryParser::Canonicalize::abstract_query2str_impl($query, 0, $OpenILS::Application::Storage::QParser);
2806 __PACKAGE__->register_method(
2807 api_name => "open-ils.storage.query_parser.abstract_query.canonicalize",
2809 method => "abstract_query2str",
2814 Abstract query parser object, with complete config data. For example input,
2815 see the 'abstract_query' part of the output of an API call like
2816 open-ils.search.biblio.multiclass.query, when called with the return_abstract
2820 return => { type => "string", desc => "String representation of abstract query object" }
2824 sub str2abstract_query {
2825 my ($self, $conn, $query, $qp_opts, $with_config) = @_;
2827 my %use_opts = ( # reasonable defaults? should these even be hardcoded here?
2829 superpage_size => 1000,
2830 core_limit => 25000,
2832 (ref $opts eq 'HASH' ? %$opts : ())
2837 # grab the query parser and initialize it
2838 my $parser = $OpenILS::Application::Storage::QParser;
2841 _initialize_parser($parser) unless $parser->initialization_complete;
2843 my $query = $parser->new(%use_opts)->parse;
2845 return $query->parse_tree->to_abstract_query(with_config => $with_config);
2848 __PACKAGE__->register_method(
2849 api_name => "open-ils.storage.query_parser.abstract_query.from_string",
2851 method => "str2abstract_query",
2855 {desc => "Query", type => "string"},
2856 {desc => q/Arguments for initializing QueryParser (optional)/,
2858 {desc => q/Flag enabling inclusion of QP config in returned object (optional, default false)/,
2861 return => { type => "object", desc => "abstract representation of query parser query" }
2865 my @available_statuses_cache;
2866 sub available_statuses {
2867 if (!scalar(@available_statuses_cache)) {
2868 @available_statuses_cache = map { $_->id } config::copy_status->search_where({is_available => 't'});
2870 return @available_statuses_cache;
2873 sub query_parser_fts {
2879 # grab the query parser and initialize it
2880 my $parser = $OpenILS::Application::Storage::QParser;
2883 _initialize_parser($parser) unless $parser->initialization_complete;
2885 # populate the locale/language map
2886 if (!$locale_map{COMPLETE}) {
2888 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2889 for my $locale ( @locales ) {
2890 $locale_map{lc($locale->code)} = $locale->marc_code;
2892 $locale_map{COMPLETE} = 1;
2896 # I hope we have a query!
2897 if (! $args{query} ) {
2898 die "No query was passed to ".$self->api_name;
2901 my $default_CD_modifiers = OpenSRF::Utils::SettingsClient->new->config_value(
2902 apps => 'open-ils.search' => app_settings => 'default_CD_modifiers'
2905 # Protect against empty / missing default_CD_modifiers setting
2906 if ($default_CD_modifiers and !ref($default_CD_modifiers)) {
2907 $args{query} = "$default_CD_modifiers $args{query}";
2910 my $simple_plan = $args{_simple_plan};
2911 # remove bad chunks of the %args hash
2912 for my $bad ( grep { /^_/ } keys(%args)) {
2913 delete($args{$bad});
2917 # parse the query and supply any query-level %arg-based defaults
2918 # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2919 my $query = $parser->new( %args )->parse;
2921 my $config = OpenSRF::Utils::SettingsClient->new();
2923 # set the locale-based default preferred location
2924 if (!$query->parse_tree->find_filter('preferred_language')) {
2925 $parser->default_preferred_language( $args{preferred_language} );
2927 if (!$parser->default_preferred_language) {
2928 my $ses_locale = $client->session ? $client->session->session_locale : '';
2929 $parser->default_preferred_language( $locale_map{ lc($ses_locale) } );
2932 if (!$parser->default_preferred_language) { # still nothing...
2933 my $tmp_dpl = $config->config_value(
2934 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2935 ) || $config->config_value(
2936 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2939 $parser->default_preferred_language( $tmp_dpl )
2944 # set the global default language multiplier
2945 if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2948 if ($tmp_dplw = $args{preferred_language_weight} || $args{preferred_language_multiplier} ) {
2949 $parser->default_preferred_language_multiplier($tmp_dplw);
2952 $tmp_dplw = $config->config_value(
2953 apps => 'open-ils.search' => app_settings => 'default_preferred_language_weight'
2954 ) || $config->config_value(
2955 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2958 $parser->default_preferred_language_multiplier( $tmp_dplw );
2962 # gather the site, if one is specified, defaulting to the in-query version
2963 my $ou = $args{org_unit};
2964 if (my ($filter) = $query->parse_tree->find_filter('site')) {
2965 $ou = $filter->args->[0] if (@{$filter->args});
2967 $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^(-)?\d+$/);
2969 # gather lasso, as with $ou
2970 my $lasso = $args{lasso};
2971 if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
2972 $lasso = $filter->args->[0] if (@{$filter->args});
2974 $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
2975 $lasso = -$lasso if ($lasso);
2978 # # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
2979 # # gather user lasso, as with $ou and lasso
2980 # my $mylasso = $args{my_lasso};
2981 # if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
2982 # $mylasso = $filter->args->[0] if (@{$filter->args});
2984 # $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
2987 # if we have a lasso, go with that, otherwise ... ou
2988 $ou = $lasso if ($lasso);
2990 # gather the preferred OU, if one is specified, as with $ou
2991 my $pref_ou = $args{pref_ou};
2992 $log->info("pref_ou = $pref_ou");
2993 if (my ($filter) = $query->parse_tree->find_filter('pref_ou')) {
2994 $pref_ou = $filter->args->[0] if (@{$filter->args});
2996 $pref_ou = actor::org_unit->search( { shortname => $pref_ou } )->next->id if ($pref_ou and $pref_ou !~ /^(-)?\d+$/);
2998 # get the default $ou if we have nothing
2999 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
3002 # 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
3003 # gather the depth, if one is specified, defaulting to the in-query version
3004 my $depth = $args{depth};
3005 if (my ($filter) = $query->parse_tree->find_filter('depth')) {
3006 $depth = $filter->args->[0] if (@{$filter->args});
3008 $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
3011 # gather the limit or default to 10
3012 my $limit = $args{check_limit};
3013 if (my ($filter) = $query->parse_tree->find_filter('limit')) {
3014 $limit = $filter->args->[0] if (@{$filter->args});
3016 if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
3017 $limit = $filter->args->[0] if (@{$filter->args});
3021 # gather the offset or default to 0
3022 my $offset = $args{skip_check} || $args{offset};
3023 if (my ($filter) = $query->parse_tree->find_filter('offset')) {
3024 $offset = $filter->args->[0] if (@{$filter->args});
3026 if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
3027 $offset = $filter->args->[0] if (@{$filter->args});
3031 # gather the estimation strategy or default to inclusion
3032 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
3033 if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
3034 $estimation_strategy = $filter->args->[0] if (@{$filter->args});
3038 # gather the estimation strategy or default to inclusion
3039 my $core_limit = $args{core_limit};
3040 if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
3041 $core_limit = $filter->args->[0] if (@{$filter->args});
3045 # gather statuses, and then forget those if we have an #available modifier
3047 if ($query->parse_tree->find_modifier('available')) {
3048 @statuses = available_statuses();
3049 } elsif (my ($filter) = $query->parse_tree->find_filter('statuses')) {
3050 @statuses = @{$filter->args} if (@{$filter->args});
3056 if (my ($filter) = $query->parse_tree->find_filter('locations')) {
3057 @location = @{$filter->args} if (@{$filter->args});
3060 # gather location_groups
3061 if (my ($filter) = $query->parse_tree->find_filter('location_groups')) {
3062 my @loc_groups = ();
3063 @loc_groups = @{$filter->args} if (@{$filter->args});
3065 # collect the mapped locations and add them to the locations() filter
3068 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3069 my $maps = $cstore->request(
3070 'open-ils.cstore.direct.asset.copy_location_group_map.search.atomic',
3071 {lgroup => \@loc_groups})->gather(1);
3073 push(@location, $_->location) for @$maps;
3078 my $param_check = $limit || $query->superpage_size || 'NULL';
3079 my $param_offset = $offset || 'NULL';
3080 my $param_limit = $core_limit || 'NULL';
3082 my $sp = $query->superpage || 1;
3084 $param_offset = ($sp - 1) * $sp_size;
3087 my $param_search_ou = $ou;
3088 my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
3089 # my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
3090 my $param_core_query = $query->parse_tree->toSQL;
3091 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
3092 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
3093 my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
3094 my $deleted_search = ($query->parse_tree->find_modifier('deleted')) ? "'t'" : "'f'";
3095 my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
3096 my $param_pref_ou = $pref_ou || 'NULL';
3098 # my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3099 # SELECT * -- bib search: $args{query}
3100 # FROM search.query_parser_fts(
3101 # $param_search_ou\:\:INT,
3102 # $param_depth\:\:INT,
3103 # $param_core_query\:\:TEXT,
3104 # $param_statuses\:\:INT[],
3105 # $param_locations\:\:INT[],
3106 # $param_offset\:\:INT,
3107 # $param_check\:\:INT,
3108 # $param_limit\:\:INT,
3109 # $metarecord\:\:BOOL,
3111 # $deleted_search\:\:BOOL,
3112 # $param_pref_ou\:\:INT
3116 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3117 -- bib search: $args{query}
3123 my $recs = $sth->fetchall_arrayref({});
3124 my $summary_row = pop @$recs;
3126 my $total = $$summary_row{total};
3127 my $checked = $$summary_row{checked};
3128 my $visible = $$summary_row{visible};
3129 my $deleted = $$summary_row{deleted};
3130 my $excluded = $$summary_row{excluded};
3132 delete $$summary_row{id};
3133 delete $$summary_row{rel};
3134 delete $$summary_row{record};
3135 delete $$summary_row{badges};
3136 delete $$summary_row{popularity};
3138 if (defined($simple_plan)) {
3139 $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3141 $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3144 if ($args{return_query}) {
3145 $$summary_row{query_struct} = $query->parse_tree->to_abstract_query();
3146 $$summary_row{canonicalized_query} = QueryParser::Canonicalize::abstract_query2str_impl($$summary_row{query_struct}, 0, $parser);
3149 $client->respond( $summary_row );
3151 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $visible.",DEBUG);
3153 for my $rec (@$recs) {
3154 delete $$rec{checked};
3155 delete $$rec{visible};
3156 delete $$rec{excluded};
3157 delete $$rec{deleted};
3158 delete $$rec{total};
3159 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3161 $client->respond( $rec );
3165 __PACKAGE__->register_method(
3166 api_name => "open-ils.storage.query_parser_search",
3168 method => 'query_parser_fts',
3176 sub query_parser_fts_wrapper {
3181 $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3182 # grab the query parser and initialize it
3183 my $parser = $OpenILS::Application::Storage::QParser;
3186 _initialize_parser($parser) unless $parser->initialization_complete;
3188 $args{searches} ||= {};
3189 if (!scalar(keys(%{$args{searches}})) && !$args{query}) {
3190 die "No search arguments were passed to ".$self->api_name;
3193 $top_org ||= actor::org_unit->search( { parent_ou => undef } )->next;
3195 my $base_query = $args{query} || '';
3196 if (scalar(keys(%{$args{searches}}))) {
3197 $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3198 for my $sclass ( keys %{$args{searches}} ) {
3199 $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3200 $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3204 my $query = $base_query;
3205 $log->debug("Full base query: $base_query", DEBUG);
3207 $query = "$args{facets} $query" if ($args{facets});
3209 if (!$locale_map{COMPLETE}) {
3211 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3212 for my $locale ( @locales ) {
3213 $locale_map{lc($locale->code)} = $locale->marc_code;
3215 $locale_map{COMPLETE} = 1;
3219 my $base_plan = $parser->new( query => $base_query )->parse;
3221 $query = "preferred_language($args{preferred_language}) $query"
3222 if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3223 $query = "preferred_language_weight($args{preferred_language_weight}) $query"
3224 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'));
3228 if (!$base_plan->parse_tree->find_filter('badge_orgs')) {
3229 # supply a suitable badge_orgs filter unless user has
3230 # explicitly supplied one
3233 my @lg_id_list = (); # We must define the variable with a static value
3234 # because an idomatic my+set causes the previous
3235 # value is remembered via closure.
3237 @lg_id_list = @{$args{location_groups}} if (ref $args{location_groups});
3239 my ($lg_filter) = $base_plan->parse_tree->find_filter('location_groups');
3240 @lg_id_list = @{$lg_filter->args} if ($lg_filter && @{$lg_filter->args});
3244 for my $lg ( grep { /^\d+$/ } @lg_id_list ) {
3245 my $lg_obj = asset::copy_location_group->retrieve($lg);
3246 next unless $lg_obj;
3248 push(@borg_list, ''.$lg_obj->owner);
3250 $borgs = join(',', @borg_list) if @borg_list;
3254 my ($site_filter) = $base_plan->parse_tree->find_filter('site');
3255 if ($site_filter && @{$site_filter->args}) {
3256 $site = $top_org if ($site_filter->args->[0] eq '-');
3257 $site = $top_org if ($site_filter->args->[0] eq $top_org->shortname);
3258 $site = actor::org_unit->search( { shortname => $site_filter->args->[0] })->next unless ($site);
3259 } elsif ($args{org_unit}) {
3260 $site = $top_org if ($args{org_unit} eq '-');
3261 $site = $top_org if ($args{org_unit} eq $top_org->shortname);
3262 $site = actor::org_unit->search( { shortname => $args{org_unit} })->next unless ($site);
3268 $borgs = OpenSRF::AppSession->create( 'open-ils.cstore' )->request(
3269 'open-ils.cstore.json_query.atomic',
3270 { from => [ 'actor.org_unit_ancestors', $site->id ] }
3273 if (ref $borgs && @$borgs) {
3274 $borgs = join(',', map { $_->{'id'} } @$borgs);
3282 # gather the limit or default to 10
3283 my $limit = delete($args{check_limit}) || $base_plan->superpage_size;
3284 if (my ($filter) = $base_plan->parse_tree->find_filter('limit')) {
3285 $limit = $filter->args->[0] if (@{$filter->args});
3287 if (my ($filter) = $base_plan->parse_tree->find_filter('check_limit')) {
3288 $limit = $filter->args->[0] if (@{$filter->args});
3291 # gather the offset or default to 0
3292 my $offset = delete($args{skip_check}) || delete($args{offset}) || 0;
3293 if (my ($filter) = $base_plan->parse_tree->find_filter('offset')) {
3294 $offset = $filter->args->[0] if (@{$filter->args});
3296 if (my ($filter) = $base_plan->parse_tree->find_filter('skip_check')) {
3297 $offset = $filter->args->[0] if (@{$filter->args});
3301 $query = "check_limit($limit) $query" if (defined $limit);
3302 $query = "skip_check($offset) $query" if (defined $offset);
3303 $query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
3304 $query = "badge_orgs($borgs) $query" if ($borgs);
3306 # XXX All of the following, down to the 'return' is basically dead code. someone higher up should handle it
3307 $query = "site($args{org_unit}) $query" if ($args{org_unit});
3308 $query = "depth($args{depth}) $query" if (defined($args{depth}));
3309 $query = "sort($args{sort}) $query" if ($args{sort});
3310 $query = "core_limit($args{core_limit}) $query" if ($args{core_limit});
3311 # $query = "limit($args{limit}) $query" if ($args{limit});
3312 # $query = "skip_check($args{skip_check}) $query" if ($args{skip_check});
3313 $query = "superpage($args{superpage}) $query" if ($args{superpage});
3314 $query = "offset($args{offset}) $query" if ($args{offset});
3315 $query = "#metarecord $query" if ($self->api_name =~ /metabib/);
3316 $query = "from_metarecord($args{from_metarecord}) $query" if ($args{from_metarecord});
3317 $query = "#available $query" if ($args{available});
3318 $query = "#descending $query" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3319 $query = "#staff $query" if ($self->api_name =~ /staff/);
3320 $query = "before($args{before}) $query" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3321 $query = "after($args{after}) $query" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3322 $query = "during($args{during}) $query" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3323 $query = "between($args{between}[0],$args{between}[1]) $query"
3324 if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3327 my (@between,@statuses,@locations,@location_groups,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3329 # XXX legacy format and item type support
3330 if ($args{format}) {
3331 my ($t, $f) = split '-', $args{format};
3332 $args{item_type} = [ split '', $t ];
3333 $args{item_form} = [ split '', $f ];
3336 for my $filter ( qw/locations location_groups statuses audience language lit_form item_form item_type bib_level vr_format badges/ ) {
3337 if (my $s = $args{$filter}) {
3338 $s = [$s] if (!ref($s));
3340 my @filter_list = @$s;
3342 next if (@filter_list == 0);
3344 my $filter_string = join ',', @filter_list;
3345 $query = "$query $filter($filter_string)";
3349 $log->debug("Full QueryParser query: $query", DEBUG);
3351 return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan, return_query => $args{return_query} );
3353 __PACKAGE__->register_method(
3354 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
3356 method => 'query_parser_fts_wrapper',
3361 __PACKAGE__->register_method(
3362 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3364 method => 'query_parser_fts_wrapper',
3369 __PACKAGE__->register_method(
3370 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
3372 method => 'query_parser_fts_wrapper',
3377 __PACKAGE__->register_method(
3378 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3380 method => 'query_parser_fts_wrapper',