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.opac_visible_copies vc ON (br.id = vc.record)';
99 $copies_visible = '' if ($self->api_name =~ /staff/o);
101 my $copies_visible_count = ',COUNT(vc.id)';
102 $copies_visible_count = '' if ($self->api_name =~ /staff/o);
104 my $descendants = '';
106 $descendants = defined($depth) ?
107 ",actor.org_unit_descendants($org, $depth) d" :
108 ",actor.org_unit_descendants($org) d" ;
115 $copies_visible_count
116 FROM metabib.metarecord_source_map sm
117 JOIN biblio.record_entry br ON (sm.source = br.id AND NOT br.deleted)
118 LEFT JOIN metabib.record_sorter s ON (s.source = br.id AND s.attr = 'titlesort')
119 LEFT JOIN config.bib_source bs ON (br.source = bs.id)
122 WHERE sm.metarecord = ?
126 if ($copies_visible) {
127 $sql .= 'AND (bs.transcendant OR ';
129 $sql .= 'vc.circ_lib = d.id)';
131 $sql .= 'vc.id IS NOT NULL)'
133 $having = 'HAVING COUNT(vc.id) > 0';
141 s.value ASC NULLS LAST
144 my $ids = metabib::metarecord_source_map->db_Main->selectcol_arrayref($sql, {}, "$mr");
145 return $ids if ($self->api_name =~ /atomic$/o);
147 $client->respond( $_ ) for ( @$ids );
151 __PACKAGE__->register_method(
152 api_name => 'open-ils.storage.ordered.metabib.metarecord.records',
153 method => 'ordered_records_from_metarecord',
157 __PACKAGE__->register_method(
158 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff',
159 method => 'ordered_records_from_metarecord',
164 __PACKAGE__->register_method(
165 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.atomic',
166 method => 'ordered_records_from_metarecord',
170 __PACKAGE__->register_method(
171 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic',
172 method => 'ordered_records_from_metarecord',
177 # XXX: this subroutine and its two registered methods are marked for
178 # deprecation, as they do not work properly in 2.x (these tags are no longer
179 # normalized in mfr) and are not in known use
183 my $isxn = lc(shift());
187 $isxn =~ s/-//o if ($self->api_name =~ /isbn/o);
189 my $tag = ($self->api_name =~ /isbn/o) ? "'020' OR f.tag = '024'" : "'022'";
191 my $fr_table = metabib::full_rec->table;
192 my $bib_table = biblio::record_entry->table;
195 SELECT DISTINCT f.record
197 JOIN $bib_table b ON (b.id = f.record)
200 AND b.deleted IS FALSE
203 my $list = metabib::full_rec->db_Main->selectcol_arrayref($sql, {}, "$isxn%");
204 $client->respond($_) for (@$list);
207 __PACKAGE__->register_method(
208 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
209 method => 'isxn_search',
213 __PACKAGE__->register_method(
214 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.issn',
215 method => 'isxn_search',
220 sub metarecord_copy_count {
226 my $sm_table = metabib::metarecord_source_map->table;
227 my $rd_table = metabib::record_descriptor->table;
228 my $cn_table = asset::call_number->table;
229 my $cp_table = asset::copy->table;
230 my $br_table = biblio::record_entry->table;
231 my $src_table = config::bib_source->table;
232 my $cl_table = asset::copy_location->table;
233 my $cs_table = config::copy_status->table;
234 my $out_table = actor::org_unit_type->table;
236 my $descendants = "actor.org_unit_descendants(u.id)";
237 my $ancestors = "actor.org_unit_ancestors(?) u JOIN $out_table t ON (u.ou_type = t.id)";
239 if ($args{org_unit} < 0) {
240 $args{org_unit} *= -1;
241 $ancestors = "(select org_unit as id from actor.org_lasso_map where lasso = ?) u CROSS JOIN (SELECT -1 AS depth) t";
244 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';
245 $copies_visible = '' if ($self->api_name =~ /staff/o);
247 my (@types,@forms,@blvl);
248 my ($t_filter, $f_filter, $b_filter) = ('','','');
251 my ($t, $f, $b) = split '-', $args{format};
252 @types = split '', $t;
253 @forms = split '', $f;
254 @blvl = split '', $b;
257 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
261 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
265 $b_filter .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
275 JOIN $cn_table cn ON (cn.record = r.source)
276 JOIN $rd_table rd ON (cn.record = rd.record)
277 JOIN $cp_table cp ON (cn.id = cp.call_number)
278 JOIN $cs_table cs ON (cp.status = cs.id)
279 JOIN $cl_table cl ON (cp.location = cl.id)
280 JOIN $descendants a ON (cp.circ_lib = a.id)
281 WHERE r.metarecord = ?
282 AND cn.deleted IS FALSE
283 AND cp.deleted IS FALSE
293 JOIN $cn_table cn ON (cn.record = r.source)
294 JOIN $rd_table rd ON (cn.record = rd.record)
295 JOIN $cp_table cp ON (cn.id = cp.call_number)
296 JOIN $cs_table cs ON (cp.status = cs.id)
297 JOIN $cl_table cl ON (cp.location = cl.id)
298 JOIN $descendants a ON (cp.circ_lib = a.id)
299 WHERE r.metarecord = ?
300 AND cp.status IN (0,7,12)
301 AND cn.deleted IS FALSE
302 AND cp.deleted IS FALSE
312 JOIN $cn_table cn ON (cn.record = r.source)
313 JOIN $rd_table rd ON (cn.record = rd.record)
314 JOIN $cp_table cp ON (cn.id = cp.call_number)
315 JOIN $cs_table cs ON (cp.status = cs.id)
316 JOIN $cl_table cl ON (cp.location = cl.id)
317 WHERE r.metarecord = ?
318 AND cn.deleted IS FALSE
319 AND cp.deleted IS FALSE
320 AND cp.opac_visible IS TRUE
321 AND cs.opac_visible IS TRUE
322 AND cl.opac_visible IS TRUE
331 JOIN $br_table br ON (br.id = r.source)
332 JOIN $src_table src ON (src.id = br.source)
333 WHERE r.metarecord = ?
334 AND src.transcendant IS TRUE
342 my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
343 $sth->execute( ''.$args{metarecord},
347 ''.$args{metarecord},
351 ''.$args{metarecord},
355 ''.$args{metarecord},
359 while ( my $row = $sth->fetchrow_hashref ) {
360 $client->respond( $row );
364 __PACKAGE__->register_method(
365 api_name => 'open-ils.storage.metabib.metarecord.copy_count',
366 method => 'metarecord_copy_count',
371 __PACKAGE__->register_method(
372 api_name => 'open-ils.storage.metabib.metarecord.copy_count.staff',
373 method => 'metarecord_copy_count',
379 sub biblio_multi_search_full_rec {
384 my $class_join = $args{class_join} || 'AND';
385 my $limit = $args{limit} || 100;
386 my $offset = $args{offset} || 0;
387 my $sort = $args{'sort'};
388 my $sort_dir = $args{sort_dir} || 'DESC';
393 for my $arg (@{ $args{searches} }) {
394 my $term = $$arg{term};
395 my $limiters = $$arg{restrict};
397 my ($index_col) = metabib::full_rec->columns('FTS');
398 $index_col ||= 'value';
399 my $search_table = metabib::full_rec->table;
401 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
403 my $fts_where = $fts->sql_where_clause();
404 my @fts_ranks = $fts->fts_rank;
406 my $rank = join(' + ', @fts_ranks);
409 for my $limit (@$limiters) {
410 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
411 # MARC control field; mfr.subfield is NULL
412 push @wheres, "( tag = ? AND $fts_where )";
413 push @binds, $$limit{tag};
414 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
416 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
417 push @binds, $$limit{tag}, $$limit{subfield};
418 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
421 my $where = join(' OR ', @wheres);
423 push @selects, "SELECT record, AVG($rank) as sum FROM $search_table WHERE $where GROUP BY record";
427 my $descendants = defined($args{depth}) ?
428 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
429 "actor.org_unit_descendants($args{org_unit})" ;
432 my $metabib_record_descriptor = metabib::record_descriptor->table;
433 my $metabib_full_rec = metabib::full_rec->table;
434 my $asset_call_number_table = asset::call_number->table;
435 my $asset_copy_table = asset::copy->table;
436 my $cs_table = config::copy_status->table;
437 my $cl_table = asset::copy_location->table;
438 my $br_table = biblio::record_entry->table;
440 my $cj = 'HAVING COUNT(x.record) = ' . scalar(@selects) if ($class_join eq 'AND');
442 '(SELECT x.record, sum(x.sum) FROM (('.
443 join(') UNION ALL (', @selects).
444 ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
446 my $has_vols = 'AND cn.owning_lib = d.id';
447 my $has_copies = 'AND cp.call_number = cn.id';
448 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';
450 if ($self->api_name =~ /staff/o) {
451 $copies_visible = '';
452 $has_copies = '' if ($ou_type == 0);
453 $has_vols = '' if ($ou_type == 0);
456 my ($t_filter, $f_filter) = ('','');
457 my ($a_filter, $l_filter, $lf_filter) = ('','','');
460 if (my $a = $args{audience}) {
461 $a = [$a] if (!ref($a));
464 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
469 if (my $l = $args{language}) {
470 $l = [$l] if (!ref($l));
473 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
478 if (my $f = $args{lit_form}) {
479 $f = [$f] if (!ref($f));
482 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
483 push @binds, @lit_form;
487 if (my $f = $args{item_form}) {
488 $f = [$f] if (!ref($f));
491 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
496 if (my $t = $args{item_type}) {
497 $t = [$t] if (!ref($t));
500 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
507 my ($t, $f) = split '-', $args{format};
508 my @types = split '', $t;
509 my @forms = split '', $f;
511 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
516 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
519 push @binds, @types, @forms;
522 my $relevance = 'sum(f.sum)';
523 $relevance = 1 if (!$copies_visible);
525 my $string_default_sort = 'zzzz';
526 $string_default_sort = 'AAAA' if ($sort_dir =~ /^DESC$/i);
528 my $number_default_sort = '9999';
529 $number_default_sort = '0000' if ($sort_dir =~/^DESC$/i);
531 my $rank = $relevance;
532 if (lc($sort) eq 'pubdate') {
535 SELECT COALESCE(SUBSTRING(MAX(frp.value) FROM E'\\\\d{4}'), '$number_default_sort')::INT
536 FROM $metabib_full_rec frp
537 WHERE frp.record = f.record
539 AND frp.subfield = 'c'
543 } elsif (lc($sort) eq 'create_date') {
545 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
547 } elsif (lc($sort) eq 'edit_date') {
549 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
551 } elsif (lc($sort) =~ /^title/i) {
554 SELECT COALESCE(LTRIM(SUBSTR(MAX(frt.value), COALESCE(SUBSTRING(MAX(frt.ind2) FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
555 FROM $metabib_full_rec frt
556 WHERE frt.record = f.record
558 AND frt.subfield = 'a'
562 } elsif (lc($sort) =~ /^author/i) {
565 SELECT COALESCE(LTRIM(MAX(query.value)), '$string_default_sort')
568 FROM $metabib_full_rec fra
569 WHERE fra.record = f.record
570 AND fra.tag LIKE '1%'
571 AND fra.subfield = 'a'
572 ORDER BY fra.tag::text::int
581 my $rd_join = $use_rd ? "$metabib_record_descriptor rd," : '';
582 my $rd_filter = $use_rd ? 'AND rd.record = f.record' : '';
584 if ($copies_visible) {
586 SELECT f.record, $relevance, count(DISTINCT cp.id), $rank
587 FROM $search_table f,
588 $asset_call_number_table cn,
589 $asset_copy_table cp,
595 WHERE br.id = f.record
596 AND cn.record = f.record
597 AND cp.status = cs.id
598 AND cp.location = cl.id
599 AND br.deleted IS FALSE
600 AND cn.deleted IS FALSE
601 AND cp.deleted IS FALSE
611 GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
612 ORDER BY 4 $sort_dir,3 DESC
616 SELECT f.record, 1, 1, $rank
617 FROM $search_table f,
620 WHERE br.id = f.record
621 AND br.deleted IS FALSE
634 $log->debug("Search SQL :: [$select]",DEBUG);
636 my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
637 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
640 $max = 1 if (!@$recs);
642 $max = $$_[1] if ($$_[1] > $max);
646 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
647 next unless ($$rec[0]);
648 my ($rid,$rank,$junk,$skip) = @$rec;
649 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
653 __PACKAGE__->register_method(
654 api_name => 'open-ils.storage.biblio.full_rec.multi_search',
655 method => 'biblio_multi_search_full_rec',
660 __PACKAGE__->register_method(
661 api_name => 'open-ils.storage.biblio.full_rec.multi_search.staff',
662 method => 'biblio_multi_search_full_rec',
668 sub search_full_rec {
674 my $term = $args{term};
675 my $limiters = $args{restrict};
677 my ($index_col) = metabib::full_rec->columns('FTS');
678 $index_col ||= 'value';
679 my $search_table = metabib::full_rec->table;
681 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
683 my $fts_where = $fts->sql_where_clause();
684 my @fts_ranks = $fts->fts_rank;
686 my $rank = join(' + ', @fts_ranks);
690 for my $limit (@$limiters) {
691 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
692 # MARC control field; mfr.subfield is NULL
693 push @wheres, "( tag = ? AND $fts_where )";
694 push @binds, $$limit{tag};
695 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
697 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
698 push @binds, $$limit{tag}, $$limit{subfield};
699 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
702 my $where = join(' OR ', @wheres);
704 my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
706 $log->debug("Search SQL :: [$select]",DEBUG);
708 my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
709 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
711 $client->respond($_) for (@$recs);
714 __PACKAGE__->register_method(
715 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
716 method => 'search_full_rec',
721 __PACKAGE__->register_method(
722 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
723 method => 'search_full_rec',
730 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
731 sub search_class_fts {
736 my $term = $args{term};
737 my $ou = $args{org_unit};
738 my $ou_type = $args{depth};
739 my $limit = $args{limit};
740 my $offset = $args{offset};
742 my $limit_clause = '';
743 my $offset_clause = '';
745 $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
746 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
749 my ($t_filter, $f_filter) = ('','');
752 my ($t, $f) = split '-', $args{format};
753 @types = split '', $t;
754 @forms = split '', $f;
756 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
760 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
766 my $descendants = defined($ou_type) ?
767 "actor.org_unit_descendants($ou, $ou_type)" :
768 "actor.org_unit_descendants($ou)";
770 my $class = $self->{cdbi};
771 my $search_table = $class->table;
773 my $metabib_record_descriptor = metabib::record_descriptor->table;
774 my $metabib_metarecord = metabib::metarecord->table;
775 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
776 my $asset_call_number_table = asset::call_number->table;
777 my $asset_copy_table = asset::copy->table;
778 my $cs_table = config::copy_status->table;
779 my $cl_table = asset::copy_location->table;
781 my ($index_col) = $class->columns('FTS');
782 $index_col ||= 'value';
784 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
785 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
787 my $fts_where = $fts->sql_where_clause;
788 my @fts_ranks = $fts->fts_rank;
790 my $rank = join(' + ', @fts_ranks);
792 my $has_vols = 'AND cn.owning_lib = d.id';
793 my $has_copies = 'AND cp.call_number = cn.id';
794 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';
796 my $visible_count = ', count(DISTINCT cp.id)';
797 my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
799 if ($self->api_name =~ /staff/o) {
800 $copies_visible = '';
801 $visible_count_test = '';
802 $has_copies = '' if ($ou_type == 0);
803 $has_vols = '' if ($ou_type == 0);
806 my $rank_calc = <<" RANK";
808 * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
809 * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
810 * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
811 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
814 $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
816 if ($copies_visible) {
818 SELECT m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
819 FROM $search_table f,
820 $metabib_metarecord_source_map_table m,
821 $asset_call_number_table cn,
822 $asset_copy_table cp,
825 $metabib_record_descriptor rd,
828 AND m.source = f.source
829 AND cn.record = m.source
830 AND rd.record = m.source
831 AND cp.status = cs.id
832 AND cp.location = cl.id
838 GROUP BY 1 $visible_count_test
840 $limit_clause $offset_clause
844 SELECT m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
845 FROM $search_table f,
846 $metabib_metarecord_source_map_table m,
847 $metabib_record_descriptor rd
849 AND m.source = f.source
850 AND rd.record = m.source
855 $limit_clause $offset_clause
859 $log->debug("Field Search SQL :: [$select]",DEBUG);
861 my $SQLstring = join('%',$fts->words);
862 my $REstring = join('\\s+',$fts->words);
863 my $first_word = ($fts->words)[0].'%';
864 my $recs = ($self->api_name =~ /unordered/o) ?
865 $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
866 $class->db_Main->selectall_arrayref($select, {},
867 '%'.lc($SQLstring).'%', # phrase order match
868 lc($first_word), # first word match
869 '^\\s*'.lc($REstring).'\\s*/?\s*$', # full exact match
873 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
875 $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
879 for my $class ( qw/title author subject keyword series identifier/ ) {
880 __PACKAGE__->register_method(
881 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord",
882 method => 'search_class_fts',
885 cdbi => "metabib::${class}_field_entry",
888 __PACKAGE__->register_method(
889 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
890 method => 'search_class_fts',
893 cdbi => "metabib::${class}_field_entry",
896 __PACKAGE__->register_method(
897 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
898 method => 'search_class_fts',
901 cdbi => "metabib::${class}_field_entry",
904 __PACKAGE__->register_method(
905 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
906 method => 'search_class_fts',
909 cdbi => "metabib::${class}_field_entry",
914 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
915 sub search_class_fts_count {
920 my $term = $args{term};
921 my $ou = $args{org_unit};
922 my $ou_type = $args{depth};
923 my $limit = $args{limit} || 100;
924 my $offset = $args{offset} || 0;
926 my $descendants = defined($ou_type) ?
927 "actor.org_unit_descendants($ou, $ou_type)" :
928 "actor.org_unit_descendants($ou)";
931 my ($t_filter, $f_filter) = ('','');
934 my ($t, $f) = split '-', $args{format};
935 @types = split '', $t;
936 @forms = split '', $f;
938 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
942 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
947 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
949 my $class = $self->{cdbi};
950 my $search_table = $class->table;
952 my $metabib_record_descriptor = metabib::record_descriptor->table;
953 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
954 my $asset_call_number_table = asset::call_number->table;
955 my $asset_copy_table = asset::copy->table;
956 my $cs_table = config::copy_status->table;
957 my $cl_table = asset::copy_location->table;
959 my ($index_col) = $class->columns('FTS');
960 $index_col ||= 'value';
962 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
964 my $fts_where = $fts->sql_where_clause;
966 my $has_vols = 'AND cn.owning_lib = d.id';
967 my $has_copies = 'AND cp.call_number = cn.id';
968 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';
969 if ($self->api_name =~ /staff/o) {
970 $copies_visible = '';
971 $has_vols = '' if ($ou_type == 0);
972 $has_copies = '' if ($ou_type == 0);
975 # XXX test an "EXISTS version of descendant checking...
977 if ($copies_visible) {
979 SELECT count(distinct m.metarecord)
980 FROM $search_table f,
981 $metabib_metarecord_source_map_table m,
982 $metabib_metarecord_source_map_table mr,
983 $asset_call_number_table cn,
984 $asset_copy_table cp,
987 $metabib_record_descriptor rd,
990 AND mr.source = f.source
991 AND mr.metarecord = m.metarecord
992 AND cn.record = m.source
993 AND rd.record = m.source
994 AND cp.status = cs.id
995 AND cp.location = cl.id
1004 SELECT count(distinct m.metarecord)
1005 FROM $search_table f,
1006 $metabib_metarecord_source_map_table m,
1007 $metabib_metarecord_source_map_table mr,
1008 $metabib_record_descriptor rd
1010 AND mr.source = f.source
1011 AND mr.metarecord = m.metarecord
1012 AND rd.record = m.source
1018 $log->debug("Field Search Count SQL :: [$select]",DEBUG);
1020 my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1022 $log->debug("Count Search yielded $recs results.",DEBUG);
1027 for my $class ( qw/title author subject keyword series identifier/ ) {
1028 __PACKAGE__->register_method(
1029 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
1030 method => 'search_class_fts_count',
1033 cdbi => "metabib::${class}_field_entry",
1036 __PACKAGE__->register_method(
1037 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
1038 method => 'search_class_fts_count',
1041 cdbi => "metabib::${class}_field_entry",
1047 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1048 sub postfilter_search_class_fts {
1053 my $term = $args{term};
1054 my $sort = $args{'sort'};
1055 my $sort_dir = $args{sort_dir} || 'DESC';
1056 my $ou = $args{org_unit};
1057 my $ou_type = $args{depth};
1058 my $limit = $args{limit} || 10;
1059 my $visibility_limit = $args{visibility_limit} || 5000;
1060 my $offset = $args{offset} || 0;
1062 my $outer_limit = 1000;
1064 my $limit_clause = '';
1065 my $offset_clause = '';
1067 $limit_clause = "LIMIT $outer_limit";
1068 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1070 my (@types,@forms,@lang,@aud,@lit_form);
1071 my ($t_filter, $f_filter) = ('','');
1072 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1073 my ($ot_filter, $of_filter) = ('','');
1074 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1076 if (my $a = $args{audience}) {
1077 $a = [$a] if (!ref($a));
1080 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1081 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1084 if (my $l = $args{language}) {
1085 $l = [$l] if (!ref($l));
1088 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1089 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1092 if (my $f = $args{lit_form}) {
1093 $f = [$f] if (!ref($f));
1096 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1097 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1100 if ($args{format}) {
1101 my ($t, $f) = split '-', $args{format};
1102 @types = split '', $t;
1103 @forms = split '', $f;
1105 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1106 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1110 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1111 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1116 my $descendants = defined($ou_type) ?
1117 "actor.org_unit_descendants($ou, $ou_type)" :
1118 "actor.org_unit_descendants($ou)";
1120 my $class = $self->{cdbi};
1121 my $search_table = $class->table;
1123 my $metabib_full_rec = metabib::full_rec->table;
1124 my $metabib_record_descriptor = metabib::record_descriptor->table;
1125 my $metabib_metarecord = metabib::metarecord->table;
1126 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1127 my $asset_call_number_table = asset::call_number->table;
1128 my $asset_copy_table = asset::copy->table;
1129 my $cs_table = config::copy_status->table;
1130 my $cl_table = asset::copy_location->table;
1131 my $br_table = biblio::record_entry->table;
1133 my ($index_col) = $class->columns('FTS');
1134 $index_col ||= 'value';
1136 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1138 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1140 my $SQLstring = join('%',map { lc($_) } $fts->words);
1141 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1142 my $first_word = lc(($fts->words)[0]).'%';
1144 my $fts_where = $fts->sql_where_clause;
1145 my @fts_ranks = $fts->fts_rank;
1148 $bonus{'metabib::identifier_field_entry'} =
1149 $bonus{'metabib::keyword_field_entry'} = [
1150 { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring }
1153 $bonus{'metabib::title_field_entry'} =
1154 $bonus{'metabib::series_field_entry'} = [
1155 { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1156 { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1157 @{ $bonus{'metabib::keyword_field_entry'} }
1160 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1161 $bonus_list ||= '1';
1163 my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1165 my $relevance = join(' + ', @fts_ranks);
1166 $relevance = <<" RANK";
1167 (SUM( ( $relevance ) * ( $bonus_list ) )/COUNT(m.source))
1170 my $string_default_sort = 'zzzz';
1171 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1173 my $number_default_sort = '9999';
1174 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1176 my $rank = $relevance;
1177 if (lc($sort) eq 'pubdate') {
1180 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1181 FROM $metabib_full_rec frp
1182 WHERE frp.record = mr.master_record
1184 AND frp.subfield = 'c'
1188 } elsif (lc($sort) eq 'create_date') {
1190 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1192 } elsif (lc($sort) eq 'edit_date') {
1194 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1196 } elsif (lc($sort) eq 'title') {
1199 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1200 FROM $metabib_full_rec frt
1201 WHERE frt.record = mr.master_record
1203 AND frt.subfield = 'a'
1207 } elsif (lc($sort) eq 'author') {
1210 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1211 FROM $metabib_full_rec fra
1212 WHERE fra.record = mr.master_record
1213 AND fra.tag LIKE '1%'
1214 AND fra.subfield = 'a'
1215 ORDER BY fra.tag::text::int
1223 my $select = <<" SQL";
1224 SELECT m.metarecord,
1226 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1228 FROM $search_table f,
1229 $metabib_metarecord_source_map_table m,
1230 $metabib_metarecord_source_map_table smrs,
1231 $metabib_metarecord mr,
1232 $metabib_record_descriptor rd
1234 AND smrs.metarecord = mr.id
1235 AND m.source = f.source
1236 AND m.metarecord = mr.id
1237 AND rd.record = smrs.source
1243 GROUP BY m.metarecord
1244 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1245 LIMIT $visibility_limit
1252 FROM $asset_call_number_table cn,
1253 $metabib_metarecord_source_map_table mrs,
1254 $asset_copy_table cp,
1259 $metabib_record_descriptor ord,
1261 WHERE mrs.metarecord = s.metarecord
1262 AND br.id = mrs.source
1263 AND cn.record = mrs.source
1264 AND cp.status = cs.id
1265 AND cp.location = cl.id
1266 AND cn.owning_lib = d.id
1267 AND cp.call_number = cn.id
1268 AND cp.opac_visible IS TRUE
1269 AND cs.opac_visible IS TRUE
1270 AND cl.opac_visible IS TRUE
1271 AND d.opac_visible IS TRUE
1272 AND br.active IS TRUE
1273 AND br.deleted IS FALSE
1274 AND ord.record = mrs.source
1280 ORDER BY 4 $sort_dir
1282 } elsif ($self->api_name !~ /staff/o) {
1289 FROM $asset_call_number_table cn,
1290 $metabib_metarecord_source_map_table mrs,
1291 $asset_copy_table cp,
1296 $metabib_record_descriptor ord
1298 WHERE mrs.metarecord = s.metarecord
1299 AND br.id = mrs.source
1300 AND cn.record = mrs.source
1301 AND cp.status = cs.id
1302 AND cp.location = cl.id
1303 AND cp.circ_lib = d.id
1304 AND cp.call_number = cn.id
1305 AND cp.opac_visible IS TRUE
1306 AND cs.opac_visible IS TRUE
1307 AND cl.opac_visible IS TRUE
1308 AND d.opac_visible IS TRUE
1309 AND br.active IS TRUE
1310 AND br.deleted IS FALSE
1311 AND ord.record = mrs.source
1319 ORDER BY 4 $sort_dir
1328 FROM $asset_call_number_table cn,
1329 $asset_copy_table cp,
1330 $metabib_metarecord_source_map_table mrs,
1333 $metabib_record_descriptor ord
1335 WHERE mrs.metarecord = s.metarecord
1336 AND br.id = mrs.source
1337 AND cn.record = mrs.source
1338 AND cn.id = cp.call_number
1339 AND br.deleted IS FALSE
1340 AND cn.deleted IS FALSE
1341 AND ord.record = mrs.source
1342 AND ( cn.owning_lib = d.id
1343 OR ( cp.circ_lib = d.id
1344 AND cp.deleted IS FALSE
1356 FROM $asset_call_number_table cn,
1357 $metabib_metarecord_source_map_table mrs,
1358 $metabib_record_descriptor ord
1359 WHERE mrs.metarecord = s.metarecord
1360 AND cn.record = mrs.source
1361 AND ord.record = mrs.source
1369 ORDER BY 4 $sort_dir
1374 $log->debug("Field Search SQL :: [$select]",DEBUG);
1376 my $recs = $class->db_Main->selectall_arrayref(
1378 (@bonus_values > 0 ? @bonus_values : () ),
1379 ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1380 @types, @forms, @aud, @lang, @lit_form,
1381 @types, @forms, @aud, @lang, @lit_form,
1382 ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1384 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1387 $max = 1 if (!@$recs);
1389 $max = $$_[1] if ($$_[1] > $max);
1392 my $count = scalar(@$recs);
1393 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1394 my ($mrid,$rank,$skip) = @$rec;
1395 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1400 for my $class ( qw/title author subject keyword series identifier/ ) {
1401 __PACKAGE__->register_method(
1402 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1403 method => 'postfilter_search_class_fts',
1406 cdbi => "metabib::${class}_field_entry",
1409 __PACKAGE__->register_method(
1410 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1411 method => 'postfilter_search_class_fts',
1414 cdbi => "metabib::${class}_field_entry",
1421 my $_cdbi = { title => "metabib::title_field_entry",
1422 author => "metabib::author_field_entry",
1423 subject => "metabib::subject_field_entry",
1424 keyword => "metabib::keyword_field_entry",
1425 series => "metabib::series_field_entry",
1426 identifier => "metabib::identifier_field_entry",
1429 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1430 sub postfilter_search_multi_class_fts {
1435 my $sort = $args{'sort'};
1436 my $sort_dir = $args{sort_dir} || 'DESC';
1437 my $ou = $args{org_unit};
1438 my $ou_type = $args{depth};
1439 my $limit = $args{limit} || 10;
1440 my $offset = $args{offset} || 0;
1441 my $visibility_limit = $args{visibility_limit} || 5000;
1444 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1447 if (!defined($args{org_unit})) {
1448 die "No target organizational unit passed to ".$self->api_name;
1451 if (! scalar( keys %{$args{searches}} )) {
1452 die "No search arguments were passed to ".$self->api_name;
1455 my $outer_limit = 1000;
1457 my $limit_clause = '';
1458 my $offset_clause = '';
1460 $limit_clause = "LIMIT $outer_limit";
1461 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1463 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1464 my ($t_filter, $f_filter, $v_filter) = ('','','');
1465 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1466 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1467 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1469 if ($args{available}) {
1470 $avail_filter = ' AND cp.status IN (0,7,12)';
1473 if (my $a = $args{audience}) {
1474 $a = [$a] if (!ref($a));
1477 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1478 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1481 if (my $l = $args{language}) {
1482 $l = [$l] if (!ref($l));
1485 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1486 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1489 if (my $f = $args{lit_form}) {
1490 $f = [$f] if (!ref($f));
1493 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1494 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1497 if (my $f = $args{item_form}) {
1498 $f = [$f] if (!ref($f));
1501 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1502 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1505 if (my $t = $args{item_type}) {
1506 $t = [$t] if (!ref($t));
1509 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1510 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1513 if (my $v = $args{vr_format}) {
1514 $v = [$v] if (!ref($v));
1517 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1518 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1522 # XXX legacy format and item type support
1523 if ($args{format}) {
1524 my ($t, $f) = split '-', $args{format};
1525 @types = split '', $t;
1526 @forms = split '', $f;
1528 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1529 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1533 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1534 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1540 my $descendants = defined($ou_type) ?
1541 "actor.org_unit_descendants($ou, $ou_type)" :
1542 "actor.org_unit_descendants($ou)";
1544 my $search_table_list = '';
1546 my $join_table_list = '';
1549 my $field_table = config::metabib_field->table;
1553 my $prev_search_group;
1554 my $curr_search_group;
1558 for my $search_group (sort keys %{$args{searches}}) {
1559 (my $search_group_name = $search_group) =~ s/\|/_/gso;
1560 ($search_class,$search_field) = split /\|/, $search_group;
1561 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1563 if ($search_field) {
1564 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1565 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1570 $prev_search_group = $curr_search_group if ($curr_search_group);
1572 $curr_search_group = $search_group_name;
1574 my $class = $_cdbi->{$search_class};
1575 my $search_table = $class->table;
1577 my ($index_col) = $class->columns('FTS');
1578 $index_col ||= 'value';
1581 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1583 my $fts_where = $fts->sql_where_clause;
1584 my @fts_ranks = $fts->fts_rank;
1586 my $SQLstring = join('%',map { lc($_) } $fts->words);
1587 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1588 my $first_word = lc(($fts->words)[0]).'%';
1590 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1591 my $rank = join(' + ', @fts_ranks);
1594 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1595 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1597 $bonus{'series'} = [
1598 { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1599 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1602 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1604 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1605 $bonus_list ||= '1';
1607 push @bonus_lists, $bonus_list;
1608 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1611 #---------------------
1613 $search_table_list .= "$search_table $search_group_name, ";
1614 push @rank_list,$rank;
1615 $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1617 if ($metabib_field) {
1618 $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1619 $metabib_field = undef;
1622 if ($prev_search_group) {
1623 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1627 my $metabib_record_descriptor = metabib::record_descriptor->table;
1628 my $metabib_full_rec = metabib::full_rec->table;
1629 my $metabib_metarecord = metabib::metarecord->table;
1630 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1631 my $asset_call_number_table = asset::call_number->table;
1632 my $asset_copy_table = asset::copy->table;
1633 my $cs_table = config::copy_status->table;
1634 my $cl_table = asset::copy_location->table;
1635 my $br_table = biblio::record_entry->table;
1636 my $source_table = config::bib_source->table;
1638 my $bonuses = join (' * ', @bonus_lists);
1639 my $relevance = join (' + ', @rank_list);
1640 $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1642 my $string_default_sort = 'zzzz';
1643 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1645 my $number_default_sort = '9999';
1646 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1650 my $secondary_sort = <<" SORT";
1652 SELECT COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1653 FROM $metabib_full_rec sfrt,
1654 $metabib_metarecord mr
1655 WHERE sfrt.record = mr.master_record
1656 AND sfrt.tag = '245'
1657 AND sfrt.subfield = 'a'
1662 my $rank = $relevance;
1663 if (lc($sort) eq 'pubdate') {
1666 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1667 FROM $metabib_full_rec frp
1668 WHERE frp.record = mr.master_record
1670 AND frp.subfield = 'c'
1674 } elsif (lc($sort) eq 'create_date') {
1676 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1678 } elsif (lc($sort) eq 'edit_date') {
1680 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1682 } elsif (lc($sort) eq 'title') {
1685 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1686 FROM $metabib_full_rec frt
1687 WHERE frt.record = mr.master_record
1689 AND frt.subfield = 'a'
1693 $secondary_sort = <<" SORT";
1695 SELECT COALESCE(SUBSTRING(sfrp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1696 FROM $metabib_full_rec sfrp
1697 WHERE sfrp.record = mr.master_record
1698 AND sfrp.tag = '260'
1699 AND sfrp.subfield = 'c'
1703 } elsif (lc($sort) eq 'author') {
1706 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1707 FROM $metabib_full_rec fra
1708 WHERE fra.record = mr.master_record
1709 AND fra.tag LIKE '1%'
1710 AND fra.subfield = 'a'
1711 ORDER BY fra.tag::text::int
1716 push @bonus_values, @bonus_values;
1721 my $select = <<" SQL";
1722 SELECT m.metarecord,
1724 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1727 FROM $search_table_list
1728 $metabib_metarecord mr,
1729 $metabib_metarecord_source_map_table m,
1730 $metabib_metarecord_source_map_table smrs
1731 WHERE m.metarecord = smrs.metarecord
1732 AND mr.id = m.metarecord
1735 GROUP BY m.metarecord
1736 -- ORDER BY 4 $sort_dir
1737 LIMIT $visibility_limit
1740 if ($self->api_name !~ /staff/o) {
1747 FROM $asset_call_number_table cn,
1748 $metabib_metarecord_source_map_table mrs,
1749 $asset_copy_table cp,
1754 $metabib_record_descriptor ord
1755 WHERE mrs.metarecord = s.metarecord
1756 AND br.id = mrs.source
1757 AND cn.record = mrs.source
1758 AND cp.status = cs.id
1759 AND cp.location = cl.id
1760 AND cp.circ_lib = d.id
1761 AND cp.call_number = cn.id
1762 AND cp.opac_visible IS TRUE
1763 AND cs.opac_visible IS TRUE
1764 AND cl.opac_visible IS TRUE
1765 AND d.opac_visible IS TRUE
1766 AND br.active IS TRUE
1767 AND br.deleted IS FALSE
1768 AND cp.deleted IS FALSE
1769 AND cn.deleted IS FALSE
1770 AND ord.record = mrs.source
1783 $metabib_metarecord_source_map_table mrs,
1784 $metabib_record_descriptor ord,
1786 WHERE mrs.metarecord = s.metarecord
1787 AND ord.record = mrs.source
1788 AND br.id = mrs.source
1789 AND br.source = src.id
1790 AND src.transcendant IS TRUE
1798 ORDER BY 4 $sort_dir, 5
1805 $metabib_metarecord_source_map_table omrs,
1806 $metabib_record_descriptor ord
1807 WHERE omrs.metarecord = s.metarecord
1808 AND ord.record = omrs.source
1811 FROM $asset_call_number_table cn,
1812 $asset_copy_table cp,
1815 WHERE br.id = omrs.source
1816 AND cn.record = omrs.source
1817 AND br.deleted IS FALSE
1818 AND cn.deleted IS FALSE
1819 AND cp.call_number = cn.id
1820 AND ( cn.owning_lib = d.id
1821 OR ( cp.circ_lib = d.id
1822 AND cp.deleted IS FALSE
1830 FROM $asset_call_number_table cn
1831 WHERE cn.record = omrs.source
1832 AND cn.deleted IS FALSE
1838 $metabib_metarecord_source_map_table mrs,
1839 $metabib_record_descriptor ord,
1841 WHERE mrs.metarecord = s.metarecord
1842 AND br.id = mrs.source
1843 AND br.source = src.id
1844 AND src.transcendant IS TRUE
1860 ORDER BY 4 $sort_dir, 5
1865 $log->debug("Field Search SQL :: [$select]",DEBUG);
1867 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1870 @types, @forms, @vformats, @aud, @lang, @lit_form,
1871 @types, @forms, @vformats, @aud, @lang, @lit_form,
1872 # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1875 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1878 $max = 1 if (!@$recs);
1880 $max = $$_[1] if ($$_[1] > $max);
1883 my $count = scalar(@$recs);
1884 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1885 next unless ($$rec[0]);
1886 my ($mrid,$rank,$skip) = @$rec;
1887 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1892 __PACKAGE__->register_method(
1893 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1894 method => 'postfilter_search_multi_class_fts',
1899 __PACKAGE__->register_method(
1900 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1901 method => 'postfilter_search_multi_class_fts',
1907 __PACKAGE__->register_method(
1908 api_name => "open-ils.storage.metabib.multiclass.search_fts",
1909 method => 'postfilter_search_multi_class_fts',
1914 __PACKAGE__->register_method(
1915 api_name => "open-ils.storage.metabib.multiclass.search_fts.staff",
1916 method => 'postfilter_search_multi_class_fts',
1922 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1923 sub biblio_search_multi_class_fts {
1928 my $sort = $args{'sort'};
1929 my $sort_dir = $args{sort_dir} || 'DESC';
1930 my $ou = $args{org_unit};
1931 my $ou_type = $args{depth};
1932 my $limit = $args{limit} || 10;
1933 my $offset = $args{offset} || 0;
1934 my $pref_lang = $args{preferred_language} || 'eng';
1935 my $visibility_limit = $args{visibility_limit} || 5000;
1938 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1941 if (! scalar( keys %{$args{searches}} )) {
1942 die "No search arguments were passed to ".$self->api_name;
1945 my $outer_limit = 1000;
1947 my $limit_clause = '';
1948 my $offset_clause = '';
1950 $limit_clause = "LIMIT $outer_limit";
1951 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1953 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1954 my ($t_filter, $f_filter, $v_filter) = ('','','');
1955 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1956 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1957 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1959 if ($args{available}) {
1960 $avail_filter = ' AND cp.status IN (0,7,12)';
1963 if (my $a = $args{audience}) {
1964 $a = [$a] if (!ref($a));
1967 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1968 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1971 if (my $l = $args{language}) {
1972 $l = [$l] if (!ref($l));
1975 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1976 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1979 if (my $f = $args{lit_form}) {
1980 $f = [$f] if (!ref($f));
1983 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1984 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1987 if (my $f = $args{item_form}) {
1988 $f = [$f] if (!ref($f));
1991 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1992 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1995 if (my $t = $args{item_type}) {
1996 $t = [$t] if (!ref($t));
1999 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2000 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2003 if (my $v = $args{vr_format}) {
2004 $v = [$v] if (!ref($v));
2007 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
2008 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
2011 # XXX legacy format and item type support
2012 if ($args{format}) {
2013 my ($t, $f) = split '-', $args{format};
2014 @types = split '', $t;
2015 @forms = split '', $f;
2017 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2018 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2022 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2023 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2028 my $descendants = defined($ou_type) ?
2029 "actor.org_unit_descendants($ou, $ou_type)" :
2030 "actor.org_unit_descendants($ou)";
2032 my $search_table_list = '';
2034 my $join_table_list = '';
2037 my $field_table = config::metabib_field->table;
2041 my $prev_search_group;
2042 my $curr_search_group;
2046 for my $search_group (sort keys %{$args{searches}}) {
2047 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2048 ($search_class,$search_field) = split /\|/, $search_group;
2049 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2051 if ($search_field) {
2052 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2053 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2058 $prev_search_group = $curr_search_group if ($curr_search_group);
2060 $curr_search_group = $search_group_name;
2062 my $class = $_cdbi->{$search_class};
2063 my $search_table = $class->table;
2065 my ($index_col) = $class->columns('FTS');
2066 $index_col ||= 'value';
2069 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2071 my $fts_where = $fts->sql_where_clause;
2072 my @fts_ranks = $fts->fts_rank;
2074 my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2075 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2076 my $first_word = lc(($fts->words)[0]).'%';
2078 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2079 my $rank = join(' + ', @fts_ranks);
2082 $bonus{'subject'} = [];
2083 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2085 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2087 $bonus{'series'} = [
2088 { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2089 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2092 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2095 push @{ $bonus{'title'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2096 push @{ $bonus{'author'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2097 push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2098 push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2099 push @{ $bonus{'series'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2102 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2103 $bonus_list ||= '1';
2105 push @bonus_lists, $bonus_list;
2106 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2108 #---------------------
2110 $search_table_list .= "$search_table $search_group_name, ";
2111 push @rank_list,$rank;
2112 $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2114 if ($metabib_field) {
2115 $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2116 $metabib_field = undef;
2119 if ($prev_search_group) {
2120 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2124 my $metabib_record_descriptor = metabib::record_descriptor->table;
2125 my $metabib_full_rec = metabib::full_rec->table;
2126 my $metabib_metarecord = metabib::metarecord->table;
2127 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2128 my $asset_call_number_table = asset::call_number->table;
2129 my $asset_copy_table = asset::copy->table;
2130 my $cs_table = config::copy_status->table;
2131 my $cl_table = asset::copy_location->table;
2132 my $br_table = biblio::record_entry->table;
2133 my $source_table = config::bib_source->table;
2136 my $bonuses = join (' * ', @bonus_lists);
2137 my $relevance = join (' + ', @rank_list);
2138 $relevance = "AVG( ($relevance) * ($bonuses) )";
2140 my $string_default_sort = 'zzzz';
2141 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2143 my $number_default_sort = '9999';
2144 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2146 my $rank = $relevance;
2147 if (lc($sort) eq 'pubdate') {
2150 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d{4}'),'$number_default_sort')::INT
2151 FROM $metabib_full_rec frp
2152 WHERE frp.record = b.id
2154 AND frp.subfield = 'c'
2158 } elsif (lc($sort) eq 'create_date') {
2160 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2162 } elsif (lc($sort) eq 'edit_date') {
2164 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2166 } elsif (lc($sort) eq 'title') {
2169 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2170 FROM $metabib_full_rec frt
2171 WHERE frt.record = b.id
2173 AND frt.subfield = 'a'
2177 } elsif (lc($sort) eq 'author') {
2180 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
2181 FROM $metabib_full_rec fra
2182 WHERE fra.record = b.id
2183 AND fra.tag LIKE '1%'
2184 AND fra.subfield = 'a'
2185 ORDER BY fra.tag::text::int
2190 push @bonus_values, @bonus_values;
2195 my $select = <<" SQL";
2200 FROM $search_table_list
2201 $metabib_record_descriptor rd,
2204 WHERE rd.record = b.id
2205 AND b.active IS TRUE
2206 AND b.deleted IS FALSE
2215 GROUP BY b.id, b.source
2216 ORDER BY 3 $sort_dir
2217 LIMIT $visibility_limit
2220 if ($self->api_name !~ /staff/o) {
2225 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2228 FROM $asset_call_number_table cn,
2229 $asset_copy_table cp,
2233 WHERE cn.record = s.id
2234 AND cp.status = cs.id
2235 AND cp.location = cl.id
2236 AND cp.call_number = cn.id
2237 AND cp.opac_visible IS TRUE
2238 AND cs.opac_visible IS TRUE
2239 AND cl.opac_visible IS TRUE
2240 AND d.opac_visible IS TRUE
2241 AND cp.deleted IS FALSE
2242 AND cn.deleted IS FALSE
2243 AND cp.circ_lib = d.id
2247 OR src.transcendant IS TRUE
2248 ORDER BY 3 $sort_dir
2255 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2258 FROM $asset_call_number_table cn,
2259 $asset_copy_table cp,
2261 WHERE cn.record = s.id
2262 AND cp.call_number = cn.id
2263 AND cn.deleted IS FALSE
2264 AND cp.circ_lib = d.id
2265 AND cp.deleted IS FALSE
2271 FROM $asset_call_number_table cn
2272 WHERE cn.record = s.id
2275 OR src.transcendant IS TRUE
2276 ORDER BY 3 $sort_dir
2281 $log->debug("Field Search SQL :: [$select]",DEBUG);
2283 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2285 @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2288 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2290 my $count = scalar(@$recs);
2291 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2292 next unless ($$rec[0]);
2293 my ($mrid,$rank) = @$rec;
2294 $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2299 __PACKAGE__->register_method(
2300 api_name => "open-ils.storage.biblio.multiclass.search_fts.record",
2301 method => 'biblio_search_multi_class_fts',
2306 __PACKAGE__->register_method(
2307 api_name => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2308 method => 'biblio_search_multi_class_fts',
2313 __PACKAGE__->register_method(
2314 api_name => "open-ils.storage.biblio.multiclass.search_fts",
2315 method => 'biblio_search_multi_class_fts',
2320 __PACKAGE__->register_method(
2321 api_name => "open-ils.storage.biblio.multiclass.search_fts.staff",
2322 method => 'biblio_search_multi_class_fts',
2330 my $default_preferred_language;
2331 my $default_preferred_language_weight;
2333 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2339 if (!$locale_map{COMPLETE}) {
2341 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2342 for my $locale ( @locales ) {
2343 $locale_map{lc($locale->code)} = $locale->marc_code;
2345 $locale_map{COMPLETE} = 1;
2349 my $config = OpenSRF::Utils::SettingsClient->new();
2351 if (!$default_preferred_language) {
2353 $default_preferred_language = $config->config_value(
2354 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2355 ) || $config->config_value(
2356 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2361 if (!$default_preferred_language_weight) {
2363 $default_preferred_language_weight = $config->config_value(
2364 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2365 ) || $config->config_value(
2366 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2370 # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2371 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2373 my $ou = $args{org_unit};
2374 my $limit = $args{limit} || 10;
2375 my $offset = $args{offset} || 0;
2378 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2381 if (! scalar( keys %{$args{searches}} )) {
2382 die "No search arguments were passed to ".$self->api_name;
2385 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2387 if (!defined($args{preferred_language})) {
2388 my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2389 $args{preferred_language} =
2390 $locale_map{ lc($ses_locale) } || 'eng';
2393 if (!defined($args{preferred_language_weight})) {
2394 $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2397 if ($args{available}) {
2398 @statuses = (0,7,12);
2401 if (my $s = $args{locations}) {
2402 $s = [$s] if (!ref($s));
2406 if (my $b = $args{between}) {
2407 if (ref($b) && @$b == 2) {
2412 if (my $s = $args{statuses}) {
2413 $s = [$s] if (!ref($s));
2417 if (my $a = $args{audience}) {
2418 $a = [$a] if (!ref($a));
2422 if (my $l = $args{language}) {
2423 $l = [$l] if (!ref($l));
2427 if (my $f = $args{lit_form}) {
2428 $f = [$f] if (!ref($f));
2432 if (my $f = $args{item_form}) {
2433 $f = [$f] if (!ref($f));
2437 if (my $t = $args{item_type}) {
2438 $t = [$t] if (!ref($t));
2442 if (my $b = $args{bib_level}) {
2443 $b = [$b] if (!ref($b));
2447 if (my $v = $args{vr_format}) {
2448 $v = [$v] if (!ref($v));
2452 # XXX legacy format and item type support
2453 if ($args{format}) {
2454 my ($t, $f) = split '-', $args{format};
2455 @types = split '', $t;
2456 @forms = split '', $f;
2459 my %stored_proc_search_args;
2460 for my $search_group (sort keys %{$args{searches}}) {
2461 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2462 my ($search_class,$search_field) = split /\|/, $search_group;
2463 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2465 if ($search_field) {
2466 unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2467 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2472 my $class = $_cdbi->{$search_class};
2473 my $search_table = $class->table;
2475 my ($index_col) = $class->columns('FTS');
2476 $index_col ||= 'value';
2479 my $fts = OpenILS::Application::Storage::FTS->compile(
2480 $search_class => $args{searches}{$search_group}{term},
2481 $search_group_name.'.value',
2482 "$search_group_name.$index_col"
2484 $fts->sql_where_clause; # this builds the ranks for us
2486 my @fts_ranks = $fts->fts_rank;
2487 my @fts_queries = $fts->fts_query;
2488 my @phrases = map { lc($_) } $fts->phrases;
2489 my @words = map { lc($_) } $fts->words;
2491 $stored_proc_search_args{$search_group} = {
2492 fts_rank => \@fts_ranks,
2493 fts_query => \@fts_queries,
2494 phrase => \@phrases,
2500 my $param_search_ou = $ou;
2501 my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2502 my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2503 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2504 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2505 my $param_audience = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud ) . '}$$';
2506 my $param_language = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang ) . '}$$';
2507 my $param_lit_form = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2508 my $param_types = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types ) . '}$$';
2509 my $param_forms = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms ) . '}$$';
2510 my $param_vformats = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2511 my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2512 my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2513 my $param_after = $args{after} ; $param_after = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2514 my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2515 my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2516 my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2517 my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2518 my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2519 my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2520 my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2521 my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2522 my $param_rel_limit = $args{core_limit}; $param_rel_limit ||= 'NULL';
2523 my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2524 my $param_skip_chk = $args{skip_check}; $param_skip_chk ||= 'NULL';
2526 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2528 FROM search.staged_fts(
2529 $param_search_ou\:\:INT,
2530 $param_depth\:\:INT,
2531 $param_searches\:\:TEXT,
2532 $param_statuses\:\:INT[],
2533 $param_locations\:\:INT[],
2534 $param_audience\:\:TEXT[],
2535 $param_language\:\:TEXT[],
2536 $param_lit_form\:\:TEXT[],
2537 $param_types\:\:TEXT[],
2538 $param_forms\:\:TEXT[],
2539 $param_vformats\:\:TEXT[],
2540 $param_bib_level\:\:TEXT[],
2541 $param_before\:\:TEXT,
2542 $param_after\:\:TEXT,
2543 $param_during\:\:TEXT,
2544 $param_between\:\:TEXT[],
2545 $param_pref_lang\:\:TEXT,
2546 $param_pref_lang_multiplier\:\:REAL,
2547 $param_sort\:\:TEXT,
2548 $param_sort_desc\:\:BOOL,
2549 $metarecord\:\:BOOL,
2551 $param_rel_limit\:\:INT,
2552 $param_chk_limit\:\:INT,
2553 $param_skip_chk\:\:INT
2559 my $recs = $sth->fetchall_arrayref({});
2560 my $summary_row = pop @$recs;
2562 my $total = $$summary_row{total};
2563 my $checked = $$summary_row{checked};
2564 my $visible = $$summary_row{visible};
2565 my $deleted = $$summary_row{deleted};
2566 my $excluded = $$summary_row{excluded};
2568 my $estimate = $visible;
2569 if ( $total > $checked && $checked ) {
2571 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2572 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2576 delete $$summary_row{id};
2577 delete $$summary_row{rel};
2578 delete $$summary_row{record};
2580 $client->respond( $summary_row );
2582 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2584 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2585 delete $$rec{checked};
2586 delete $$rec{visible};
2587 delete $$rec{excluded};
2588 delete $$rec{deleted};
2589 delete $$rec{total};
2590 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2592 $client->respond( $rec );
2596 __PACKAGE__->register_method(
2597 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
2598 method => 'staged_fts',
2603 __PACKAGE__->register_method(
2604 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2605 method => 'staged_fts',
2610 __PACKAGE__->register_method(
2611 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
2612 method => 'staged_fts',
2617 __PACKAGE__->register_method(
2618 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2619 method => 'staged_fts',
2625 sub FTS_paging_estimate {
2629 my $checked = shift;
2630 my $visible = shift;
2631 my $excluded = shift;
2632 my $deleted = shift;
2635 my $deleted_ratio = $deleted / $checked;
2636 my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2638 my $exclusion_ratio = $excluded / $checked;
2639 my $delete_adjusted_exclusion_ratio = $checked - $deleted ? $excluded / ($checked - $deleted) : 1;
2641 my $inclusion_ratio = $visible / $checked;
2642 my $delete_adjusted_inclusion_ratio = $checked - $deleted ? $visible / ($checked - $deleted) : 0;
2645 exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2646 inclusion => int($delete_adjusted_total * $inclusion_ratio),
2647 delete_adjusted_exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2648 delete_adjusted_inclusion => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2651 __PACKAGE__->register_method(
2652 api_name => "open-ils.storage.fts_paging_estimate",
2653 method => 'FTS_paging_estimate',
2659 Hash of estimation values based on four variant estimation strategies:
2660 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2661 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2662 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2663 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2666 Helper method used to determin the approximate number of
2667 hits for a search that spans multiple superpages. For
2668 sparse superpages, the inclusion estimate will likely be the
2669 best estimate. The exclusion strategy is the original, but
2670 inclusion is the default.
2673 { name => 'checked',
2674 desc => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2677 { name => 'visible',
2678 desc => 'Number of records visible to the search location on the current superpage.',
2681 { name => 'excluded',
2682 desc => 'Number of records excluded from the search location on the current superpage.',
2685 { name => 'deleted',
2686 desc => 'Number of deleted records on the current superpage.',
2690 desc => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2703 my $term = $$args{term};
2704 my $limit = $$args{max} || 1;
2705 my $min = $$args{min} || 1;
2706 my @classes = @{$$args{class}};
2708 $limit = $min if ($min > $limit);
2711 @classes = ( qw/ title author subject series keyword / );
2715 my $bre_table = biblio::record_entry->table;
2716 my $cn_table = asset::call_number->table;
2717 my $cp_table = asset::copy->table;
2719 for my $search_class ( @classes ) {
2721 my $class = $_cdbi->{$search_class};
2722 my $search_table = $class->table;
2724 my ($index_col) = $class->columns('FTS');
2725 $index_col ||= 'value';
2728 my $where = OpenILS::Application::Storage::FTS
2729 ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2733 SELECT COUNT(DISTINCT X.source)
2734 FROM (SELECT $search_class.source
2735 FROM $search_table $search_class
2736 JOIN $bre_table b ON (b.id = $search_class.source)
2741 HAVING COUNT(DISTINCT X.source) >= $min;
2744 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2745 $matches{$search_class} = $res ? $res->[0] : 0;
2750 __PACKAGE__->register_method(
2751 api_name => "open-ils.storage.search.xref",
2752 method => 'xref_count',
2756 # Takes an abstract query object and recursively turns it back into a string
2758 sub abstract_query2str {
2759 my ($self, $conn, $query) = @_;
2761 return QueryParser::Canonicalize::abstract_query2str_impl($query, 0);
2764 __PACKAGE__->register_method(
2765 api_name => "open-ils.storage.query_parser.abstract_query.canonicalize",
2766 method => "abstract_query2str",
2771 Abstract query parser object, with complete config data. For example input,
2772 see the 'abstract_query' part of the output of an API call like
2773 open-ils.search.biblio.multiclass.query, when called with the return_abstract
2777 return => { type => "string", desc => "String representation of abstract query object" }
2781 sub str2abstract_query {
2782 my ($self, $conn, $query, $qp_opts, $with_config) = @_;
2784 my %use_opts = ( # reasonable defaults? should these even be hardcoded here?
2786 superpage_size => 1000,
2787 core_limit => 25000,
2789 (ref $opts eq 'HASH' ? %$opts : ())
2794 # grab the query parser and initialize it
2795 my $parser = $OpenILS::Application::Storage::QParser;
2798 _initialize_parser($parser) unless $parser->initialization_complete;
2800 my $query = $parser->new(%use_opts)->parse;
2802 return $query->parse_tree->to_abstract_query(with_config => $with_config);
2805 __PACKAGE__->register_method(
2806 api_name => "open-ils.storage.query_parser.abstract_query.from_string",
2807 method => "str2abstract_query",
2811 {desc => "Query", type => "string"},
2812 {desc => q/Arguments for initializing QueryParser (optional)/,
2814 {desc => q/Flag enabling inclusion of QP config in returned object (optional, default false)/,
2817 return => { type => "object", desc => "abstract representation of query parser query" }
2821 my @available_statuses_cache;
2822 sub available_statuses {
2823 if (!scalar(@available_statuses_cache)) {
2824 @available_statuses_cache = map { $_->id } config::copy_status->search_where({is_available => 't'});
2826 return @available_statuses_cache;
2829 sub query_parser_fts {
2835 # grab the query parser and initialize it
2836 my $parser = $OpenILS::Application::Storage::QParser;
2839 _initialize_parser($parser) unless $parser->initialization_complete;
2841 # populate the locale/language map
2842 if (!$locale_map{COMPLETE}) {
2844 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2845 for my $locale ( @locales ) {
2846 $locale_map{lc($locale->code)} = $locale->marc_code;
2848 $locale_map{COMPLETE} = 1;
2852 # I hope we have a query!
2853 if (! $args{query} ) {
2854 die "No query was passed to ".$self->api_name;
2857 my $default_CD_modifiers = OpenSRF::Utils::SettingsClient->new->config_value(
2858 apps => 'open-ils.search' => app_settings => 'default_CD_modifiers'
2861 # Protect against empty / missing default_CD_modifiers setting
2862 if ($default_CD_modifiers and !ref($default_CD_modifiers)) {
2863 $args{query} = "$default_CD_modifiers $args{query}";
2866 my $simple_plan = $args{_simple_plan};
2867 # remove bad chunks of the %args hash
2868 for my $bad ( grep { /^_/ } keys(%args)) {
2869 delete($args{$bad});
2873 # parse the query and supply any query-level %arg-based defaults
2874 # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2875 my $query = $parser->new( %args )->parse;
2877 my $config = OpenSRF::Utils::SettingsClient->new();
2879 # set the locale-based default preferred location
2880 if (!$query->parse_tree->find_filter('preferred_language')) {
2881 $parser->default_preferred_language( $args{preferred_language} );
2883 if (!$parser->default_preferred_language) {
2884 my $ses_locale = $client->session ? $client->session->session_locale : '';
2885 $parser->default_preferred_language( $locale_map{ lc($ses_locale) } );
2888 if (!$parser->default_preferred_language) { # still nothing...
2889 my $tmp_dpl = $config->config_value(
2890 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2891 ) || $config->config_value(
2892 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2895 $parser->default_preferred_language( $tmp_dpl )
2900 # set the global default language multiplier
2901 if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2904 if ($tmp_dplw = $args{preferred_language_weight} || $args{preferred_language_multiplier} ) {
2905 $parser->default_preferred_language_multiplier($tmp_dplw);
2908 $tmp_dplw = $config->config_value(
2909 apps => 'open-ils.search' => app_settings => 'default_preferred_language_weight'
2910 ) || $config->config_value(
2911 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2914 $parser->default_preferred_language_multiplier( $tmp_dplw );
2918 # gather the site, if one is specified, defaulting to the in-query version
2919 my $ou = $args{org_unit};
2920 if (my ($filter) = $query->parse_tree->find_filter('site')) {
2921 $ou = $filter->args->[0] if (@{$filter->args});
2923 $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^(-)?\d+$/);
2925 # gather lasso, as with $ou
2926 my $lasso = $args{lasso};
2927 if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
2928 $lasso = $filter->args->[0] if (@{$filter->args});
2930 $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
2931 $lasso = -$lasso if ($lasso);
2934 # # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
2935 # # gather user lasso, as with $ou and lasso
2936 # my $mylasso = $args{my_lasso};
2937 # if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
2938 # $mylasso = $filter->args->[0] if (@{$filter->args});
2940 # $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
2943 # if we have a lasso, go with that, otherwise ... ou
2944 $ou = $lasso if ($lasso);
2946 # gather the preferred OU, if one is specified, as with $ou
2947 my $pref_ou = $args{pref_ou};
2948 $log->info("pref_ou = $pref_ou");
2949 if (my ($filter) = $query->parse_tree->find_filter('pref_ou')) {
2950 $pref_ou = $filter->args->[0] if (@{$filter->args});
2952 $pref_ou = actor::org_unit->search( { shortname => $pref_ou } )->next->id if ($pref_ou and $pref_ou !~ /^(-)?\d+$/);
2954 # get the default $ou if we have nothing
2955 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
2958 # 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
2959 # gather the depth, if one is specified, defaulting to the in-query version
2960 my $depth = $args{depth};
2961 if (my ($filter) = $query->parse_tree->find_filter('depth')) {
2962 $depth = $filter->args->[0] if (@{$filter->args});
2964 $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
2967 # gather the limit or default to 10
2968 my $limit = $args{check_limit} || 'NULL';
2969 if (my ($filter) = $query->parse_tree->find_filter('limit')) {
2970 $limit = $filter->args->[0] if (@{$filter->args});
2972 if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
2973 $limit = $filter->args->[0] if (@{$filter->args});
2977 # gather the offset or default to 0
2978 my $offset = $args{skip_check} || $args{offset} || 0;
2979 if (my ($filter) = $query->parse_tree->find_filter('offset')) {
2980 $offset = $filter->args->[0] if (@{$filter->args});
2982 if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
2983 $offset = $filter->args->[0] if (@{$filter->args});
2987 # gather the estimation strategy or default to inclusion
2988 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2989 if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
2990 $estimation_strategy = $filter->args->[0] if (@{$filter->args});
2994 # gather the estimation strategy or default to inclusion
2995 my $core_limit = $args{core_limit};
2996 if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
2997 $core_limit = $filter->args->[0] if (@{$filter->args});
3001 # gather statuses, and then forget those if we have an #available modifier
3003 if ($query->parse_tree->find_modifier('available')) {
3004 @statuses = available_statuses();
3005 } elsif (my ($filter) = $query->parse_tree->find_filter('statuses')) {
3006 @statuses = @{$filter->args} if (@{$filter->args});
3012 if (my ($filter) = $query->parse_tree->find_filter('locations')) {
3013 @location = @{$filter->args} if (@{$filter->args});
3016 # gather location_groups
3017 if (my ($filter) = $query->parse_tree->find_filter('location_groups')) {
3018 my @loc_groups = @{$filter->args} if (@{$filter->args});
3020 # collect the mapped locations and add them to the locations() filter
3023 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3024 my $maps = $cstore->request(
3025 'open-ils.cstore.direct.asset.copy_location_group_map.search.atomic',
3026 {lgroup => \@loc_groups})->gather(1);
3028 push(@location, $_->location) for @$maps;
3033 my $param_check = $limit || $query->superpage_size || 'NULL';
3034 my $param_offset = $offset || 'NULL';
3035 my $param_limit = $core_limit || 'NULL';
3037 my $sp = $query->superpage || 1;
3039 $param_offset = ($sp - 1) * $sp_size;
3042 my $param_search_ou = $ou;
3043 my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
3044 my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
3045 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
3046 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
3047 my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
3048 my $deleted_search = ($query->parse_tree->find_modifier('deleted')) ? "'t'" : "'f'";
3049 my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
3050 my $param_pref_ou = $pref_ou || 'NULL';
3052 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3053 SELECT * -- bib search: $args{query}
3054 FROM search.query_parser_fts(
3055 $param_search_ou\:\:INT,
3056 $param_depth\:\:INT,
3057 $param_core_query\:\:TEXT,
3058 $param_statuses\:\:INT[],
3059 $param_locations\:\:INT[],
3060 $param_offset\:\:INT,
3061 $param_check\:\:INT,
3062 $param_limit\:\:INT,
3063 $metarecord\:\:BOOL,
3065 $deleted_search\:\:BOOL,
3066 $param_pref_ou\:\:INT
3072 my $recs = $sth->fetchall_arrayref({});
3073 my $summary_row = pop @$recs;
3075 my $total = $$summary_row{total};
3076 my $checked = $$summary_row{checked};
3077 my $visible = $$summary_row{visible};
3078 my $deleted = $$summary_row{deleted};
3079 my $excluded = $$summary_row{excluded};
3081 my $estimate = $visible;
3082 if ( $total > $checked && $checked ) {
3084 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
3085 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
3089 delete $$summary_row{id};
3090 delete $$summary_row{rel};
3091 delete $$summary_row{record};
3092 delete $$summary_row{badges};
3093 delete $$summary_row{popularity};
3095 if (defined($simple_plan)) {
3096 $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3098 $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3101 $client->respond( $summary_row );
3103 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
3105 for my $rec (@$recs) {
3106 delete $$rec{checked};
3107 delete $$rec{visible};
3108 delete $$rec{excluded};
3109 delete $$rec{deleted};
3110 delete $$rec{total};
3111 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3113 $client->respond( $rec );
3117 __PACKAGE__->register_method(
3118 api_name => "open-ils.storage.query_parser_search",
3119 method => 'query_parser_fts',
3127 sub query_parser_fts_wrapper {
3132 $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3133 # grab the query parser and initialize it
3134 my $parser = $OpenILS::Application::Storage::QParser;
3137 _initialize_parser($parser) unless $parser->initialization_complete;
3139 if (! scalar( keys %{$args{searches}} )) {
3140 die "No search arguments were passed to ".$self->api_name;
3143 $top_org ||= actor::org_unit->search( { parent_ou => undef } )->next;
3145 $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3146 my $base_query = '';
3147 for my $sclass ( keys %{$args{searches}} ) {
3148 $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3149 $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3152 my $query = $base_query;
3153 $log->debug("Full base query: $base_query", DEBUG);
3155 $query = "$args{facets} $query" if ($args{facets});
3157 if (!$locale_map{COMPLETE}) {
3159 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3160 for my $locale ( @locales ) {
3161 $locale_map{lc($locale->code)} = $locale->marc_code;
3163 $locale_map{COMPLETE} = 1;
3167 my $base_plan = $parser->new( query => $base_query )->parse;
3169 $query = "preferred_language($args{preferred_language}) $query"
3170 if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3171 $query = "preferred_language_weight($args{preferred_language_weight}) $query"
3172 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'));
3176 if (!$base_plan->parse_tree->find_filter('badge_orgs')) {
3177 # supply a suitable badge_orgs filter unless user has
3178 # explicitly supplied one
3181 my @lg_id_list = @{$args{location_groups}} if (ref $args{location_groups});
3183 my ($lg_filter) = $base_plan->parse_tree->find_filter('location_groups');
3184 @lg_id_list = @{$lg_filter->args} if ($lg_filter && @{$lg_filter->args});
3188 for my $lg ( grep { /^\d+$/ } @lg_id_list ) {
3189 my $lg_obj = asset::copy_location_group->retrieve($lg);
3190 next unless $lg_obj;
3192 push(@borg_list, ''.$lg_obj->owner);
3194 $borgs = join(',', @borg_list) if @borg_list;
3198 my ($site_filter) = $base_plan->parse_tree->find_filter('site');
3199 if ($site_filter && @{$site_filter->args}) {
3200 $site = $top_org if ($site_filter->args->[0] eq '-');
3201 $site = $top_org if ($site_filter->args->[0] eq $top_org->shortname);
3202 $site = actor::org_unit->search( { shortname => $site_filter->args->[0] })->next unless ($site);
3203 } elsif ($args{org_unit}) {
3204 $site = $top_org if ($args{org_unit} eq '-');
3205 $site = $top_org if ($args{org_unit} eq $top_org->shortname);
3206 $site = actor::org_unit->search( { shortname => $args{org_unit} })->next unless ($site);
3212 $borgs = OpenSRF::AppSession->create( 'open-ils.cstore' )->request(
3213 'open-ils.cstore.json_query.atomic',
3214 { from => [ 'actor.org_unit_ancestors', $site->id ] }
3217 if (ref $borgs && @$borgs) {
3218 $borgs = join(',', map { $_->{'id'} } @$borgs);
3226 $query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
3227 $query = "badge_orgs($borgs) $query" if ($borgs);
3228 $query = "site($args{org_unit}) $query" if ($args{org_unit});
3229 $query = "depth($args{depth}) $query" if (defined($args{depth}));
3230 $query = "sort($args{sort}) $query" if ($args{sort});
3231 $query = "limit($args{limit}) $query" if ($args{limit});
3232 $query = "core_limit($args{core_limit}) $query" if ($args{core_limit});
3233 $query = "skip_check($args{skip_check}) $query" if ($args{skip_check});
3234 $query = "superpage($args{superpage}) $query" if ($args{superpage});
3235 $query = "offset($args{offset}) $query" if ($args{offset});
3236 $query = "#metarecord $query" if ($self->api_name =~ /metabib/);
3237 $query = "#available $query" if ($args{available});
3238 $query = "#descending $query" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3239 $query = "#staff $query" if ($self->api_name =~ /staff/);
3240 $query = "before($args{before}) $query" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3241 $query = "after($args{after}) $query" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3242 $query = "during($args{during}) $query" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3243 $query = "between($args{between}[0],$args{between}[1]) $query"
3244 if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3247 my (@between,@statuses,@locations,@location_groups,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3249 # XXX legacy format and item type support
3250 if ($args{format}) {
3251 my ($t, $f) = split '-', $args{format};
3252 $args{item_type} = [ split '', $t ];
3253 $args{item_form} = [ split '', $f ];
3256 for my $filter ( qw/locations location_groups statuses between audience language lit_form item_form item_type bib_level vr_format badges/ ) {
3257 if (my $s = $args{$filter}) {
3258 $s = [$s] if (!ref($s));
3260 my @filter_list = @$s;
3262 next if ($filter eq 'between' and scalar(@filter_list) != 2);
3263 next if (@filter_list == 0);
3265 my $filter_string = join ',', @filter_list;
3266 $query = "$query $filter($filter_string)";
3270 $log->debug("Full QueryParser query: $query", DEBUG);
3272 return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan );
3274 __PACKAGE__->register_method(
3275 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
3276 method => 'query_parser_fts_wrapper',
3281 __PACKAGE__->register_method(
3282 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3283 method => 'query_parser_fts_wrapper',
3288 __PACKAGE__->register_method(
3289 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
3290 method => 'query_parser_fts_wrapper',
3295 __PACKAGE__->register_method(
3296 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3297 method => 'query_parser_fts_wrapper',