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 fetch_highlighted_display_fields {
99 my $highlight_map = shift;
102 $client->respond_complete;
106 my $hl_map_string = "''::HSTORE";
107 if (ref($highlight_map)) {
109 for my $tsq (keys %$highlight_map) {
110 my $field_list = join(',', @{$$highlight_map{$tsq}});
111 $hl_map_string .= ' || ' if $hl_map_string;
112 $hl_map_string .= "hstore(($tsq)\:\:TEXT,'$field_list')";
116 my $sth = metabib::metarecord_source_map->db_Main->prepare(
117 "SELECT * FROM search.highlight_display_fields(?, $hl_map_string)"
120 $records = [$records] unless ref($records);
121 for my $record ( @$records ) {
123 $sth->execute($record);
124 my $rows = $sth->fetchall_arrayref({});
125 $client->respond($rows);
130 __PACKAGE__->register_method(
131 api_name => 'open-ils.storage.fetch.metabib.display_field.highlight',
132 method => 'fetch_highlighted_display_fields',
138 sub ordered_records_from_metarecord { # XXX Replace with QP-based search-within-MR
142 my $formats = shift; # dead
146 my $copies_visible = 'LEFT JOIN asset.copy_vis_attr_cache vc ON (br.id = vc.record '.
147 'AND vc.vis_attr_vector @@ (SELECT c_attrs::query_int FROM asset.patron_default_visibility_mask() LIMIT 1))';
148 $copies_visible = '' if ($self->api_name =~ /staff/o);
150 my $copies_visible_count = ',COUNT(vc.id)';
151 $copies_visible_count = '' if ($self->api_name =~ /staff/o);
153 my $descendants = '';
155 $descendants = defined($depth) ?
156 ",actor.org_unit_descendants($org, $depth) d" :
157 ",actor.org_unit_descendants($org) d" ;
164 $copies_visible_count
165 FROM metabib.metarecord_source_map sm
166 JOIN biblio.record_entry br ON (sm.source = br.id AND NOT br.deleted)
167 LEFT JOIN metabib.record_sorter s ON (s.source = br.id AND s.attr = 'titlesort')
168 LEFT JOIN config.bib_source bs ON (br.source = bs.id)
171 WHERE sm.metarecord = ?
175 if ($copies_visible) {
176 $sql .= 'AND (bs.transcendant OR ';
178 $sql .= 'vc.circ_lib = d.id)';
180 $sql .= 'vc.id IS NOT NULL)'
182 $having = 'HAVING COUNT(vc.id) > 0';
190 s.value ASC NULLS LAST
193 my $ids = metabib::metarecord_source_map->db_Main->selectcol_arrayref($sql, {}, "$mr");
194 return $ids if ($self->api_name =~ /atomic$/o);
196 $client->respond( $_ ) for ( @$ids );
200 __PACKAGE__->register_method(
201 api_name => 'open-ils.storage.ordered.metabib.metarecord.records',
203 method => 'ordered_records_from_metarecord',
207 __PACKAGE__->register_method(
208 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff',
210 method => 'ordered_records_from_metarecord',
215 __PACKAGE__->register_method(
216 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.atomic',
218 method => 'ordered_records_from_metarecord',
222 __PACKAGE__->register_method(
223 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic',
225 method => 'ordered_records_from_metarecord',
230 # XXX: this subroutine and its two registered methods are marked for
231 # deprecation, as they do not work properly in 2.x (these tags are no longer
232 # normalized in mfr) and are not in known use
236 my $isxn = lc(shift());
240 $isxn =~ s/-//o if ($self->api_name =~ /isbn/o);
242 my $tag = ($self->api_name =~ /isbn/o) ? "'020' OR f.tag = '024'" : "'022'";
244 my $fr_table = metabib::full_rec->table;
245 my $bib_table = biblio::record_entry->table;
248 SELECT DISTINCT f.record
250 JOIN $bib_table b ON (b.id = f.record)
253 AND b.deleted IS FALSE
256 my $list = metabib::full_rec->db_Main->selectcol_arrayref($sql, {}, "$isxn%");
257 $client->respond($_) for (@$list);
260 __PACKAGE__->register_method(
261 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
263 method => 'isxn_search',
267 __PACKAGE__->register_method(
268 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.issn',
270 method => 'isxn_search',
275 sub metarecord_copy_count {
281 my $sm_table = metabib::metarecord_source_map->table;
282 my $rd_table = metabib::record_descriptor->table;
283 my $cn_table = asset::call_number->table;
284 my $cp_table = asset::copy->table;
285 my $br_table = biblio::record_entry->table;
286 my $src_table = config::bib_source->table;
287 my $cl_table = asset::copy_location->table;
288 my $cs_table = config::copy_status->table;
289 my $out_table = actor::org_unit_type->table;
291 my $descendants = "actor.org_unit_descendants(u.id)";
292 my $ancestors = "actor.org_unit_ancestors(?) u JOIN $out_table t ON (u.ou_type = t.id)";
294 if ($args{org_unit} < 0) {
295 $args{org_unit} *= -1;
296 $ancestors = "(select org_unit as id from actor.org_lasso_map where lasso = ?) u CROSS JOIN (SELECT -1 AS depth) t";
299 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';
300 $copies_visible = '' if ($self->api_name =~ /staff/o);
302 my (@types,@forms,@blvl);
303 my ($t_filter, $f_filter, $b_filter) = ('','','');
306 my ($t, $f, $b) = split '-', $args{format};
307 @types = split '', $t;
308 @forms = split '', $f;
309 @blvl = split '', $b;
312 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
316 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
320 $b_filter .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
330 JOIN $cn_table cn ON (cn.record = r.source)
331 JOIN $rd_table rd ON (cn.record = rd.record)
332 JOIN $cp_table cp ON (cn.id = cp.call_number)
333 JOIN $cs_table cs ON (cp.status = cs.id)
334 JOIN $cl_table cl ON (cp.location = cl.id)
335 JOIN $descendants a ON (cp.circ_lib = a.id)
336 WHERE r.metarecord = ?
337 AND cn.deleted IS FALSE
338 AND cp.deleted IS FALSE
348 JOIN $cn_table cn ON (cn.record = r.source)
349 JOIN $rd_table rd ON (cn.record = rd.record)
350 JOIN $cp_table cp ON (cn.id = cp.call_number)
351 JOIN $cs_table cs ON (cp.status = cs.id)
352 JOIN $cl_table cl ON (cp.location = cl.id)
353 JOIN $descendants a ON (cp.circ_lib = a.id)
354 WHERE r.metarecord = ?
355 AND cp.status IN (0,7,12)
356 AND cn.deleted IS FALSE
357 AND cp.deleted IS FALSE
367 JOIN $cn_table cn ON (cn.record = r.source)
368 JOIN $rd_table rd ON (cn.record = rd.record)
369 JOIN $cp_table cp ON (cn.id = cp.call_number)
370 JOIN $cs_table cs ON (cp.status = cs.id)
371 JOIN $cl_table cl ON (cp.location = cl.id)
372 WHERE r.metarecord = ?
373 AND cn.deleted IS FALSE
374 AND cp.deleted IS FALSE
375 AND cp.opac_visible IS TRUE
376 AND cs.opac_visible IS TRUE
377 AND cl.opac_visible IS TRUE
386 JOIN $br_table br ON (br.id = r.source)
387 JOIN $src_table src ON (src.id = br.source)
388 WHERE r.metarecord = ?
389 AND src.transcendant IS TRUE
397 my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
398 $sth->execute( ''.$args{metarecord},
402 ''.$args{metarecord},
406 ''.$args{metarecord},
410 ''.$args{metarecord},
414 while ( my $row = $sth->fetchrow_hashref ) {
415 $client->respond( $row );
419 __PACKAGE__->register_method(
420 api_name => 'open-ils.storage.metabib.metarecord.copy_count',
422 method => 'metarecord_copy_count',
427 __PACKAGE__->register_method(
428 api_name => 'open-ils.storage.metabib.metarecord.copy_count.staff',
430 method => 'metarecord_copy_count',
436 sub biblio_multi_search_full_rec {
441 my $class_join = $args{class_join} || 'AND';
442 my $limit = $args{limit} || 100;
443 my $offset = $args{offset} || 0;
444 my $sort = $args{'sort'};
445 my $sort_dir = $args{sort_dir} || 'DESC';
450 for my $arg (@{ $args{searches} }) {
451 my $term = $$arg{term};
452 my $limiters = $$arg{restrict};
454 my ($index_col) = metabib::full_rec->columns('FTS');
455 $index_col ||= 'value';
456 my $search_table = metabib::full_rec->table;
458 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
460 my $fts_where = $fts->sql_where_clause();
461 my @fts_ranks = $fts->fts_rank;
463 my $rank = join(' + ', @fts_ranks);
466 for my $limit (@$limiters) {
467 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
468 # MARC control field; mfr.subfield is NULL
469 push @wheres, "( tag = ? AND $fts_where )";
470 push @binds, $$limit{tag};
471 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
473 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
474 push @binds, $$limit{tag}, $$limit{subfield};
475 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
478 my $where = join(' OR ', @wheres);
480 push @selects, "SELECT record, AVG($rank) as sum FROM $search_table WHERE $where GROUP BY record";
484 my $descendants = defined($args{depth}) ?
485 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
486 "actor.org_unit_descendants($args{org_unit})" ;
489 my $metabib_record_descriptor = metabib::record_descriptor->table;
490 my $metabib_full_rec = metabib::full_rec->table;
491 my $asset_call_number_table = asset::call_number->table;
492 my $asset_copy_table = asset::copy->table;
493 my $cs_table = config::copy_status->table;
494 my $cl_table = asset::copy_location->table;
495 my $br_table = biblio::record_entry->table;
497 my $cj = 'HAVING COUNT(x.record) = ' . scalar(@selects) if ($class_join eq 'AND');
499 '(SELECT x.record, sum(x.sum) FROM (('.
500 join(') UNION ALL (', @selects).
501 ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
503 my $has_vols = 'AND cn.owning_lib = d.id';
504 my $has_copies = 'AND cp.call_number = cn.id';
505 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';
507 if ($self->api_name =~ /staff/o) {
508 $copies_visible = '';
509 $has_copies = '' if ($ou_type == 0);
510 $has_vols = '' if ($ou_type == 0);
513 my ($t_filter, $f_filter) = ('','');
514 my ($a_filter, $l_filter, $lf_filter) = ('','','');
517 if (my $a = $args{audience}) {
518 $a = [$a] if (!ref($a));
521 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
526 if (my $l = $args{language}) {
527 $l = [$l] if (!ref($l));
530 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
535 if (my $f = $args{lit_form}) {
536 $f = [$f] if (!ref($f));
539 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
540 push @binds, @lit_form;
544 if (my $f = $args{item_form}) {
545 $f = [$f] if (!ref($f));
548 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
553 if (my $t = $args{item_type}) {
554 $t = [$t] if (!ref($t));
557 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
564 my ($t, $f) = split '-', $args{format};
565 my @types = split '', $t;
566 my @forms = split '', $f;
568 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
573 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
576 push @binds, @types, @forms;
579 my $relevance = 'sum(f.sum)';
580 $relevance = 1 if (!$copies_visible);
582 my $string_default_sort = 'zzzz';
583 $string_default_sort = 'AAAA' if ($sort_dir =~ /^DESC$/i);
585 my $number_default_sort = '9999';
586 $number_default_sort = '0000' if ($sort_dir =~/^DESC$/i);
588 my $rank = $relevance;
589 if (lc($sort) eq 'pubdate') {
592 SELECT COALESCE(SUBSTRING(MAX(frp.value) FROM E'\\\\d{4}'), '$number_default_sort')::INT
593 FROM $metabib_full_rec frp
594 WHERE frp.record = f.record
596 AND frp.subfield = 'c'
600 } elsif (lc($sort) eq 'create_date') {
602 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
604 } elsif (lc($sort) eq 'edit_date') {
606 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
608 } elsif (lc($sort) =~ /^title/i) {
611 SELECT COALESCE(LTRIM(SUBSTR(MAX(frt.value), COALESCE(SUBSTRING(MAX(frt.ind2) FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
612 FROM $metabib_full_rec frt
613 WHERE frt.record = f.record
615 AND frt.subfield = 'a'
619 } elsif (lc($sort) =~ /^author/i) {
622 SELECT COALESCE(LTRIM(MAX(query.value)), '$string_default_sort')
625 FROM $metabib_full_rec fra
626 WHERE fra.record = f.record
627 AND fra.tag LIKE '1%'
628 AND fra.subfield = 'a'
629 ORDER BY fra.tag::text::int
638 my $rd_join = $use_rd ? "$metabib_record_descriptor rd," : '';
639 my $rd_filter = $use_rd ? 'AND rd.record = f.record' : '';
641 if ($copies_visible) {
643 SELECT f.record, $relevance, count(DISTINCT cp.id), $rank
644 FROM $search_table f,
645 $asset_call_number_table cn,
646 $asset_copy_table cp,
652 WHERE br.id = f.record
653 AND cn.record = f.record
654 AND cp.status = cs.id
655 AND cp.location = cl.id
656 AND br.deleted IS FALSE
657 AND cn.deleted IS FALSE
658 AND cp.deleted IS FALSE
668 GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
669 ORDER BY 4 $sort_dir,3 DESC
673 SELECT f.record, 1, 1, $rank
674 FROM $search_table f,
677 WHERE br.id = f.record
678 AND br.deleted IS FALSE
691 $log->debug("Search SQL :: [$select]",DEBUG);
693 my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
694 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
697 $max = 1 if (!@$recs);
699 $max = $$_[1] if ($$_[1] > $max);
703 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
704 next unless ($$rec[0]);
705 my ($rid,$rank,$junk,$skip) = @$rec;
706 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
710 __PACKAGE__->register_method(
711 api_name => 'open-ils.storage.biblio.full_rec.multi_search',
713 method => 'biblio_multi_search_full_rec',
718 __PACKAGE__->register_method(
719 api_name => 'open-ils.storage.biblio.full_rec.multi_search.staff',
721 method => 'biblio_multi_search_full_rec',
727 sub search_full_rec {
733 my $term = $args{term};
734 my $limiters = $args{restrict};
736 my ($index_col) = metabib::full_rec->columns('FTS');
737 $index_col ||= 'value';
738 my $search_table = metabib::full_rec->table;
740 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
742 my $fts_where = $fts->sql_where_clause();
743 my @fts_ranks = $fts->fts_rank;
745 my $rank = join(' + ', @fts_ranks);
749 for my $limit (@$limiters) {
750 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
751 # MARC control field; mfr.subfield is NULL
752 push @wheres, "( tag = ? AND $fts_where )";
753 push @binds, $$limit{tag};
754 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
756 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
757 push @binds, $$limit{tag}, $$limit{subfield};
758 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
761 my $where = join(' OR ', @wheres);
763 my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
765 $log->debug("Search SQL :: [$select]",DEBUG);
767 my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
768 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
770 $client->respond($_) for (@$recs);
773 __PACKAGE__->register_method(
774 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
776 method => 'search_full_rec',
781 __PACKAGE__->register_method(
782 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
784 method => 'search_full_rec',
791 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
792 sub search_class_fts {
797 my $term = $args{term};
798 my $ou = $args{org_unit};
799 my $ou_type = $args{depth};
800 my $limit = $args{limit};
801 my $offset = $args{offset};
803 my $limit_clause = '';
804 my $offset_clause = '';
806 $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
807 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
810 my ($t_filter, $f_filter) = ('','');
813 my ($t, $f) = split '-', $args{format};
814 @types = split '', $t;
815 @forms = split '', $f;
817 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
821 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
827 my $descendants = defined($ou_type) ?
828 "actor.org_unit_descendants($ou, $ou_type)" :
829 "actor.org_unit_descendants($ou)";
831 my $class = $self->{cdbi};
832 my $search_table = $class->table;
834 my $metabib_record_descriptor = metabib::record_descriptor->table;
835 my $metabib_metarecord = metabib::metarecord->table;
836 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
837 my $asset_call_number_table = asset::call_number->table;
838 my $asset_copy_table = asset::copy->table;
839 my $cs_table = config::copy_status->table;
840 my $cl_table = asset::copy_location->table;
842 my ($index_col) = $class->columns('FTS');
843 $index_col ||= 'value';
845 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
846 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
848 my $fts_where = $fts->sql_where_clause;
849 my @fts_ranks = $fts->fts_rank;
851 my $rank = join(' + ', @fts_ranks);
853 my $has_vols = 'AND cn.owning_lib = d.id';
854 my $has_copies = 'AND cp.call_number = cn.id';
855 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';
857 my $visible_count = ', count(DISTINCT cp.id)';
858 my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
860 if ($self->api_name =~ /staff/o) {
861 $copies_visible = '';
862 $visible_count_test = '';
863 $has_copies = '' if ($ou_type == 0);
864 $has_vols = '' if ($ou_type == 0);
867 my $rank_calc = <<" RANK";
869 * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
870 * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
871 * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
872 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
875 $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
877 if ($copies_visible) {
879 SELECT m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
880 FROM $search_table f,
881 $metabib_metarecord_source_map_table m,
882 $asset_call_number_table cn,
883 $asset_copy_table cp,
886 $metabib_record_descriptor rd,
889 AND m.source = f.source
890 AND cn.record = m.source
891 AND rd.record = m.source
892 AND cp.status = cs.id
893 AND cp.location = cl.id
899 GROUP BY 1 $visible_count_test
901 $limit_clause $offset_clause
905 SELECT m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
906 FROM $search_table f,
907 $metabib_metarecord_source_map_table m,
908 $metabib_record_descriptor rd
910 AND m.source = f.source
911 AND rd.record = m.source
916 $limit_clause $offset_clause
920 $log->debug("Field Search SQL :: [$select]",DEBUG);
922 my $SQLstring = join('%',$fts->words);
923 my $REstring = join('\\s+',$fts->words);
924 my $first_word = ($fts->words)[0].'%';
925 my $recs = ($self->api_name =~ /unordered/o) ?
926 $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
927 $class->db_Main->selectall_arrayref($select, {},
928 '%'.lc($SQLstring).'%', # phrase order match
929 lc($first_word), # first word match
930 '^\\s*'.lc($REstring).'\\s*/?\s*$', # full exact match
934 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
936 $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
940 for my $class ( qw/title author subject keyword series identifier/ ) {
941 __PACKAGE__->register_method(
942 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord",
944 method => 'search_class_fts',
947 cdbi => "metabib::${class}_field_entry",
950 __PACKAGE__->register_method(
951 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
953 method => 'search_class_fts',
956 cdbi => "metabib::${class}_field_entry",
959 __PACKAGE__->register_method(
960 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
962 method => 'search_class_fts',
965 cdbi => "metabib::${class}_field_entry",
968 __PACKAGE__->register_method(
969 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
971 method => 'search_class_fts',
974 cdbi => "metabib::${class}_field_entry",
979 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
980 sub search_class_fts_count {
985 my $term = $args{term};
986 my $ou = $args{org_unit};
987 my $ou_type = $args{depth};
988 my $limit = $args{limit} || 100;
989 my $offset = $args{offset} || 0;
991 my $descendants = defined($ou_type) ?
992 "actor.org_unit_descendants($ou, $ou_type)" :
993 "actor.org_unit_descendants($ou)";
996 my ($t_filter, $f_filter) = ('','');
999 my ($t, $f) = split '-', $args{format};
1000 @types = split '', $t;
1001 @forms = split '', $f;
1003 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1007 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1012 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
1014 my $class = $self->{cdbi};
1015 my $search_table = $class->table;
1017 my $metabib_record_descriptor = metabib::record_descriptor->table;
1018 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1019 my $asset_call_number_table = asset::call_number->table;
1020 my $asset_copy_table = asset::copy->table;
1021 my $cs_table = config::copy_status->table;
1022 my $cl_table = asset::copy_location->table;
1024 my ($index_col) = $class->columns('FTS');
1025 $index_col ||= 'value';
1027 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
1029 my $fts_where = $fts->sql_where_clause;
1031 my $has_vols = 'AND cn.owning_lib = d.id';
1032 my $has_copies = 'AND cp.call_number = cn.id';
1033 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';
1034 if ($self->api_name =~ /staff/o) {
1035 $copies_visible = '';
1036 $has_vols = '' if ($ou_type == 0);
1037 $has_copies = '' if ($ou_type == 0);
1040 # XXX test an "EXISTS version of descendant checking...
1042 if ($copies_visible) {
1044 SELECT count(distinct m.metarecord)
1045 FROM $search_table f,
1046 $metabib_metarecord_source_map_table m,
1047 $metabib_metarecord_source_map_table mr,
1048 $asset_call_number_table cn,
1049 $asset_copy_table cp,
1052 $metabib_record_descriptor rd,
1055 AND mr.source = f.source
1056 AND mr.metarecord = m.metarecord
1057 AND cn.record = m.source
1058 AND rd.record = m.source
1059 AND cp.status = cs.id
1060 AND cp.location = cl.id
1069 SELECT count(distinct m.metarecord)
1070 FROM $search_table f,
1071 $metabib_metarecord_source_map_table m,
1072 $metabib_metarecord_source_map_table mr,
1073 $metabib_record_descriptor rd
1075 AND mr.source = f.source
1076 AND mr.metarecord = m.metarecord
1077 AND rd.record = m.source
1083 $log->debug("Field Search Count SQL :: [$select]",DEBUG);
1085 my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1087 $log->debug("Count Search yielded $recs results.",DEBUG);
1092 for my $class ( qw/title author subject keyword series identifier/ ) {
1093 __PACKAGE__->register_method(
1094 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
1096 method => 'search_class_fts_count',
1099 cdbi => "metabib::${class}_field_entry",
1102 __PACKAGE__->register_method(
1103 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
1105 method => 'search_class_fts_count',
1108 cdbi => "metabib::${class}_field_entry",
1114 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1115 sub postfilter_search_class_fts {
1120 my $term = $args{term};
1121 my $sort = $args{'sort'};
1122 my $sort_dir = $args{sort_dir} || 'DESC';
1123 my $ou = $args{org_unit};
1124 my $ou_type = $args{depth};
1125 my $limit = $args{limit} || 10;
1126 my $visibility_limit = $args{visibility_limit} || 5000;
1127 my $offset = $args{offset} || 0;
1129 my $outer_limit = 1000;
1131 my $limit_clause = '';
1132 my $offset_clause = '';
1134 $limit_clause = "LIMIT $outer_limit";
1135 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1137 my (@types,@forms,@lang,@aud,@lit_form);
1138 my ($t_filter, $f_filter) = ('','');
1139 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1140 my ($ot_filter, $of_filter) = ('','');
1141 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1143 if (my $a = $args{audience}) {
1144 $a = [$a] if (!ref($a));
1147 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1148 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1151 if (my $l = $args{language}) {
1152 $l = [$l] if (!ref($l));
1155 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1156 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1159 if (my $f = $args{lit_form}) {
1160 $f = [$f] if (!ref($f));
1163 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1164 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1167 if ($args{format}) {
1168 my ($t, $f) = split '-', $args{format};
1169 @types = split '', $t;
1170 @forms = split '', $f;
1172 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1173 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1177 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1178 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1183 my $descendants = defined($ou_type) ?
1184 "actor.org_unit_descendants($ou, $ou_type)" :
1185 "actor.org_unit_descendants($ou)";
1187 my $class = $self->{cdbi};
1188 my $search_table = $class->table;
1190 my $metabib_full_rec = metabib::full_rec->table;
1191 my $metabib_record_descriptor = metabib::record_descriptor->table;
1192 my $metabib_metarecord = metabib::metarecord->table;
1193 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1194 my $asset_call_number_table = asset::call_number->table;
1195 my $asset_copy_table = asset::copy->table;
1196 my $cs_table = config::copy_status->table;
1197 my $cl_table = asset::copy_location->table;
1198 my $br_table = biblio::record_entry->table;
1200 my ($index_col) = $class->columns('FTS');
1201 $index_col ||= 'value';
1203 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1205 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1207 my $SQLstring = join('%',map { lc($_) } $fts->words);
1208 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1209 my $first_word = lc(($fts->words)[0]).'%';
1211 my $fts_where = $fts->sql_where_clause;
1212 my @fts_ranks = $fts->fts_rank;
1215 $bonus{'metabib::identifier_field_entry'} =
1216 $bonus{'metabib::keyword_field_entry'} = [
1217 { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring }
1220 $bonus{'metabib::title_field_entry'} =
1221 $bonus{'metabib::series_field_entry'} = [
1222 { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1223 { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1224 @{ $bonus{'metabib::keyword_field_entry'} }
1227 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1228 $bonus_list ||= '1';
1230 my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1232 my $relevance = join(' + ', @fts_ranks);
1233 $relevance = <<" RANK";
1234 (SUM( ( $relevance ) * ( $bonus_list ) )/COUNT(m.source))
1237 my $string_default_sort = 'zzzz';
1238 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1240 my $number_default_sort = '9999';
1241 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1243 my $rank = $relevance;
1244 if (lc($sort) eq 'pubdate') {
1247 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1248 FROM $metabib_full_rec frp
1249 WHERE frp.record = mr.master_record
1251 AND frp.subfield = 'c'
1255 } elsif (lc($sort) eq 'create_date') {
1257 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1259 } elsif (lc($sort) eq 'edit_date') {
1261 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1263 } elsif (lc($sort) eq 'title') {
1266 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1267 FROM $metabib_full_rec frt
1268 WHERE frt.record = mr.master_record
1270 AND frt.subfield = 'a'
1274 } elsif (lc($sort) eq 'author') {
1277 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1278 FROM $metabib_full_rec fra
1279 WHERE fra.record = mr.master_record
1280 AND fra.tag LIKE '1%'
1281 AND fra.subfield = 'a'
1282 ORDER BY fra.tag::text::int
1290 my $select = <<" SQL";
1291 SELECT m.metarecord,
1293 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1295 FROM $search_table f,
1296 $metabib_metarecord_source_map_table m,
1297 $metabib_metarecord_source_map_table smrs,
1298 $metabib_metarecord mr,
1299 $metabib_record_descriptor rd
1301 AND smrs.metarecord = mr.id
1302 AND m.source = f.source
1303 AND m.metarecord = mr.id
1304 AND rd.record = smrs.source
1310 GROUP BY m.metarecord
1311 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1312 LIMIT $visibility_limit
1319 FROM $asset_call_number_table cn,
1320 $metabib_metarecord_source_map_table mrs,
1321 $asset_copy_table cp,
1326 $metabib_record_descriptor ord,
1328 WHERE mrs.metarecord = s.metarecord
1329 AND br.id = mrs.source
1330 AND cn.record = mrs.source
1331 AND cp.status = cs.id
1332 AND cp.location = cl.id
1333 AND cn.owning_lib = d.id
1334 AND cp.call_number = cn.id
1335 AND cp.opac_visible IS TRUE
1336 AND cs.opac_visible IS TRUE
1337 AND cl.opac_visible IS TRUE
1338 AND d.opac_visible IS TRUE
1339 AND br.active IS TRUE
1340 AND br.deleted IS FALSE
1341 AND ord.record = mrs.source
1347 ORDER BY 4 $sort_dir
1349 } elsif ($self->api_name !~ /staff/o) {
1356 FROM $asset_call_number_table cn,
1357 $metabib_metarecord_source_map_table mrs,
1358 $asset_copy_table cp,
1363 $metabib_record_descriptor ord
1365 WHERE mrs.metarecord = s.metarecord
1366 AND br.id = mrs.source
1367 AND cn.record = mrs.source
1368 AND cp.status = cs.id
1369 AND cp.location = cl.id
1370 AND cp.circ_lib = d.id
1371 AND cp.call_number = cn.id
1372 AND cp.opac_visible IS TRUE
1373 AND cs.opac_visible IS TRUE
1374 AND cl.opac_visible IS TRUE
1375 AND d.opac_visible IS TRUE
1376 AND br.active IS TRUE
1377 AND br.deleted IS FALSE
1378 AND ord.record = mrs.source
1386 ORDER BY 4 $sort_dir
1395 FROM $asset_call_number_table cn,
1396 $asset_copy_table cp,
1397 $metabib_metarecord_source_map_table mrs,
1400 $metabib_record_descriptor ord
1402 WHERE mrs.metarecord = s.metarecord
1403 AND br.id = mrs.source
1404 AND cn.record = mrs.source
1405 AND cn.id = cp.call_number
1406 AND br.deleted IS FALSE
1407 AND cn.deleted IS FALSE
1408 AND ord.record = mrs.source
1409 AND ( cn.owning_lib = d.id
1410 OR ( cp.circ_lib = d.id
1411 AND cp.deleted IS FALSE
1423 FROM $asset_call_number_table cn,
1424 $metabib_metarecord_source_map_table mrs,
1425 $metabib_record_descriptor ord
1426 WHERE mrs.metarecord = s.metarecord
1427 AND cn.record = mrs.source
1428 AND ord.record = mrs.source
1436 ORDER BY 4 $sort_dir
1441 $log->debug("Field Search SQL :: [$select]",DEBUG);
1443 my $recs = $class->db_Main->selectall_arrayref(
1445 (@bonus_values > 0 ? @bonus_values : () ),
1446 ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1447 @types, @forms, @aud, @lang, @lit_form,
1448 @types, @forms, @aud, @lang, @lit_form,
1449 ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1451 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1454 $max = 1 if (!@$recs);
1456 $max = $$_[1] if ($$_[1] > $max);
1459 my $count = scalar(@$recs);
1460 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1461 my ($mrid,$rank,$skip) = @$rec;
1462 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1467 for my $class ( qw/title author subject keyword series identifier/ ) {
1468 __PACKAGE__->register_method(
1469 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1471 method => 'postfilter_search_class_fts',
1474 cdbi => "metabib::${class}_field_entry",
1477 __PACKAGE__->register_method(
1478 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1480 method => 'postfilter_search_class_fts',
1483 cdbi => "metabib::${class}_field_entry",
1490 my $_cdbi = { title => "metabib::title_field_entry",
1491 author => "metabib::author_field_entry",
1492 subject => "metabib::subject_field_entry",
1493 keyword => "metabib::keyword_field_entry",
1494 series => "metabib::series_field_entry",
1495 identifier => "metabib::identifier_field_entry",
1498 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1499 sub postfilter_search_multi_class_fts {
1504 my $sort = $args{'sort'};
1505 my $sort_dir = $args{sort_dir} || 'DESC';
1506 my $ou = $args{org_unit};
1507 my $ou_type = $args{depth};
1508 my $limit = $args{limit} || 10;
1509 my $offset = $args{offset} || 0;
1510 my $visibility_limit = $args{visibility_limit} || 5000;
1513 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1516 if (!defined($args{org_unit})) {
1517 die "No target organizational unit passed to ".$self->api_name;
1520 if (! scalar( keys %{$args{searches}} )) {
1521 die "No search arguments were passed to ".$self->api_name;
1524 my $outer_limit = 1000;
1526 my $limit_clause = '';
1527 my $offset_clause = '';
1529 $limit_clause = "LIMIT $outer_limit";
1530 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1532 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1533 my ($t_filter, $f_filter, $v_filter) = ('','','');
1534 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1535 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1536 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1538 if ($args{available}) {
1539 $avail_filter = ' AND cp.status IN (0,7,12)';
1542 if (my $a = $args{audience}) {
1543 $a = [$a] if (!ref($a));
1546 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1547 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1550 if (my $l = $args{language}) {
1551 $l = [$l] if (!ref($l));
1554 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1555 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1558 if (my $f = $args{lit_form}) {
1559 $f = [$f] if (!ref($f));
1562 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1563 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1566 if (my $f = $args{item_form}) {
1567 $f = [$f] if (!ref($f));
1570 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1571 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1574 if (my $t = $args{item_type}) {
1575 $t = [$t] if (!ref($t));
1578 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1579 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1582 if (my $v = $args{vr_format}) {
1583 $v = [$v] if (!ref($v));
1586 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1587 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1591 # XXX legacy format and item type support
1592 if ($args{format}) {
1593 my ($t, $f) = split '-', $args{format};
1594 @types = split '', $t;
1595 @forms = split '', $f;
1597 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1598 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1602 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1603 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1609 my $descendants = defined($ou_type) ?
1610 "actor.org_unit_descendants($ou, $ou_type)" :
1611 "actor.org_unit_descendants($ou)";
1613 my $search_table_list = '';
1615 my $join_table_list = '';
1618 my $field_table = config::metabib_field->table;
1622 my $prev_search_group;
1623 my $curr_search_group;
1627 for my $search_group (sort keys %{$args{searches}}) {
1628 (my $search_group_name = $search_group) =~ s/\|/_/gso;
1629 ($search_class,$search_field) = split /\|/, $search_group;
1630 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1632 if ($search_field) {
1633 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1634 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1639 $prev_search_group = $curr_search_group if ($curr_search_group);
1641 $curr_search_group = $search_group_name;
1643 my $class = $_cdbi->{$search_class};
1644 my $search_table = $class->table;
1646 my ($index_col) = $class->columns('FTS');
1647 $index_col ||= 'value';
1650 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1652 my $fts_where = $fts->sql_where_clause;
1653 my @fts_ranks = $fts->fts_rank;
1655 my $SQLstring = join('%',map { lc($_) } $fts->words);
1656 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1657 my $first_word = lc(($fts->words)[0]).'%';
1659 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1660 my $rank = join(' + ', @fts_ranks);
1663 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1664 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1666 $bonus{'series'} = [
1667 { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1668 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1671 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1673 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1674 $bonus_list ||= '1';
1676 push @bonus_lists, $bonus_list;
1677 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1680 #---------------------
1682 $search_table_list .= "$search_table $search_group_name, ";
1683 push @rank_list,$rank;
1684 $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1686 if ($metabib_field) {
1687 $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1688 $metabib_field = undef;
1691 if ($prev_search_group) {
1692 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1696 my $metabib_record_descriptor = metabib::record_descriptor->table;
1697 my $metabib_full_rec = metabib::full_rec->table;
1698 my $metabib_metarecord = metabib::metarecord->table;
1699 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1700 my $asset_call_number_table = asset::call_number->table;
1701 my $asset_copy_table = asset::copy->table;
1702 my $cs_table = config::copy_status->table;
1703 my $cl_table = asset::copy_location->table;
1704 my $br_table = biblio::record_entry->table;
1705 my $source_table = config::bib_source->table;
1707 my $bonuses = join (' * ', @bonus_lists);
1708 my $relevance = join (' + ', @rank_list);
1709 $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1711 my $string_default_sort = 'zzzz';
1712 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1714 my $number_default_sort = '9999';
1715 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1719 my $secondary_sort = <<" SORT";
1721 SELECT COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1722 FROM $metabib_full_rec sfrt,
1723 $metabib_metarecord mr
1724 WHERE sfrt.record = mr.master_record
1725 AND sfrt.tag = '245'
1726 AND sfrt.subfield = 'a'
1731 my $rank = $relevance;
1732 if (lc($sort) eq 'pubdate') {
1735 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1736 FROM $metabib_full_rec frp
1737 WHERE frp.record = mr.master_record
1739 AND frp.subfield = 'c'
1743 } elsif (lc($sort) eq 'create_date') {
1745 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1747 } elsif (lc($sort) eq 'edit_date') {
1749 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1751 } elsif (lc($sort) eq 'title') {
1754 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1755 FROM $metabib_full_rec frt
1756 WHERE frt.record = mr.master_record
1758 AND frt.subfield = 'a'
1762 $secondary_sort = <<" SORT";
1764 SELECT COALESCE(SUBSTRING(sfrp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1765 FROM $metabib_full_rec sfrp
1766 WHERE sfrp.record = mr.master_record
1767 AND sfrp.tag = '260'
1768 AND sfrp.subfield = 'c'
1772 } elsif (lc($sort) eq 'author') {
1775 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1776 FROM $metabib_full_rec fra
1777 WHERE fra.record = mr.master_record
1778 AND fra.tag LIKE '1%'
1779 AND fra.subfield = 'a'
1780 ORDER BY fra.tag::text::int
1785 push @bonus_values, @bonus_values;
1790 my $select = <<" SQL";
1791 SELECT m.metarecord,
1793 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1796 FROM $search_table_list
1797 $metabib_metarecord mr,
1798 $metabib_metarecord_source_map_table m,
1799 $metabib_metarecord_source_map_table smrs
1800 WHERE m.metarecord = smrs.metarecord
1801 AND mr.id = m.metarecord
1804 GROUP BY m.metarecord
1805 -- ORDER BY 4 $sort_dir
1806 LIMIT $visibility_limit
1809 if ($self->api_name !~ /staff/o) {
1816 FROM $asset_call_number_table cn,
1817 $metabib_metarecord_source_map_table mrs,
1818 $asset_copy_table cp,
1823 $metabib_record_descriptor ord
1824 WHERE mrs.metarecord = s.metarecord
1825 AND br.id = mrs.source
1826 AND cn.record = mrs.source
1827 AND cp.status = cs.id
1828 AND cp.location = cl.id
1829 AND cp.circ_lib = d.id
1830 AND cp.call_number = cn.id
1831 AND cp.opac_visible IS TRUE
1832 AND cs.opac_visible IS TRUE
1833 AND cl.opac_visible IS TRUE
1834 AND d.opac_visible IS TRUE
1835 AND br.active IS TRUE
1836 AND br.deleted IS FALSE
1837 AND cp.deleted IS FALSE
1838 AND cn.deleted IS FALSE
1839 AND ord.record = mrs.source
1852 $metabib_metarecord_source_map_table mrs,
1853 $metabib_record_descriptor ord,
1855 WHERE mrs.metarecord = s.metarecord
1856 AND ord.record = mrs.source
1857 AND br.id = mrs.source
1858 AND br.source = src.id
1859 AND src.transcendant IS TRUE
1867 ORDER BY 4 $sort_dir, 5
1874 $metabib_metarecord_source_map_table omrs,
1875 $metabib_record_descriptor ord
1876 WHERE omrs.metarecord = s.metarecord
1877 AND ord.record = omrs.source
1880 FROM $asset_call_number_table cn,
1881 $asset_copy_table cp,
1884 WHERE br.id = omrs.source
1885 AND cn.record = omrs.source
1886 AND br.deleted IS FALSE
1887 AND cn.deleted IS FALSE
1888 AND cp.call_number = cn.id
1889 AND ( cn.owning_lib = d.id
1890 OR ( cp.circ_lib = d.id
1891 AND cp.deleted IS FALSE
1899 FROM $asset_call_number_table cn
1900 WHERE cn.record = omrs.source
1901 AND cn.deleted IS FALSE
1907 $metabib_metarecord_source_map_table mrs,
1908 $metabib_record_descriptor ord,
1910 WHERE mrs.metarecord = s.metarecord
1911 AND br.id = mrs.source
1912 AND br.source = src.id
1913 AND src.transcendant IS TRUE
1929 ORDER BY 4 $sort_dir, 5
1934 $log->debug("Field Search SQL :: [$select]",DEBUG);
1936 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1939 @types, @forms, @vformats, @aud, @lang, @lit_form,
1940 @types, @forms, @vformats, @aud, @lang, @lit_form,
1941 # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1944 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1947 $max = 1 if (!@$recs);
1949 $max = $$_[1] if ($$_[1] > $max);
1952 my $count = scalar(@$recs);
1953 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1954 next unless ($$rec[0]);
1955 my ($mrid,$rank,$skip) = @$rec;
1956 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1961 __PACKAGE__->register_method(
1962 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1964 method => 'postfilter_search_multi_class_fts',
1969 __PACKAGE__->register_method(
1970 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1972 method => 'postfilter_search_multi_class_fts',
1978 __PACKAGE__->register_method(
1979 api_name => "open-ils.storage.metabib.multiclass.search_fts",
1981 method => 'postfilter_search_multi_class_fts',
1986 __PACKAGE__->register_method(
1987 api_name => "open-ils.storage.metabib.multiclass.search_fts.staff",
1989 method => 'postfilter_search_multi_class_fts',
1995 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1996 sub biblio_search_multi_class_fts {
2001 my $sort = $args{'sort'};
2002 my $sort_dir = $args{sort_dir} || 'DESC';
2003 my $ou = $args{org_unit};
2004 my $ou_type = $args{depth};
2005 my $limit = $args{limit} || 10;
2006 my $offset = $args{offset} || 0;
2007 my $pref_lang = $args{preferred_language} || 'eng';
2008 my $visibility_limit = $args{visibility_limit} || 5000;
2011 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2014 if (! scalar( keys %{$args{searches}} )) {
2015 die "No search arguments were passed to ".$self->api_name;
2018 my $outer_limit = 1000;
2020 my $limit_clause = '';
2021 my $offset_clause = '';
2023 $limit_clause = "LIMIT $outer_limit";
2024 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
2026 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
2027 my ($t_filter, $f_filter, $v_filter) = ('','','');
2028 my ($a_filter, $l_filter, $lf_filter) = ('','','');
2029 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
2030 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
2032 if ($args{available}) {
2033 $avail_filter = ' AND cp.status IN (0,7,12)';
2036 if (my $a = $args{audience}) {
2037 $a = [$a] if (!ref($a));
2040 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
2041 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
2044 if (my $l = $args{language}) {
2045 $l = [$l] if (!ref($l));
2048 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
2049 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
2052 if (my $f = $args{lit_form}) {
2053 $f = [$f] if (!ref($f));
2056 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2057 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2060 if (my $f = $args{item_form}) {
2061 $f = [$f] if (!ref($f));
2064 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2065 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2068 if (my $t = $args{item_type}) {
2069 $t = [$t] if (!ref($t));
2072 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2073 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2076 if (my $v = $args{vr_format}) {
2077 $v = [$v] if (!ref($v));
2080 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
2081 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
2084 # XXX legacy format and item type support
2085 if ($args{format}) {
2086 my ($t, $f) = split '-', $args{format};
2087 @types = split '', $t;
2088 @forms = split '', $f;
2090 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2091 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2095 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2096 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2101 my $descendants = defined($ou_type) ?
2102 "actor.org_unit_descendants($ou, $ou_type)" :
2103 "actor.org_unit_descendants($ou)";
2105 my $search_table_list = '';
2107 my $join_table_list = '';
2110 my $field_table = config::metabib_field->table;
2114 my $prev_search_group;
2115 my $curr_search_group;
2119 for my $search_group (sort keys %{$args{searches}}) {
2120 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2121 ($search_class,$search_field) = split /\|/, $search_group;
2122 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2124 if ($search_field) {
2125 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2126 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2131 $prev_search_group = $curr_search_group if ($curr_search_group);
2133 $curr_search_group = $search_group_name;
2135 my $class = $_cdbi->{$search_class};
2136 my $search_table = $class->table;
2138 my ($index_col) = $class->columns('FTS');
2139 $index_col ||= 'value';
2142 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2144 my $fts_where = $fts->sql_where_clause;
2145 my @fts_ranks = $fts->fts_rank;
2147 my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2148 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2149 my $first_word = lc(($fts->words)[0]).'%';
2151 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2152 my $rank = join(' + ', @fts_ranks);
2155 $bonus{'subject'} = [];
2156 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2158 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2160 $bonus{'series'} = [
2161 { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2162 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2165 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2168 push @{ $bonus{'title'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2169 push @{ $bonus{'author'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2170 push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2171 push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2172 push @{ $bonus{'series'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2175 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2176 $bonus_list ||= '1';
2178 push @bonus_lists, $bonus_list;
2179 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2181 #---------------------
2183 $search_table_list .= "$search_table $search_group_name, ";
2184 push @rank_list,$rank;
2185 $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2187 if ($metabib_field) {
2188 $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2189 $metabib_field = undef;
2192 if ($prev_search_group) {
2193 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2197 my $metabib_record_descriptor = metabib::record_descriptor->table;
2198 my $metabib_full_rec = metabib::full_rec->table;
2199 my $metabib_metarecord = metabib::metarecord->table;
2200 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2201 my $asset_call_number_table = asset::call_number->table;
2202 my $asset_copy_table = asset::copy->table;
2203 my $cs_table = config::copy_status->table;
2204 my $cl_table = asset::copy_location->table;
2205 my $br_table = biblio::record_entry->table;
2206 my $source_table = config::bib_source->table;
2209 my $bonuses = join (' * ', @bonus_lists);
2210 my $relevance = join (' + ', @rank_list);
2211 $relevance = "AVG( ($relevance) * ($bonuses) )";
2213 my $string_default_sort = 'zzzz';
2214 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2216 my $number_default_sort = '9999';
2217 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2219 my $rank = $relevance;
2220 if (lc($sort) eq 'pubdate') {
2223 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d{4}'),'$number_default_sort')::INT
2224 FROM $metabib_full_rec frp
2225 WHERE frp.record = b.id
2227 AND frp.subfield = 'c'
2231 } elsif (lc($sort) eq 'create_date') {
2233 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2235 } elsif (lc($sort) eq 'edit_date') {
2237 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2239 } elsif (lc($sort) eq 'title') {
2242 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2243 FROM $metabib_full_rec frt
2244 WHERE frt.record = b.id
2246 AND frt.subfield = 'a'
2250 } elsif (lc($sort) eq 'author') {
2253 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
2254 FROM $metabib_full_rec fra
2255 WHERE fra.record = b.id
2256 AND fra.tag LIKE '1%'
2257 AND fra.subfield = 'a'
2258 ORDER BY fra.tag::text::int
2263 push @bonus_values, @bonus_values;
2268 my $select = <<" SQL";
2273 FROM $search_table_list
2274 $metabib_record_descriptor rd,
2277 WHERE rd.record = b.id
2278 AND b.active IS TRUE
2279 AND b.deleted IS FALSE
2288 GROUP BY b.id, b.source
2289 ORDER BY 3 $sort_dir
2290 LIMIT $visibility_limit
2293 if ($self->api_name !~ /staff/o) {
2298 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2301 FROM $asset_call_number_table cn,
2302 $asset_copy_table cp,
2306 WHERE cn.record = s.id
2307 AND cp.status = cs.id
2308 AND cp.location = cl.id
2309 AND cp.call_number = cn.id
2310 AND cp.opac_visible IS TRUE
2311 AND cs.opac_visible IS TRUE
2312 AND cl.opac_visible IS TRUE
2313 AND d.opac_visible IS TRUE
2314 AND cp.deleted IS FALSE
2315 AND cn.deleted IS FALSE
2316 AND cp.circ_lib = d.id
2320 OR src.transcendant IS TRUE
2321 ORDER BY 3 $sort_dir
2328 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2331 FROM $asset_call_number_table cn,
2332 $asset_copy_table cp,
2334 WHERE cn.record = s.id
2335 AND cp.call_number = cn.id
2336 AND cn.deleted IS FALSE
2337 AND cp.circ_lib = d.id
2338 AND cp.deleted IS FALSE
2344 FROM $asset_call_number_table cn
2345 WHERE cn.record = s.id
2348 OR src.transcendant IS TRUE
2349 ORDER BY 3 $sort_dir
2354 $log->debug("Field Search SQL :: [$select]",DEBUG);
2356 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2358 @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2361 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2363 my $count = scalar(@$recs);
2364 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2365 next unless ($$rec[0]);
2366 my ($mrid,$rank) = @$rec;
2367 $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2372 __PACKAGE__->register_method(
2373 api_name => "open-ils.storage.biblio.multiclass.search_fts.record",
2375 method => 'biblio_search_multi_class_fts',
2380 __PACKAGE__->register_method(
2381 api_name => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2383 method => 'biblio_search_multi_class_fts',
2388 __PACKAGE__->register_method(
2389 api_name => "open-ils.storage.biblio.multiclass.search_fts",
2391 method => 'biblio_search_multi_class_fts',
2396 __PACKAGE__->register_method(
2397 api_name => "open-ils.storage.biblio.multiclass.search_fts.staff",
2399 method => 'biblio_search_multi_class_fts',
2407 my $default_preferred_language;
2408 my $default_preferred_language_weight;
2410 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2416 if (!$locale_map{COMPLETE}) {
2418 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2419 for my $locale ( @locales ) {
2420 $locale_map{lc($locale->code)} = $locale->marc_code;
2422 $locale_map{COMPLETE} = 1;
2426 my $config = OpenSRF::Utils::SettingsClient->new();
2428 if (!$default_preferred_language) {
2430 $default_preferred_language = $config->config_value(
2431 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2432 ) || $config->config_value(
2433 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2438 if (!$default_preferred_language_weight) {
2440 $default_preferred_language_weight = $config->config_value(
2441 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2442 ) || $config->config_value(
2443 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2447 # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2448 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2450 my $ou = $args{org_unit};
2451 my $limit = $args{limit} || 10;
2452 my $offset = $args{offset} || 0;
2455 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2458 if (! scalar( keys %{$args{searches}} )) {
2459 die "No search arguments were passed to ".$self->api_name;
2462 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2464 if (!defined($args{preferred_language})) {
2465 my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2466 $args{preferred_language} =
2467 $locale_map{ lc($ses_locale) } || 'eng';
2470 if (!defined($args{preferred_language_weight})) {
2471 $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2474 if ($args{available}) {
2475 @statuses = (0,7,12);
2478 if (my $s = $args{locations}) {
2479 $s = [$s] if (!ref($s));
2483 if (my $b = $args{between}) {
2484 if (ref($b) && @$b == 2) {
2489 if (my $s = $args{statuses}) {
2490 $s = [$s] if (!ref($s));
2494 if (my $a = $args{audience}) {
2495 $a = [$a] if (!ref($a));
2499 if (my $l = $args{language}) {
2500 $l = [$l] if (!ref($l));
2504 if (my $f = $args{lit_form}) {
2505 $f = [$f] if (!ref($f));
2509 if (my $f = $args{item_form}) {
2510 $f = [$f] if (!ref($f));
2514 if (my $t = $args{item_type}) {
2515 $t = [$t] if (!ref($t));
2519 if (my $b = $args{bib_level}) {
2520 $b = [$b] if (!ref($b));
2524 if (my $v = $args{vr_format}) {
2525 $v = [$v] if (!ref($v));
2529 # XXX legacy format and item type support
2530 if ($args{format}) {
2531 my ($t, $f) = split '-', $args{format};
2532 @types = split '', $t;
2533 @forms = split '', $f;
2536 my %stored_proc_search_args;
2537 for my $search_group (sort keys %{$args{searches}}) {
2538 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2539 my ($search_class,$search_field) = split /\|/, $search_group;
2540 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2542 if ($search_field) {
2543 unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2544 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2549 my $class = $_cdbi->{$search_class};
2550 my $search_table = $class->table;
2552 my ($index_col) = $class->columns('FTS');
2553 $index_col ||= 'value';
2556 my $fts = OpenILS::Application::Storage::FTS->compile(
2557 $search_class => $args{searches}{$search_group}{term},
2558 $search_group_name.'.value',
2559 "$search_group_name.$index_col"
2561 $fts->sql_where_clause; # this builds the ranks for us
2563 my @fts_ranks = $fts->fts_rank;
2564 my @fts_queries = $fts->fts_query;
2565 my @phrases = map { lc($_) } $fts->phrases;
2566 my @words = map { lc($_) } $fts->words;
2568 $stored_proc_search_args{$search_group} = {
2569 fts_rank => \@fts_ranks,
2570 fts_query => \@fts_queries,
2571 phrase => \@phrases,
2577 my $param_search_ou = $ou;
2578 my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2579 my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2580 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2581 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2582 my $param_audience = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud ) . '}$$';
2583 my $param_language = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang ) . '}$$';
2584 my $param_lit_form = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2585 my $param_types = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types ) . '}$$';
2586 my $param_forms = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms ) . '}$$';
2587 my $param_vformats = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2588 my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2589 my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2590 my $param_after = $args{after} ; $param_after = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2591 my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2592 my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2593 my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2594 my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2595 my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2596 my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2597 my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2598 my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2599 my $param_rel_limit = $args{core_limit}; $param_rel_limit ||= 'NULL';
2600 my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2601 my $param_skip_chk = $args{skip_check}; $param_skip_chk ||= 'NULL';
2603 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2605 FROM search.staged_fts(
2606 $param_search_ou\:\:INT,
2607 $param_depth\:\:INT,
2608 $param_searches\:\:TEXT,
2609 $param_statuses\:\:INT[],
2610 $param_locations\:\:INT[],
2611 $param_audience\:\:TEXT[],
2612 $param_language\:\:TEXT[],
2613 $param_lit_form\:\:TEXT[],
2614 $param_types\:\:TEXT[],
2615 $param_forms\:\:TEXT[],
2616 $param_vformats\:\:TEXT[],
2617 $param_bib_level\:\:TEXT[],
2618 $param_before\:\:TEXT,
2619 $param_after\:\:TEXT,
2620 $param_during\:\:TEXT,
2621 $param_between\:\:TEXT[],
2622 $param_pref_lang\:\:TEXT,
2623 $param_pref_lang_multiplier\:\:REAL,
2624 $param_sort\:\:TEXT,
2625 $param_sort_desc\:\:BOOL,
2626 $metarecord\:\:BOOL,
2628 $param_rel_limit\:\:INT,
2629 $param_chk_limit\:\:INT,
2630 $param_skip_chk\:\:INT
2636 my $recs = $sth->fetchall_arrayref({});
2637 my $summary_row = pop @$recs;
2639 my $total = $$summary_row{total};
2640 my $checked = $$summary_row{checked};
2641 my $visible = $$summary_row{visible};
2642 my $deleted = $$summary_row{deleted};
2643 my $excluded = $$summary_row{excluded};
2645 my $estimate = $visible;
2646 if ( $total > $checked && $checked ) {
2648 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2649 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2653 delete $$summary_row{id};
2654 delete $$summary_row{rel};
2655 delete $$summary_row{record};
2657 $client->respond( $summary_row );
2659 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2661 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2662 delete $$rec{checked};
2663 delete $$rec{visible};
2664 delete $$rec{excluded};
2665 delete $$rec{deleted};
2666 delete $$rec{total};
2667 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2669 $client->respond( $rec );
2673 __PACKAGE__->register_method(
2674 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
2676 method => 'staged_fts',
2681 __PACKAGE__->register_method(
2682 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2684 method => 'staged_fts',
2689 __PACKAGE__->register_method(
2690 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
2692 method => 'staged_fts',
2697 __PACKAGE__->register_method(
2698 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2700 method => 'staged_fts',
2706 sub FTS_paging_estimate {
2710 my $checked = shift;
2711 my $visible = shift;
2712 my $excluded = shift;
2713 my $deleted = shift;
2716 my $deleted_ratio = $deleted / $checked;
2717 my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2719 my $exclusion_ratio = $excluded / $checked;
2720 my $delete_adjusted_exclusion_ratio = $checked - $deleted ? $excluded / ($checked - $deleted) : 1;
2722 my $inclusion_ratio = $visible / $checked;
2723 my $delete_adjusted_inclusion_ratio = $checked - $deleted ? $visible / ($checked - $deleted) : 0;
2726 exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2727 inclusion => int($delete_adjusted_total * $inclusion_ratio),
2728 delete_adjusted_exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2729 delete_adjusted_inclusion => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2732 __PACKAGE__->register_method(
2733 api_name => "open-ils.storage.fts_paging_estimate",
2735 method => 'FTS_paging_estimate',
2741 Hash of estimation values based on four variant estimation strategies:
2742 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2743 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2744 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2745 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2748 Helper method used to determin the approximate number of
2749 hits for a search that spans multiple superpages. For
2750 sparse superpages, the inclusion estimate will likely be the
2751 best estimate. The exclusion strategy is the original, but
2752 inclusion is the default.
2755 { name => 'checked',
2756 desc => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2759 { name => 'visible',
2760 desc => 'Number of records visible to the search location on the current superpage.',
2763 { name => 'excluded',
2764 desc => 'Number of records excluded from the search location on the current superpage.',
2767 { name => 'deleted',
2768 desc => 'Number of deleted records on the current superpage.',
2772 desc => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2785 my $term = $$args{term};
2786 my $limit = $$args{max} || 1;
2787 my $min = $$args{min} || 1;
2788 my @classes = @{$$args{class}};
2790 $limit = $min if ($min > $limit);
2793 @classes = ( qw/ title author subject series keyword / );
2797 my $bre_table = biblio::record_entry->table;
2798 my $cn_table = asset::call_number->table;
2799 my $cp_table = asset::copy->table;
2801 for my $search_class ( @classes ) {
2803 my $class = $_cdbi->{$search_class};
2804 my $search_table = $class->table;
2806 my ($index_col) = $class->columns('FTS');
2807 $index_col ||= 'value';
2810 my $where = OpenILS::Application::Storage::FTS
2811 ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2815 SELECT COUNT(DISTINCT X.source)
2816 FROM (SELECT $search_class.source
2817 FROM $search_table $search_class
2818 JOIN $bre_table b ON (b.id = $search_class.source)
2823 HAVING COUNT(DISTINCT X.source) >= $min;
2826 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2827 $matches{$search_class} = $res ? $res->[0] : 0;
2832 __PACKAGE__->register_method(
2833 api_name => "open-ils.storage.search.xref",
2835 method => 'xref_count',
2839 # Takes an abstract query object and recursively turns it back into a string
2841 sub abstract_query2str {
2842 my ($self, $conn, $query) = @_;
2844 return QueryParser::Canonicalize::abstract_query2str_impl($query, 0, $OpenILS::Application::Storage::QParser);
2847 __PACKAGE__->register_method(
2848 api_name => "open-ils.storage.query_parser.abstract_query.canonicalize",
2850 method => "abstract_query2str",
2855 Abstract query parser object, with complete config data. For example input,
2856 see the 'abstract_query' part of the output of an API call like
2857 open-ils.search.biblio.multiclass.query, when called with the return_abstract
2861 return => { type => "string", desc => "String representation of abstract query object" }
2865 sub str2abstract_query {
2866 my ($self, $conn, $query, $qp_opts, $with_config) = @_;
2868 my %use_opts = ( # reasonable defaults? should these even be hardcoded here?
2870 superpage_size => 1000,
2871 core_limit => 25000,
2873 (ref $opts eq 'HASH' ? %$opts : ())
2878 # grab the query parser and initialize it
2879 my $parser = $OpenILS::Application::Storage::QParser;
2882 _initialize_parser($parser) unless $parser->initialization_complete;
2884 my $query = $parser->new(%use_opts)->parse;
2886 return $query->parse_tree->to_abstract_query(with_config => $with_config);
2889 __PACKAGE__->register_method(
2890 api_name => "open-ils.storage.query_parser.abstract_query.from_string",
2892 method => "str2abstract_query",
2896 {desc => "Query", type => "string"},
2897 {desc => q/Arguments for initializing QueryParser (optional)/,
2899 {desc => q/Flag enabling inclusion of QP config in returned object (optional, default false)/,
2902 return => { type => "object", desc => "abstract representation of query parser query" }
2906 my @available_statuses_cache;
2907 sub available_statuses {
2908 if (!scalar(@available_statuses_cache)) {
2909 @available_statuses_cache = map { $_->id } config::copy_status->search_where({is_available => 't'});
2911 return @available_statuses_cache;
2914 sub query_parser_fts {
2920 # grab the query parser and initialize it
2921 my $parser = $OpenILS::Application::Storage::QParser;
2924 _initialize_parser($parser) unless $parser->initialization_complete;
2926 # populate the locale/language map
2927 if (!$locale_map{COMPLETE}) {
2929 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2930 for my $locale ( @locales ) {
2931 $locale_map{lc($locale->code)} = $locale->marc_code;
2933 $locale_map{COMPLETE} = 1;
2937 # I hope we have a query!
2938 if (! $args{query} ) {
2939 die "No query was passed to ".$self->api_name;
2942 my $default_CD_modifiers = OpenSRF::Utils::SettingsClient->new->config_value(
2943 apps => 'open-ils.search' => app_settings => 'default_CD_modifiers'
2946 # Protect against empty / missing default_CD_modifiers setting
2947 if ($default_CD_modifiers and !ref($default_CD_modifiers)) {
2948 $args{query} = "$default_CD_modifiers $args{query}";
2951 my $simple_plan = $args{_simple_plan};
2952 # remove bad chunks of the %args hash
2953 for my $bad ( grep { /^_/ } keys(%args)) {
2954 delete($args{$bad});
2958 # parse the query and supply any query-level %arg-based defaults
2959 # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2960 my $query = $parser->new( %args )->parse;
2962 my $config = OpenSRF::Utils::SettingsClient->new();
2964 # set the locale-based default preferred location
2965 if (!$query->parse_tree->find_filter('preferred_language')) {
2966 $parser->default_preferred_language( $args{preferred_language} );
2968 if (!$parser->default_preferred_language) {
2969 my $ses_locale = $client->session ? $client->session->session_locale : '';
2970 $parser->default_preferred_language( $locale_map{ lc($ses_locale) } );
2973 if (!$parser->default_preferred_language) { # still nothing...
2974 my $tmp_dpl = $config->config_value(
2975 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2976 ) || $config->config_value(
2977 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2980 $parser->default_preferred_language( $tmp_dpl )
2985 # set the global default language multiplier
2986 if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2989 if ($tmp_dplw = $args{preferred_language_weight} || $args{preferred_language_multiplier} ) {
2990 $parser->default_preferred_language_multiplier($tmp_dplw);
2993 $tmp_dplw = $config->config_value(
2994 apps => 'open-ils.search' => app_settings => 'default_preferred_language_weight'
2995 ) || $config->config_value(
2996 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2999 $parser->default_preferred_language_multiplier( $tmp_dplw );
3003 # gather the site, if one is specified, defaulting to the in-query version
3004 my $ou = $args{org_unit};
3005 if (my ($filter) = $query->parse_tree->find_filter('site')) {
3006 $ou = $filter->args->[0] if (@{$filter->args});
3008 $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^(-)?\d+$/);
3010 # gather lasso, as with $ou
3011 my $lasso = $args{lasso};
3012 if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
3013 $lasso = $filter->args->[0] if (@{$filter->args});
3015 $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
3016 $lasso = -$lasso if ($lasso);
3019 # # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
3020 # # gather user lasso, as with $ou and lasso
3021 # my $mylasso = $args{my_lasso};
3022 # if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
3023 # $mylasso = $filter->args->[0] if (@{$filter->args});
3025 # $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
3028 # if we have a lasso, go with that, otherwise ... ou
3029 $ou = $lasso if ($lasso);
3031 # gather the preferred OU, if one is specified, as with $ou
3032 my $pref_ou = $args{pref_ou};
3033 $log->info("pref_ou = $pref_ou");
3034 if (my ($filter) = $query->parse_tree->find_filter('pref_ou')) {
3035 $pref_ou = $filter->args->[0] if (@{$filter->args});
3037 $pref_ou = actor::org_unit->search( { shortname => $pref_ou } )->next->id if ($pref_ou and $pref_ou !~ /^(-)?\d+$/);
3039 # get the default $ou if we have nothing
3040 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
3043 # 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
3044 # gather the depth, if one is specified, defaulting to the in-query version
3045 my $depth = $args{depth};
3046 if (my ($filter) = $query->parse_tree->find_filter('depth')) {
3047 $depth = $filter->args->[0] if (@{$filter->args});
3049 $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
3052 # gather the limit or default to 10
3053 my $limit = $args{check_limit};
3054 if (my ($filter) = $query->parse_tree->find_filter('limit')) {
3055 $limit = $filter->args->[0] if (@{$filter->args});
3057 if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
3058 $limit = $filter->args->[0] if (@{$filter->args});
3062 # gather the offset or default to 0
3063 my $offset = $args{skip_check} || $args{offset};
3064 if (my ($filter) = $query->parse_tree->find_filter('offset')) {
3065 $offset = $filter->args->[0] if (@{$filter->args});
3067 if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
3068 $offset = $filter->args->[0] if (@{$filter->args});
3072 # gather the estimation strategy or default to inclusion
3073 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
3074 if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
3075 $estimation_strategy = $filter->args->[0] if (@{$filter->args});
3079 # gather the estimation strategy or default to inclusion
3080 my $core_limit = $args{core_limit};
3081 if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
3082 $core_limit = $filter->args->[0] if (@{$filter->args});
3086 # gather statuses, and then forget those if we have an #available modifier
3088 if ($query->parse_tree->find_modifier('available')) {
3089 @statuses = available_statuses();
3090 } elsif (my ($filter) = $query->parse_tree->find_filter('statuses')) {
3091 @statuses = @{$filter->args} if (@{$filter->args});
3097 if (my ($filter) = $query->parse_tree->find_filter('locations')) {
3098 @location = @{$filter->args} if (@{$filter->args});
3101 # gather location_groups
3102 if (my ($filter) = $query->parse_tree->find_filter('location_groups')) {
3103 my @loc_groups = @{$filter->args} if (@{$filter->args});
3105 # collect the mapped locations and add them to the locations() filter
3108 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3109 my $maps = $cstore->request(
3110 'open-ils.cstore.direct.asset.copy_location_group_map.search.atomic',
3111 {lgroup => \@loc_groups})->gather(1);
3113 push(@location, $_->location) for @$maps;
3118 my $param_check = $limit || $query->superpage_size || 'NULL';
3119 my $param_offset = $offset || 'NULL';
3120 my $param_limit = $core_limit || 'NULL';
3122 my $sp = $query->superpage || 1;
3124 $param_offset = ($sp - 1) * $sp_size;
3127 my $param_search_ou = $ou;
3128 my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
3129 # my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
3130 my $param_core_query = $query->parse_tree->toSQL;
3131 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
3132 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
3133 my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
3134 my $deleted_search = ($query->parse_tree->find_modifier('deleted')) ? "'t'" : "'f'";
3135 my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
3136 my $param_pref_ou = $pref_ou || 'NULL';
3138 # my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3139 # SELECT * -- bib search: $args{query}
3140 # FROM search.query_parser_fts(
3141 # $param_search_ou\:\:INT,
3142 # $param_depth\:\:INT,
3143 # $param_core_query\:\:TEXT,
3144 # $param_statuses\:\:INT[],
3145 # $param_locations\:\:INT[],
3146 # $param_offset\:\:INT,
3147 # $param_check\:\:INT,
3148 # $param_limit\:\:INT,
3149 # $metarecord\:\:BOOL,
3151 # $deleted_search\:\:BOOL,
3152 # $param_pref_ou\:\:INT
3156 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3157 -- bib search: $args{query}
3163 my $recs = $sth->fetchall_arrayref({});
3164 my $summary_row = pop @$recs;
3166 my $total = $$summary_row{total};
3167 my $checked = $$summary_row{checked};
3168 my $visible = $$summary_row{visible};
3169 my $deleted = $$summary_row{deleted};
3170 my $excluded = $$summary_row{excluded};
3172 delete $$summary_row{id};
3173 delete $$summary_row{rel};
3174 delete $$summary_row{record};
3175 delete $$summary_row{badges};
3176 delete $$summary_row{popularity};
3178 if (defined($simple_plan)) {
3179 $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3181 $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3184 if ($args{return_query}) {
3185 $$summary_row{query_struct} = $query->parse_tree->to_abstract_query();
3186 $$summary_row{canonicalized_query} = QueryParser::Canonicalize::abstract_query2str_impl($$summary_row{query_struct}, 0, $parser);
3189 $client->respond( $summary_row );
3191 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $visible.",DEBUG);
3193 for my $rec (@$recs) {
3194 delete $$rec{checked};
3195 delete $$rec{visible};
3196 delete $$rec{excluded};
3197 delete $$rec{deleted};
3198 delete $$rec{total};
3199 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3201 $client->respond( $rec );
3205 __PACKAGE__->register_method(
3206 api_name => "open-ils.storage.query_parser_search",
3208 method => 'query_parser_fts',
3216 sub query_parser_fts_wrapper {
3221 $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3222 # grab the query parser and initialize it
3223 my $parser = $OpenILS::Application::Storage::QParser;
3226 _initialize_parser($parser) unless $parser->initialization_complete;
3228 $args{searches} ||= {};
3229 if (!scalar(keys(%{$args{searches}})) && !$args{query}) {
3230 die "No search arguments were passed to ".$self->api_name;
3233 $top_org ||= actor::org_unit->search( { parent_ou => undef } )->next;
3235 my $base_query = $args{query} || '';
3236 if (scalar(keys(%{$args{searches}}))) {
3237 $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3238 for my $sclass ( keys %{$args{searches}} ) {
3239 $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3240 $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3244 my $query = $base_query;
3245 $log->debug("Full base query: $base_query", DEBUG);
3247 $query = "$args{facets} $query" if ($args{facets});
3249 if (!$locale_map{COMPLETE}) {
3251 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3252 for my $locale ( @locales ) {
3253 $locale_map{lc($locale->code)} = $locale->marc_code;
3255 $locale_map{COMPLETE} = 1;
3259 my $base_plan = $parser->new( query => $base_query )->parse;
3261 $query = "preferred_language($args{preferred_language}) $query"
3262 if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3263 $query = "preferred_language_weight($args{preferred_language_weight}) $query"
3264 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'));
3268 if (!$base_plan->parse_tree->find_filter('badge_orgs')) {
3269 # supply a suitable badge_orgs filter unless user has
3270 # explicitly supplied one
3273 my @lg_id_list = @{$args{location_groups}} if (ref $args{location_groups});
3275 my ($lg_filter) = $base_plan->parse_tree->find_filter('location_groups');
3276 @lg_id_list = @{$lg_filter->args} if ($lg_filter && @{$lg_filter->args});
3280 for my $lg ( grep { /^\d+$/ } @lg_id_list ) {
3281 my $lg_obj = asset::copy_location_group->retrieve($lg);
3282 next unless $lg_obj;
3284 push(@borg_list, ''.$lg_obj->owner);
3286 $borgs = join(',', @borg_list) if @borg_list;
3290 my ($site_filter) = $base_plan->parse_tree->find_filter('site');
3291 if ($site_filter && @{$site_filter->args}) {
3292 $site = $top_org if ($site_filter->args->[0] eq '-');
3293 $site = $top_org if ($site_filter->args->[0] eq $top_org->shortname);
3294 $site = actor::org_unit->search( { shortname => $site_filter->args->[0] })->next unless ($site);
3295 } elsif ($args{org_unit}) {
3296 $site = $top_org if ($args{org_unit} eq '-');
3297 $site = $top_org if ($args{org_unit} eq $top_org->shortname);
3298 $site = actor::org_unit->search( { shortname => $args{org_unit} })->next unless ($site);
3304 $borgs = OpenSRF::AppSession->create( 'open-ils.cstore' )->request(
3305 'open-ils.cstore.json_query.atomic',
3306 { from => [ 'actor.org_unit_ancestors', $site->id ] }
3309 if (ref $borgs && @$borgs) {
3310 $borgs = join(',', map { $_->{'id'} } @$borgs);
3318 # gather the limit or default to 10
3319 my $limit = delete($args{check_limit}) || $base_plan->superpage_size;
3320 if (my ($filter) = $base_plan->parse_tree->find_filter('limit')) {
3321 $limit = $filter->args->[0] if (@{$filter->args});
3323 if (my ($filter) = $base_plan->parse_tree->find_filter('check_limit')) {
3324 $limit = $filter->args->[0] if (@{$filter->args});
3327 # gather the offset or default to 0
3328 my $offset = delete($args{skip_check}) || delete($args{offset}) || 0;
3329 if (my ($filter) = $base_plan->parse_tree->find_filter('offset')) {
3330 $offset = $filter->args->[0] if (@{$filter->args});
3332 if (my ($filter) = $base_plan->parse_tree->find_filter('skip_check')) {
3333 $offset = $filter->args->[0] if (@{$filter->args});
3337 $query = "check_limit($limit) $query" if (defined $limit);
3338 $query = "skip_check($offset) $query" if (defined $offset);
3339 $query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
3340 $query = "badge_orgs($borgs) $query" if ($borgs);
3342 # XXX All of the following, down to the 'return' is basically dead code. someone higher up should handle it
3343 $query = "site($args{org_unit}) $query" if ($args{org_unit});
3344 $query = "depth($args{depth}) $query" if (defined($args{depth}));
3345 $query = "sort($args{sort}) $query" if ($args{sort});
3346 $query = "core_limit($args{core_limit}) $query" if ($args{core_limit});
3347 # $query = "limit($args{limit}) $query" if ($args{limit});
3348 # $query = "skip_check($args{skip_check}) $query" if ($args{skip_check});
3349 $query = "superpage($args{superpage}) $query" if ($args{superpage});
3350 $query = "offset($args{offset}) $query" if ($args{offset});
3351 $query = "#metarecord $query" if ($self->api_name =~ /metabib/);
3352 $query = "from_metarecord($args{from_metarecord}) $query" if ($args{from_metarecord});
3353 $query = "#available $query" if ($args{available});
3354 $query = "#descending $query" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3355 $query = "#staff $query" if ($self->api_name =~ /staff/);
3356 $query = "before($args{before}) $query" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3357 $query = "after($args{after}) $query" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3358 $query = "during($args{during}) $query" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3359 $query = "between($args{between}[0],$args{between}[1]) $query"
3360 if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3363 my (@between,@statuses,@locations,@location_groups,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3365 # XXX legacy format and item type support
3366 if ($args{format}) {
3367 my ($t, $f) = split '-', $args{format};
3368 $args{item_type} = [ split '', $t ];
3369 $args{item_form} = [ split '', $f ];
3372 for my $filter ( qw/locations location_groups statuses audience language lit_form item_form item_type bib_level vr_format badges/ ) {
3373 if (my $s = $args{$filter}) {
3374 $s = [$s] if (!ref($s));
3376 my @filter_list = @$s;
3378 next if (@filter_list == 0);
3380 my $filter_string = join ',', @filter_list;
3381 $query = "$query $filter($filter_string)";
3385 $log->debug("Full QueryParser query: $query", DEBUG);
3387 return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan, return_query => $args{return_query} );
3389 __PACKAGE__->register_method(
3390 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
3392 method => 'query_parser_fts_wrapper',
3397 __PACKAGE__->register_method(
3398 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3400 method => 'query_parser_fts_wrapper',
3405 __PACKAGE__->register_method(
3406 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
3408 method => 'query_parser_fts_wrapper',
3413 __PACKAGE__->register_method(
3414 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3416 method => 'query_parser_fts_wrapper',