1 package OpenILS::Application::Storage::Publisher::metabib;
2 use base qw/OpenILS::Application::Storage::Publisher/;
4 use OpenSRF::EX qw/:try/;
5 use OpenILS::Application::Storage::FTS;
6 use OpenILS::Utils::Fieldmapper;
7 use OpenSRF::Utils::Logger qw/:level/;
8 use OpenILS::Application::AppUtils;
9 use OpenSRF::Utils::Cache;
10 use OpenSRF::Utils::JSON;
12 use Digest::MD5 qw/md5_hex/;
14 use OpenILS::Application::Storage::QueryParser;
16 my $U = 'OpenILS::Application::AppUtils';
18 my $log = 'OpenSRF::Utils::Logger';
22 sub _initialize_parser {
25 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
27 config_record_attr_index_norm_map =>
29 'open-ils.cstore.direct.config.record_attr_index_norm_map.search.atomic',
30 { id => { "!=" => undef } },
31 { flesh => 1, flesh_fields => { crainm => [qw/norm/] }, order_by => [{ class => "crainm", field => "pos" }] }
33 search_relevance_adjustment =>
35 'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
36 { id => { "!=" => undef } }
38 config_metabib_field =>
40 'open-ils.cstore.direct.config.metabib_field.search.atomic',
41 { id => { "!=" => undef } }
43 config_metabib_field_virtual_map =>
45 'open-ils.cstore.direct.config.metabib_field_virtual_map.search.atomic',
46 { id => { "!=" => undef } }
48 config_metabib_search_alias =>
50 'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
51 { alias => { "!=" => undef } }
53 config_metabib_field_index_norm_map =>
55 'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
56 { id => { "!=" => undef } },
57 { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
59 config_record_attr_definition =>
61 'open-ils.cstore.direct.config.record_attr_definition.search.atomic',
62 { name => { "!=" => undef } }
64 config_metabib_class_ts_map =>
66 'open-ils.cstore.direct.config.metabib_class_ts_map.search.atomic',
69 config_metabib_field_ts_map =>
71 'open-ils.cstore.direct.config.metabib_field_ts_map.search.atomic',
74 config_metabib_class =>
76 'open-ils.cstore.direct.config.metabib_class.search.atomic',
77 { name => { "!=" => undef } }
82 my $cgf = $cstore->request(
83 'open-ils.cstore.direct.config.global_flag.retrieve',
84 'search.max_popularity_importance_multiplier'
86 $max_mult = $cgf->value if $cgf && $U->is_true($cgf->enabled);
88 $max_mult = 2.0 unless $max_mult =~ /^-?(?:\d+\.?|\.\d)\d*\z/; # just in case
89 $parser->max_popularity_importance_multiplier($max_mult);
92 die("Cannot initialize $parser!") unless ($parser->initialization_complete);
95 sub ordered_records_from_metarecord { # XXX Replace with QP-based search-within-MR
99 my $formats = shift; # dead
103 my $copies_visible = 'LEFT JOIN asset.copy_vis_attr_cache vc ON (br.id = vc.record '.
104 'AND vc.vis_attr_vector @@ (SELECT c_attrs::query_int FROM asset.patron_default_visibility_mask() LIMIT 1))';
105 $copies_visible = '' if ($self->api_name =~ /staff/o);
107 my $copies_visible_count = ',COUNT(vc.id)';
108 $copies_visible_count = '' if ($self->api_name =~ /staff/o);
110 my $descendants = '';
112 $descendants = defined($depth) ?
113 ",actor.org_unit_descendants($org, $depth) d" :
114 ",actor.org_unit_descendants($org) d" ;
121 $copies_visible_count
122 FROM metabib.metarecord_source_map sm
123 JOIN biblio.record_entry br ON (sm.source = br.id AND NOT br.deleted)
124 LEFT JOIN metabib.record_sorter s ON (s.source = br.id AND s.attr = 'titlesort')
125 LEFT JOIN config.bib_source bs ON (br.source = bs.id)
128 WHERE sm.metarecord = ?
132 if ($copies_visible) {
133 $sql .= 'AND (bs.transcendant OR ';
135 $sql .= 'vc.circ_lib = d.id)';
137 $sql .= 'vc.id IS NOT NULL)'
139 $having = 'HAVING COUNT(vc.id) > 0';
147 s.value ASC NULLS LAST
150 my $ids = metabib::metarecord_source_map->db_Main->selectcol_arrayref($sql, {}, "$mr");
151 return $ids if ($self->api_name =~ /atomic$/o);
153 $client->respond( $_ ) for ( @$ids );
157 __PACKAGE__->register_method(
158 api_name => 'open-ils.storage.ordered.metabib.metarecord.records',
160 method => 'ordered_records_from_metarecord',
164 __PACKAGE__->register_method(
165 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff',
167 method => 'ordered_records_from_metarecord',
172 __PACKAGE__->register_method(
173 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.atomic',
175 method => 'ordered_records_from_metarecord',
179 __PACKAGE__->register_method(
180 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic',
182 method => 'ordered_records_from_metarecord',
187 # XXX: this subroutine and its two registered methods are marked for
188 # deprecation, as they do not work properly in 2.x (these tags are no longer
189 # normalized in mfr) and are not in known use
193 my $isxn = lc(shift());
197 $isxn =~ s/-//o if ($self->api_name =~ /isbn/o);
199 my $tag = ($self->api_name =~ /isbn/o) ? "'020' OR f.tag = '024'" : "'022'";
201 my $fr_table = metabib::full_rec->table;
202 my $bib_table = biblio::record_entry->table;
205 SELECT DISTINCT f.record
207 JOIN $bib_table b ON (b.id = f.record)
210 AND b.deleted IS FALSE
213 my $list = metabib::full_rec->db_Main->selectcol_arrayref($sql, {}, "$isxn%");
214 $client->respond($_) for (@$list);
217 __PACKAGE__->register_method(
218 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
220 method => 'isxn_search',
224 __PACKAGE__->register_method(
225 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.issn',
227 method => 'isxn_search',
232 sub metarecord_copy_count {
238 my $sm_table = metabib::metarecord_source_map->table;
239 my $rd_table = metabib::record_descriptor->table;
240 my $cn_table = asset::call_number->table;
241 my $cp_table = asset::copy->table;
242 my $br_table = biblio::record_entry->table;
243 my $src_table = config::bib_source->table;
244 my $cl_table = asset::copy_location->table;
245 my $cs_table = config::copy_status->table;
246 my $out_table = actor::org_unit_type->table;
248 my $descendants = "actor.org_unit_descendants(u.id)";
249 my $ancestors = "actor.org_unit_ancestors(?) u JOIN $out_table t ON (u.ou_type = t.id)";
251 if ($args{org_unit} < 0) {
252 $args{org_unit} *= -1;
253 $ancestors = "(select org_unit as id from actor.org_lasso_map where lasso = ?) u CROSS JOIN (SELECT -1 AS depth) t";
256 my $copies_visible = 'AND a.opac_visible IS TRUE AND cp.opac_visible IS TRUE AND cs.opac_visible IS TRUE AND cl.opac_visible IS TRUE';
257 $copies_visible = '' if ($self->api_name =~ /staff/o);
259 my (@types,@forms,@blvl);
260 my ($t_filter, $f_filter, $b_filter) = ('','','');
263 my ($t, $f, $b) = split '-', $args{format};
264 @types = split '', $t;
265 @forms = split '', $f;
266 @blvl = split '', $b;
269 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
273 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
277 $b_filter .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
287 JOIN $cn_table cn ON (cn.record = r.source)
288 JOIN $rd_table rd ON (cn.record = rd.record)
289 JOIN $cp_table cp ON (cn.id = cp.call_number)
290 JOIN $cs_table cs ON (cp.status = cs.id)
291 JOIN $cl_table cl ON (cp.location = cl.id)
292 JOIN $descendants a ON (cp.circ_lib = a.id)
293 WHERE r.metarecord = ?
294 AND cn.deleted IS FALSE
295 AND cp.deleted IS FALSE
305 JOIN $cn_table cn ON (cn.record = r.source)
306 JOIN $rd_table rd ON (cn.record = rd.record)
307 JOIN $cp_table cp ON (cn.id = cp.call_number)
308 JOIN $cs_table cs ON (cp.status = cs.id)
309 JOIN $cl_table cl ON (cp.location = cl.id)
310 JOIN $descendants a ON (cp.circ_lib = a.id)
311 WHERE r.metarecord = ?
312 AND cp.status IN (0,7,12)
313 AND cn.deleted IS FALSE
314 AND cp.deleted IS FALSE
324 JOIN $cn_table cn ON (cn.record = r.source)
325 JOIN $rd_table rd ON (cn.record = rd.record)
326 JOIN $cp_table cp ON (cn.id = cp.call_number)
327 JOIN $cs_table cs ON (cp.status = cs.id)
328 JOIN $cl_table cl ON (cp.location = cl.id)
329 WHERE r.metarecord = ?
330 AND cn.deleted IS FALSE
331 AND cp.deleted IS FALSE
332 AND cp.opac_visible IS TRUE
333 AND cs.opac_visible IS TRUE
334 AND cl.opac_visible IS TRUE
343 JOIN $br_table br ON (br.id = r.source)
344 JOIN $src_table src ON (src.id = br.source)
345 WHERE r.metarecord = ?
346 AND src.transcendant IS TRUE
354 my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
355 $sth->execute( ''.$args{metarecord},
359 ''.$args{metarecord},
363 ''.$args{metarecord},
367 ''.$args{metarecord},
371 while ( my $row = $sth->fetchrow_hashref ) {
372 $client->respond( $row );
376 __PACKAGE__->register_method(
377 api_name => 'open-ils.storage.metabib.metarecord.copy_count',
379 method => 'metarecord_copy_count',
384 __PACKAGE__->register_method(
385 api_name => 'open-ils.storage.metabib.metarecord.copy_count.staff',
387 method => 'metarecord_copy_count',
393 sub biblio_multi_search_full_rec {
398 my $class_join = $args{class_join} || 'AND';
399 my $limit = $args{limit} || 100;
400 my $offset = $args{offset} || 0;
401 my $sort = $args{'sort'};
402 my $sort_dir = $args{sort_dir} || 'DESC';
407 for my $arg (@{ $args{searches} }) {
408 my $term = $$arg{term};
409 my $limiters = $$arg{restrict};
411 my ($index_col) = metabib::full_rec->columns('FTS');
412 $index_col ||= 'value';
413 my $search_table = metabib::full_rec->table;
415 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
417 my $fts_where = $fts->sql_where_clause();
418 my @fts_ranks = $fts->fts_rank;
420 my $rank = join(' + ', @fts_ranks);
423 for my $limit (@$limiters) {
424 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
425 # MARC control field; mfr.subfield is NULL
426 push @wheres, "( tag = ? AND $fts_where )";
427 push @binds, $$limit{tag};
428 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
430 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
431 push @binds, $$limit{tag}, $$limit{subfield};
432 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
435 my $where = join(' OR ', @wheres);
437 push @selects, "SELECT record, AVG($rank) as sum FROM $search_table WHERE $where GROUP BY record";
441 my $descendants = defined($args{depth}) ?
442 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
443 "actor.org_unit_descendants($args{org_unit})" ;
446 my $metabib_record_descriptor = metabib::record_descriptor->table;
447 my $metabib_full_rec = metabib::full_rec->table;
448 my $asset_call_number_table = asset::call_number->table;
449 my $asset_copy_table = asset::copy->table;
450 my $cs_table = config::copy_status->table;
451 my $cl_table = asset::copy_location->table;
452 my $br_table = biblio::record_entry->table;
454 my $cj = 'HAVING COUNT(x.record) = ' . scalar(@selects) if ($class_join eq 'AND');
456 '(SELECT x.record, sum(x.sum) FROM (('.
457 join(') UNION ALL (', @selects).
458 ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
460 my $has_vols = 'AND cn.owning_lib = d.id';
461 my $has_copies = 'AND cp.call_number = cn.id';
462 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';
464 if ($self->api_name =~ /staff/o) {
465 $copies_visible = '';
466 $has_copies = '' if ($ou_type == 0);
467 $has_vols = '' if ($ou_type == 0);
470 my ($t_filter, $f_filter) = ('','');
471 my ($a_filter, $l_filter, $lf_filter) = ('','','');
474 if (my $a = $args{audience}) {
475 $a = [$a] if (!ref($a));
478 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
483 if (my $l = $args{language}) {
484 $l = [$l] if (!ref($l));
487 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
492 if (my $f = $args{lit_form}) {
493 $f = [$f] if (!ref($f));
496 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
497 push @binds, @lit_form;
501 if (my $f = $args{item_form}) {
502 $f = [$f] if (!ref($f));
505 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
510 if (my $t = $args{item_type}) {
511 $t = [$t] if (!ref($t));
514 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
521 my ($t, $f) = split '-', $args{format};
522 my @types = split '', $t;
523 my @forms = split '', $f;
525 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
530 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
533 push @binds, @types, @forms;
536 my $relevance = 'sum(f.sum)';
537 $relevance = 1 if (!$copies_visible);
539 my $string_default_sort = 'zzzz';
540 $string_default_sort = 'AAAA' if ($sort_dir =~ /^DESC$/i);
542 my $number_default_sort = '9999';
543 $number_default_sort = '0000' if ($sort_dir =~/^DESC$/i);
545 my $rank = $relevance;
546 if (lc($sort) eq 'pubdate') {
549 SELECT COALESCE(SUBSTRING(MAX(frp.value) FROM E'\\\\d{4}'), '$number_default_sort')::INT
550 FROM $metabib_full_rec frp
551 WHERE frp.record = f.record
553 AND frp.subfield = 'c'
557 } elsif (lc($sort) eq 'create_date') {
559 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
561 } elsif (lc($sort) eq 'edit_date') {
563 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
565 } elsif (lc($sort) =~ /^title/i) {
568 SELECT COALESCE(LTRIM(SUBSTR(MAX(frt.value), COALESCE(SUBSTRING(MAX(frt.ind2) FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
569 FROM $metabib_full_rec frt
570 WHERE frt.record = f.record
572 AND frt.subfield = 'a'
576 } elsif (lc($sort) =~ /^author/i) {
579 SELECT COALESCE(LTRIM(MAX(query.value)), '$string_default_sort')
582 FROM $metabib_full_rec fra
583 WHERE fra.record = f.record
584 AND fra.tag LIKE '1%'
585 AND fra.subfield = 'a'
586 ORDER BY fra.tag::text::int
595 my $rd_join = $use_rd ? "$metabib_record_descriptor rd," : '';
596 my $rd_filter = $use_rd ? 'AND rd.record = f.record' : '';
598 if ($copies_visible) {
600 SELECT f.record, $relevance, count(DISTINCT cp.id), $rank
601 FROM $search_table f,
602 $asset_call_number_table cn,
603 $asset_copy_table cp,
609 WHERE br.id = f.record
610 AND cn.record = f.record
611 AND cp.status = cs.id
612 AND cp.location = cl.id
613 AND br.deleted IS FALSE
614 AND cn.deleted IS FALSE
615 AND cp.deleted IS FALSE
625 GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
626 ORDER BY 4 $sort_dir,3 DESC
630 SELECT f.record, 1, 1, $rank
631 FROM $search_table f,
634 WHERE br.id = f.record
635 AND br.deleted IS FALSE
648 $log->debug("Search SQL :: [$select]",DEBUG);
650 my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
651 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
654 $max = 1 if (!@$recs);
656 $max = $$_[1] if ($$_[1] > $max);
660 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
661 next unless ($$rec[0]);
662 my ($rid,$rank,$junk,$skip) = @$rec;
663 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
667 __PACKAGE__->register_method(
668 api_name => 'open-ils.storage.biblio.full_rec.multi_search',
670 method => 'biblio_multi_search_full_rec',
675 __PACKAGE__->register_method(
676 api_name => 'open-ils.storage.biblio.full_rec.multi_search.staff',
678 method => 'biblio_multi_search_full_rec',
684 sub search_full_rec {
690 my $term = $args{term};
691 my $limiters = $args{restrict};
693 my ($index_col) = metabib::full_rec->columns('FTS');
694 $index_col ||= 'value';
695 my $search_table = metabib::full_rec->table;
697 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
699 my $fts_where = $fts->sql_where_clause();
700 my @fts_ranks = $fts->fts_rank;
702 my $rank = join(' + ', @fts_ranks);
706 for my $limit (@$limiters) {
707 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
708 # MARC control field; mfr.subfield is NULL
709 push @wheres, "( tag = ? AND $fts_where )";
710 push @binds, $$limit{tag};
711 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
713 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
714 push @binds, $$limit{tag}, $$limit{subfield};
715 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
718 my $where = join(' OR ', @wheres);
720 my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
722 $log->debug("Search SQL :: [$select]",DEBUG);
724 my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
725 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
727 $client->respond($_) for (@$recs);
730 __PACKAGE__->register_method(
731 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
733 method => 'search_full_rec',
738 __PACKAGE__->register_method(
739 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
741 method => 'search_full_rec',
748 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
749 sub search_class_fts {
754 my $term = $args{term};
755 my $ou = $args{org_unit};
756 my $ou_type = $args{depth};
757 my $limit = $args{limit};
758 my $offset = $args{offset};
760 my $limit_clause = '';
761 my $offset_clause = '';
763 $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
764 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
767 my ($t_filter, $f_filter) = ('','');
770 my ($t, $f) = split '-', $args{format};
771 @types = split '', $t;
772 @forms = split '', $f;
774 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
778 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
784 my $descendants = defined($ou_type) ?
785 "actor.org_unit_descendants($ou, $ou_type)" :
786 "actor.org_unit_descendants($ou)";
788 my $class = $self->{cdbi};
789 my $search_table = $class->table;
791 my $metabib_record_descriptor = metabib::record_descriptor->table;
792 my $metabib_metarecord = metabib::metarecord->table;
793 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
794 my $asset_call_number_table = asset::call_number->table;
795 my $asset_copy_table = asset::copy->table;
796 my $cs_table = config::copy_status->table;
797 my $cl_table = asset::copy_location->table;
799 my ($index_col) = $class->columns('FTS');
800 $index_col ||= 'value';
802 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
803 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
805 my $fts_where = $fts->sql_where_clause;
806 my @fts_ranks = $fts->fts_rank;
808 my $rank = join(' + ', @fts_ranks);
810 my $has_vols = 'AND cn.owning_lib = d.id';
811 my $has_copies = 'AND cp.call_number = cn.id';
812 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';
814 my $visible_count = ', count(DISTINCT cp.id)';
815 my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
817 if ($self->api_name =~ /staff/o) {
818 $copies_visible = '';
819 $visible_count_test = '';
820 $has_copies = '' if ($ou_type == 0);
821 $has_vols = '' if ($ou_type == 0);
824 my $rank_calc = <<" RANK";
826 * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
827 * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
828 * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
829 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
832 $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
834 if ($copies_visible) {
836 SELECT m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
837 FROM $search_table f,
838 $metabib_metarecord_source_map_table m,
839 $asset_call_number_table cn,
840 $asset_copy_table cp,
843 $metabib_record_descriptor rd,
846 AND m.source = f.source
847 AND cn.record = m.source
848 AND rd.record = m.source
849 AND cp.status = cs.id
850 AND cp.location = cl.id
856 GROUP BY 1 $visible_count_test
858 $limit_clause $offset_clause
862 SELECT m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
863 FROM $search_table f,
864 $metabib_metarecord_source_map_table m,
865 $metabib_record_descriptor rd
867 AND m.source = f.source
868 AND rd.record = m.source
873 $limit_clause $offset_clause
877 $log->debug("Field Search SQL :: [$select]",DEBUG);
879 my $SQLstring = join('%',$fts->words);
880 my $REstring = join('\\s+',$fts->words);
881 my $first_word = ($fts->words)[0].'%';
882 my $recs = ($self->api_name =~ /unordered/o) ?
883 $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
884 $class->db_Main->selectall_arrayref($select, {},
885 '%'.lc($SQLstring).'%', # phrase order match
886 lc($first_word), # first word match
887 '^\\s*'.lc($REstring).'\\s*/?\s*$', # full exact match
891 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
893 $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
897 for my $class ( qw/title author subject keyword series identifier/ ) {
898 __PACKAGE__->register_method(
899 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord",
901 method => 'search_class_fts',
904 cdbi => "metabib::${class}_field_entry",
907 __PACKAGE__->register_method(
908 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
910 method => 'search_class_fts',
913 cdbi => "metabib::${class}_field_entry",
916 __PACKAGE__->register_method(
917 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
919 method => 'search_class_fts',
922 cdbi => "metabib::${class}_field_entry",
925 __PACKAGE__->register_method(
926 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
928 method => 'search_class_fts',
931 cdbi => "metabib::${class}_field_entry",
936 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
937 sub search_class_fts_count {
942 my $term = $args{term};
943 my $ou = $args{org_unit};
944 my $ou_type = $args{depth};
945 my $limit = $args{limit} || 100;
946 my $offset = $args{offset} || 0;
948 my $descendants = defined($ou_type) ?
949 "actor.org_unit_descendants($ou, $ou_type)" :
950 "actor.org_unit_descendants($ou)";
953 my ($t_filter, $f_filter) = ('','');
956 my ($t, $f) = split '-', $args{format};
957 @types = split '', $t;
958 @forms = split '', $f;
960 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
964 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
969 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
971 my $class = $self->{cdbi};
972 my $search_table = $class->table;
974 my $metabib_record_descriptor = metabib::record_descriptor->table;
975 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
976 my $asset_call_number_table = asset::call_number->table;
977 my $asset_copy_table = asset::copy->table;
978 my $cs_table = config::copy_status->table;
979 my $cl_table = asset::copy_location->table;
981 my ($index_col) = $class->columns('FTS');
982 $index_col ||= 'value';
984 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
986 my $fts_where = $fts->sql_where_clause;
988 my $has_vols = 'AND cn.owning_lib = d.id';
989 my $has_copies = 'AND cp.call_number = cn.id';
990 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';
991 if ($self->api_name =~ /staff/o) {
992 $copies_visible = '';
993 $has_vols = '' if ($ou_type == 0);
994 $has_copies = '' if ($ou_type == 0);
997 # XXX test an "EXISTS version of descendant checking...
999 if ($copies_visible) {
1001 SELECT count(distinct m.metarecord)
1002 FROM $search_table f,
1003 $metabib_metarecord_source_map_table m,
1004 $metabib_metarecord_source_map_table mr,
1005 $asset_call_number_table cn,
1006 $asset_copy_table cp,
1009 $metabib_record_descriptor rd,
1012 AND mr.source = f.source
1013 AND mr.metarecord = m.metarecord
1014 AND cn.record = m.source
1015 AND rd.record = m.source
1016 AND cp.status = cs.id
1017 AND cp.location = cl.id
1026 SELECT count(distinct m.metarecord)
1027 FROM $search_table f,
1028 $metabib_metarecord_source_map_table m,
1029 $metabib_metarecord_source_map_table mr,
1030 $metabib_record_descriptor rd
1032 AND mr.source = f.source
1033 AND mr.metarecord = m.metarecord
1034 AND rd.record = m.source
1040 $log->debug("Field Search Count SQL :: [$select]",DEBUG);
1042 my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1044 $log->debug("Count Search yielded $recs results.",DEBUG);
1049 for my $class ( qw/title author subject keyword series identifier/ ) {
1050 __PACKAGE__->register_method(
1051 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
1053 method => 'search_class_fts_count',
1056 cdbi => "metabib::${class}_field_entry",
1059 __PACKAGE__->register_method(
1060 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
1062 method => 'search_class_fts_count',
1065 cdbi => "metabib::${class}_field_entry",
1071 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1072 sub postfilter_search_class_fts {
1077 my $term = $args{term};
1078 my $sort = $args{'sort'};
1079 my $sort_dir = $args{sort_dir} || 'DESC';
1080 my $ou = $args{org_unit};
1081 my $ou_type = $args{depth};
1082 my $limit = $args{limit} || 10;
1083 my $visibility_limit = $args{visibility_limit} || 5000;
1084 my $offset = $args{offset} || 0;
1086 my $outer_limit = 1000;
1088 my $limit_clause = '';
1089 my $offset_clause = '';
1091 $limit_clause = "LIMIT $outer_limit";
1092 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1094 my (@types,@forms,@lang,@aud,@lit_form);
1095 my ($t_filter, $f_filter) = ('','');
1096 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1097 my ($ot_filter, $of_filter) = ('','');
1098 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1100 if (my $a = $args{audience}) {
1101 $a = [$a] if (!ref($a));
1104 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1105 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1108 if (my $l = $args{language}) {
1109 $l = [$l] if (!ref($l));
1112 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1113 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1116 if (my $f = $args{lit_form}) {
1117 $f = [$f] if (!ref($f));
1120 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1121 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1124 if ($args{format}) {
1125 my ($t, $f) = split '-', $args{format};
1126 @types = split '', $t;
1127 @forms = split '', $f;
1129 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1130 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1134 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1135 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1140 my $descendants = defined($ou_type) ?
1141 "actor.org_unit_descendants($ou, $ou_type)" :
1142 "actor.org_unit_descendants($ou)";
1144 my $class = $self->{cdbi};
1145 my $search_table = $class->table;
1147 my $metabib_full_rec = metabib::full_rec->table;
1148 my $metabib_record_descriptor = metabib::record_descriptor->table;
1149 my $metabib_metarecord = metabib::metarecord->table;
1150 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1151 my $asset_call_number_table = asset::call_number->table;
1152 my $asset_copy_table = asset::copy->table;
1153 my $cs_table = config::copy_status->table;
1154 my $cl_table = asset::copy_location->table;
1155 my $br_table = biblio::record_entry->table;
1157 my ($index_col) = $class->columns('FTS');
1158 $index_col ||= 'value';
1160 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1162 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1164 my $SQLstring = join('%',map { lc($_) } $fts->words);
1165 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1166 my $first_word = lc(($fts->words)[0]).'%';
1168 my $fts_where = $fts->sql_where_clause;
1169 my @fts_ranks = $fts->fts_rank;
1172 $bonus{'metabib::identifier_field_entry'} =
1173 $bonus{'metabib::keyword_field_entry'} = [
1174 { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring }
1177 $bonus{'metabib::title_field_entry'} =
1178 $bonus{'metabib::series_field_entry'} = [
1179 { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1180 { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1181 @{ $bonus{'metabib::keyword_field_entry'} }
1184 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1185 $bonus_list ||= '1';
1187 my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1189 my $relevance = join(' + ', @fts_ranks);
1190 $relevance = <<" RANK";
1191 (SUM( ( $relevance ) * ( $bonus_list ) )/COUNT(m.source))
1194 my $string_default_sort = 'zzzz';
1195 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1197 my $number_default_sort = '9999';
1198 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1200 my $rank = $relevance;
1201 if (lc($sort) eq 'pubdate') {
1204 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1205 FROM $metabib_full_rec frp
1206 WHERE frp.record = mr.master_record
1208 AND frp.subfield = 'c'
1212 } elsif (lc($sort) eq 'create_date') {
1214 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1216 } elsif (lc($sort) eq 'edit_date') {
1218 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1220 } elsif (lc($sort) eq 'title') {
1223 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1224 FROM $metabib_full_rec frt
1225 WHERE frt.record = mr.master_record
1227 AND frt.subfield = 'a'
1231 } elsif (lc($sort) eq 'author') {
1234 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1235 FROM $metabib_full_rec fra
1236 WHERE fra.record = mr.master_record
1237 AND fra.tag LIKE '1%'
1238 AND fra.subfield = 'a'
1239 ORDER BY fra.tag::text::int
1247 my $select = <<" SQL";
1248 SELECT m.metarecord,
1250 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1252 FROM $search_table f,
1253 $metabib_metarecord_source_map_table m,
1254 $metabib_metarecord_source_map_table smrs,
1255 $metabib_metarecord mr,
1256 $metabib_record_descriptor rd
1258 AND smrs.metarecord = mr.id
1259 AND m.source = f.source
1260 AND m.metarecord = mr.id
1261 AND rd.record = smrs.source
1267 GROUP BY m.metarecord
1268 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1269 LIMIT $visibility_limit
1276 FROM $asset_call_number_table cn,
1277 $metabib_metarecord_source_map_table mrs,
1278 $asset_copy_table cp,
1283 $metabib_record_descriptor ord,
1285 WHERE mrs.metarecord = s.metarecord
1286 AND br.id = mrs.source
1287 AND cn.record = mrs.source
1288 AND cp.status = cs.id
1289 AND cp.location = cl.id
1290 AND cn.owning_lib = d.id
1291 AND cp.call_number = cn.id
1292 AND cp.opac_visible IS TRUE
1293 AND cs.opac_visible IS TRUE
1294 AND cl.opac_visible IS TRUE
1295 AND d.opac_visible IS TRUE
1296 AND br.active IS TRUE
1297 AND br.deleted IS FALSE
1298 AND ord.record = mrs.source
1304 ORDER BY 4 $sort_dir
1306 } elsif ($self->api_name !~ /staff/o) {
1313 FROM $asset_call_number_table cn,
1314 $metabib_metarecord_source_map_table mrs,
1315 $asset_copy_table cp,
1320 $metabib_record_descriptor ord
1322 WHERE mrs.metarecord = s.metarecord
1323 AND br.id = mrs.source
1324 AND cn.record = mrs.source
1325 AND cp.status = cs.id
1326 AND cp.location = cl.id
1327 AND cp.circ_lib = d.id
1328 AND cp.call_number = cn.id
1329 AND cp.opac_visible IS TRUE
1330 AND cs.opac_visible IS TRUE
1331 AND cl.opac_visible IS TRUE
1332 AND d.opac_visible IS TRUE
1333 AND br.active IS TRUE
1334 AND br.deleted IS FALSE
1335 AND ord.record = mrs.source
1343 ORDER BY 4 $sort_dir
1352 FROM $asset_call_number_table cn,
1353 $asset_copy_table cp,
1354 $metabib_metarecord_source_map_table mrs,
1357 $metabib_record_descriptor ord
1359 WHERE mrs.metarecord = s.metarecord
1360 AND br.id = mrs.source
1361 AND cn.record = mrs.source
1362 AND cn.id = cp.call_number
1363 AND br.deleted IS FALSE
1364 AND cn.deleted IS FALSE
1365 AND ord.record = mrs.source
1366 AND ( cn.owning_lib = d.id
1367 OR ( cp.circ_lib = d.id
1368 AND cp.deleted IS FALSE
1380 FROM $asset_call_number_table cn,
1381 $metabib_metarecord_source_map_table mrs,
1382 $metabib_record_descriptor ord
1383 WHERE mrs.metarecord = s.metarecord
1384 AND cn.record = mrs.source
1385 AND ord.record = mrs.source
1393 ORDER BY 4 $sort_dir
1398 $log->debug("Field Search SQL :: [$select]",DEBUG);
1400 my $recs = $class->db_Main->selectall_arrayref(
1402 (@bonus_values > 0 ? @bonus_values : () ),
1403 ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1404 @types, @forms, @aud, @lang, @lit_form,
1405 @types, @forms, @aud, @lang, @lit_form,
1406 ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1408 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1411 $max = 1 if (!@$recs);
1413 $max = $$_[1] if ($$_[1] > $max);
1416 my $count = scalar(@$recs);
1417 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1418 my ($mrid,$rank,$skip) = @$rec;
1419 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1424 for my $class ( qw/title author subject keyword series identifier/ ) {
1425 __PACKAGE__->register_method(
1426 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1428 method => 'postfilter_search_class_fts',
1431 cdbi => "metabib::${class}_field_entry",
1434 __PACKAGE__->register_method(
1435 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1437 method => 'postfilter_search_class_fts',
1440 cdbi => "metabib::${class}_field_entry",
1447 my $_cdbi = { title => "metabib::title_field_entry",
1448 author => "metabib::author_field_entry",
1449 subject => "metabib::subject_field_entry",
1450 keyword => "metabib::keyword_field_entry",
1451 series => "metabib::series_field_entry",
1452 identifier => "metabib::identifier_field_entry",
1455 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1456 sub postfilter_search_multi_class_fts {
1461 my $sort = $args{'sort'};
1462 my $sort_dir = $args{sort_dir} || 'DESC';
1463 my $ou = $args{org_unit};
1464 my $ou_type = $args{depth};
1465 my $limit = $args{limit} || 10;
1466 my $offset = $args{offset} || 0;
1467 my $visibility_limit = $args{visibility_limit} || 5000;
1470 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1473 if (!defined($args{org_unit})) {
1474 die "No target organizational unit passed to ".$self->api_name;
1477 if (! scalar( keys %{$args{searches}} )) {
1478 die "No search arguments were passed to ".$self->api_name;
1481 my $outer_limit = 1000;
1483 my $limit_clause = '';
1484 my $offset_clause = '';
1486 $limit_clause = "LIMIT $outer_limit";
1487 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1489 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1490 my ($t_filter, $f_filter, $v_filter) = ('','','');
1491 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1492 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1493 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1495 if ($args{available}) {
1496 $avail_filter = ' AND cp.status IN (0,7,12)';
1499 if (my $a = $args{audience}) {
1500 $a = [$a] if (!ref($a));
1503 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1504 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1507 if (my $l = $args{language}) {
1508 $l = [$l] if (!ref($l));
1511 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1512 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1515 if (my $f = $args{lit_form}) {
1516 $f = [$f] if (!ref($f));
1519 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1520 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1523 if (my $f = $args{item_form}) {
1524 $f = [$f] if (!ref($f));
1527 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1528 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1531 if (my $t = $args{item_type}) {
1532 $t = [$t] if (!ref($t));
1535 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1536 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1539 if (my $v = $args{vr_format}) {
1540 $v = [$v] if (!ref($v));
1543 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1544 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1548 # XXX legacy format and item type support
1549 if ($args{format}) {
1550 my ($t, $f) = split '-', $args{format};
1551 @types = split '', $t;
1552 @forms = split '', $f;
1554 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1555 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1559 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1560 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1566 my $descendants = defined($ou_type) ?
1567 "actor.org_unit_descendants($ou, $ou_type)" :
1568 "actor.org_unit_descendants($ou)";
1570 my $search_table_list = '';
1572 my $join_table_list = '';
1575 my $field_table = config::metabib_field->table;
1579 my $prev_search_group;
1580 my $curr_search_group;
1584 for my $search_group (sort keys %{$args{searches}}) {
1585 (my $search_group_name = $search_group) =~ s/\|/_/gso;
1586 ($search_class,$search_field) = split /\|/, $search_group;
1587 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1589 if ($search_field) {
1590 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1591 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1596 $prev_search_group = $curr_search_group if ($curr_search_group);
1598 $curr_search_group = $search_group_name;
1600 my $class = $_cdbi->{$search_class};
1601 my $search_table = $class->table;
1603 my ($index_col) = $class->columns('FTS');
1604 $index_col ||= 'value';
1607 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1609 my $fts_where = $fts->sql_where_clause;
1610 my @fts_ranks = $fts->fts_rank;
1612 my $SQLstring = join('%',map { lc($_) } $fts->words);
1613 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1614 my $first_word = lc(($fts->words)[0]).'%';
1616 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1617 my $rank = join(' + ', @fts_ranks);
1620 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1621 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1623 $bonus{'series'} = [
1624 { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1625 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1628 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1630 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1631 $bonus_list ||= '1';
1633 push @bonus_lists, $bonus_list;
1634 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1637 #---------------------
1639 $search_table_list .= "$search_table $search_group_name, ";
1640 push @rank_list,$rank;
1641 $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1643 if ($metabib_field) {
1644 $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1645 $metabib_field = undef;
1648 if ($prev_search_group) {
1649 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1653 my $metabib_record_descriptor = metabib::record_descriptor->table;
1654 my $metabib_full_rec = metabib::full_rec->table;
1655 my $metabib_metarecord = metabib::metarecord->table;
1656 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1657 my $asset_call_number_table = asset::call_number->table;
1658 my $asset_copy_table = asset::copy->table;
1659 my $cs_table = config::copy_status->table;
1660 my $cl_table = asset::copy_location->table;
1661 my $br_table = biblio::record_entry->table;
1662 my $source_table = config::bib_source->table;
1664 my $bonuses = join (' * ', @bonus_lists);
1665 my $relevance = join (' + ', @rank_list);
1666 $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1668 my $string_default_sort = 'zzzz';
1669 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1671 my $number_default_sort = '9999';
1672 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1676 my $secondary_sort = <<" SORT";
1678 SELECT COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1679 FROM $metabib_full_rec sfrt,
1680 $metabib_metarecord mr
1681 WHERE sfrt.record = mr.master_record
1682 AND sfrt.tag = '245'
1683 AND sfrt.subfield = 'a'
1688 my $rank = $relevance;
1689 if (lc($sort) eq 'pubdate') {
1692 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1693 FROM $metabib_full_rec frp
1694 WHERE frp.record = mr.master_record
1696 AND frp.subfield = 'c'
1700 } elsif (lc($sort) eq 'create_date') {
1702 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1704 } elsif (lc($sort) eq 'edit_date') {
1706 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1708 } elsif (lc($sort) eq 'title') {
1711 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1712 FROM $metabib_full_rec frt
1713 WHERE frt.record = mr.master_record
1715 AND frt.subfield = 'a'
1719 $secondary_sort = <<" SORT";
1721 SELECT COALESCE(SUBSTRING(sfrp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1722 FROM $metabib_full_rec sfrp
1723 WHERE sfrp.record = mr.master_record
1724 AND sfrp.tag = '260'
1725 AND sfrp.subfield = 'c'
1729 } elsif (lc($sort) eq 'author') {
1732 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1733 FROM $metabib_full_rec fra
1734 WHERE fra.record = mr.master_record
1735 AND fra.tag LIKE '1%'
1736 AND fra.subfield = 'a'
1737 ORDER BY fra.tag::text::int
1742 push @bonus_values, @bonus_values;
1747 my $select = <<" SQL";
1748 SELECT m.metarecord,
1750 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1753 FROM $search_table_list
1754 $metabib_metarecord mr,
1755 $metabib_metarecord_source_map_table m,
1756 $metabib_metarecord_source_map_table smrs
1757 WHERE m.metarecord = smrs.metarecord
1758 AND mr.id = m.metarecord
1761 GROUP BY m.metarecord
1762 -- ORDER BY 4 $sort_dir
1763 LIMIT $visibility_limit
1766 if ($self->api_name !~ /staff/o) {
1773 FROM $asset_call_number_table cn,
1774 $metabib_metarecord_source_map_table mrs,
1775 $asset_copy_table cp,
1780 $metabib_record_descriptor ord
1781 WHERE mrs.metarecord = s.metarecord
1782 AND br.id = mrs.source
1783 AND cn.record = mrs.source
1784 AND cp.status = cs.id
1785 AND cp.location = cl.id
1786 AND cp.circ_lib = d.id
1787 AND cp.call_number = cn.id
1788 AND cp.opac_visible IS TRUE
1789 AND cs.opac_visible IS TRUE
1790 AND cl.opac_visible IS TRUE
1791 AND d.opac_visible IS TRUE
1792 AND br.active IS TRUE
1793 AND br.deleted IS FALSE
1794 AND cp.deleted IS FALSE
1795 AND cn.deleted IS FALSE
1796 AND ord.record = mrs.source
1809 $metabib_metarecord_source_map_table mrs,
1810 $metabib_record_descriptor ord,
1812 WHERE mrs.metarecord = s.metarecord
1813 AND ord.record = mrs.source
1814 AND br.id = mrs.source
1815 AND br.source = src.id
1816 AND src.transcendant IS TRUE
1824 ORDER BY 4 $sort_dir, 5
1831 $metabib_metarecord_source_map_table omrs,
1832 $metabib_record_descriptor ord
1833 WHERE omrs.metarecord = s.metarecord
1834 AND ord.record = omrs.source
1837 FROM $asset_call_number_table cn,
1838 $asset_copy_table cp,
1841 WHERE br.id = omrs.source
1842 AND cn.record = omrs.source
1843 AND br.deleted IS FALSE
1844 AND cn.deleted IS FALSE
1845 AND cp.call_number = cn.id
1846 AND ( cn.owning_lib = d.id
1847 OR ( cp.circ_lib = d.id
1848 AND cp.deleted IS FALSE
1856 FROM $asset_call_number_table cn
1857 WHERE cn.record = omrs.source
1858 AND cn.deleted IS FALSE
1864 $metabib_metarecord_source_map_table mrs,
1865 $metabib_record_descriptor ord,
1867 WHERE mrs.metarecord = s.metarecord
1868 AND br.id = mrs.source
1869 AND br.source = src.id
1870 AND src.transcendant IS TRUE
1886 ORDER BY 4 $sort_dir, 5
1891 $log->debug("Field Search SQL :: [$select]",DEBUG);
1893 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1896 @types, @forms, @vformats, @aud, @lang, @lit_form,
1897 @types, @forms, @vformats, @aud, @lang, @lit_form,
1898 # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1901 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1904 $max = 1 if (!@$recs);
1906 $max = $$_[1] if ($$_[1] > $max);
1909 my $count = scalar(@$recs);
1910 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1911 next unless ($$rec[0]);
1912 my ($mrid,$rank,$skip) = @$rec;
1913 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1918 __PACKAGE__->register_method(
1919 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1921 method => 'postfilter_search_multi_class_fts',
1926 __PACKAGE__->register_method(
1927 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1929 method => 'postfilter_search_multi_class_fts',
1935 __PACKAGE__->register_method(
1936 api_name => "open-ils.storage.metabib.multiclass.search_fts",
1938 method => 'postfilter_search_multi_class_fts',
1943 __PACKAGE__->register_method(
1944 api_name => "open-ils.storage.metabib.multiclass.search_fts.staff",
1946 method => 'postfilter_search_multi_class_fts',
1952 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1953 sub biblio_search_multi_class_fts {
1958 my $sort = $args{'sort'};
1959 my $sort_dir = $args{sort_dir} || 'DESC';
1960 my $ou = $args{org_unit};
1961 my $ou_type = $args{depth};
1962 my $limit = $args{limit} || 10;
1963 my $offset = $args{offset} || 0;
1964 my $pref_lang = $args{preferred_language} || 'eng';
1965 my $visibility_limit = $args{visibility_limit} || 5000;
1968 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1971 if (! scalar( keys %{$args{searches}} )) {
1972 die "No search arguments were passed to ".$self->api_name;
1975 my $outer_limit = 1000;
1977 my $limit_clause = '';
1978 my $offset_clause = '';
1980 $limit_clause = "LIMIT $outer_limit";
1981 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1983 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1984 my ($t_filter, $f_filter, $v_filter) = ('','','');
1985 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1986 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1987 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1989 if ($args{available}) {
1990 $avail_filter = ' AND cp.status IN (0,7,12)';
1993 if (my $a = $args{audience}) {
1994 $a = [$a] if (!ref($a));
1997 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1998 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
2001 if (my $l = $args{language}) {
2002 $l = [$l] if (!ref($l));
2005 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
2006 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
2009 if (my $f = $args{lit_form}) {
2010 $f = [$f] if (!ref($f));
2013 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2014 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2017 if (my $f = $args{item_form}) {
2018 $f = [$f] if (!ref($f));
2021 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2022 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2025 if (my $t = $args{item_type}) {
2026 $t = [$t] if (!ref($t));
2029 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2030 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2033 if (my $v = $args{vr_format}) {
2034 $v = [$v] if (!ref($v));
2037 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
2038 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
2041 # XXX legacy format and item type support
2042 if ($args{format}) {
2043 my ($t, $f) = split '-', $args{format};
2044 @types = split '', $t;
2045 @forms = split '', $f;
2047 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2048 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2052 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2053 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2058 my $descendants = defined($ou_type) ?
2059 "actor.org_unit_descendants($ou, $ou_type)" :
2060 "actor.org_unit_descendants($ou)";
2062 my $search_table_list = '';
2064 my $join_table_list = '';
2067 my $field_table = config::metabib_field->table;
2071 my $prev_search_group;
2072 my $curr_search_group;
2076 for my $search_group (sort keys %{$args{searches}}) {
2077 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2078 ($search_class,$search_field) = split /\|/, $search_group;
2079 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2081 if ($search_field) {
2082 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2083 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2088 $prev_search_group = $curr_search_group if ($curr_search_group);
2090 $curr_search_group = $search_group_name;
2092 my $class = $_cdbi->{$search_class};
2093 my $search_table = $class->table;
2095 my ($index_col) = $class->columns('FTS');
2096 $index_col ||= 'value';
2099 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2101 my $fts_where = $fts->sql_where_clause;
2102 my @fts_ranks = $fts->fts_rank;
2104 my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2105 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2106 my $first_word = lc(($fts->words)[0]).'%';
2108 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2109 my $rank = join(' + ', @fts_ranks);
2112 $bonus{'subject'} = [];
2113 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2115 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2117 $bonus{'series'} = [
2118 { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2119 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2122 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2125 push @{ $bonus{'title'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2126 push @{ $bonus{'author'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2127 push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2128 push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2129 push @{ $bonus{'series'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2132 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2133 $bonus_list ||= '1';
2135 push @bonus_lists, $bonus_list;
2136 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2138 #---------------------
2140 $search_table_list .= "$search_table $search_group_name, ";
2141 push @rank_list,$rank;
2142 $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2144 if ($metabib_field) {
2145 $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2146 $metabib_field = undef;
2149 if ($prev_search_group) {
2150 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2154 my $metabib_record_descriptor = metabib::record_descriptor->table;
2155 my $metabib_full_rec = metabib::full_rec->table;
2156 my $metabib_metarecord = metabib::metarecord->table;
2157 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2158 my $asset_call_number_table = asset::call_number->table;
2159 my $asset_copy_table = asset::copy->table;
2160 my $cs_table = config::copy_status->table;
2161 my $cl_table = asset::copy_location->table;
2162 my $br_table = biblio::record_entry->table;
2163 my $source_table = config::bib_source->table;
2166 my $bonuses = join (' * ', @bonus_lists);
2167 my $relevance = join (' + ', @rank_list);
2168 $relevance = "AVG( ($relevance) * ($bonuses) )";
2170 my $string_default_sort = 'zzzz';
2171 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2173 my $number_default_sort = '9999';
2174 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2176 my $rank = $relevance;
2177 if (lc($sort) eq 'pubdate') {
2180 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d{4}'),'$number_default_sort')::INT
2181 FROM $metabib_full_rec frp
2182 WHERE frp.record = b.id
2184 AND frp.subfield = 'c'
2188 } elsif (lc($sort) eq 'create_date') {
2190 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2192 } elsif (lc($sort) eq 'edit_date') {
2194 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2196 } elsif (lc($sort) eq 'title') {
2199 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2200 FROM $metabib_full_rec frt
2201 WHERE frt.record = b.id
2203 AND frt.subfield = 'a'
2207 } elsif (lc($sort) eq 'author') {
2210 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
2211 FROM $metabib_full_rec fra
2212 WHERE fra.record = b.id
2213 AND fra.tag LIKE '1%'
2214 AND fra.subfield = 'a'
2215 ORDER BY fra.tag::text::int
2220 push @bonus_values, @bonus_values;
2225 my $select = <<" SQL";
2230 FROM $search_table_list
2231 $metabib_record_descriptor rd,
2234 WHERE rd.record = b.id
2235 AND b.active IS TRUE
2236 AND b.deleted IS FALSE
2245 GROUP BY b.id, b.source
2246 ORDER BY 3 $sort_dir
2247 LIMIT $visibility_limit
2250 if ($self->api_name !~ /staff/o) {
2255 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2258 FROM $asset_call_number_table cn,
2259 $asset_copy_table cp,
2263 WHERE cn.record = s.id
2264 AND cp.status = cs.id
2265 AND cp.location = cl.id
2266 AND cp.call_number = cn.id
2267 AND cp.opac_visible IS TRUE
2268 AND cs.opac_visible IS TRUE
2269 AND cl.opac_visible IS TRUE
2270 AND d.opac_visible IS TRUE
2271 AND cp.deleted IS FALSE
2272 AND cn.deleted IS FALSE
2273 AND cp.circ_lib = d.id
2277 OR src.transcendant IS TRUE
2278 ORDER BY 3 $sort_dir
2285 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2288 FROM $asset_call_number_table cn,
2289 $asset_copy_table cp,
2291 WHERE cn.record = s.id
2292 AND cp.call_number = cn.id
2293 AND cn.deleted IS FALSE
2294 AND cp.circ_lib = d.id
2295 AND cp.deleted IS FALSE
2301 FROM $asset_call_number_table cn
2302 WHERE cn.record = s.id
2305 OR src.transcendant IS TRUE
2306 ORDER BY 3 $sort_dir
2311 $log->debug("Field Search SQL :: [$select]",DEBUG);
2313 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2315 @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2318 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2320 my $count = scalar(@$recs);
2321 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2322 next unless ($$rec[0]);
2323 my ($mrid,$rank) = @$rec;
2324 $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2329 __PACKAGE__->register_method(
2330 api_name => "open-ils.storage.biblio.multiclass.search_fts.record",
2332 method => 'biblio_search_multi_class_fts',
2337 __PACKAGE__->register_method(
2338 api_name => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2340 method => 'biblio_search_multi_class_fts',
2345 __PACKAGE__->register_method(
2346 api_name => "open-ils.storage.biblio.multiclass.search_fts",
2348 method => 'biblio_search_multi_class_fts',
2353 __PACKAGE__->register_method(
2354 api_name => "open-ils.storage.biblio.multiclass.search_fts.staff",
2356 method => 'biblio_search_multi_class_fts',
2364 my $default_preferred_language;
2365 my $default_preferred_language_weight;
2367 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2373 if (!$locale_map{COMPLETE}) {
2375 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2376 for my $locale ( @locales ) {
2377 $locale_map{lc($locale->code)} = $locale->marc_code;
2379 $locale_map{COMPLETE} = 1;
2383 my $config = OpenSRF::Utils::SettingsClient->new();
2385 if (!$default_preferred_language) {
2387 $default_preferred_language = $config->config_value(
2388 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2389 ) || $config->config_value(
2390 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2395 if (!$default_preferred_language_weight) {
2397 $default_preferred_language_weight = $config->config_value(
2398 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2399 ) || $config->config_value(
2400 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2404 # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2405 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2407 my $ou = $args{org_unit};
2408 my $limit = $args{limit} || 10;
2409 my $offset = $args{offset} || 0;
2412 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2415 if (! scalar( keys %{$args{searches}} )) {
2416 die "No search arguments were passed to ".$self->api_name;
2419 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2421 if (!defined($args{preferred_language})) {
2422 my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2423 $args{preferred_language} =
2424 $locale_map{ lc($ses_locale) } || 'eng';
2427 if (!defined($args{preferred_language_weight})) {
2428 $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2431 if ($args{available}) {
2432 @statuses = (0,7,12);
2435 if (my $s = $args{locations}) {
2436 $s = [$s] if (!ref($s));
2440 if (my $b = $args{between}) {
2441 if (ref($b) && @$b == 2) {
2446 if (my $s = $args{statuses}) {
2447 $s = [$s] if (!ref($s));
2451 if (my $a = $args{audience}) {
2452 $a = [$a] if (!ref($a));
2456 if (my $l = $args{language}) {
2457 $l = [$l] if (!ref($l));
2461 if (my $f = $args{lit_form}) {
2462 $f = [$f] if (!ref($f));
2466 if (my $f = $args{item_form}) {
2467 $f = [$f] if (!ref($f));
2471 if (my $t = $args{item_type}) {
2472 $t = [$t] if (!ref($t));
2476 if (my $b = $args{bib_level}) {
2477 $b = [$b] if (!ref($b));
2481 if (my $v = $args{vr_format}) {
2482 $v = [$v] if (!ref($v));
2486 # XXX legacy format and item type support
2487 if ($args{format}) {
2488 my ($t, $f) = split '-', $args{format};
2489 @types = split '', $t;
2490 @forms = split '', $f;
2493 my %stored_proc_search_args;
2494 for my $search_group (sort keys %{$args{searches}}) {
2495 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2496 my ($search_class,$search_field) = split /\|/, $search_group;
2497 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2499 if ($search_field) {
2500 unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2501 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2506 my $class = $_cdbi->{$search_class};
2507 my $search_table = $class->table;
2509 my ($index_col) = $class->columns('FTS');
2510 $index_col ||= 'value';
2513 my $fts = OpenILS::Application::Storage::FTS->compile(
2514 $search_class => $args{searches}{$search_group}{term},
2515 $search_group_name.'.value',
2516 "$search_group_name.$index_col"
2518 $fts->sql_where_clause; # this builds the ranks for us
2520 my @fts_ranks = $fts->fts_rank;
2521 my @fts_queries = $fts->fts_query;
2522 my @phrases = map { lc($_) } $fts->phrases;
2523 my @words = map { lc($_) } $fts->words;
2525 $stored_proc_search_args{$search_group} = {
2526 fts_rank => \@fts_ranks,
2527 fts_query => \@fts_queries,
2528 phrase => \@phrases,
2534 my $param_search_ou = $ou;
2535 my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2536 my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2537 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2538 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2539 my $param_audience = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud ) . '}$$';
2540 my $param_language = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang ) . '}$$';
2541 my $param_lit_form = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2542 my $param_types = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types ) . '}$$';
2543 my $param_forms = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms ) . '}$$';
2544 my $param_vformats = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2545 my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2546 my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2547 my $param_after = $args{after} ; $param_after = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2548 my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2549 my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2550 my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2551 my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2552 my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2553 my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2554 my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2555 my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2556 my $param_rel_limit = $args{core_limit}; $param_rel_limit ||= 'NULL';
2557 my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2558 my $param_skip_chk = $args{skip_check}; $param_skip_chk ||= 'NULL';
2560 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2562 FROM search.staged_fts(
2563 $param_search_ou\:\:INT,
2564 $param_depth\:\:INT,
2565 $param_searches\:\:TEXT,
2566 $param_statuses\:\:INT[],
2567 $param_locations\:\:INT[],
2568 $param_audience\:\:TEXT[],
2569 $param_language\:\:TEXT[],
2570 $param_lit_form\:\:TEXT[],
2571 $param_types\:\:TEXT[],
2572 $param_forms\:\:TEXT[],
2573 $param_vformats\:\:TEXT[],
2574 $param_bib_level\:\:TEXT[],
2575 $param_before\:\:TEXT,
2576 $param_after\:\:TEXT,
2577 $param_during\:\:TEXT,
2578 $param_between\:\:TEXT[],
2579 $param_pref_lang\:\:TEXT,
2580 $param_pref_lang_multiplier\:\:REAL,
2581 $param_sort\:\:TEXT,
2582 $param_sort_desc\:\:BOOL,
2583 $metarecord\:\:BOOL,
2585 $param_rel_limit\:\:INT,
2586 $param_chk_limit\:\:INT,
2587 $param_skip_chk\:\:INT
2593 my $recs = $sth->fetchall_arrayref({});
2594 my $summary_row = pop @$recs;
2596 my $total = $$summary_row{total};
2597 my $checked = $$summary_row{checked};
2598 my $visible = $$summary_row{visible};
2599 my $deleted = $$summary_row{deleted};
2600 my $excluded = $$summary_row{excluded};
2602 my $estimate = $visible;
2603 if ( $total > $checked && $checked ) {
2605 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2606 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2610 delete $$summary_row{id};
2611 delete $$summary_row{rel};
2612 delete $$summary_row{record};
2614 $client->respond( $summary_row );
2616 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2618 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2619 delete $$rec{checked};
2620 delete $$rec{visible};
2621 delete $$rec{excluded};
2622 delete $$rec{deleted};
2623 delete $$rec{total};
2624 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2626 $client->respond( $rec );
2630 __PACKAGE__->register_method(
2631 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
2633 method => 'staged_fts',
2638 __PACKAGE__->register_method(
2639 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2641 method => 'staged_fts',
2646 __PACKAGE__->register_method(
2647 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
2649 method => 'staged_fts',
2654 __PACKAGE__->register_method(
2655 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2657 method => 'staged_fts',
2663 sub FTS_paging_estimate {
2667 my $checked = shift;
2668 my $visible = shift;
2669 my $excluded = shift;
2670 my $deleted = shift;
2673 my $deleted_ratio = $deleted / $checked;
2674 my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2676 my $exclusion_ratio = $excluded / $checked;
2677 my $delete_adjusted_exclusion_ratio = $checked - $deleted ? $excluded / ($checked - $deleted) : 1;
2679 my $inclusion_ratio = $visible / $checked;
2680 my $delete_adjusted_inclusion_ratio = $checked - $deleted ? $visible / ($checked - $deleted) : 0;
2683 exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2684 inclusion => int($delete_adjusted_total * $inclusion_ratio),
2685 delete_adjusted_exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2686 delete_adjusted_inclusion => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2689 __PACKAGE__->register_method(
2690 api_name => "open-ils.storage.fts_paging_estimate",
2692 method => 'FTS_paging_estimate',
2698 Hash of estimation values based on four variant estimation strategies:
2699 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2700 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2701 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2702 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2705 Helper method used to determin the approximate number of
2706 hits for a search that spans multiple superpages. For
2707 sparse superpages, the inclusion estimate will likely be the
2708 best estimate. The exclusion strategy is the original, but
2709 inclusion is the default.
2712 { name => 'checked',
2713 desc => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2716 { name => 'visible',
2717 desc => 'Number of records visible to the search location on the current superpage.',
2720 { name => 'excluded',
2721 desc => 'Number of records excluded from the search location on the current superpage.',
2724 { name => 'deleted',
2725 desc => 'Number of deleted records on the current superpage.',
2729 desc => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2742 my $term = $$args{term};
2743 my $limit = $$args{max} || 1;
2744 my $min = $$args{min} || 1;
2745 my @classes = @{$$args{class}};
2747 $limit = $min if ($min > $limit);
2750 @classes = ( qw/ title author subject series keyword / );
2754 my $bre_table = biblio::record_entry->table;
2755 my $cn_table = asset::call_number->table;
2756 my $cp_table = asset::copy->table;
2758 for my $search_class ( @classes ) {
2760 my $class = $_cdbi->{$search_class};
2761 my $search_table = $class->table;
2763 my ($index_col) = $class->columns('FTS');
2764 $index_col ||= 'value';
2767 my $where = OpenILS::Application::Storage::FTS
2768 ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2772 SELECT COUNT(DISTINCT X.source)
2773 FROM (SELECT $search_class.source
2774 FROM $search_table $search_class
2775 JOIN $bre_table b ON (b.id = $search_class.source)
2780 HAVING COUNT(DISTINCT X.source) >= $min;
2783 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2784 $matches{$search_class} = $res ? $res->[0] : 0;
2789 __PACKAGE__->register_method(
2790 api_name => "open-ils.storage.search.xref",
2792 method => 'xref_count',
2796 # Takes an abstract query object and recursively turns it back into a string
2798 sub abstract_query2str {
2799 my ($self, $conn, $query) = @_;
2801 return QueryParser::Canonicalize::abstract_query2str_impl($query, 0, $OpenILS::Application::Storage::QParser);
2804 __PACKAGE__->register_method(
2805 api_name => "open-ils.storage.query_parser.abstract_query.canonicalize",
2807 method => "abstract_query2str",
2812 Abstract query parser object, with complete config data. For example input,
2813 see the 'abstract_query' part of the output of an API call like
2814 open-ils.search.biblio.multiclass.query, when called with the return_abstract
2818 return => { type => "string", desc => "String representation of abstract query object" }
2822 sub str2abstract_query {
2823 my ($self, $conn, $query, $qp_opts, $with_config) = @_;
2825 my %use_opts = ( # reasonable defaults? should these even be hardcoded here?
2827 superpage_size => 1000,
2828 core_limit => 25000,
2830 (ref $opts eq 'HASH' ? %$opts : ())
2835 # grab the query parser and initialize it
2836 my $parser = $OpenILS::Application::Storage::QParser;
2839 _initialize_parser($parser) unless $parser->initialization_complete;
2841 my $query = $parser->new(%use_opts)->parse;
2843 return $query->parse_tree->to_abstract_query(with_config => $with_config);
2846 __PACKAGE__->register_method(
2847 api_name => "open-ils.storage.query_parser.abstract_query.from_string",
2849 method => "str2abstract_query",
2853 {desc => "Query", type => "string"},
2854 {desc => q/Arguments for initializing QueryParser (optional)/,
2856 {desc => q/Flag enabling inclusion of QP config in returned object (optional, default false)/,
2859 return => { type => "object", desc => "abstract representation of query parser query" }
2863 my @available_statuses_cache;
2864 sub available_statuses {
2865 if (!scalar(@available_statuses_cache)) {
2866 @available_statuses_cache = map { $_->id } config::copy_status->search_where({is_available => 't'});
2868 return @available_statuses_cache;
2871 sub query_parser_fts {
2877 # grab the query parser and initialize it
2878 my $parser = $OpenILS::Application::Storage::QParser;
2881 _initialize_parser($parser) unless $parser->initialization_complete;
2883 # populate the locale/language map
2884 if (!$locale_map{COMPLETE}) {
2886 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2887 for my $locale ( @locales ) {
2888 $locale_map{lc($locale->code)} = $locale->marc_code;
2890 $locale_map{COMPLETE} = 1;
2894 # I hope we have a query!
2895 if (! $args{query} ) {
2896 die "No query was passed to ".$self->api_name;
2899 my $default_CD_modifiers = OpenSRF::Utils::SettingsClient->new->config_value(
2900 apps => 'open-ils.search' => app_settings => 'default_CD_modifiers'
2903 # Protect against empty / missing default_CD_modifiers setting
2904 if ($default_CD_modifiers and !ref($default_CD_modifiers)) {
2905 $args{query} = "$default_CD_modifiers $args{query}";
2908 my $simple_plan = $args{_simple_plan};
2909 # remove bad chunks of the %args hash
2910 for my $bad ( grep { /^_/ } keys(%args)) {
2911 delete($args{$bad});
2915 # parse the query and supply any query-level %arg-based defaults
2916 # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2917 my $query = $parser->new( %args )->parse;
2919 my $config = OpenSRF::Utils::SettingsClient->new();
2921 # set the locale-based default preferred location
2922 if (!$query->parse_tree->find_filter('preferred_language')) {
2923 $parser->default_preferred_language( $args{preferred_language} );
2925 if (!$parser->default_preferred_language) {
2926 my $ses_locale = $client->session ? $client->session->session_locale : '';
2927 $parser->default_preferred_language( $locale_map{ lc($ses_locale) } );
2930 if (!$parser->default_preferred_language) { # still nothing...
2931 my $tmp_dpl = $config->config_value(
2932 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2933 ) || $config->config_value(
2934 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2937 $parser->default_preferred_language( $tmp_dpl )
2942 # set the global default language multiplier
2943 if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2946 if ($tmp_dplw = $args{preferred_language_weight} || $args{preferred_language_multiplier} ) {
2947 $parser->default_preferred_language_multiplier($tmp_dplw);
2950 $tmp_dplw = $config->config_value(
2951 apps => 'open-ils.search' => app_settings => 'default_preferred_language_weight'
2952 ) || $config->config_value(
2953 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2956 $parser->default_preferred_language_multiplier( $tmp_dplw );
2960 # gather the site, if one is specified, defaulting to the in-query version
2961 my $ou = $args{org_unit};
2962 if (my ($filter) = $query->parse_tree->find_filter('site')) {
2963 $ou = $filter->args->[0] if (@{$filter->args});
2965 $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^(-)?\d+$/);
2967 # gather lasso, as with $ou
2968 my $lasso = $args{lasso};
2969 if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
2970 $lasso = $filter->args->[0] if (@{$filter->args});
2972 $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
2973 $lasso = -$lasso if ($lasso);
2976 # # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
2977 # # gather user lasso, as with $ou and lasso
2978 # my $mylasso = $args{my_lasso};
2979 # if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
2980 # $mylasso = $filter->args->[0] if (@{$filter->args});
2982 # $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
2985 # if we have a lasso, go with that, otherwise ... ou
2986 $ou = $lasso if ($lasso);
2988 # gather the preferred OU, if one is specified, as with $ou
2989 my $pref_ou = $args{pref_ou};
2990 $log->info("pref_ou = $pref_ou");
2991 if (my ($filter) = $query->parse_tree->find_filter('pref_ou')) {
2992 $pref_ou = $filter->args->[0] if (@{$filter->args});
2994 $pref_ou = actor::org_unit->search( { shortname => $pref_ou } )->next->id if ($pref_ou and $pref_ou !~ /^(-)?\d+$/);
2996 # get the default $ou if we have nothing
2997 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
3000 # 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
3001 # gather the depth, if one is specified, defaulting to the in-query version
3002 my $depth = $args{depth};
3003 if (my ($filter) = $query->parse_tree->find_filter('depth')) {
3004 $depth = $filter->args->[0] if (@{$filter->args});
3006 $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
3009 # gather the limit or default to 10
3010 my $limit = $args{check_limit};
3011 if (my ($filter) = $query->parse_tree->find_filter('limit')) {
3012 $limit = $filter->args->[0] if (@{$filter->args});
3014 if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
3015 $limit = $filter->args->[0] if (@{$filter->args});
3019 # gather the offset or default to 0
3020 my $offset = $args{skip_check} || $args{offset};
3021 if (my ($filter) = $query->parse_tree->find_filter('offset')) {
3022 $offset = $filter->args->[0] if (@{$filter->args});
3024 if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
3025 $offset = $filter->args->[0] if (@{$filter->args});
3029 # gather the estimation strategy or default to inclusion
3030 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
3031 if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
3032 $estimation_strategy = $filter->args->[0] if (@{$filter->args});
3036 # gather the estimation strategy or default to inclusion
3037 my $core_limit = $args{core_limit};
3038 if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
3039 $core_limit = $filter->args->[0] if (@{$filter->args});
3043 # gather statuses, and then forget those if we have an #available modifier
3045 if ($query->parse_tree->find_modifier('available')) {
3046 @statuses = available_statuses();
3047 } elsif (my ($filter) = $query->parse_tree->find_filter('statuses')) {
3048 @statuses = @{$filter->args} if (@{$filter->args});
3054 if (my ($filter) = $query->parse_tree->find_filter('locations')) {
3055 @location = @{$filter->args} if (@{$filter->args});
3058 # gather location_groups
3059 if (my ($filter) = $query->parse_tree->find_filter('location_groups')) {
3060 my @loc_groups = @{$filter->args} if (@{$filter->args});
3062 # collect the mapped locations and add them to the locations() filter
3065 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3066 my $maps = $cstore->request(
3067 'open-ils.cstore.direct.asset.copy_location_group_map.search.atomic',
3068 {lgroup => \@loc_groups})->gather(1);
3070 push(@location, $_->location) for @$maps;
3075 my $param_check = $limit || $query->superpage_size || 'NULL';
3076 my $param_offset = $offset || 'NULL';
3077 my $param_limit = $core_limit || 'NULL';
3079 my $sp = $query->superpage || 1;
3081 $param_offset = ($sp - 1) * $sp_size;
3084 my $param_search_ou = $ou;
3085 my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
3086 # my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
3087 my $param_core_query = $query->parse_tree->toSQL;
3088 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
3089 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
3090 my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
3091 my $deleted_search = ($query->parse_tree->find_modifier('deleted')) ? "'t'" : "'f'";
3092 my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
3093 my $param_pref_ou = $pref_ou || 'NULL';
3095 # my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3096 # SELECT * -- bib search: $args{query}
3097 # FROM search.query_parser_fts(
3098 # $param_search_ou\:\:INT,
3099 # $param_depth\:\:INT,
3100 # $param_core_query\:\:TEXT,
3101 # $param_statuses\:\:INT[],
3102 # $param_locations\:\:INT[],
3103 # $param_offset\:\:INT,
3104 # $param_check\:\:INT,
3105 # $param_limit\:\:INT,
3106 # $metarecord\:\:BOOL,
3108 # $deleted_search\:\:BOOL,
3109 # $param_pref_ou\:\:INT
3113 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3114 -- bib search: $args{query}
3120 my $recs = $sth->fetchall_arrayref({});
3121 my $summary_row = pop @$recs;
3123 my $total = $$summary_row{total};
3124 my $checked = $$summary_row{checked};
3125 my $visible = $$summary_row{visible};
3126 my $deleted = $$summary_row{deleted};
3127 my $excluded = $$summary_row{excluded};
3129 delete $$summary_row{id};
3130 delete $$summary_row{rel};
3131 delete $$summary_row{record};
3132 delete $$summary_row{badges};
3133 delete $$summary_row{popularity};
3135 if (defined($simple_plan)) {
3136 $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3138 $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3141 if ($args{return_query}) {
3142 $$summary_row{query_struct} = $query->parse_tree->to_abstract_query();
3143 $$summary_row{canonicalized_query} = QueryParser::Canonicalize::abstract_query2str_impl($$summary_row{query_struct}, 0, $parser);
3146 $client->respond( $summary_row );
3148 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $visible.",DEBUG);
3150 for my $rec (@$recs) {
3151 delete $$rec{checked};
3152 delete $$rec{visible};
3153 delete $$rec{excluded};
3154 delete $$rec{deleted};
3155 delete $$rec{total};
3156 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3158 $client->respond( $rec );
3162 __PACKAGE__->register_method(
3163 api_name => "open-ils.storage.query_parser_search",
3165 method => 'query_parser_fts',
3173 sub query_parser_fts_wrapper {
3178 $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3179 # grab the query parser and initialize it
3180 my $parser = $OpenILS::Application::Storage::QParser;
3183 _initialize_parser($parser) unless $parser->initialization_complete;
3185 $args{searches} ||= {};
3186 if (!scalar(keys(%{$args{searches}})) && !$args{query}) {
3187 die "No search arguments were passed to ".$self->api_name;
3190 $top_org ||= actor::org_unit->search( { parent_ou => undef } )->next;
3192 my $base_query = $args{query} || '';
3193 if (scalar(keys(%{$args{searches}}))) {
3194 $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3195 for my $sclass ( keys %{$args{searches}} ) {
3196 $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3197 $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3201 my $query = $base_query;
3202 $log->debug("Full base query: $base_query", DEBUG);
3204 $query = "$args{facets} $query" if ($args{facets});
3206 if (!$locale_map{COMPLETE}) {
3208 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3209 for my $locale ( @locales ) {
3210 $locale_map{lc($locale->code)} = $locale->marc_code;
3212 $locale_map{COMPLETE} = 1;
3216 my $base_plan = $parser->new( query => $base_query )->parse;
3218 $query = "preferred_language($args{preferred_language}) $query"
3219 if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3220 $query = "preferred_language_weight($args{preferred_language_weight}) $query"
3221 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'));
3225 if (!$base_plan->parse_tree->find_filter('badge_orgs')) {
3226 # supply a suitable badge_orgs filter unless user has
3227 # explicitly supplied one
3230 my @lg_id_list = @{$args{location_groups}} if (ref $args{location_groups});
3232 my ($lg_filter) = $base_plan->parse_tree->find_filter('location_groups');
3233 @lg_id_list = @{$lg_filter->args} if ($lg_filter && @{$lg_filter->args});
3237 for my $lg ( grep { /^\d+$/ } @lg_id_list ) {
3238 my $lg_obj = asset::copy_location_group->retrieve($lg);
3239 next unless $lg_obj;
3241 push(@borg_list, ''.$lg_obj->owner);
3243 $borgs = join(',', @borg_list) if @borg_list;
3247 my ($site_filter) = $base_plan->parse_tree->find_filter('site');
3248 if ($site_filter && @{$site_filter->args}) {
3249 $site = $top_org if ($site_filter->args->[0] eq '-');
3250 $site = $top_org if ($site_filter->args->[0] eq $top_org->shortname);
3251 $site = actor::org_unit->search( { shortname => $site_filter->args->[0] })->next unless ($site);
3252 } elsif ($args{org_unit}) {
3253 $site = $top_org if ($args{org_unit} eq '-');
3254 $site = $top_org if ($args{org_unit} eq $top_org->shortname);
3255 $site = actor::org_unit->search( { shortname => $args{org_unit} })->next unless ($site);
3261 $borgs = OpenSRF::AppSession->create( 'open-ils.cstore' )->request(
3262 'open-ils.cstore.json_query.atomic',
3263 { from => [ 'actor.org_unit_ancestors', $site->id ] }
3266 if (ref $borgs && @$borgs) {
3267 $borgs = join(',', map { $_->{'id'} } @$borgs);
3275 # gather the limit or default to 10
3276 my $limit = delete($args{check_limit}) || $base_plan->superpage_size;
3277 if (my ($filter) = $base_plan->parse_tree->find_filter('limit')) {
3278 $limit = $filter->args->[0] if (@{$filter->args});
3280 if (my ($filter) = $base_plan->parse_tree->find_filter('check_limit')) {
3281 $limit = $filter->args->[0] if (@{$filter->args});
3284 # gather the offset or default to 0
3285 my $offset = delete($args{skip_check}) || delete($args{offset}) || 0;
3286 if (my ($filter) = $base_plan->parse_tree->find_filter('offset')) {
3287 $offset = $filter->args->[0] if (@{$filter->args});
3289 if (my ($filter) = $base_plan->parse_tree->find_filter('skip_check')) {
3290 $offset = $filter->args->[0] if (@{$filter->args});
3294 $query = "check_limit($limit) $query" if (defined $limit);
3295 $query = "skip_check($offset) $query" if (defined $offset);
3296 $query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
3297 $query = "badge_orgs($borgs) $query" if ($borgs);
3299 # XXX All of the following, down to the 'return' is basically dead code. someone higher up should handle it
3300 $query = "site($args{org_unit}) $query" if ($args{org_unit});
3301 $query = "depth($args{depth}) $query" if (defined($args{depth}));
3302 $query = "sort($args{sort}) $query" if ($args{sort});
3303 $query = "core_limit($args{core_limit}) $query" if ($args{core_limit});
3304 # $query = "limit($args{limit}) $query" if ($args{limit});
3305 # $query = "skip_check($args{skip_check}) $query" if ($args{skip_check});
3306 $query = "superpage($args{superpage}) $query" if ($args{superpage});
3307 $query = "offset($args{offset}) $query" if ($args{offset});
3308 $query = "#metarecord $query" if ($self->api_name =~ /metabib/);
3309 $query = "from_metarecord($args{from_metarecord}) $query" if ($args{from_metarecord});
3310 $query = "#available $query" if ($args{available});
3311 $query = "#descending $query" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3312 $query = "#staff $query" if ($self->api_name =~ /staff/);
3313 $query = "before($args{before}) $query" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3314 $query = "after($args{after}) $query" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3315 $query = "during($args{during}) $query" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3316 $query = "between($args{between}[0],$args{between}[1]) $query"
3317 if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3320 my (@between,@statuses,@locations,@location_groups,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3322 # XXX legacy format and item type support
3323 if ($args{format}) {
3324 my ($t, $f) = split '-', $args{format};
3325 $args{item_type} = [ split '', $t ];
3326 $args{item_form} = [ split '', $f ];
3329 for my $filter ( qw/locations location_groups statuses audience language lit_form item_form item_type bib_level vr_format badges/ ) {
3330 if (my $s = $args{$filter}) {
3331 $s = [$s] if (!ref($s));
3333 my @filter_list = @$s;
3335 next if (@filter_list == 0);
3337 my $filter_string = join ',', @filter_list;
3338 $query = "$query $filter($filter_string)";
3342 $log->debug("Full QueryParser query: $query", DEBUG);
3344 return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan, return_query => $args{return_query} );
3346 __PACKAGE__->register_method(
3347 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
3349 method => 'query_parser_fts_wrapper',
3354 __PACKAGE__->register_method(
3355 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3357 method => 'query_parser_fts_wrapper',
3362 __PACKAGE__->register_method(
3363 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
3365 method => 'query_parser_fts_wrapper',
3370 __PACKAGE__->register_method(
3371 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3373 method => 'query_parser_fts_wrapper',