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',
154 method => 'ordered_records_from_metarecord',
158 __PACKAGE__->register_method(
159 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff',
161 method => 'ordered_records_from_metarecord',
166 __PACKAGE__->register_method(
167 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.atomic',
169 method => 'ordered_records_from_metarecord',
173 __PACKAGE__->register_method(
174 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic',
176 method => 'ordered_records_from_metarecord',
181 # XXX: this subroutine and its two registered methods are marked for
182 # deprecation, as they do not work properly in 2.x (these tags are no longer
183 # normalized in mfr) and are not in known use
187 my $isxn = lc(shift());
191 $isxn =~ s/-//o if ($self->api_name =~ /isbn/o);
193 my $tag = ($self->api_name =~ /isbn/o) ? "'020' OR f.tag = '024'" : "'022'";
195 my $fr_table = metabib::full_rec->table;
196 my $bib_table = biblio::record_entry->table;
199 SELECT DISTINCT f.record
201 JOIN $bib_table b ON (b.id = f.record)
204 AND b.deleted IS FALSE
207 my $list = metabib::full_rec->db_Main->selectcol_arrayref($sql, {}, "$isxn%");
208 $client->respond($_) for (@$list);
211 __PACKAGE__->register_method(
212 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
214 method => 'isxn_search',
218 __PACKAGE__->register_method(
219 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.issn',
221 method => 'isxn_search',
226 sub metarecord_copy_count {
232 my $sm_table = metabib::metarecord_source_map->table;
233 my $rd_table = metabib::record_descriptor->table;
234 my $cn_table = asset::call_number->table;
235 my $cp_table = asset::copy->table;
236 my $br_table = biblio::record_entry->table;
237 my $src_table = config::bib_source->table;
238 my $cl_table = asset::copy_location->table;
239 my $cs_table = config::copy_status->table;
240 my $out_table = actor::org_unit_type->table;
242 my $descendants = "actor.org_unit_descendants(u.id)";
243 my $ancestors = "actor.org_unit_ancestors(?) u JOIN $out_table t ON (u.ou_type = t.id)";
245 if ($args{org_unit} < 0) {
246 $args{org_unit} *= -1;
247 $ancestors = "(select org_unit as id from actor.org_lasso_map where lasso = ?) u CROSS JOIN (SELECT -1 AS depth) t";
250 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';
251 $copies_visible = '' if ($self->api_name =~ /staff/o);
253 my (@types,@forms,@blvl);
254 my ($t_filter, $f_filter, $b_filter) = ('','','');
257 my ($t, $f, $b) = split '-', $args{format};
258 @types = split '', $t;
259 @forms = split '', $f;
260 @blvl = split '', $b;
263 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
267 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
271 $b_filter .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
281 JOIN $cn_table cn ON (cn.record = r.source)
282 JOIN $rd_table rd ON (cn.record = rd.record)
283 JOIN $cp_table cp ON (cn.id = cp.call_number)
284 JOIN $cs_table cs ON (cp.status = cs.id)
285 JOIN $cl_table cl ON (cp.location = cl.id)
286 JOIN $descendants a ON (cp.circ_lib = a.id)
287 WHERE r.metarecord = ?
288 AND cn.deleted IS FALSE
289 AND cp.deleted IS FALSE
299 JOIN $cn_table cn ON (cn.record = r.source)
300 JOIN $rd_table rd ON (cn.record = rd.record)
301 JOIN $cp_table cp ON (cn.id = cp.call_number)
302 JOIN $cs_table cs ON (cp.status = cs.id)
303 JOIN $cl_table cl ON (cp.location = cl.id)
304 JOIN $descendants a ON (cp.circ_lib = a.id)
305 WHERE r.metarecord = ?
306 AND cp.status IN (0,7,12)
307 AND cn.deleted IS FALSE
308 AND cp.deleted IS FALSE
318 JOIN $cn_table cn ON (cn.record = r.source)
319 JOIN $rd_table rd ON (cn.record = rd.record)
320 JOIN $cp_table cp ON (cn.id = cp.call_number)
321 JOIN $cs_table cs ON (cp.status = cs.id)
322 JOIN $cl_table cl ON (cp.location = cl.id)
323 WHERE r.metarecord = ?
324 AND cn.deleted IS FALSE
325 AND cp.deleted IS FALSE
326 AND cp.opac_visible IS TRUE
327 AND cs.opac_visible IS TRUE
328 AND cl.opac_visible IS TRUE
337 JOIN $br_table br ON (br.id = r.source)
338 JOIN $src_table src ON (src.id = br.source)
339 WHERE r.metarecord = ?
340 AND src.transcendant IS TRUE
348 my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
349 $sth->execute( ''.$args{metarecord},
353 ''.$args{metarecord},
357 ''.$args{metarecord},
361 ''.$args{metarecord},
365 while ( my $row = $sth->fetchrow_hashref ) {
366 $client->respond( $row );
370 __PACKAGE__->register_method(
371 api_name => 'open-ils.storage.metabib.metarecord.copy_count',
373 method => 'metarecord_copy_count',
378 __PACKAGE__->register_method(
379 api_name => 'open-ils.storage.metabib.metarecord.copy_count.staff',
381 method => 'metarecord_copy_count',
387 sub biblio_multi_search_full_rec {
392 my $class_join = $args{class_join} || 'AND';
393 my $limit = $args{limit} || 100;
394 my $offset = $args{offset} || 0;
395 my $sort = $args{'sort'};
396 my $sort_dir = $args{sort_dir} || 'DESC';
401 for my $arg (@{ $args{searches} }) {
402 my $term = $$arg{term};
403 my $limiters = $$arg{restrict};
405 my ($index_col) = metabib::full_rec->columns('FTS');
406 $index_col ||= 'value';
407 my $search_table = metabib::full_rec->table;
409 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
411 my $fts_where = $fts->sql_where_clause();
412 my @fts_ranks = $fts->fts_rank;
414 my $rank = join(' + ', @fts_ranks);
417 for my $limit (@$limiters) {
418 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
419 # MARC control field; mfr.subfield is NULL
420 push @wheres, "( tag = ? AND $fts_where )";
421 push @binds, $$limit{tag};
422 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
424 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
425 push @binds, $$limit{tag}, $$limit{subfield};
426 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
429 my $where = join(' OR ', @wheres);
431 push @selects, "SELECT record, AVG($rank) as sum FROM $search_table WHERE $where GROUP BY record";
435 my $descendants = defined($args{depth}) ?
436 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
437 "actor.org_unit_descendants($args{org_unit})" ;
440 my $metabib_record_descriptor = metabib::record_descriptor->table;
441 my $metabib_full_rec = metabib::full_rec->table;
442 my $asset_call_number_table = asset::call_number->table;
443 my $asset_copy_table = asset::copy->table;
444 my $cs_table = config::copy_status->table;
445 my $cl_table = asset::copy_location->table;
446 my $br_table = biblio::record_entry->table;
448 my $cj = 'HAVING COUNT(x.record) = ' . scalar(@selects) if ($class_join eq 'AND');
450 '(SELECT x.record, sum(x.sum) FROM (('.
451 join(') UNION ALL (', @selects).
452 ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
454 my $has_vols = 'AND cn.owning_lib = d.id';
455 my $has_copies = 'AND cp.call_number = cn.id';
456 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';
458 if ($self->api_name =~ /staff/o) {
459 $copies_visible = '';
460 $has_copies = '' if ($ou_type == 0);
461 $has_vols = '' if ($ou_type == 0);
464 my ($t_filter, $f_filter) = ('','');
465 my ($a_filter, $l_filter, $lf_filter) = ('','','');
468 if (my $a = $args{audience}) {
469 $a = [$a] if (!ref($a));
472 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
477 if (my $l = $args{language}) {
478 $l = [$l] if (!ref($l));
481 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
486 if (my $f = $args{lit_form}) {
487 $f = [$f] if (!ref($f));
490 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
491 push @binds, @lit_form;
495 if (my $f = $args{item_form}) {
496 $f = [$f] if (!ref($f));
499 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
504 if (my $t = $args{item_type}) {
505 $t = [$t] if (!ref($t));
508 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
515 my ($t, $f) = split '-', $args{format};
516 my @types = split '', $t;
517 my @forms = split '', $f;
519 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
524 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
527 push @binds, @types, @forms;
530 my $relevance = 'sum(f.sum)';
531 $relevance = 1 if (!$copies_visible);
533 my $string_default_sort = 'zzzz';
534 $string_default_sort = 'AAAA' if ($sort_dir =~ /^DESC$/i);
536 my $number_default_sort = '9999';
537 $number_default_sort = '0000' if ($sort_dir =~/^DESC$/i);
539 my $rank = $relevance;
540 if (lc($sort) eq 'pubdate') {
543 SELECT COALESCE(SUBSTRING(MAX(frp.value) FROM E'\\\\d{4}'), '$number_default_sort')::INT
544 FROM $metabib_full_rec frp
545 WHERE frp.record = f.record
547 AND frp.subfield = 'c'
551 } elsif (lc($sort) eq 'create_date') {
553 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
555 } elsif (lc($sort) eq 'edit_date') {
557 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
559 } elsif (lc($sort) =~ /^title/i) {
562 SELECT COALESCE(LTRIM(SUBSTR(MAX(frt.value), COALESCE(SUBSTRING(MAX(frt.ind2) FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
563 FROM $metabib_full_rec frt
564 WHERE frt.record = f.record
566 AND frt.subfield = 'a'
570 } elsif (lc($sort) =~ /^author/i) {
573 SELECT COALESCE(LTRIM(MAX(query.value)), '$string_default_sort')
576 FROM $metabib_full_rec fra
577 WHERE fra.record = f.record
578 AND fra.tag LIKE '1%'
579 AND fra.subfield = 'a'
580 ORDER BY fra.tag::text::int
589 my $rd_join = $use_rd ? "$metabib_record_descriptor rd," : '';
590 my $rd_filter = $use_rd ? 'AND rd.record = f.record' : '';
592 if ($copies_visible) {
594 SELECT f.record, $relevance, count(DISTINCT cp.id), $rank
595 FROM $search_table f,
596 $asset_call_number_table cn,
597 $asset_copy_table cp,
603 WHERE br.id = f.record
604 AND cn.record = f.record
605 AND cp.status = cs.id
606 AND cp.location = cl.id
607 AND br.deleted IS FALSE
608 AND cn.deleted IS FALSE
609 AND cp.deleted IS FALSE
619 GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
620 ORDER BY 4 $sort_dir,3 DESC
624 SELECT f.record, 1, 1, $rank
625 FROM $search_table f,
628 WHERE br.id = f.record
629 AND br.deleted IS FALSE
642 $log->debug("Search SQL :: [$select]",DEBUG);
644 my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
645 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
648 $max = 1 if (!@$recs);
650 $max = $$_[1] if ($$_[1] > $max);
654 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
655 next unless ($$rec[0]);
656 my ($rid,$rank,$junk,$skip) = @$rec;
657 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
661 __PACKAGE__->register_method(
662 api_name => 'open-ils.storage.biblio.full_rec.multi_search',
664 method => 'biblio_multi_search_full_rec',
669 __PACKAGE__->register_method(
670 api_name => 'open-ils.storage.biblio.full_rec.multi_search.staff',
672 method => 'biblio_multi_search_full_rec',
678 sub search_full_rec {
684 my $term = $args{term};
685 my $limiters = $args{restrict};
687 my ($index_col) = metabib::full_rec->columns('FTS');
688 $index_col ||= 'value';
689 my $search_table = metabib::full_rec->table;
691 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
693 my $fts_where = $fts->sql_where_clause();
694 my @fts_ranks = $fts->fts_rank;
696 my $rank = join(' + ', @fts_ranks);
700 for my $limit (@$limiters) {
701 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
702 # MARC control field; mfr.subfield is NULL
703 push @wheres, "( tag = ? AND $fts_where )";
704 push @binds, $$limit{tag};
705 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
707 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
708 push @binds, $$limit{tag}, $$limit{subfield};
709 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
712 my $where = join(' OR ', @wheres);
714 my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
716 $log->debug("Search SQL :: [$select]",DEBUG);
718 my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
719 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
721 $client->respond($_) for (@$recs);
724 __PACKAGE__->register_method(
725 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
727 method => 'search_full_rec',
732 __PACKAGE__->register_method(
733 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
735 method => 'search_full_rec',
742 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
743 sub search_class_fts {
748 my $term = $args{term};
749 my $ou = $args{org_unit};
750 my $ou_type = $args{depth};
751 my $limit = $args{limit};
752 my $offset = $args{offset};
754 my $limit_clause = '';
755 my $offset_clause = '';
757 $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
758 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
761 my ($t_filter, $f_filter) = ('','');
764 my ($t, $f) = split '-', $args{format};
765 @types = split '', $t;
766 @forms = split '', $f;
768 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
772 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
778 my $descendants = defined($ou_type) ?
779 "actor.org_unit_descendants($ou, $ou_type)" :
780 "actor.org_unit_descendants($ou)";
782 my $class = $self->{cdbi};
783 my $search_table = $class->table;
785 my $metabib_record_descriptor = metabib::record_descriptor->table;
786 my $metabib_metarecord = metabib::metarecord->table;
787 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
788 my $asset_call_number_table = asset::call_number->table;
789 my $asset_copy_table = asset::copy->table;
790 my $cs_table = config::copy_status->table;
791 my $cl_table = asset::copy_location->table;
793 my ($index_col) = $class->columns('FTS');
794 $index_col ||= 'value';
796 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
797 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
799 my $fts_where = $fts->sql_where_clause;
800 my @fts_ranks = $fts->fts_rank;
802 my $rank = join(' + ', @fts_ranks);
804 my $has_vols = 'AND cn.owning_lib = d.id';
805 my $has_copies = 'AND cp.call_number = cn.id';
806 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';
808 my $visible_count = ', count(DISTINCT cp.id)';
809 my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
811 if ($self->api_name =~ /staff/o) {
812 $copies_visible = '';
813 $visible_count_test = '';
814 $has_copies = '' if ($ou_type == 0);
815 $has_vols = '' if ($ou_type == 0);
818 my $rank_calc = <<" RANK";
820 * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
821 * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
822 * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
823 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
826 $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
828 if ($copies_visible) {
830 SELECT m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
831 FROM $search_table f,
832 $metabib_metarecord_source_map_table m,
833 $asset_call_number_table cn,
834 $asset_copy_table cp,
837 $metabib_record_descriptor rd,
840 AND m.source = f.source
841 AND cn.record = m.source
842 AND rd.record = m.source
843 AND cp.status = cs.id
844 AND cp.location = cl.id
850 GROUP BY 1 $visible_count_test
852 $limit_clause $offset_clause
856 SELECT m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
857 FROM $search_table f,
858 $metabib_metarecord_source_map_table m,
859 $metabib_record_descriptor rd
861 AND m.source = f.source
862 AND rd.record = m.source
867 $limit_clause $offset_clause
871 $log->debug("Field Search SQL :: [$select]",DEBUG);
873 my $SQLstring = join('%',$fts->words);
874 my $REstring = join('\\s+',$fts->words);
875 my $first_word = ($fts->words)[0].'%';
876 my $recs = ($self->api_name =~ /unordered/o) ?
877 $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
878 $class->db_Main->selectall_arrayref($select, {},
879 '%'.lc($SQLstring).'%', # phrase order match
880 lc($first_word), # first word match
881 '^\\s*'.lc($REstring).'\\s*/?\s*$', # full exact match
885 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
887 $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
891 for my $class ( qw/title author subject keyword series identifier/ ) {
892 __PACKAGE__->register_method(
893 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord",
895 method => 'search_class_fts',
898 cdbi => "metabib::${class}_field_entry",
901 __PACKAGE__->register_method(
902 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
904 method => 'search_class_fts',
907 cdbi => "metabib::${class}_field_entry",
910 __PACKAGE__->register_method(
911 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
913 method => 'search_class_fts',
916 cdbi => "metabib::${class}_field_entry",
919 __PACKAGE__->register_method(
920 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
922 method => 'search_class_fts',
925 cdbi => "metabib::${class}_field_entry",
930 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
931 sub search_class_fts_count {
936 my $term = $args{term};
937 my $ou = $args{org_unit};
938 my $ou_type = $args{depth};
939 my $limit = $args{limit} || 100;
940 my $offset = $args{offset} || 0;
942 my $descendants = defined($ou_type) ?
943 "actor.org_unit_descendants($ou, $ou_type)" :
944 "actor.org_unit_descendants($ou)";
947 my ($t_filter, $f_filter) = ('','');
950 my ($t, $f) = split '-', $args{format};
951 @types = split '', $t;
952 @forms = split '', $f;
954 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
958 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
963 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
965 my $class = $self->{cdbi};
966 my $search_table = $class->table;
968 my $metabib_record_descriptor = metabib::record_descriptor->table;
969 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
970 my $asset_call_number_table = asset::call_number->table;
971 my $asset_copy_table = asset::copy->table;
972 my $cs_table = config::copy_status->table;
973 my $cl_table = asset::copy_location->table;
975 my ($index_col) = $class->columns('FTS');
976 $index_col ||= 'value';
978 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
980 my $fts_where = $fts->sql_where_clause;
982 my $has_vols = 'AND cn.owning_lib = d.id';
983 my $has_copies = 'AND cp.call_number = cn.id';
984 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';
985 if ($self->api_name =~ /staff/o) {
986 $copies_visible = '';
987 $has_vols = '' if ($ou_type == 0);
988 $has_copies = '' if ($ou_type == 0);
991 # XXX test an "EXISTS version of descendant checking...
993 if ($copies_visible) {
995 SELECT count(distinct m.metarecord)
996 FROM $search_table f,
997 $metabib_metarecord_source_map_table m,
998 $metabib_metarecord_source_map_table mr,
999 $asset_call_number_table cn,
1000 $asset_copy_table cp,
1003 $metabib_record_descriptor rd,
1006 AND mr.source = f.source
1007 AND mr.metarecord = m.metarecord
1008 AND cn.record = m.source
1009 AND rd.record = m.source
1010 AND cp.status = cs.id
1011 AND cp.location = cl.id
1020 SELECT count(distinct m.metarecord)
1021 FROM $search_table f,
1022 $metabib_metarecord_source_map_table m,
1023 $metabib_metarecord_source_map_table mr,
1024 $metabib_record_descriptor rd
1026 AND mr.source = f.source
1027 AND mr.metarecord = m.metarecord
1028 AND rd.record = m.source
1034 $log->debug("Field Search Count SQL :: [$select]",DEBUG);
1036 my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1038 $log->debug("Count Search yielded $recs results.",DEBUG);
1043 for my $class ( qw/title author subject keyword series identifier/ ) {
1044 __PACKAGE__->register_method(
1045 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
1047 method => 'search_class_fts_count',
1050 cdbi => "metabib::${class}_field_entry",
1053 __PACKAGE__->register_method(
1054 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
1056 method => 'search_class_fts_count',
1059 cdbi => "metabib::${class}_field_entry",
1065 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1066 sub postfilter_search_class_fts {
1071 my $term = $args{term};
1072 my $sort = $args{'sort'};
1073 my $sort_dir = $args{sort_dir} || 'DESC';
1074 my $ou = $args{org_unit};
1075 my $ou_type = $args{depth};
1076 my $limit = $args{limit} || 10;
1077 my $visibility_limit = $args{visibility_limit} || 5000;
1078 my $offset = $args{offset} || 0;
1080 my $outer_limit = 1000;
1082 my $limit_clause = '';
1083 my $offset_clause = '';
1085 $limit_clause = "LIMIT $outer_limit";
1086 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1088 my (@types,@forms,@lang,@aud,@lit_form);
1089 my ($t_filter, $f_filter) = ('','');
1090 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1091 my ($ot_filter, $of_filter) = ('','');
1092 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1094 if (my $a = $args{audience}) {
1095 $a = [$a] if (!ref($a));
1098 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1099 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1102 if (my $l = $args{language}) {
1103 $l = [$l] if (!ref($l));
1106 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1107 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1110 if (my $f = $args{lit_form}) {
1111 $f = [$f] if (!ref($f));
1114 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1115 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1118 if ($args{format}) {
1119 my ($t, $f) = split '-', $args{format};
1120 @types = split '', $t;
1121 @forms = split '', $f;
1123 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1124 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1128 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1129 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1134 my $descendants = defined($ou_type) ?
1135 "actor.org_unit_descendants($ou, $ou_type)" :
1136 "actor.org_unit_descendants($ou)";
1138 my $class = $self->{cdbi};
1139 my $search_table = $class->table;
1141 my $metabib_full_rec = metabib::full_rec->table;
1142 my $metabib_record_descriptor = metabib::record_descriptor->table;
1143 my $metabib_metarecord = metabib::metarecord->table;
1144 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1145 my $asset_call_number_table = asset::call_number->table;
1146 my $asset_copy_table = asset::copy->table;
1147 my $cs_table = config::copy_status->table;
1148 my $cl_table = asset::copy_location->table;
1149 my $br_table = biblio::record_entry->table;
1151 my ($index_col) = $class->columns('FTS');
1152 $index_col ||= 'value';
1154 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1156 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1158 my $SQLstring = join('%',map { lc($_) } $fts->words);
1159 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1160 my $first_word = lc(($fts->words)[0]).'%';
1162 my $fts_where = $fts->sql_where_clause;
1163 my @fts_ranks = $fts->fts_rank;
1166 $bonus{'metabib::identifier_field_entry'} =
1167 $bonus{'metabib::keyword_field_entry'} = [
1168 { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring }
1171 $bonus{'metabib::title_field_entry'} =
1172 $bonus{'metabib::series_field_entry'} = [
1173 { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1174 { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1175 @{ $bonus{'metabib::keyword_field_entry'} }
1178 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1179 $bonus_list ||= '1';
1181 my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1183 my $relevance = join(' + ', @fts_ranks);
1184 $relevance = <<" RANK";
1185 (SUM( ( $relevance ) * ( $bonus_list ) )/COUNT(m.source))
1188 my $string_default_sort = 'zzzz';
1189 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1191 my $number_default_sort = '9999';
1192 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1194 my $rank = $relevance;
1195 if (lc($sort) eq 'pubdate') {
1198 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1199 FROM $metabib_full_rec frp
1200 WHERE frp.record = mr.master_record
1202 AND frp.subfield = 'c'
1206 } elsif (lc($sort) eq 'create_date') {
1208 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1210 } elsif (lc($sort) eq 'edit_date') {
1212 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1214 } elsif (lc($sort) eq 'title') {
1217 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1218 FROM $metabib_full_rec frt
1219 WHERE frt.record = mr.master_record
1221 AND frt.subfield = 'a'
1225 } elsif (lc($sort) eq 'author') {
1228 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1229 FROM $metabib_full_rec fra
1230 WHERE fra.record = mr.master_record
1231 AND fra.tag LIKE '1%'
1232 AND fra.subfield = 'a'
1233 ORDER BY fra.tag::text::int
1241 my $select = <<" SQL";
1242 SELECT m.metarecord,
1244 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1246 FROM $search_table f,
1247 $metabib_metarecord_source_map_table m,
1248 $metabib_metarecord_source_map_table smrs,
1249 $metabib_metarecord mr,
1250 $metabib_record_descriptor rd
1252 AND smrs.metarecord = mr.id
1253 AND m.source = f.source
1254 AND m.metarecord = mr.id
1255 AND rd.record = smrs.source
1261 GROUP BY m.metarecord
1262 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1263 LIMIT $visibility_limit
1270 FROM $asset_call_number_table cn,
1271 $metabib_metarecord_source_map_table mrs,
1272 $asset_copy_table cp,
1277 $metabib_record_descriptor ord,
1279 WHERE mrs.metarecord = s.metarecord
1280 AND br.id = mrs.source
1281 AND cn.record = mrs.source
1282 AND cp.status = cs.id
1283 AND cp.location = cl.id
1284 AND cn.owning_lib = d.id
1285 AND cp.call_number = cn.id
1286 AND cp.opac_visible IS TRUE
1287 AND cs.opac_visible IS TRUE
1288 AND cl.opac_visible IS TRUE
1289 AND d.opac_visible IS TRUE
1290 AND br.active IS TRUE
1291 AND br.deleted IS FALSE
1292 AND ord.record = mrs.source
1298 ORDER BY 4 $sort_dir
1300 } elsif ($self->api_name !~ /staff/o) {
1307 FROM $asset_call_number_table cn,
1308 $metabib_metarecord_source_map_table mrs,
1309 $asset_copy_table cp,
1314 $metabib_record_descriptor ord
1316 WHERE mrs.metarecord = s.metarecord
1317 AND br.id = mrs.source
1318 AND cn.record = mrs.source
1319 AND cp.status = cs.id
1320 AND cp.location = cl.id
1321 AND cp.circ_lib = d.id
1322 AND cp.call_number = cn.id
1323 AND cp.opac_visible IS TRUE
1324 AND cs.opac_visible IS TRUE
1325 AND cl.opac_visible IS TRUE
1326 AND d.opac_visible IS TRUE
1327 AND br.active IS TRUE
1328 AND br.deleted IS FALSE
1329 AND ord.record = mrs.source
1337 ORDER BY 4 $sort_dir
1346 FROM $asset_call_number_table cn,
1347 $asset_copy_table cp,
1348 $metabib_metarecord_source_map_table mrs,
1351 $metabib_record_descriptor ord
1353 WHERE mrs.metarecord = s.metarecord
1354 AND br.id = mrs.source
1355 AND cn.record = mrs.source
1356 AND cn.id = cp.call_number
1357 AND br.deleted IS FALSE
1358 AND cn.deleted IS FALSE
1359 AND ord.record = mrs.source
1360 AND ( cn.owning_lib = d.id
1361 OR ( cp.circ_lib = d.id
1362 AND cp.deleted IS FALSE
1374 FROM $asset_call_number_table cn,
1375 $metabib_metarecord_source_map_table mrs,
1376 $metabib_record_descriptor ord
1377 WHERE mrs.metarecord = s.metarecord
1378 AND cn.record = mrs.source
1379 AND ord.record = mrs.source
1387 ORDER BY 4 $sort_dir
1392 $log->debug("Field Search SQL :: [$select]",DEBUG);
1394 my $recs = $class->db_Main->selectall_arrayref(
1396 (@bonus_values > 0 ? @bonus_values : () ),
1397 ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1398 @types, @forms, @aud, @lang, @lit_form,
1399 @types, @forms, @aud, @lang, @lit_form,
1400 ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1402 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1405 $max = 1 if (!@$recs);
1407 $max = $$_[1] if ($$_[1] > $max);
1410 my $count = scalar(@$recs);
1411 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1412 my ($mrid,$rank,$skip) = @$rec;
1413 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1418 for my $class ( qw/title author subject keyword series identifier/ ) {
1419 __PACKAGE__->register_method(
1420 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1422 method => 'postfilter_search_class_fts',
1425 cdbi => "metabib::${class}_field_entry",
1428 __PACKAGE__->register_method(
1429 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1431 method => 'postfilter_search_class_fts',
1434 cdbi => "metabib::${class}_field_entry",
1441 my $_cdbi = { title => "metabib::title_field_entry",
1442 author => "metabib::author_field_entry",
1443 subject => "metabib::subject_field_entry",
1444 keyword => "metabib::keyword_field_entry",
1445 series => "metabib::series_field_entry",
1446 identifier => "metabib::identifier_field_entry",
1449 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1450 sub postfilter_search_multi_class_fts {
1455 my $sort = $args{'sort'};
1456 my $sort_dir = $args{sort_dir} || 'DESC';
1457 my $ou = $args{org_unit};
1458 my $ou_type = $args{depth};
1459 my $limit = $args{limit} || 10;
1460 my $offset = $args{offset} || 0;
1461 my $visibility_limit = $args{visibility_limit} || 5000;
1464 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1467 if (!defined($args{org_unit})) {
1468 die "No target organizational unit passed to ".$self->api_name;
1471 if (! scalar( keys %{$args{searches}} )) {
1472 die "No search arguments were passed to ".$self->api_name;
1475 my $outer_limit = 1000;
1477 my $limit_clause = '';
1478 my $offset_clause = '';
1480 $limit_clause = "LIMIT $outer_limit";
1481 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1483 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1484 my ($t_filter, $f_filter, $v_filter) = ('','','');
1485 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1486 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1487 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1489 if ($args{available}) {
1490 $avail_filter = ' AND cp.status IN (0,7,12)';
1493 if (my $a = $args{audience}) {
1494 $a = [$a] if (!ref($a));
1497 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1498 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1501 if (my $l = $args{language}) {
1502 $l = [$l] if (!ref($l));
1505 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1506 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1509 if (my $f = $args{lit_form}) {
1510 $f = [$f] if (!ref($f));
1513 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1514 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1517 if (my $f = $args{item_form}) {
1518 $f = [$f] if (!ref($f));
1521 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1522 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1525 if (my $t = $args{item_type}) {
1526 $t = [$t] if (!ref($t));
1529 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1530 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1533 if (my $v = $args{vr_format}) {
1534 $v = [$v] if (!ref($v));
1537 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1538 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1542 # XXX legacy format and item type support
1543 if ($args{format}) {
1544 my ($t, $f) = split '-', $args{format};
1545 @types = split '', $t;
1546 @forms = split '', $f;
1548 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1549 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1553 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1554 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1560 my $descendants = defined($ou_type) ?
1561 "actor.org_unit_descendants($ou, $ou_type)" :
1562 "actor.org_unit_descendants($ou)";
1564 my $search_table_list = '';
1566 my $join_table_list = '';
1569 my $field_table = config::metabib_field->table;
1573 my $prev_search_group;
1574 my $curr_search_group;
1578 for my $search_group (sort keys %{$args{searches}}) {
1579 (my $search_group_name = $search_group) =~ s/\|/_/gso;
1580 ($search_class,$search_field) = split /\|/, $search_group;
1581 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1583 if ($search_field) {
1584 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1585 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1590 $prev_search_group = $curr_search_group if ($curr_search_group);
1592 $curr_search_group = $search_group_name;
1594 my $class = $_cdbi->{$search_class};
1595 my $search_table = $class->table;
1597 my ($index_col) = $class->columns('FTS');
1598 $index_col ||= 'value';
1601 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1603 my $fts_where = $fts->sql_where_clause;
1604 my @fts_ranks = $fts->fts_rank;
1606 my $SQLstring = join('%',map { lc($_) } $fts->words);
1607 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1608 my $first_word = lc(($fts->words)[0]).'%';
1610 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1611 my $rank = join(' + ', @fts_ranks);
1614 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1615 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1617 $bonus{'series'} = [
1618 { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1619 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1622 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1624 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1625 $bonus_list ||= '1';
1627 push @bonus_lists, $bonus_list;
1628 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1631 #---------------------
1633 $search_table_list .= "$search_table $search_group_name, ";
1634 push @rank_list,$rank;
1635 $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1637 if ($metabib_field) {
1638 $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1639 $metabib_field = undef;
1642 if ($prev_search_group) {
1643 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1647 my $metabib_record_descriptor = metabib::record_descriptor->table;
1648 my $metabib_full_rec = metabib::full_rec->table;
1649 my $metabib_metarecord = metabib::metarecord->table;
1650 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1651 my $asset_call_number_table = asset::call_number->table;
1652 my $asset_copy_table = asset::copy->table;
1653 my $cs_table = config::copy_status->table;
1654 my $cl_table = asset::copy_location->table;
1655 my $br_table = biblio::record_entry->table;
1656 my $source_table = config::bib_source->table;
1658 my $bonuses = join (' * ', @bonus_lists);
1659 my $relevance = join (' + ', @rank_list);
1660 $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1662 my $string_default_sort = 'zzzz';
1663 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1665 my $number_default_sort = '9999';
1666 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1670 my $secondary_sort = <<" SORT";
1672 SELECT COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1673 FROM $metabib_full_rec sfrt,
1674 $metabib_metarecord mr
1675 WHERE sfrt.record = mr.master_record
1676 AND sfrt.tag = '245'
1677 AND sfrt.subfield = 'a'
1682 my $rank = $relevance;
1683 if (lc($sort) eq 'pubdate') {
1686 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1687 FROM $metabib_full_rec frp
1688 WHERE frp.record = mr.master_record
1690 AND frp.subfield = 'c'
1694 } elsif (lc($sort) eq 'create_date') {
1696 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1698 } elsif (lc($sort) eq 'edit_date') {
1700 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1702 } elsif (lc($sort) eq 'title') {
1705 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1706 FROM $metabib_full_rec frt
1707 WHERE frt.record = mr.master_record
1709 AND frt.subfield = 'a'
1713 $secondary_sort = <<" SORT";
1715 SELECT COALESCE(SUBSTRING(sfrp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1716 FROM $metabib_full_rec sfrp
1717 WHERE sfrp.record = mr.master_record
1718 AND sfrp.tag = '260'
1719 AND sfrp.subfield = 'c'
1723 } elsif (lc($sort) eq 'author') {
1726 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1727 FROM $metabib_full_rec fra
1728 WHERE fra.record = mr.master_record
1729 AND fra.tag LIKE '1%'
1730 AND fra.subfield = 'a'
1731 ORDER BY fra.tag::text::int
1736 push @bonus_values, @bonus_values;
1741 my $select = <<" SQL";
1742 SELECT m.metarecord,
1744 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1747 FROM $search_table_list
1748 $metabib_metarecord mr,
1749 $metabib_metarecord_source_map_table m,
1750 $metabib_metarecord_source_map_table smrs
1751 WHERE m.metarecord = smrs.metarecord
1752 AND mr.id = m.metarecord
1755 GROUP BY m.metarecord
1756 -- ORDER BY 4 $sort_dir
1757 LIMIT $visibility_limit
1760 if ($self->api_name !~ /staff/o) {
1767 FROM $asset_call_number_table cn,
1768 $metabib_metarecord_source_map_table mrs,
1769 $asset_copy_table cp,
1774 $metabib_record_descriptor ord
1775 WHERE mrs.metarecord = s.metarecord
1776 AND br.id = mrs.source
1777 AND cn.record = mrs.source
1778 AND cp.status = cs.id
1779 AND cp.location = cl.id
1780 AND cp.circ_lib = d.id
1781 AND cp.call_number = cn.id
1782 AND cp.opac_visible IS TRUE
1783 AND cs.opac_visible IS TRUE
1784 AND cl.opac_visible IS TRUE
1785 AND d.opac_visible IS TRUE
1786 AND br.active IS TRUE
1787 AND br.deleted IS FALSE
1788 AND cp.deleted IS FALSE
1789 AND cn.deleted IS FALSE
1790 AND ord.record = mrs.source
1803 $metabib_metarecord_source_map_table mrs,
1804 $metabib_record_descriptor ord,
1806 WHERE mrs.metarecord = s.metarecord
1807 AND ord.record = mrs.source
1808 AND br.id = mrs.source
1809 AND br.source = src.id
1810 AND src.transcendant IS TRUE
1818 ORDER BY 4 $sort_dir, 5
1825 $metabib_metarecord_source_map_table omrs,
1826 $metabib_record_descriptor ord
1827 WHERE omrs.metarecord = s.metarecord
1828 AND ord.record = omrs.source
1831 FROM $asset_call_number_table cn,
1832 $asset_copy_table cp,
1835 WHERE br.id = omrs.source
1836 AND cn.record = omrs.source
1837 AND br.deleted IS FALSE
1838 AND cn.deleted IS FALSE
1839 AND cp.call_number = cn.id
1840 AND ( cn.owning_lib = d.id
1841 OR ( cp.circ_lib = d.id
1842 AND cp.deleted IS FALSE
1850 FROM $asset_call_number_table cn
1851 WHERE cn.record = omrs.source
1852 AND cn.deleted IS FALSE
1858 $metabib_metarecord_source_map_table mrs,
1859 $metabib_record_descriptor ord,
1861 WHERE mrs.metarecord = s.metarecord
1862 AND br.id = mrs.source
1863 AND br.source = src.id
1864 AND src.transcendant IS TRUE
1880 ORDER BY 4 $sort_dir, 5
1885 $log->debug("Field Search SQL :: [$select]",DEBUG);
1887 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1890 @types, @forms, @vformats, @aud, @lang, @lit_form,
1891 @types, @forms, @vformats, @aud, @lang, @lit_form,
1892 # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1895 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1898 $max = 1 if (!@$recs);
1900 $max = $$_[1] if ($$_[1] > $max);
1903 my $count = scalar(@$recs);
1904 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1905 next unless ($$rec[0]);
1906 my ($mrid,$rank,$skip) = @$rec;
1907 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1912 __PACKAGE__->register_method(
1913 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1915 method => 'postfilter_search_multi_class_fts',
1920 __PACKAGE__->register_method(
1921 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1923 method => 'postfilter_search_multi_class_fts',
1929 __PACKAGE__->register_method(
1930 api_name => "open-ils.storage.metabib.multiclass.search_fts",
1932 method => 'postfilter_search_multi_class_fts',
1937 __PACKAGE__->register_method(
1938 api_name => "open-ils.storage.metabib.multiclass.search_fts.staff",
1940 method => 'postfilter_search_multi_class_fts',
1946 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1947 sub biblio_search_multi_class_fts {
1952 my $sort = $args{'sort'};
1953 my $sort_dir = $args{sort_dir} || 'DESC';
1954 my $ou = $args{org_unit};
1955 my $ou_type = $args{depth};
1956 my $limit = $args{limit} || 10;
1957 my $offset = $args{offset} || 0;
1958 my $pref_lang = $args{preferred_language} || 'eng';
1959 my $visibility_limit = $args{visibility_limit} || 5000;
1962 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1965 if (! scalar( keys %{$args{searches}} )) {
1966 die "No search arguments were passed to ".$self->api_name;
1969 my $outer_limit = 1000;
1971 my $limit_clause = '';
1972 my $offset_clause = '';
1974 $limit_clause = "LIMIT $outer_limit";
1975 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1977 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1978 my ($t_filter, $f_filter, $v_filter) = ('','','');
1979 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1980 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1981 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1983 if ($args{available}) {
1984 $avail_filter = ' AND cp.status IN (0,7,12)';
1987 if (my $a = $args{audience}) {
1988 $a = [$a] if (!ref($a));
1991 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1992 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1995 if (my $l = $args{language}) {
1996 $l = [$l] if (!ref($l));
1999 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
2000 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
2003 if (my $f = $args{lit_form}) {
2004 $f = [$f] if (!ref($f));
2007 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2008 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2011 if (my $f = $args{item_form}) {
2012 $f = [$f] if (!ref($f));
2015 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2016 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2019 if (my $t = $args{item_type}) {
2020 $t = [$t] if (!ref($t));
2023 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2024 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2027 if (my $v = $args{vr_format}) {
2028 $v = [$v] if (!ref($v));
2031 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
2032 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
2035 # XXX legacy format and item type support
2036 if ($args{format}) {
2037 my ($t, $f) = split '-', $args{format};
2038 @types = split '', $t;
2039 @forms = split '', $f;
2041 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2042 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2046 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2047 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2052 my $descendants = defined($ou_type) ?
2053 "actor.org_unit_descendants($ou, $ou_type)" :
2054 "actor.org_unit_descendants($ou)";
2056 my $search_table_list = '';
2058 my $join_table_list = '';
2061 my $field_table = config::metabib_field->table;
2065 my $prev_search_group;
2066 my $curr_search_group;
2070 for my $search_group (sort keys %{$args{searches}}) {
2071 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2072 ($search_class,$search_field) = split /\|/, $search_group;
2073 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2075 if ($search_field) {
2076 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2077 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2082 $prev_search_group = $curr_search_group if ($curr_search_group);
2084 $curr_search_group = $search_group_name;
2086 my $class = $_cdbi->{$search_class};
2087 my $search_table = $class->table;
2089 my ($index_col) = $class->columns('FTS');
2090 $index_col ||= 'value';
2093 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2095 my $fts_where = $fts->sql_where_clause;
2096 my @fts_ranks = $fts->fts_rank;
2098 my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2099 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2100 my $first_word = lc(($fts->words)[0]).'%';
2102 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2103 my $rank = join(' + ', @fts_ranks);
2106 $bonus{'subject'} = [];
2107 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2109 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2111 $bonus{'series'} = [
2112 { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2113 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2116 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2119 push @{ $bonus{'title'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2120 push @{ $bonus{'author'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2121 push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2122 push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2123 push @{ $bonus{'series'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2126 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2127 $bonus_list ||= '1';
2129 push @bonus_lists, $bonus_list;
2130 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2132 #---------------------
2134 $search_table_list .= "$search_table $search_group_name, ";
2135 push @rank_list,$rank;
2136 $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2138 if ($metabib_field) {
2139 $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2140 $metabib_field = undef;
2143 if ($prev_search_group) {
2144 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2148 my $metabib_record_descriptor = metabib::record_descriptor->table;
2149 my $metabib_full_rec = metabib::full_rec->table;
2150 my $metabib_metarecord = metabib::metarecord->table;
2151 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2152 my $asset_call_number_table = asset::call_number->table;
2153 my $asset_copy_table = asset::copy->table;
2154 my $cs_table = config::copy_status->table;
2155 my $cl_table = asset::copy_location->table;
2156 my $br_table = biblio::record_entry->table;
2157 my $source_table = config::bib_source->table;
2160 my $bonuses = join (' * ', @bonus_lists);
2161 my $relevance = join (' + ', @rank_list);
2162 $relevance = "AVG( ($relevance) * ($bonuses) )";
2164 my $string_default_sort = 'zzzz';
2165 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2167 my $number_default_sort = '9999';
2168 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2170 my $rank = $relevance;
2171 if (lc($sort) eq 'pubdate') {
2174 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d{4}'),'$number_default_sort')::INT
2175 FROM $metabib_full_rec frp
2176 WHERE frp.record = b.id
2178 AND frp.subfield = 'c'
2182 } elsif (lc($sort) eq 'create_date') {
2184 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2186 } elsif (lc($sort) eq 'edit_date') {
2188 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2190 } elsif (lc($sort) eq 'title') {
2193 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2194 FROM $metabib_full_rec frt
2195 WHERE frt.record = b.id
2197 AND frt.subfield = 'a'
2201 } elsif (lc($sort) eq 'author') {
2204 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
2205 FROM $metabib_full_rec fra
2206 WHERE fra.record = b.id
2207 AND fra.tag LIKE '1%'
2208 AND fra.subfield = 'a'
2209 ORDER BY fra.tag::text::int
2214 push @bonus_values, @bonus_values;
2219 my $select = <<" SQL";
2224 FROM $search_table_list
2225 $metabib_record_descriptor rd,
2228 WHERE rd.record = b.id
2229 AND b.active IS TRUE
2230 AND b.deleted IS FALSE
2239 GROUP BY b.id, b.source
2240 ORDER BY 3 $sort_dir
2241 LIMIT $visibility_limit
2244 if ($self->api_name !~ /staff/o) {
2249 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2252 FROM $asset_call_number_table cn,
2253 $asset_copy_table cp,
2257 WHERE cn.record = s.id
2258 AND cp.status = cs.id
2259 AND cp.location = cl.id
2260 AND cp.call_number = cn.id
2261 AND cp.opac_visible IS TRUE
2262 AND cs.opac_visible IS TRUE
2263 AND cl.opac_visible IS TRUE
2264 AND d.opac_visible IS TRUE
2265 AND cp.deleted IS FALSE
2266 AND cn.deleted IS FALSE
2267 AND cp.circ_lib = d.id
2271 OR src.transcendant IS TRUE
2272 ORDER BY 3 $sort_dir
2279 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2282 FROM $asset_call_number_table cn,
2283 $asset_copy_table cp,
2285 WHERE cn.record = s.id
2286 AND cp.call_number = cn.id
2287 AND cn.deleted IS FALSE
2288 AND cp.circ_lib = d.id
2289 AND cp.deleted IS FALSE
2295 FROM $asset_call_number_table cn
2296 WHERE cn.record = s.id
2299 OR src.transcendant IS TRUE
2300 ORDER BY 3 $sort_dir
2305 $log->debug("Field Search SQL :: [$select]",DEBUG);
2307 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2309 @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2312 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2314 my $count = scalar(@$recs);
2315 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2316 next unless ($$rec[0]);
2317 my ($mrid,$rank) = @$rec;
2318 $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2323 __PACKAGE__->register_method(
2324 api_name => "open-ils.storage.biblio.multiclass.search_fts.record",
2326 method => 'biblio_search_multi_class_fts',
2331 __PACKAGE__->register_method(
2332 api_name => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2334 method => 'biblio_search_multi_class_fts',
2339 __PACKAGE__->register_method(
2340 api_name => "open-ils.storage.biblio.multiclass.search_fts",
2342 method => 'biblio_search_multi_class_fts',
2347 __PACKAGE__->register_method(
2348 api_name => "open-ils.storage.biblio.multiclass.search_fts.staff",
2350 method => 'biblio_search_multi_class_fts',
2358 my $default_preferred_language;
2359 my $default_preferred_language_weight;
2361 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2367 if (!$locale_map{COMPLETE}) {
2369 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2370 for my $locale ( @locales ) {
2371 $locale_map{lc($locale->code)} = $locale->marc_code;
2373 $locale_map{COMPLETE} = 1;
2377 my $config = OpenSRF::Utils::SettingsClient->new();
2379 if (!$default_preferred_language) {
2381 $default_preferred_language = $config->config_value(
2382 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2383 ) || $config->config_value(
2384 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2389 if (!$default_preferred_language_weight) {
2391 $default_preferred_language_weight = $config->config_value(
2392 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2393 ) || $config->config_value(
2394 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2398 # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2399 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2401 my $ou = $args{org_unit};
2402 my $limit = $args{limit} || 10;
2403 my $offset = $args{offset} || 0;
2406 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2409 if (! scalar( keys %{$args{searches}} )) {
2410 die "No search arguments were passed to ".$self->api_name;
2413 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2415 if (!defined($args{preferred_language})) {
2416 my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2417 $args{preferred_language} =
2418 $locale_map{ lc($ses_locale) } || 'eng';
2421 if (!defined($args{preferred_language_weight})) {
2422 $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2425 if ($args{available}) {
2426 @statuses = (0,7,12);
2429 if (my $s = $args{locations}) {
2430 $s = [$s] if (!ref($s));
2434 if (my $b = $args{between}) {
2435 if (ref($b) && @$b == 2) {
2440 if (my $s = $args{statuses}) {
2441 $s = [$s] if (!ref($s));
2445 if (my $a = $args{audience}) {
2446 $a = [$a] if (!ref($a));
2450 if (my $l = $args{language}) {
2451 $l = [$l] if (!ref($l));
2455 if (my $f = $args{lit_form}) {
2456 $f = [$f] if (!ref($f));
2460 if (my $f = $args{item_form}) {
2461 $f = [$f] if (!ref($f));
2465 if (my $t = $args{item_type}) {
2466 $t = [$t] if (!ref($t));
2470 if (my $b = $args{bib_level}) {
2471 $b = [$b] if (!ref($b));
2475 if (my $v = $args{vr_format}) {
2476 $v = [$v] if (!ref($v));
2480 # XXX legacy format and item type support
2481 if ($args{format}) {
2482 my ($t, $f) = split '-', $args{format};
2483 @types = split '', $t;
2484 @forms = split '', $f;
2487 my %stored_proc_search_args;
2488 for my $search_group (sort keys %{$args{searches}}) {
2489 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2490 my ($search_class,$search_field) = split /\|/, $search_group;
2491 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2493 if ($search_field) {
2494 unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2495 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2500 my $class = $_cdbi->{$search_class};
2501 my $search_table = $class->table;
2503 my ($index_col) = $class->columns('FTS');
2504 $index_col ||= 'value';
2507 my $fts = OpenILS::Application::Storage::FTS->compile(
2508 $search_class => $args{searches}{$search_group}{term},
2509 $search_group_name.'.value',
2510 "$search_group_name.$index_col"
2512 $fts->sql_where_clause; # this builds the ranks for us
2514 my @fts_ranks = $fts->fts_rank;
2515 my @fts_queries = $fts->fts_query;
2516 my @phrases = map { lc($_) } $fts->phrases;
2517 my @words = map { lc($_) } $fts->words;
2519 $stored_proc_search_args{$search_group} = {
2520 fts_rank => \@fts_ranks,
2521 fts_query => \@fts_queries,
2522 phrase => \@phrases,
2528 my $param_search_ou = $ou;
2529 my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2530 my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2531 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2532 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2533 my $param_audience = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud ) . '}$$';
2534 my $param_language = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang ) . '}$$';
2535 my $param_lit_form = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2536 my $param_types = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types ) . '}$$';
2537 my $param_forms = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms ) . '}$$';
2538 my $param_vformats = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2539 my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2540 my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2541 my $param_after = $args{after} ; $param_after = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2542 my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2543 my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2544 my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2545 my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2546 my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2547 my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2548 my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2549 my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2550 my $param_rel_limit = $args{core_limit}; $param_rel_limit ||= 'NULL';
2551 my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2552 my $param_skip_chk = $args{skip_check}; $param_skip_chk ||= 'NULL';
2554 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2556 FROM search.staged_fts(
2557 $param_search_ou\:\:INT,
2558 $param_depth\:\:INT,
2559 $param_searches\:\:TEXT,
2560 $param_statuses\:\:INT[],
2561 $param_locations\:\:INT[],
2562 $param_audience\:\:TEXT[],
2563 $param_language\:\:TEXT[],
2564 $param_lit_form\:\:TEXT[],
2565 $param_types\:\:TEXT[],
2566 $param_forms\:\:TEXT[],
2567 $param_vformats\:\:TEXT[],
2568 $param_bib_level\:\:TEXT[],
2569 $param_before\:\:TEXT,
2570 $param_after\:\:TEXT,
2571 $param_during\:\:TEXT,
2572 $param_between\:\:TEXT[],
2573 $param_pref_lang\:\:TEXT,
2574 $param_pref_lang_multiplier\:\:REAL,
2575 $param_sort\:\:TEXT,
2576 $param_sort_desc\:\:BOOL,
2577 $metarecord\:\:BOOL,
2579 $param_rel_limit\:\:INT,
2580 $param_chk_limit\:\:INT,
2581 $param_skip_chk\:\:INT
2587 my $recs = $sth->fetchall_arrayref({});
2588 my $summary_row = pop @$recs;
2590 my $total = $$summary_row{total};
2591 my $checked = $$summary_row{checked};
2592 my $visible = $$summary_row{visible};
2593 my $deleted = $$summary_row{deleted};
2594 my $excluded = $$summary_row{excluded};
2596 my $estimate = $visible;
2597 if ( $total > $checked && $checked ) {
2599 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2600 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2604 delete $$summary_row{id};
2605 delete $$summary_row{rel};
2606 delete $$summary_row{record};
2608 $client->respond( $summary_row );
2610 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2612 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2613 delete $$rec{checked};
2614 delete $$rec{visible};
2615 delete $$rec{excluded};
2616 delete $$rec{deleted};
2617 delete $$rec{total};
2618 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2620 $client->respond( $rec );
2624 __PACKAGE__->register_method(
2625 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
2627 method => 'staged_fts',
2632 __PACKAGE__->register_method(
2633 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2635 method => 'staged_fts',
2640 __PACKAGE__->register_method(
2641 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
2643 method => 'staged_fts',
2648 __PACKAGE__->register_method(
2649 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2651 method => 'staged_fts',
2657 sub FTS_paging_estimate {
2661 my $checked = shift;
2662 my $visible = shift;
2663 my $excluded = shift;
2664 my $deleted = shift;
2667 my $deleted_ratio = $deleted / $checked;
2668 my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2670 my $exclusion_ratio = $excluded / $checked;
2671 my $delete_adjusted_exclusion_ratio = $checked - $deleted ? $excluded / ($checked - $deleted) : 1;
2673 my $inclusion_ratio = $visible / $checked;
2674 my $delete_adjusted_inclusion_ratio = $checked - $deleted ? $visible / ($checked - $deleted) : 0;
2677 exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2678 inclusion => int($delete_adjusted_total * $inclusion_ratio),
2679 delete_adjusted_exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2680 delete_adjusted_inclusion => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2683 __PACKAGE__->register_method(
2684 api_name => "open-ils.storage.fts_paging_estimate",
2686 method => 'FTS_paging_estimate',
2692 Hash of estimation values based on four variant estimation strategies:
2693 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2694 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2695 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2696 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2699 Helper method used to determin the approximate number of
2700 hits for a search that spans multiple superpages. For
2701 sparse superpages, the inclusion estimate will likely be the
2702 best estimate. The exclusion strategy is the original, but
2703 inclusion is the default.
2706 { name => 'checked',
2707 desc => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2710 { name => 'visible',
2711 desc => 'Number of records visible to the search location on the current superpage.',
2714 { name => 'excluded',
2715 desc => 'Number of records excluded from the search location on the current superpage.',
2718 { name => 'deleted',
2719 desc => 'Number of deleted records on the current superpage.',
2723 desc => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2736 my $term = $$args{term};
2737 my $limit = $$args{max} || 1;
2738 my $min = $$args{min} || 1;
2739 my @classes = @{$$args{class}};
2741 $limit = $min if ($min > $limit);
2744 @classes = ( qw/ title author subject series keyword / );
2748 my $bre_table = biblio::record_entry->table;
2749 my $cn_table = asset::call_number->table;
2750 my $cp_table = asset::copy->table;
2752 for my $search_class ( @classes ) {
2754 my $class = $_cdbi->{$search_class};
2755 my $search_table = $class->table;
2757 my ($index_col) = $class->columns('FTS');
2758 $index_col ||= 'value';
2761 my $where = OpenILS::Application::Storage::FTS
2762 ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2766 SELECT COUNT(DISTINCT X.source)
2767 FROM (SELECT $search_class.source
2768 FROM $search_table $search_class
2769 JOIN $bre_table b ON (b.id = $search_class.source)
2774 HAVING COUNT(DISTINCT X.source) >= $min;
2777 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2778 $matches{$search_class} = $res ? $res->[0] : 0;
2783 __PACKAGE__->register_method(
2784 api_name => "open-ils.storage.search.xref",
2786 method => 'xref_count',
2790 # Takes an abstract query object and recursively turns it back into a string
2792 sub abstract_query2str {
2793 my ($self, $conn, $query) = @_;
2795 return QueryParser::Canonicalize::abstract_query2str_impl($query, 0, $OpenILS::Application::Storage::QParser);
2798 __PACKAGE__->register_method(
2799 api_name => "open-ils.storage.query_parser.abstract_query.canonicalize",
2801 method => "abstract_query2str",
2806 Abstract query parser object, with complete config data. For example input,
2807 see the 'abstract_query' part of the output of an API call like
2808 open-ils.search.biblio.multiclass.query, when called with the return_abstract
2812 return => { type => "string", desc => "String representation of abstract query object" }
2816 sub str2abstract_query {
2817 my ($self, $conn, $query, $qp_opts, $with_config) = @_;
2819 my %use_opts = ( # reasonable defaults? should these even be hardcoded here?
2821 superpage_size => 1000,
2822 core_limit => 25000,
2824 (ref $opts eq 'HASH' ? %$opts : ())
2829 # grab the query parser and initialize it
2830 my $parser = $OpenILS::Application::Storage::QParser;
2833 _initialize_parser($parser) unless $parser->initialization_complete;
2835 my $query = $parser->new(%use_opts)->parse;
2837 return $query->parse_tree->to_abstract_query(with_config => $with_config);
2840 __PACKAGE__->register_method(
2841 api_name => "open-ils.storage.query_parser.abstract_query.from_string",
2843 method => "str2abstract_query",
2847 {desc => "Query", type => "string"},
2848 {desc => q/Arguments for initializing QueryParser (optional)/,
2850 {desc => q/Flag enabling inclusion of QP config in returned object (optional, default false)/,
2853 return => { type => "object", desc => "abstract representation of query parser query" }
2857 my @available_statuses_cache;
2858 sub available_statuses {
2859 if (!scalar(@available_statuses_cache)) {
2860 @available_statuses_cache = map { $_->id } config::copy_status->search_where({is_available => 't'});
2862 return @available_statuses_cache;
2865 sub query_parser_fts {
2871 # grab the query parser and initialize it
2872 my $parser = $OpenILS::Application::Storage::QParser;
2875 _initialize_parser($parser) unless $parser->initialization_complete;
2877 # populate the locale/language map
2878 if (!$locale_map{COMPLETE}) {
2880 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2881 for my $locale ( @locales ) {
2882 $locale_map{lc($locale->code)} = $locale->marc_code;
2884 $locale_map{COMPLETE} = 1;
2888 # I hope we have a query!
2889 if (! $args{query} ) {
2890 die "No query was passed to ".$self->api_name;
2893 my $default_CD_modifiers = OpenSRF::Utils::SettingsClient->new->config_value(
2894 apps => 'open-ils.search' => app_settings => 'default_CD_modifiers'
2897 # Protect against empty / missing default_CD_modifiers setting
2898 if ($default_CD_modifiers and !ref($default_CD_modifiers)) {
2899 $args{query} = "$default_CD_modifiers $args{query}";
2902 my $simple_plan = $args{_simple_plan};
2903 # remove bad chunks of the %args hash
2904 for my $bad ( grep { /^_/ } keys(%args)) {
2905 delete($args{$bad});
2909 # parse the query and supply any query-level %arg-based defaults
2910 # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2911 my $query = $parser->new( %args )->parse;
2913 my $config = OpenSRF::Utils::SettingsClient->new();
2915 # set the locale-based default preferred location
2916 if (!$query->parse_tree->find_filter('preferred_language')) {
2917 $parser->default_preferred_language( $args{preferred_language} );
2919 if (!$parser->default_preferred_language) {
2920 my $ses_locale = $client->session ? $client->session->session_locale : '';
2921 $parser->default_preferred_language( $locale_map{ lc($ses_locale) } );
2924 if (!$parser->default_preferred_language) { # still nothing...
2925 my $tmp_dpl = $config->config_value(
2926 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2927 ) || $config->config_value(
2928 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2931 $parser->default_preferred_language( $tmp_dpl )
2936 # set the global default language multiplier
2937 if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2940 if ($tmp_dplw = $args{preferred_language_weight} || $args{preferred_language_multiplier} ) {
2941 $parser->default_preferred_language_multiplier($tmp_dplw);
2944 $tmp_dplw = $config->config_value(
2945 apps => 'open-ils.search' => app_settings => 'default_preferred_language_weight'
2946 ) || $config->config_value(
2947 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2950 $parser->default_preferred_language_multiplier( $tmp_dplw );
2954 # gather the site, if one is specified, defaulting to the in-query version
2955 my $ou = $args{org_unit};
2956 if (my ($filter) = $query->parse_tree->find_filter('site')) {
2957 $ou = $filter->args->[0] if (@{$filter->args});
2959 $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^(-)?\d+$/);
2961 # gather lasso, as with $ou
2962 my $lasso = $args{lasso};
2963 if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
2964 $lasso = $filter->args->[0] if (@{$filter->args});
2966 $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
2967 $lasso = -$lasso if ($lasso);
2970 # # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
2971 # # gather user lasso, as with $ou and lasso
2972 # my $mylasso = $args{my_lasso};
2973 # if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
2974 # $mylasso = $filter->args->[0] if (@{$filter->args});
2976 # $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
2979 # if we have a lasso, go with that, otherwise ... ou
2980 $ou = $lasso if ($lasso);
2982 # gather the preferred OU, if one is specified, as with $ou
2983 my $pref_ou = $args{pref_ou};
2984 $log->info("pref_ou = $pref_ou");
2985 if (my ($filter) = $query->parse_tree->find_filter('pref_ou')) {
2986 $pref_ou = $filter->args->[0] if (@{$filter->args});
2988 $pref_ou = actor::org_unit->search( { shortname => $pref_ou } )->next->id if ($pref_ou and $pref_ou !~ /^(-)?\d+$/);
2990 # get the default $ou if we have nothing
2991 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
2994 # 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
2995 # gather the depth, if one is specified, defaulting to the in-query version
2996 my $depth = $args{depth};
2997 if (my ($filter) = $query->parse_tree->find_filter('depth')) {
2998 $depth = $filter->args->[0] if (@{$filter->args});
3000 $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
3003 # gather the limit or default to 10
3004 my $limit = $args{check_limit};
3005 if (my ($filter) = $query->parse_tree->find_filter('limit')) {
3006 $limit = $filter->args->[0] if (@{$filter->args});
3008 if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
3009 $limit = $filter->args->[0] if (@{$filter->args});
3013 # gather the offset or default to 0
3014 my $offset = $args{skip_check} || $args{offset};
3015 if (my ($filter) = $query->parse_tree->find_filter('offset')) {
3016 $offset = $filter->args->[0] if (@{$filter->args});
3018 if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
3019 $offset = $filter->args->[0] if (@{$filter->args});
3023 # gather the estimation strategy or default to inclusion
3024 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
3025 if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
3026 $estimation_strategy = $filter->args->[0] if (@{$filter->args});
3030 # gather the estimation strategy or default to inclusion
3031 my $core_limit = $args{core_limit};
3032 if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
3033 $core_limit = $filter->args->[0] if (@{$filter->args});
3037 # gather statuses, and then forget those if we have an #available modifier
3039 if ($query->parse_tree->find_modifier('available')) {
3040 @statuses = available_statuses();
3041 } elsif (my ($filter) = $query->parse_tree->find_filter('statuses')) {
3042 @statuses = @{$filter->args} if (@{$filter->args});
3048 if (my ($filter) = $query->parse_tree->find_filter('locations')) {
3049 @location = @{$filter->args} if (@{$filter->args});
3052 # gather location_groups
3053 if (my ($filter) = $query->parse_tree->find_filter('location_groups')) {
3054 my @loc_groups = @{$filter->args} if (@{$filter->args});
3056 # collect the mapped locations and add them to the locations() filter
3059 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3060 my $maps = $cstore->request(
3061 'open-ils.cstore.direct.asset.copy_location_group_map.search.atomic',
3062 {lgroup => \@loc_groups})->gather(1);
3064 push(@location, $_->location) for @$maps;
3069 my $param_check = $limit || $query->superpage_size || 'NULL';
3070 my $param_offset = $offset || 'NULL';
3071 my $param_limit = $core_limit || 'NULL';
3073 my $sp = $query->superpage || 1;
3075 $param_offset = ($sp - 1) * $sp_size;
3078 my $param_search_ou = $ou;
3079 my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
3080 # my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
3081 my $param_core_query = $query->parse_tree->toSQL;
3082 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
3083 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
3084 my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
3085 my $deleted_search = ($query->parse_tree->find_modifier('deleted')) ? "'t'" : "'f'";
3086 my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
3087 my $param_pref_ou = $pref_ou || 'NULL';
3089 # my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3090 # SELECT * -- bib search: $args{query}
3091 # FROM search.query_parser_fts(
3092 # $param_search_ou\:\:INT,
3093 # $param_depth\:\:INT,
3094 # $param_core_query\:\:TEXT,
3095 # $param_statuses\:\:INT[],
3096 # $param_locations\:\:INT[],
3097 # $param_offset\:\:INT,
3098 # $param_check\:\:INT,
3099 # $param_limit\:\:INT,
3100 # $metarecord\:\:BOOL,
3102 # $deleted_search\:\:BOOL,
3103 # $param_pref_ou\:\:INT
3107 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3108 -- bib search: $args{query}
3114 my $recs = $sth->fetchall_arrayref({});
3115 my $summary_row = pop @$recs;
3117 my $total = $$summary_row{total};
3118 my $checked = $$summary_row{checked};
3119 my $visible = $$summary_row{visible};
3120 my $deleted = $$summary_row{deleted};
3121 my $excluded = $$summary_row{excluded};
3123 delete $$summary_row{id};
3124 delete $$summary_row{rel};
3125 delete $$summary_row{record};
3126 delete $$summary_row{badges};
3127 delete $$summary_row{popularity};
3129 if (defined($simple_plan)) {
3130 $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3132 $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3135 if ($args{return_query}) {
3136 $$summary_row{query_struct} = $query->parse_tree->to_abstract_query();
3137 $$summary_row{canonicalized_query} = QueryParser::Canonicalize::abstract_query2str_impl($$summary_row{query_struct}, 0, $parser);
3140 $client->respond( $summary_row );
3142 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $visible.",DEBUG);
3144 for my $rec (@$recs) {
3145 delete $$rec{checked};
3146 delete $$rec{visible};
3147 delete $$rec{excluded};
3148 delete $$rec{deleted};
3149 delete $$rec{total};
3150 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3152 $client->respond( $rec );
3156 __PACKAGE__->register_method(
3157 api_name => "open-ils.storage.query_parser_search",
3159 method => 'query_parser_fts',
3167 sub query_parser_fts_wrapper {
3172 $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3173 # grab the query parser and initialize it
3174 my $parser = $OpenILS::Application::Storage::QParser;
3177 _initialize_parser($parser) unless $parser->initialization_complete;
3179 $args{searches} ||= {};
3180 if (!scalar(keys(%{$args{searches}})) && !$args{query}) {
3181 die "No search arguments were passed to ".$self->api_name;
3184 $top_org ||= actor::org_unit->search( { parent_ou => undef } )->next;
3186 my $base_query = $args{query} || '';
3187 if (scalar(keys(%{$args{searches}}))) {
3188 $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3189 for my $sclass ( keys %{$args{searches}} ) {
3190 $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3191 $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3195 my $query = $base_query;
3196 $log->debug("Full base query: $base_query", DEBUG);
3198 $query = "$args{facets} $query" if ($args{facets});
3200 if (!$locale_map{COMPLETE}) {
3202 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3203 for my $locale ( @locales ) {
3204 $locale_map{lc($locale->code)} = $locale->marc_code;
3206 $locale_map{COMPLETE} = 1;
3210 my $base_plan = $parser->new( query => $base_query )->parse;
3212 $query = "preferred_language($args{preferred_language}) $query"
3213 if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3214 $query = "preferred_language_weight($args{preferred_language_weight}) $query"
3215 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'));
3219 if (!$base_plan->parse_tree->find_filter('badge_orgs')) {
3220 # supply a suitable badge_orgs filter unless user has
3221 # explicitly supplied one
3224 my @lg_id_list = @{$args{location_groups}} if (ref $args{location_groups});
3226 my ($lg_filter) = $base_plan->parse_tree->find_filter('location_groups');
3227 @lg_id_list = @{$lg_filter->args} if ($lg_filter && @{$lg_filter->args});
3231 for my $lg ( grep { /^\d+$/ } @lg_id_list ) {
3232 my $lg_obj = asset::copy_location_group->retrieve($lg);
3233 next unless $lg_obj;
3235 push(@borg_list, ''.$lg_obj->owner);
3237 $borgs = join(',', @borg_list) if @borg_list;
3241 my ($site_filter) = $base_plan->parse_tree->find_filter('site');
3242 if ($site_filter && @{$site_filter->args}) {
3243 $site = $top_org if ($site_filter->args->[0] eq '-');
3244 $site = $top_org if ($site_filter->args->[0] eq $top_org->shortname);
3245 $site = actor::org_unit->search( { shortname => $site_filter->args->[0] })->next unless ($site);
3246 } elsif ($args{org_unit}) {
3247 $site = $top_org if ($args{org_unit} eq '-');
3248 $site = $top_org if ($args{org_unit} eq $top_org->shortname);
3249 $site = actor::org_unit->search( { shortname => $args{org_unit} })->next unless ($site);
3255 $borgs = OpenSRF::AppSession->create( 'open-ils.cstore' )->request(
3256 'open-ils.cstore.json_query.atomic',
3257 { from => [ 'actor.org_unit_ancestors', $site->id ] }
3260 if (ref $borgs && @$borgs) {
3261 $borgs = join(',', map { $_->{'id'} } @$borgs);
3269 # gather the limit or default to 10
3270 my $limit = delete($args{check_limit}) || $base_plan->superpage_size;
3271 if (my ($filter) = $base_plan->parse_tree->find_filter('limit')) {
3272 $limit = $filter->args->[0] if (@{$filter->args});
3274 if (my ($filter) = $base_plan->parse_tree->find_filter('check_limit')) {
3275 $limit = $filter->args->[0] if (@{$filter->args});
3278 # gather the offset or default to 0
3279 my $offset = delete($args{skip_check}) || delete($args{offset}) || 0;
3280 if (my ($filter) = $base_plan->parse_tree->find_filter('offset')) {
3281 $offset = $filter->args->[0] if (@{$filter->args});
3283 if (my ($filter) = $base_plan->parse_tree->find_filter('skip_check')) {
3284 $offset = $filter->args->[0] if (@{$filter->args});
3288 $query = "check_limit($limit) $query" if (defined $limit);
3289 $query = "skip_check($offset) $query" if (defined $offset);
3290 $query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
3291 $query = "badge_orgs($borgs) $query" if ($borgs);
3293 # XXX All of the following, down to the 'return' is basically dead code. someone higher up should handle it
3294 $query = "site($args{org_unit}) $query" if ($args{org_unit});
3295 $query = "depth($args{depth}) $query" if (defined($args{depth}));
3296 $query = "sort($args{sort}) $query" if ($args{sort});
3297 $query = "core_limit($args{core_limit}) $query" if ($args{core_limit});
3298 # $query = "limit($args{limit}) $query" if ($args{limit});
3299 # $query = "skip_check($args{skip_check}) $query" if ($args{skip_check});
3300 $query = "superpage($args{superpage}) $query" if ($args{superpage});
3301 $query = "offset($args{offset}) $query" if ($args{offset});
3302 $query = "#metarecord $query" if ($self->api_name =~ /metabib/);
3303 $query = "from_metarecord($args{from_metarecord}) $query" if ($args{from_metarecord});
3304 $query = "#available $query" if ($args{available});
3305 $query = "#descending $query" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3306 $query = "#staff $query" if ($self->api_name =~ /staff/);
3307 $query = "before($args{before}) $query" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3308 $query = "after($args{after}) $query" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3309 $query = "during($args{during}) $query" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3310 $query = "between($args{between}[0],$args{between}[1]) $query"
3311 if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3314 my (@between,@statuses,@locations,@location_groups,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3316 # XXX legacy format and item type support
3317 if ($args{format}) {
3318 my ($t, $f) = split '-', $args{format};
3319 $args{item_type} = [ split '', $t ];
3320 $args{item_form} = [ split '', $f ];
3323 for my $filter ( qw/locations location_groups statuses audience language lit_form item_form item_type bib_level vr_format badges/ ) {
3324 if (my $s = $args{$filter}) {
3325 $s = [$s] if (!ref($s));
3327 my @filter_list = @$s;
3329 next if (@filter_list == 0);
3331 my $filter_string = join ',', @filter_list;
3332 $query = "$query $filter($filter_string)";
3336 $log->debug("Full QueryParser query: $query", DEBUG);
3338 return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan, return_query => $args{return_query} );
3340 __PACKAGE__->register_method(
3341 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
3343 method => 'query_parser_fts_wrapper',
3348 __PACKAGE__->register_method(
3349 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3351 method => 'query_parser_fts_wrapper',
3356 __PACKAGE__->register_method(
3357 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
3359 method => 'query_parser_fts_wrapper',
3364 __PACKAGE__->register_method(
3365 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3367 method => 'query_parser_fts_wrapper',