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 OpenSRF::Utils::Cache;
9 use OpenSRF::Utils::JSON;
11 use Digest::MD5 qw/md5_hex/;
13 use OpenILS::Application::Storage::QueryParser;
15 my $log = 'OpenSRF::Utils::Logger';
19 sub _initialize_parser {
22 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
24 config_record_attr_index_norm_map =>
26 'open-ils.cstore.direct.config.record_attr_index_norm_map.search.atomic',
27 { id => { "!=" => undef } },
28 { flesh => 1, flesh_fields => { crainm => [qw/norm/] }, order_by => [{ class => "crainm", field => "pos" }] }
30 search_relevance_adjustment =>
32 'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
33 { id => { "!=" => undef } }
35 config_metabib_field =>
37 'open-ils.cstore.direct.config.metabib_field.search.atomic',
38 { id => { "!=" => undef } }
40 config_metabib_search_alias =>
42 'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
43 { alias => { "!=" => undef } }
45 config_metabib_field_index_norm_map =>
47 'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
48 { id => { "!=" => undef } },
49 { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
51 config_record_attr_definition =>
53 'open-ils.cstore.direct.config.record_attr_definition.search.atomic',
54 { name => { "!=" => undef } }
56 config_metabib_class_ts_map =>
58 'open-ils.cstore.direct.config.metabib_class_ts_map.search.atomic',
61 config_metabib_field_ts_map =>
63 'open-ils.cstore.direct.config.metabib_field_ts_map.search.atomic',
66 config_metabib_class =>
68 'open-ils.cstore.direct.config.metabib_class.search.atomic',
69 { name => { "!=" => undef } }
74 die("Cannot initialize $parser!") unless ($parser->initialization_complete);
77 sub ordered_records_from_metarecord {
85 my (@types,@forms,@blvl);
88 my ($t, $f, $b) = split '-', $formats;
89 @types = split '', $t;
90 @forms = split '', $f;
96 "actor.org_unit_descendants($org, $depth)" :
97 "actor.org_unit_descendants($org)" ;
100 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';
101 $copies_visible = '' if ($self->api_name =~ /staff/o);
103 my $sm_table = metabib::metarecord_source_map->table;
104 my $rd_table = metabib::record_descriptor->table;
105 my $fr_table = metabib::full_rec->table;
106 my $cn_table = asset::call_number->table;
107 my $cl_table = asset::copy_location->table;
108 my $cp_table = asset::copy->table;
109 my $cs_table = config::copy_status->table;
110 my $src_table = config::bib_source->table;
111 my $out_table = actor::org_unit_type->table;
112 my $br_table = biblio::record_entry->table;
119 FIRST(COALESCE(LTRIM(SUBSTR( value, COALESCE(SUBSTRING(ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'zzzzzzzz')) AS title
131 if ($copies_visible) {
137 WHERE rd.record = sm.source
138 AND fr.record = sm.source
139 AND br.id = sm.source
140 AND sm.metarecord = ?
141 AND (EXISTS ((SELECT 1
143 JOIN $cn_table cn ON (cp.call_number = cn.id)
144 JOIN $cs_table cs ON (cp.status = cs.id)
145 JOIN $cl_table cl ON (cp.location = cl.id)
146 JOIN $descendants d ON (cp.circ_lib = d.id)
147 WHERE cn.record = sm.source
153 WHERE src.id = br.source
154 AND src.transcendant IS TRUE))
161 JOIN $br_table br ON (sm.source = br.id)
162 JOIN $fr_table fr ON (fr.record = br.id)
163 JOIN $rd_table rd ON (rd.record = br.id)
164 WHERE sm.metarecord = ?
170 WHERE cn.record = br.id
171 AND cn.deleted = FALSE
172 AND cp.deleted = FALSE
173 AND cp.circ_lib = d.id
174 AND cn.id = cp.call_number
180 WHERE cn.record = br.id
181 AND cn.deleted = FALSE
182 AND cp.deleted = FALSE
183 AND cn.id = cp.call_number
189 WHERE src.id = br.source
190 AND src.transcendant IS TRUE))
196 $sql .= ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
200 $sql .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
204 $sql .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
214 GROUP BY record, item_type, item_form, quality
217 WHEN item_type IS NULL -- default
219 WHEN item_type = '' -- default
221 WHEN item_type IN ('a','t') -- books
223 WHEN item_type = 'g' -- movies
225 WHEN item_type IN ('i','j') -- sound recordings
227 WHEN item_type = 'm' -- software
229 WHEN item_type = 'k' -- images
231 WHEN item_type IN ('e','f') -- maps
233 WHEN item_type IN ('o','p') -- mixed
235 WHEN item_type IN ('c','d') -- music
237 WHEN item_type = 'r' -- 3d
244 my $ids = metabib::metarecord_source_map->db_Main->selectcol_arrayref($sql, {}, "$mr", @types, @forms, @blvl);
245 return $ids if ($self->api_name =~ /atomic$/o);
247 $client->respond( $_ ) for ( @$ids );
251 __PACKAGE__->register_method(
252 api_name => 'open-ils.storage.ordered.metabib.metarecord.records',
253 method => 'ordered_records_from_metarecord',
257 __PACKAGE__->register_method(
258 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff',
259 method => 'ordered_records_from_metarecord',
264 __PACKAGE__->register_method(
265 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.atomic',
266 method => 'ordered_records_from_metarecord',
270 __PACKAGE__->register_method(
271 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic',
272 method => 'ordered_records_from_metarecord',
277 # XXX: this subroutine and its two registered methods are marked for
278 # deprecation, as they do not work properly in 2.x (these tags are no longer
279 # normalized in mfr) and are not in known use
283 my $isxn = lc(shift());
287 $isxn =~ s/-//o if ($self->api_name =~ /isbn/o);
289 my $tag = ($self->api_name =~ /isbn/o) ? "'020' OR f.tag = '024'" : "'022'";
291 my $fr_table = metabib::full_rec->table;
292 my $bib_table = biblio::record_entry->table;
295 SELECT DISTINCT f.record
297 JOIN $bib_table b ON (b.id = f.record)
300 AND b.deleted IS FALSE
303 my $list = metabib::full_rec->db_Main->selectcol_arrayref($sql, {}, "$isxn%");
304 $client->respond($_) for (@$list);
307 __PACKAGE__->register_method(
308 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
309 method => 'isxn_search',
313 __PACKAGE__->register_method(
314 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.issn',
315 method => 'isxn_search',
320 sub metarecord_copy_count {
326 my $sm_table = metabib::metarecord_source_map->table;
327 my $rd_table = metabib::record_descriptor->table;
328 my $cn_table = asset::call_number->table;
329 my $cp_table = asset::copy->table;
330 my $br_table = biblio::record_entry->table;
331 my $src_table = config::bib_source->table;
332 my $cl_table = asset::copy_location->table;
333 my $cs_table = config::copy_status->table;
334 my $out_table = actor::org_unit_type->table;
336 my $descendants = "actor.org_unit_descendants(u.id)";
337 my $ancestors = "actor.org_unit_ancestors(?) u JOIN $out_table t ON (u.ou_type = t.id)";
339 if ($args{org_unit} < 0) {
340 $args{org_unit} *= -1;
341 $ancestors = "(select org_unit as id from actor.org_lasso_map where lasso = ?) u CROSS JOIN (SELECT -1 AS depth) t";
344 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';
345 $copies_visible = '' if ($self->api_name =~ /staff/o);
347 my (@types,@forms,@blvl);
348 my ($t_filter, $f_filter, $b_filter) = ('','','');
351 my ($t, $f, $b) = split '-', $args{format};
352 @types = split '', $t;
353 @forms = split '', $f;
354 @blvl = split '', $b;
357 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
361 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
365 $b_filter .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
375 JOIN $cn_table cn ON (cn.record = r.source)
376 JOIN $rd_table rd ON (cn.record = rd.record)
377 JOIN $cp_table cp ON (cn.id = cp.call_number)
378 JOIN $cs_table cs ON (cp.status = cs.id)
379 JOIN $cl_table cl ON (cp.location = cl.id)
380 JOIN $descendants a ON (cp.circ_lib = a.id)
381 WHERE r.metarecord = ?
382 AND cn.deleted IS FALSE
383 AND cp.deleted IS FALSE
393 JOIN $cn_table cn ON (cn.record = r.source)
394 JOIN $rd_table rd ON (cn.record = rd.record)
395 JOIN $cp_table cp ON (cn.id = cp.call_number)
396 JOIN $cs_table cs ON (cp.status = cs.id)
397 JOIN $cl_table cl ON (cp.location = cl.id)
398 JOIN $descendants a ON (cp.circ_lib = a.id)
399 WHERE r.metarecord = ?
400 AND cp.status IN (0,7,12)
401 AND cn.deleted IS FALSE
402 AND cp.deleted IS FALSE
412 JOIN $cn_table cn ON (cn.record = r.source)
413 JOIN $rd_table rd ON (cn.record = rd.record)
414 JOIN $cp_table cp ON (cn.id = cp.call_number)
415 JOIN $cs_table cs ON (cp.status = cs.id)
416 JOIN $cl_table cl ON (cp.location = cl.id)
417 WHERE r.metarecord = ?
418 AND cn.deleted IS FALSE
419 AND cp.deleted IS FALSE
420 AND cp.opac_visible IS TRUE
421 AND cs.opac_visible IS TRUE
422 AND cl.opac_visible IS TRUE
431 JOIN $br_table br ON (br.id = r.source)
432 JOIN $src_table src ON (src.id = br.source)
433 WHERE r.metarecord = ?
434 AND src.transcendant IS TRUE
442 my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
443 $sth->execute( ''.$args{metarecord},
447 ''.$args{metarecord},
451 ''.$args{metarecord},
455 ''.$args{metarecord},
459 while ( my $row = $sth->fetchrow_hashref ) {
460 $client->respond( $row );
464 __PACKAGE__->register_method(
465 api_name => 'open-ils.storage.metabib.metarecord.copy_count',
466 method => 'metarecord_copy_count',
471 __PACKAGE__->register_method(
472 api_name => 'open-ils.storage.metabib.metarecord.copy_count.staff',
473 method => 'metarecord_copy_count',
479 sub biblio_multi_search_full_rec {
484 my $class_join = $args{class_join} || 'AND';
485 my $limit = $args{limit} || 100;
486 my $offset = $args{offset} || 0;
487 my $sort = $args{'sort'};
488 my $sort_dir = $args{sort_dir} || 'DESC';
493 for my $arg (@{ $args{searches} }) {
494 my $term = $$arg{term};
495 my $limiters = $$arg{restrict};
497 my ($index_col) = metabib::full_rec->columns('FTS');
498 $index_col ||= 'value';
499 my $search_table = metabib::full_rec->table;
501 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
503 my $fts_where = $fts->sql_where_clause();
504 my @fts_ranks = $fts->fts_rank;
506 my $rank = join(' + ', @fts_ranks);
509 for my $limit (@$limiters) {
510 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
511 # MARC control field; mfr.subfield is NULL
512 push @wheres, "( tag = ? AND $fts_where )";
513 push @binds, $$limit{tag};
514 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
516 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
517 push @binds, $$limit{tag}, $$limit{subfield};
518 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
521 my $where = join(' OR ', @wheres);
523 push @selects, "SELECT record, AVG($rank) as sum FROM $search_table WHERE $where GROUP BY record";
527 my $descendants = defined($args{depth}) ?
528 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
529 "actor.org_unit_descendants($args{org_unit})" ;
532 my $metabib_record_descriptor = metabib::record_descriptor->table;
533 my $metabib_full_rec = metabib::full_rec->table;
534 my $asset_call_number_table = asset::call_number->table;
535 my $asset_copy_table = asset::copy->table;
536 my $cs_table = config::copy_status->table;
537 my $cl_table = asset::copy_location->table;
538 my $br_table = biblio::record_entry->table;
540 my $cj = 'HAVING COUNT(x.record) = ' . scalar(@selects) if ($class_join eq 'AND');
542 '(SELECT x.record, sum(x.sum) FROM (('.
543 join(') UNION ALL (', @selects).
544 ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
546 my $has_vols = 'AND cn.owning_lib = d.id';
547 my $has_copies = 'AND cp.call_number = cn.id';
548 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';
550 if ($self->api_name =~ /staff/o) {
551 $copies_visible = '';
552 $has_copies = '' if ($ou_type == 0);
553 $has_vols = '' if ($ou_type == 0);
556 my ($t_filter, $f_filter) = ('','');
557 my ($a_filter, $l_filter, $lf_filter) = ('','','');
559 if (my $a = $args{audience}) {
560 $a = [$a] if (!ref($a));
563 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
567 if (my $l = $args{language}) {
568 $l = [$l] if (!ref($l));
571 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
575 if (my $f = $args{lit_form}) {
576 $f = [$f] if (!ref($f));
579 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
580 push @binds, @lit_form;
583 if (my $f = $args{item_form}) {
584 $f = [$f] if (!ref($f));
587 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
591 if (my $t = $args{item_type}) {
592 $t = [$t] if (!ref($t));
595 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
601 my ($t, $f) = split '-', $args{format};
602 my @types = split '', $t;
603 my @forms = split '', $f;
605 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
609 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
611 push @binds, @types, @forms;
614 my $relevance = 'sum(f.sum)';
615 $relevance = 1 if (!$copies_visible);
617 my $rank = $relevance;
618 if (lc($sort) eq 'pubdate') {
621 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'9999')::INT
622 FROM $metabib_full_rec frp
623 WHERE frp.record = f.record
625 AND frp.subfield = 'c'
629 } elsif (lc($sort) eq 'create_date') {
631 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
633 } elsif (lc($sort) eq 'edit_date') {
635 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
637 } elsif (lc($sort) eq 'title') {
640 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'zzzzzzzz')
641 FROM $metabib_full_rec frt
642 WHERE frt.record = f.record
644 AND frt.subfield = 'a'
648 } elsif (lc($sort) eq 'author') {
651 SELECT COALESCE(LTRIM(fra.value),'zzzzzzzz')
652 FROM $metabib_full_rec fra
653 WHERE fra.record = f.record
654 AND fra.tag LIKE '1%'
655 AND fra.subfield = 'a'
656 ORDER BY fra.tag::text::int
665 if ($copies_visible) {
667 SELECT f.record, $relevance, count(DISTINCT cp.id), $rank
668 FROM $search_table f,
669 $asset_call_number_table cn,
670 $asset_copy_table cp,
674 $metabib_record_descriptor rd,
676 WHERE br.id = f.record
677 AND cn.record = f.record
678 AND rd.record = f.record
679 AND cp.status = cs.id
680 AND cp.location = cl.id
681 AND br.deleted IS FALSE
682 AND cn.deleted IS FALSE
683 AND cp.deleted IS FALSE
692 GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
693 ORDER BY 4 $sort_dir,3 DESC
697 SELECT f.record, 1, 1, $rank
698 FROM $search_table f,
700 $metabib_record_descriptor rd
701 WHERE br.id = f.record
702 AND rd.record = f.record
703 AND br.deleted IS FALSE
715 $log->debug("Search SQL :: [$select]",DEBUG);
717 my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
718 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
721 $max = 1 if (!@$recs);
723 $max = $$_[1] if ($$_[1] > $max);
727 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
728 next unless ($$rec[0]);
729 my ($rid,$rank,$junk,$skip) = @$rec;
730 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
734 __PACKAGE__->register_method(
735 api_name => 'open-ils.storage.biblio.full_rec.multi_search',
736 method => 'biblio_multi_search_full_rec',
741 __PACKAGE__->register_method(
742 api_name => 'open-ils.storage.biblio.full_rec.multi_search.staff',
743 method => 'biblio_multi_search_full_rec',
749 sub search_full_rec {
755 my $term = $args{term};
756 my $limiters = $args{restrict};
758 my ($index_col) = metabib::full_rec->columns('FTS');
759 $index_col ||= 'value';
760 my $search_table = metabib::full_rec->table;
762 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
764 my $fts_where = $fts->sql_where_clause();
765 my @fts_ranks = $fts->fts_rank;
767 my $rank = join(' + ', @fts_ranks);
771 for my $limit (@$limiters) {
772 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
773 # MARC control field; mfr.subfield is NULL
774 push @wheres, "( tag = ? AND $fts_where )";
775 push @binds, $$limit{tag};
776 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
778 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
779 push @binds, $$limit{tag}, $$limit{subfield};
780 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
783 my $where = join(' OR ', @wheres);
785 my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
787 $log->debug("Search SQL :: [$select]",DEBUG);
789 my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
790 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
792 $client->respond($_) for (@$recs);
795 __PACKAGE__->register_method(
796 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
797 method => 'search_full_rec',
802 __PACKAGE__->register_method(
803 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
804 method => 'search_full_rec',
811 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
812 sub search_class_fts {
817 my $term = $args{term};
818 my $ou = $args{org_unit};
819 my $ou_type = $args{depth};
820 my $limit = $args{limit};
821 my $offset = $args{offset};
823 my $limit_clause = '';
824 my $offset_clause = '';
826 $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
827 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
830 my ($t_filter, $f_filter) = ('','');
833 my ($t, $f) = split '-', $args{format};
834 @types = split '', $t;
835 @forms = split '', $f;
837 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
841 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
847 my $descendants = defined($ou_type) ?
848 "actor.org_unit_descendants($ou, $ou_type)" :
849 "actor.org_unit_descendants($ou)";
851 my $class = $self->{cdbi};
852 my $search_table = $class->table;
854 my $metabib_record_descriptor = metabib::record_descriptor->table;
855 my $metabib_metarecord = metabib::metarecord->table;
856 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
857 my $asset_call_number_table = asset::call_number->table;
858 my $asset_copy_table = asset::copy->table;
859 my $cs_table = config::copy_status->table;
860 my $cl_table = asset::copy_location->table;
862 my ($index_col) = $class->columns('FTS');
863 $index_col ||= 'value';
865 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
866 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
868 my $fts_where = $fts->sql_where_clause;
869 my @fts_ranks = $fts->fts_rank;
871 my $rank = join(' + ', @fts_ranks);
873 my $has_vols = 'AND cn.owning_lib = d.id';
874 my $has_copies = 'AND cp.call_number = cn.id';
875 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';
877 my $visible_count = ', count(DISTINCT cp.id)';
878 my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
880 if ($self->api_name =~ /staff/o) {
881 $copies_visible = '';
882 $visible_count_test = '';
883 $has_copies = '' if ($ou_type == 0);
884 $has_vols = '' if ($ou_type == 0);
887 my $rank_calc = <<" RANK";
889 * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
890 * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
891 * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
892 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
895 $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
897 if ($copies_visible) {
899 SELECT m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
900 FROM $search_table f,
901 $metabib_metarecord_source_map_table m,
902 $asset_call_number_table cn,
903 $asset_copy_table cp,
906 $metabib_record_descriptor rd,
909 AND m.source = f.source
910 AND cn.record = m.source
911 AND rd.record = m.source
912 AND cp.status = cs.id
913 AND cp.location = cl.id
919 GROUP BY 1 $visible_count_test
921 $limit_clause $offset_clause
925 SELECT m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
926 FROM $search_table f,
927 $metabib_metarecord_source_map_table m,
928 $metabib_record_descriptor rd
930 AND m.source = f.source
931 AND rd.record = m.source
936 $limit_clause $offset_clause
940 $log->debug("Field Search SQL :: [$select]",DEBUG);
942 my $SQLstring = join('%',$fts->words);
943 my $REstring = join('\\s+',$fts->words);
944 my $first_word = ($fts->words)[0].'%';
945 my $recs = ($self->api_name =~ /unordered/o) ?
946 $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
947 $class->db_Main->selectall_arrayref($select, {},
948 '%'.lc($SQLstring).'%', # phrase order match
949 lc($first_word), # first word match
950 '^\\s*'.lc($REstring).'\\s*/?\s*$', # full exact match
954 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
956 $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
960 for my $class ( qw/title author subject keyword series identifier/ ) {
961 __PACKAGE__->register_method(
962 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord",
963 method => 'search_class_fts',
966 cdbi => "metabib::${class}_field_entry",
969 __PACKAGE__->register_method(
970 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
971 method => 'search_class_fts',
974 cdbi => "metabib::${class}_field_entry",
977 __PACKAGE__->register_method(
978 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
979 method => 'search_class_fts',
982 cdbi => "metabib::${class}_field_entry",
985 __PACKAGE__->register_method(
986 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
987 method => 'search_class_fts',
990 cdbi => "metabib::${class}_field_entry",
995 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
996 sub search_class_fts_count {
1001 my $term = $args{term};
1002 my $ou = $args{org_unit};
1003 my $ou_type = $args{depth};
1004 my $limit = $args{limit} || 100;
1005 my $offset = $args{offset} || 0;
1007 my $descendants = defined($ou_type) ?
1008 "actor.org_unit_descendants($ou, $ou_type)" :
1009 "actor.org_unit_descendants($ou)";
1012 my ($t_filter, $f_filter) = ('','');
1014 if ($args{format}) {
1015 my ($t, $f) = split '-', $args{format};
1016 @types = split '', $t;
1017 @forms = split '', $f;
1019 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1023 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1028 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
1030 my $class = $self->{cdbi};
1031 my $search_table = $class->table;
1033 my $metabib_record_descriptor = metabib::record_descriptor->table;
1034 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1035 my $asset_call_number_table = asset::call_number->table;
1036 my $asset_copy_table = asset::copy->table;
1037 my $cs_table = config::copy_status->table;
1038 my $cl_table = asset::copy_location->table;
1040 my ($index_col) = $class->columns('FTS');
1041 $index_col ||= 'value';
1043 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
1045 my $fts_where = $fts->sql_where_clause;
1047 my $has_vols = 'AND cn.owning_lib = d.id';
1048 my $has_copies = 'AND cp.call_number = cn.id';
1049 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';
1050 if ($self->api_name =~ /staff/o) {
1051 $copies_visible = '';
1052 $has_vols = '' if ($ou_type == 0);
1053 $has_copies = '' if ($ou_type == 0);
1056 # XXX test an "EXISTS version of descendant checking...
1058 if ($copies_visible) {
1060 SELECT count(distinct m.metarecord)
1061 FROM $search_table f,
1062 $metabib_metarecord_source_map_table m,
1063 $metabib_metarecord_source_map_table mr,
1064 $asset_call_number_table cn,
1065 $asset_copy_table cp,
1068 $metabib_record_descriptor rd,
1071 AND mr.source = f.source
1072 AND mr.metarecord = m.metarecord
1073 AND cn.record = m.source
1074 AND rd.record = m.source
1075 AND cp.status = cs.id
1076 AND cp.location = cl.id
1085 SELECT count(distinct m.metarecord)
1086 FROM $search_table f,
1087 $metabib_metarecord_source_map_table m,
1088 $metabib_metarecord_source_map_table mr,
1089 $metabib_record_descriptor rd
1091 AND mr.source = f.source
1092 AND mr.metarecord = m.metarecord
1093 AND rd.record = m.source
1099 $log->debug("Field Search Count SQL :: [$select]",DEBUG);
1101 my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1103 $log->debug("Count Search yielded $recs results.",DEBUG);
1108 for my $class ( qw/title author subject keyword series identifier/ ) {
1109 __PACKAGE__->register_method(
1110 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
1111 method => 'search_class_fts_count',
1114 cdbi => "metabib::${class}_field_entry",
1117 __PACKAGE__->register_method(
1118 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
1119 method => 'search_class_fts_count',
1122 cdbi => "metabib::${class}_field_entry",
1128 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1129 sub postfilter_search_class_fts {
1134 my $term = $args{term};
1135 my $sort = $args{'sort'};
1136 my $sort_dir = $args{sort_dir} || 'DESC';
1137 my $ou = $args{org_unit};
1138 my $ou_type = $args{depth};
1139 my $limit = $args{limit} || 10;
1140 my $visibility_limit = $args{visibility_limit} || 5000;
1141 my $offset = $args{offset} || 0;
1143 my $outer_limit = 1000;
1145 my $limit_clause = '';
1146 my $offset_clause = '';
1148 $limit_clause = "LIMIT $outer_limit";
1149 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1151 my (@types,@forms,@lang,@aud,@lit_form);
1152 my ($t_filter, $f_filter) = ('','');
1153 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1154 my ($ot_filter, $of_filter) = ('','');
1155 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1157 if (my $a = $args{audience}) {
1158 $a = [$a] if (!ref($a));
1161 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1162 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1165 if (my $l = $args{language}) {
1166 $l = [$l] if (!ref($l));
1169 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1170 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1173 if (my $f = $args{lit_form}) {
1174 $f = [$f] if (!ref($f));
1177 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1178 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1181 if ($args{format}) {
1182 my ($t, $f) = split '-', $args{format};
1183 @types = split '', $t;
1184 @forms = split '', $f;
1186 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1187 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1191 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1192 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1197 my $descendants = defined($ou_type) ?
1198 "actor.org_unit_descendants($ou, $ou_type)" :
1199 "actor.org_unit_descendants($ou)";
1201 my $class = $self->{cdbi};
1202 my $search_table = $class->table;
1204 my $metabib_full_rec = metabib::full_rec->table;
1205 my $metabib_record_descriptor = metabib::record_descriptor->table;
1206 my $metabib_metarecord = metabib::metarecord->table;
1207 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1208 my $asset_call_number_table = asset::call_number->table;
1209 my $asset_copy_table = asset::copy->table;
1210 my $cs_table = config::copy_status->table;
1211 my $cl_table = asset::copy_location->table;
1212 my $br_table = biblio::record_entry->table;
1214 my ($index_col) = $class->columns('FTS');
1215 $index_col ||= 'value';
1217 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1219 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1221 my $SQLstring = join('%',map { lc($_) } $fts->words);
1222 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1223 my $first_word = lc(($fts->words)[0]).'%';
1225 my $fts_where = $fts->sql_where_clause;
1226 my @fts_ranks = $fts->fts_rank;
1229 $bonus{'metabib::identifier_field_entry'} =
1230 $bonus{'metabib::keyword_field_entry'} = [
1231 { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring }
1234 $bonus{'metabib::title_field_entry'} =
1235 $bonus{'metabib::series_field_entry'} = [
1236 { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1237 { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1238 @{ $bonus{'metabib::keyword_field_entry'} }
1241 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1242 $bonus_list ||= '1';
1244 my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1246 my $relevance = join(' + ', @fts_ranks);
1247 $relevance = <<" RANK";
1248 (SUM( ( $relevance ) * ( $bonus_list ) )/COUNT(m.source))
1251 my $string_default_sort = 'zzzz';
1252 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1254 my $number_default_sort = '9999';
1255 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1257 my $rank = $relevance;
1258 if (lc($sort) eq 'pubdate') {
1261 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1262 FROM $metabib_full_rec frp
1263 WHERE frp.record = mr.master_record
1265 AND frp.subfield = 'c'
1269 } elsif (lc($sort) eq 'create_date') {
1271 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1273 } elsif (lc($sort) eq 'edit_date') {
1275 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1277 } elsif (lc($sort) eq 'title') {
1280 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1281 FROM $metabib_full_rec frt
1282 WHERE frt.record = mr.master_record
1284 AND frt.subfield = 'a'
1288 } elsif (lc($sort) eq 'author') {
1291 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1292 FROM $metabib_full_rec fra
1293 WHERE fra.record = mr.master_record
1294 AND fra.tag LIKE '1%'
1295 AND fra.subfield = 'a'
1296 ORDER BY fra.tag::text::int
1304 my $select = <<" SQL";
1305 SELECT m.metarecord,
1307 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1309 FROM $search_table f,
1310 $metabib_metarecord_source_map_table m,
1311 $metabib_metarecord_source_map_table smrs,
1312 $metabib_metarecord mr,
1313 $metabib_record_descriptor rd
1315 AND smrs.metarecord = mr.id
1316 AND m.source = f.source
1317 AND m.metarecord = mr.id
1318 AND rd.record = smrs.source
1324 GROUP BY m.metarecord
1325 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1326 LIMIT $visibility_limit
1333 FROM $asset_call_number_table cn,
1334 $metabib_metarecord_source_map_table mrs,
1335 $asset_copy_table cp,
1340 $metabib_record_descriptor ord,
1342 WHERE mrs.metarecord = s.metarecord
1343 AND br.id = mrs.source
1344 AND cn.record = mrs.source
1345 AND cp.status = cs.id
1346 AND cp.location = cl.id
1347 AND cn.owning_lib = d.id
1348 AND cp.call_number = cn.id
1349 AND cp.opac_visible IS TRUE
1350 AND cs.opac_visible IS TRUE
1351 AND cl.opac_visible IS TRUE
1352 AND d.opac_visible IS TRUE
1353 AND br.active IS TRUE
1354 AND br.deleted IS FALSE
1355 AND ord.record = mrs.source
1361 ORDER BY 4 $sort_dir
1363 } elsif ($self->api_name !~ /staff/o) {
1370 FROM $asset_call_number_table cn,
1371 $metabib_metarecord_source_map_table mrs,
1372 $asset_copy_table cp,
1377 $metabib_record_descriptor ord
1379 WHERE mrs.metarecord = s.metarecord
1380 AND br.id = mrs.source
1381 AND cn.record = mrs.source
1382 AND cp.status = cs.id
1383 AND cp.location = cl.id
1384 AND cp.circ_lib = d.id
1385 AND cp.call_number = cn.id
1386 AND cp.opac_visible IS TRUE
1387 AND cs.opac_visible IS TRUE
1388 AND cl.opac_visible IS TRUE
1389 AND d.opac_visible IS TRUE
1390 AND br.active IS TRUE
1391 AND br.deleted IS FALSE
1392 AND ord.record = mrs.source
1400 ORDER BY 4 $sort_dir
1409 FROM $asset_call_number_table cn,
1410 $asset_copy_table cp,
1411 $metabib_metarecord_source_map_table mrs,
1414 $metabib_record_descriptor ord
1416 WHERE mrs.metarecord = s.metarecord
1417 AND br.id = mrs.source
1418 AND cn.record = mrs.source
1419 AND cn.id = cp.call_number
1420 AND br.deleted IS FALSE
1421 AND cn.deleted IS FALSE
1422 AND ord.record = mrs.source
1423 AND ( cn.owning_lib = d.id
1424 OR ( cp.circ_lib = d.id
1425 AND cp.deleted IS FALSE
1437 FROM $asset_call_number_table cn,
1438 $metabib_metarecord_source_map_table mrs,
1439 $metabib_record_descriptor ord
1440 WHERE mrs.metarecord = s.metarecord
1441 AND cn.record = mrs.source
1442 AND ord.record = mrs.source
1450 ORDER BY 4 $sort_dir
1455 $log->debug("Field Search SQL :: [$select]",DEBUG);
1457 my $recs = $class->db_Main->selectall_arrayref(
1459 (@bonus_values > 0 ? @bonus_values : () ),
1460 ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1461 @types, @forms, @aud, @lang, @lit_form,
1462 @types, @forms, @aud, @lang, @lit_form,
1463 ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1465 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1468 $max = 1 if (!@$recs);
1470 $max = $$_[1] if ($$_[1] > $max);
1473 my $count = scalar(@$recs);
1474 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1475 my ($mrid,$rank,$skip) = @$rec;
1476 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1481 for my $class ( qw/title author subject keyword series identifier/ ) {
1482 __PACKAGE__->register_method(
1483 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1484 method => 'postfilter_search_class_fts',
1487 cdbi => "metabib::${class}_field_entry",
1490 __PACKAGE__->register_method(
1491 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1492 method => 'postfilter_search_class_fts',
1495 cdbi => "metabib::${class}_field_entry",
1502 my $_cdbi = { title => "metabib::title_field_entry",
1503 author => "metabib::author_field_entry",
1504 subject => "metabib::subject_field_entry",
1505 keyword => "metabib::keyword_field_entry",
1506 series => "metabib::series_field_entry",
1507 identifier => "metabib::identifier_field_entry",
1510 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1511 sub postfilter_search_multi_class_fts {
1516 my $sort = $args{'sort'};
1517 my $sort_dir = $args{sort_dir} || 'DESC';
1518 my $ou = $args{org_unit};
1519 my $ou_type = $args{depth};
1520 my $limit = $args{limit} || 10;
1521 my $offset = $args{offset} || 0;
1522 my $visibility_limit = $args{visibility_limit} || 5000;
1525 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1528 if (!defined($args{org_unit})) {
1529 die "No target organizational unit passed to ".$self->api_name;
1532 if (! scalar( keys %{$args{searches}} )) {
1533 die "No search arguments were passed to ".$self->api_name;
1536 my $outer_limit = 1000;
1538 my $limit_clause = '';
1539 my $offset_clause = '';
1541 $limit_clause = "LIMIT $outer_limit";
1542 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1544 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1545 my ($t_filter, $f_filter, $v_filter) = ('','','');
1546 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1547 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1548 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1550 if ($args{available}) {
1551 $avail_filter = ' AND cp.status IN (0,7,12)';
1554 if (my $a = $args{audience}) {
1555 $a = [$a] if (!ref($a));
1558 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1559 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1562 if (my $l = $args{language}) {
1563 $l = [$l] if (!ref($l));
1566 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1567 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1570 if (my $f = $args{lit_form}) {
1571 $f = [$f] if (!ref($f));
1574 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1575 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1578 if (my $f = $args{item_form}) {
1579 $f = [$f] if (!ref($f));
1582 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1583 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1586 if (my $t = $args{item_type}) {
1587 $t = [$t] if (!ref($t));
1590 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1591 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1594 if (my $v = $args{vr_format}) {
1595 $v = [$v] if (!ref($v));
1598 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1599 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1603 # XXX legacy format and item type support
1604 if ($args{format}) {
1605 my ($t, $f) = split '-', $args{format};
1606 @types = split '', $t;
1607 @forms = split '', $f;
1609 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1610 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1614 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1615 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1621 my $descendants = defined($ou_type) ?
1622 "actor.org_unit_descendants($ou, $ou_type)" :
1623 "actor.org_unit_descendants($ou)";
1625 my $search_table_list = '';
1627 my $join_table_list = '';
1630 my $field_table = config::metabib_field->table;
1634 my $prev_search_group;
1635 my $curr_search_group;
1639 for my $search_group (sort keys %{$args{searches}}) {
1640 (my $search_group_name = $search_group) =~ s/\|/_/gso;
1641 ($search_class,$search_field) = split /\|/, $search_group;
1642 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1644 if ($search_field) {
1645 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1646 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1651 $prev_search_group = $curr_search_group if ($curr_search_group);
1653 $curr_search_group = $search_group_name;
1655 my $class = $_cdbi->{$search_class};
1656 my $search_table = $class->table;
1658 my ($index_col) = $class->columns('FTS');
1659 $index_col ||= 'value';
1662 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1664 my $fts_where = $fts->sql_where_clause;
1665 my @fts_ranks = $fts->fts_rank;
1667 my $SQLstring = join('%',map { lc($_) } $fts->words);
1668 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1669 my $first_word = lc(($fts->words)[0]).'%';
1671 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1672 my $rank = join(' + ', @fts_ranks);
1675 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1676 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1678 $bonus{'series'} = [
1679 { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1680 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1683 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1685 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1686 $bonus_list ||= '1';
1688 push @bonus_lists, $bonus_list;
1689 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1692 #---------------------
1694 $search_table_list .= "$search_table $search_group_name, ";
1695 push @rank_list,$rank;
1696 $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1698 if ($metabib_field) {
1699 $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1700 $metabib_field = undef;
1703 if ($prev_search_group) {
1704 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1708 my $metabib_record_descriptor = metabib::record_descriptor->table;
1709 my $metabib_full_rec = metabib::full_rec->table;
1710 my $metabib_metarecord = metabib::metarecord->table;
1711 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1712 my $asset_call_number_table = asset::call_number->table;
1713 my $asset_copy_table = asset::copy->table;
1714 my $cs_table = config::copy_status->table;
1715 my $cl_table = asset::copy_location->table;
1716 my $br_table = biblio::record_entry->table;
1717 my $source_table = config::bib_source->table;
1719 my $bonuses = join (' * ', @bonus_lists);
1720 my $relevance = join (' + ', @rank_list);
1721 $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1723 my $string_default_sort = 'zzzz';
1724 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1726 my $number_default_sort = '9999';
1727 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1731 my $secondary_sort = <<" SORT";
1733 SELECT COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1734 FROM $metabib_full_rec sfrt,
1735 $metabib_metarecord mr
1736 WHERE sfrt.record = mr.master_record
1737 AND sfrt.tag = '245'
1738 AND sfrt.subfield = 'a'
1743 my $rank = $relevance;
1744 if (lc($sort) eq 'pubdate') {
1747 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1748 FROM $metabib_full_rec frp
1749 WHERE frp.record = mr.master_record
1751 AND frp.subfield = 'c'
1755 } elsif (lc($sort) eq 'create_date') {
1757 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1759 } elsif (lc($sort) eq 'edit_date') {
1761 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1763 } elsif (lc($sort) eq 'title') {
1766 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1767 FROM $metabib_full_rec frt
1768 WHERE frt.record = mr.master_record
1770 AND frt.subfield = 'a'
1774 $secondary_sort = <<" SORT";
1776 SELECT COALESCE(SUBSTRING(sfrp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1777 FROM $metabib_full_rec sfrp
1778 WHERE sfrp.record = mr.master_record
1779 AND sfrp.tag = '260'
1780 AND sfrp.subfield = 'c'
1784 } elsif (lc($sort) eq 'author') {
1787 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1788 FROM $metabib_full_rec fra
1789 WHERE fra.record = mr.master_record
1790 AND fra.tag LIKE '1%'
1791 AND fra.subfield = 'a'
1792 ORDER BY fra.tag::text::int
1797 push @bonus_values, @bonus_values;
1802 my $select = <<" SQL";
1803 SELECT m.metarecord,
1805 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1808 FROM $search_table_list
1809 $metabib_metarecord mr,
1810 $metabib_metarecord_source_map_table m,
1811 $metabib_metarecord_source_map_table smrs
1812 WHERE m.metarecord = smrs.metarecord
1813 AND mr.id = m.metarecord
1816 GROUP BY m.metarecord
1817 -- ORDER BY 4 $sort_dir
1818 LIMIT $visibility_limit
1821 if ($self->api_name !~ /staff/o) {
1828 FROM $asset_call_number_table cn,
1829 $metabib_metarecord_source_map_table mrs,
1830 $asset_copy_table cp,
1835 $metabib_record_descriptor ord
1836 WHERE mrs.metarecord = s.metarecord
1837 AND br.id = mrs.source
1838 AND cn.record = mrs.source
1839 AND cp.status = cs.id
1840 AND cp.location = cl.id
1841 AND cp.circ_lib = d.id
1842 AND cp.call_number = cn.id
1843 AND cp.opac_visible IS TRUE
1844 AND cs.opac_visible IS TRUE
1845 AND cl.opac_visible IS TRUE
1846 AND d.opac_visible IS TRUE
1847 AND br.active IS TRUE
1848 AND br.deleted IS FALSE
1849 AND cp.deleted IS FALSE
1850 AND cn.deleted IS FALSE
1851 AND ord.record = mrs.source
1864 $metabib_metarecord_source_map_table mrs,
1865 $metabib_record_descriptor ord,
1867 WHERE mrs.metarecord = s.metarecord
1868 AND ord.record = mrs.source
1869 AND br.id = mrs.source
1870 AND br.source = src.id
1871 AND src.transcendant IS TRUE
1879 ORDER BY 4 $sort_dir, 5
1886 $metabib_metarecord_source_map_table omrs,
1887 $metabib_record_descriptor ord
1888 WHERE omrs.metarecord = s.metarecord
1889 AND ord.record = omrs.source
1892 FROM $asset_call_number_table cn,
1893 $asset_copy_table cp,
1896 WHERE br.id = omrs.source
1897 AND cn.record = omrs.source
1898 AND br.deleted IS FALSE
1899 AND cn.deleted IS FALSE
1900 AND cp.call_number = cn.id
1901 AND ( cn.owning_lib = d.id
1902 OR ( cp.circ_lib = d.id
1903 AND cp.deleted IS FALSE
1911 FROM $asset_call_number_table cn
1912 WHERE cn.record = omrs.source
1913 AND cn.deleted IS FALSE
1919 $metabib_metarecord_source_map_table mrs,
1920 $metabib_record_descriptor ord,
1922 WHERE mrs.metarecord = s.metarecord
1923 AND br.id = mrs.source
1924 AND br.source = src.id
1925 AND src.transcendant IS TRUE
1941 ORDER BY 4 $sort_dir, 5
1946 $log->debug("Field Search SQL :: [$select]",DEBUG);
1948 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1951 @types, @forms, @vformats, @aud, @lang, @lit_form,
1952 @types, @forms, @vformats, @aud, @lang, @lit_form,
1953 # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1956 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1959 $max = 1 if (!@$recs);
1961 $max = $$_[1] if ($$_[1] > $max);
1964 my $count = scalar(@$recs);
1965 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1966 next unless ($$rec[0]);
1967 my ($mrid,$rank,$skip) = @$rec;
1968 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1973 __PACKAGE__->register_method(
1974 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1975 method => 'postfilter_search_multi_class_fts',
1980 __PACKAGE__->register_method(
1981 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1982 method => 'postfilter_search_multi_class_fts',
1988 __PACKAGE__->register_method(
1989 api_name => "open-ils.storage.metabib.multiclass.search_fts",
1990 method => 'postfilter_search_multi_class_fts',
1995 __PACKAGE__->register_method(
1996 api_name => "open-ils.storage.metabib.multiclass.search_fts.staff",
1997 method => 'postfilter_search_multi_class_fts',
2003 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2004 sub biblio_search_multi_class_fts {
2009 my $sort = $args{'sort'};
2010 my $sort_dir = $args{sort_dir} || 'DESC';
2011 my $ou = $args{org_unit};
2012 my $ou_type = $args{depth};
2013 my $limit = $args{limit} || 10;
2014 my $offset = $args{offset} || 0;
2015 my $pref_lang = $args{preferred_language} || 'eng';
2016 my $visibility_limit = $args{visibility_limit} || 5000;
2019 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2022 if (! scalar( keys %{$args{searches}} )) {
2023 die "No search arguments were passed to ".$self->api_name;
2026 my $outer_limit = 1000;
2028 my $limit_clause = '';
2029 my $offset_clause = '';
2031 $limit_clause = "LIMIT $outer_limit";
2032 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
2034 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
2035 my ($t_filter, $f_filter, $v_filter) = ('','','');
2036 my ($a_filter, $l_filter, $lf_filter) = ('','','');
2037 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
2038 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
2040 if ($args{available}) {
2041 $avail_filter = ' AND cp.status IN (0,7,12)';
2044 if (my $a = $args{audience}) {
2045 $a = [$a] if (!ref($a));
2048 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
2049 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
2052 if (my $l = $args{language}) {
2053 $l = [$l] if (!ref($l));
2056 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
2057 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
2060 if (my $f = $args{lit_form}) {
2061 $f = [$f] if (!ref($f));
2064 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2065 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2068 if (my $f = $args{item_form}) {
2069 $f = [$f] if (!ref($f));
2072 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2073 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2076 if (my $t = $args{item_type}) {
2077 $t = [$t] if (!ref($t));
2080 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2081 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2084 if (my $v = $args{vr_format}) {
2085 $v = [$v] if (!ref($v));
2088 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
2089 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
2092 # XXX legacy format and item type support
2093 if ($args{format}) {
2094 my ($t, $f) = split '-', $args{format};
2095 @types = split '', $t;
2096 @forms = split '', $f;
2098 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2099 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2103 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2104 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2109 my $descendants = defined($ou_type) ?
2110 "actor.org_unit_descendants($ou, $ou_type)" :
2111 "actor.org_unit_descendants($ou)";
2113 my $search_table_list = '';
2115 my $join_table_list = '';
2118 my $field_table = config::metabib_field->table;
2122 my $prev_search_group;
2123 my $curr_search_group;
2127 for my $search_group (sort keys %{$args{searches}}) {
2128 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2129 ($search_class,$search_field) = split /\|/, $search_group;
2130 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2132 if ($search_field) {
2133 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2134 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2139 $prev_search_group = $curr_search_group if ($curr_search_group);
2141 $curr_search_group = $search_group_name;
2143 my $class = $_cdbi->{$search_class};
2144 my $search_table = $class->table;
2146 my ($index_col) = $class->columns('FTS');
2147 $index_col ||= 'value';
2150 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2152 my $fts_where = $fts->sql_where_clause;
2153 my @fts_ranks = $fts->fts_rank;
2155 my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2156 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2157 my $first_word = lc(($fts->words)[0]).'%';
2159 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2160 my $rank = join(' + ', @fts_ranks);
2163 $bonus{'subject'} = [];
2164 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2166 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2168 $bonus{'series'} = [
2169 { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2170 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2173 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2176 push @{ $bonus{'title'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2177 push @{ $bonus{'author'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2178 push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2179 push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2180 push @{ $bonus{'series'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2183 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2184 $bonus_list ||= '1';
2186 push @bonus_lists, $bonus_list;
2187 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2189 #---------------------
2191 $search_table_list .= "$search_table $search_group_name, ";
2192 push @rank_list,$rank;
2193 $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2195 if ($metabib_field) {
2196 $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2197 $metabib_field = undef;
2200 if ($prev_search_group) {
2201 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2205 my $metabib_record_descriptor = metabib::record_descriptor->table;
2206 my $metabib_full_rec = metabib::full_rec->table;
2207 my $metabib_metarecord = metabib::metarecord->table;
2208 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2209 my $asset_call_number_table = asset::call_number->table;
2210 my $asset_copy_table = asset::copy->table;
2211 my $cs_table = config::copy_status->table;
2212 my $cl_table = asset::copy_location->table;
2213 my $br_table = biblio::record_entry->table;
2214 my $source_table = config::bib_source->table;
2217 my $bonuses = join (' * ', @bonus_lists);
2218 my $relevance = join (' + ', @rank_list);
2219 $relevance = "AVG( ($relevance) * ($bonuses) )";
2221 my $string_default_sort = 'zzzz';
2222 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2224 my $number_default_sort = '9999';
2225 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2227 my $rank = $relevance;
2228 if (lc($sort) eq 'pubdate') {
2231 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d{4}'),'$number_default_sort')::INT
2232 FROM $metabib_full_rec frp
2233 WHERE frp.record = b.id
2235 AND frp.subfield = 'c'
2239 } elsif (lc($sort) eq 'create_date') {
2241 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2243 } elsif (lc($sort) eq 'edit_date') {
2245 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2247 } elsif (lc($sort) eq 'title') {
2250 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2251 FROM $metabib_full_rec frt
2252 WHERE frt.record = b.id
2254 AND frt.subfield = 'a'
2258 } elsif (lc($sort) eq 'author') {
2261 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
2262 FROM $metabib_full_rec fra
2263 WHERE fra.record = b.id
2264 AND fra.tag LIKE '1%'
2265 AND fra.subfield = 'a'
2266 ORDER BY fra.tag::text::int
2271 push @bonus_values, @bonus_values;
2276 my $select = <<" SQL";
2281 FROM $search_table_list
2282 $metabib_record_descriptor rd,
2285 WHERE rd.record = b.id
2286 AND b.active IS TRUE
2287 AND b.deleted IS FALSE
2296 GROUP BY b.id, b.source
2297 ORDER BY 3 $sort_dir
2298 LIMIT $visibility_limit
2301 if ($self->api_name !~ /staff/o) {
2306 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2309 FROM $asset_call_number_table cn,
2310 $asset_copy_table cp,
2314 WHERE cn.record = s.id
2315 AND cp.status = cs.id
2316 AND cp.location = cl.id
2317 AND cp.call_number = cn.id
2318 AND cp.opac_visible IS TRUE
2319 AND cs.opac_visible IS TRUE
2320 AND cl.opac_visible IS TRUE
2321 AND d.opac_visible IS TRUE
2322 AND cp.deleted IS FALSE
2323 AND cn.deleted IS FALSE
2324 AND cp.circ_lib = d.id
2328 OR src.transcendant IS TRUE
2329 ORDER BY 3 $sort_dir
2336 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2339 FROM $asset_call_number_table cn,
2340 $asset_copy_table cp,
2342 WHERE cn.record = s.id
2343 AND cp.call_number = cn.id
2344 AND cn.deleted IS FALSE
2345 AND cp.circ_lib = d.id
2346 AND cp.deleted IS FALSE
2352 FROM $asset_call_number_table cn
2353 WHERE cn.record = s.id
2356 OR src.transcendant IS TRUE
2357 ORDER BY 3 $sort_dir
2362 $log->debug("Field Search SQL :: [$select]",DEBUG);
2364 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2366 @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2369 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2371 my $count = scalar(@$recs);
2372 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2373 next unless ($$rec[0]);
2374 my ($mrid,$rank) = @$rec;
2375 $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2380 __PACKAGE__->register_method(
2381 api_name => "open-ils.storage.biblio.multiclass.search_fts.record",
2382 method => 'biblio_search_multi_class_fts',
2387 __PACKAGE__->register_method(
2388 api_name => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2389 method => 'biblio_search_multi_class_fts',
2394 __PACKAGE__->register_method(
2395 api_name => "open-ils.storage.biblio.multiclass.search_fts",
2396 method => 'biblio_search_multi_class_fts',
2401 __PACKAGE__->register_method(
2402 api_name => "open-ils.storage.biblio.multiclass.search_fts.staff",
2403 method => 'biblio_search_multi_class_fts',
2411 my $default_preferred_language;
2412 my $default_preferred_language_weight;
2414 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2420 if (!$locale_map{COMPLETE}) {
2422 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2423 for my $locale ( @locales ) {
2424 $locale_map{lc($locale->code)} = $locale->marc_code;
2426 $locale_map{COMPLETE} = 1;
2430 my $config = OpenSRF::Utils::SettingsClient->new();
2432 if (!$default_preferred_language) {
2434 $default_preferred_language = $config->config_value(
2435 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2436 ) || $config->config_value(
2437 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2442 if (!$default_preferred_language_weight) {
2444 $default_preferred_language_weight = $config->config_value(
2445 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2446 ) || $config->config_value(
2447 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2451 # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2452 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2454 my $ou = $args{org_unit};
2455 my $limit = $args{limit} || 10;
2456 my $offset = $args{offset} || 0;
2459 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2462 if (! scalar( keys %{$args{searches}} )) {
2463 die "No search arguments were passed to ".$self->api_name;
2466 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2468 if (!defined($args{preferred_language})) {
2469 my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2470 $args{preferred_language} =
2471 $locale_map{ lc($ses_locale) } || 'eng';
2474 if (!defined($args{preferred_language_weight})) {
2475 $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2478 if ($args{available}) {
2479 @statuses = (0,7,12);
2482 if (my $s = $args{locations}) {
2483 $s = [$s] if (!ref($s));
2487 if (my $b = $args{between}) {
2488 if (ref($b) && @$b == 2) {
2493 if (my $s = $args{statuses}) {
2494 $s = [$s] if (!ref($s));
2498 if (my $a = $args{audience}) {
2499 $a = [$a] if (!ref($a));
2503 if (my $l = $args{language}) {
2504 $l = [$l] if (!ref($l));
2508 if (my $f = $args{lit_form}) {
2509 $f = [$f] if (!ref($f));
2513 if (my $f = $args{item_form}) {
2514 $f = [$f] if (!ref($f));
2518 if (my $t = $args{item_type}) {
2519 $t = [$t] if (!ref($t));
2523 if (my $b = $args{bib_level}) {
2524 $b = [$b] if (!ref($b));
2528 if (my $v = $args{vr_format}) {
2529 $v = [$v] if (!ref($v));
2533 # XXX legacy format and item type support
2534 if ($args{format}) {
2535 my ($t, $f) = split '-', $args{format};
2536 @types = split '', $t;
2537 @forms = split '', $f;
2540 my %stored_proc_search_args;
2541 for my $search_group (sort keys %{$args{searches}}) {
2542 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2543 my ($search_class,$search_field) = split /\|/, $search_group;
2544 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2546 if ($search_field) {
2547 unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2548 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2553 my $class = $_cdbi->{$search_class};
2554 my $search_table = $class->table;
2556 my ($index_col) = $class->columns('FTS');
2557 $index_col ||= 'value';
2560 my $fts = OpenILS::Application::Storage::FTS->compile(
2561 $search_class => $args{searches}{$search_group}{term},
2562 $search_group_name.'.value',
2563 "$search_group_name.$index_col"
2565 $fts->sql_where_clause; # this builds the ranks for us
2567 my @fts_ranks = $fts->fts_rank;
2568 my @fts_queries = $fts->fts_query;
2569 my @phrases = map { lc($_) } $fts->phrases;
2570 my @words = map { lc($_) } $fts->words;
2572 $stored_proc_search_args{$search_group} = {
2573 fts_rank => \@fts_ranks,
2574 fts_query => \@fts_queries,
2575 phrase => \@phrases,
2581 my $param_search_ou = $ou;
2582 my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2583 my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2584 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2585 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2586 my $param_audience = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud ) . '}$$';
2587 my $param_language = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang ) . '}$$';
2588 my $param_lit_form = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2589 my $param_types = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types ) . '}$$';
2590 my $param_forms = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms ) . '}$$';
2591 my $param_vformats = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2592 my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2593 my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2594 my $param_after = $args{after} ; $param_after = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2595 my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2596 my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2597 my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2598 my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2599 my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2600 my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2601 my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2602 my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2603 my $param_rel_limit = $args{core_limit}; $param_rel_limit ||= 'NULL';
2604 my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2605 my $param_skip_chk = $args{skip_check}; $param_skip_chk ||= 'NULL';
2607 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2609 FROM search.staged_fts(
2610 $param_search_ou\:\:INT,
2611 $param_depth\:\:INT,
2612 $param_searches\:\:TEXT,
2613 $param_statuses\:\:INT[],
2614 $param_locations\:\:INT[],
2615 $param_audience\:\:TEXT[],
2616 $param_language\:\:TEXT[],
2617 $param_lit_form\:\:TEXT[],
2618 $param_types\:\:TEXT[],
2619 $param_forms\:\:TEXT[],
2620 $param_vformats\:\:TEXT[],
2621 $param_bib_level\:\:TEXT[],
2622 $param_before\:\:TEXT,
2623 $param_after\:\:TEXT,
2624 $param_during\:\:TEXT,
2625 $param_between\:\:TEXT[],
2626 $param_pref_lang\:\:TEXT,
2627 $param_pref_lang_multiplier\:\:REAL,
2628 $param_sort\:\:TEXT,
2629 $param_sort_desc\:\:BOOL,
2630 $metarecord\:\:BOOL,
2632 $param_rel_limit\:\:INT,
2633 $param_chk_limit\:\:INT,
2634 $param_skip_chk\:\:INT
2640 my $recs = $sth->fetchall_arrayref({});
2641 my $summary_row = pop @$recs;
2643 my $total = $$summary_row{total};
2644 my $checked = $$summary_row{checked};
2645 my $visible = $$summary_row{visible};
2646 my $deleted = $$summary_row{deleted};
2647 my $excluded = $$summary_row{excluded};
2649 my $estimate = $visible;
2650 if ( $total > $checked && $checked ) {
2652 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2653 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2657 delete $$summary_row{id};
2658 delete $$summary_row{rel};
2659 delete $$summary_row{record};
2661 $client->respond( $summary_row );
2663 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2665 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2666 delete $$rec{checked};
2667 delete $$rec{visible};
2668 delete $$rec{excluded};
2669 delete $$rec{deleted};
2670 delete $$rec{total};
2671 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2673 $client->respond( $rec );
2677 __PACKAGE__->register_method(
2678 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
2679 method => 'staged_fts',
2684 __PACKAGE__->register_method(
2685 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2686 method => 'staged_fts',
2691 __PACKAGE__->register_method(
2692 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
2693 method => 'staged_fts',
2698 __PACKAGE__->register_method(
2699 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",
2734 method => 'FTS_paging_estimate',
2740 Hash of estimation values based on four variant estimation strategies:
2741 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2742 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2743 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2744 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2747 Helper method used to determin the approximate number of
2748 hits for a search that spans multiple superpages. For
2749 sparse superpages, the inclusion estimate will likely be the
2750 best estimate. The exclusion strategy is the original, but
2751 inclusion is the default.
2754 { name => 'checked',
2755 desc => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2758 { name => 'visible',
2759 desc => 'Number of records visible to the search location on the current superpage.',
2762 { name => 'excluded',
2763 desc => 'Number of records excluded from the search location on the current superpage.',
2766 { name => 'deleted',
2767 desc => 'Number of deleted records on the current superpage.',
2771 desc => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2784 my $term = $$args{term};
2785 my $limit = $$args{max} || 1;
2786 my $min = $$args{min} || 1;
2787 my @classes = @{$$args{class}};
2789 $limit = $min if ($min > $limit);
2792 @classes = ( qw/ title author subject series keyword / );
2796 my $bre_table = biblio::record_entry->table;
2797 my $cn_table = asset::call_number->table;
2798 my $cp_table = asset::copy->table;
2800 for my $search_class ( @classes ) {
2802 my $class = $_cdbi->{$search_class};
2803 my $search_table = $class->table;
2805 my ($index_col) = $class->columns('FTS');
2806 $index_col ||= 'value';
2809 my $where = OpenILS::Application::Storage::FTS
2810 ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2814 SELECT COUNT(DISTINCT X.source)
2815 FROM (SELECT $search_class.source
2816 FROM $search_table $search_class
2817 JOIN $bre_table b ON (b.id = $search_class.source)
2822 HAVING COUNT(DISTINCT X.source) >= $min;
2825 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2826 $matches{$search_class} = $res ? $res->[0] : 0;
2831 __PACKAGE__->register_method(
2832 api_name => "open-ils.storage.search.xref",
2833 method => 'xref_count',
2837 # Takes an abstract query object and recursively turns it back into a string
2839 sub abstract_query2str {
2840 my ($self, $conn, $query) = @_;
2842 return QueryParser::Canonicalize::abstract_query2str_impl($query, 0);
2845 __PACKAGE__->register_method(
2846 api_name => "open-ils.storage.query_parser.abstract_query.canonicalize",
2847 method => "abstract_query2str",
2852 Abstract query parser object, with complete config data. For example input,
2853 see the 'abstract_query' part of the output of an API call like
2854 open-ils.search.biblio.multiclass.query, when called with the return_abstract
2858 return => { type => "string", desc => "String representation of abstract query object" }
2862 sub str2abstract_query {
2863 my ($self, $conn, $query, $qp_opts, $with_config) = @_;
2865 my %use_opts = ( # reasonable defaults? should these even be hardcoded here?
2867 superpage_size => 1000,
2868 core_limit => 25000,
2870 (ref $opts eq 'HASH' ? %$opts : ())
2875 # grab the query parser and initialize it
2876 my $parser = $OpenILS::Application::Storage::QParser;
2879 _initialize_parser($parser) unless $parser->initialization_complete;
2881 my $query = $parser->new(%use_opts)->parse;
2883 return $query->parse_tree->to_abstract_query(with_config => $with_config);
2886 __PACKAGE__->register_method(
2887 api_name => "open-ils.storage.query_parser.abstract_query.from_string",
2888 method => "str2abstract_query",
2892 {desc => "Query", type => "string"},
2893 {desc => q/Arguments for initializing QueryParser (optional)/,
2895 {desc => q/Flag enabling inclusion of QP config in returned object (optional, default false)/,
2898 return => { type => "object", desc => "abstract representation of query parser query" }
2902 sub query_parser_fts {
2908 # grab the query parser and initialize it
2909 my $parser = $OpenILS::Application::Storage::QParser;
2912 _initialize_parser($parser) unless $parser->initialization_complete;
2914 # populate the locale/language map
2915 if (!$locale_map{COMPLETE}) {
2917 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2918 for my $locale ( @locales ) {
2919 $locale_map{lc($locale->code)} = $locale->marc_code;
2921 $locale_map{COMPLETE} = 1;
2925 # I hope we have a query!
2926 if (! $args{query} ) {
2927 die "No query was passed to ".$self->api_name;
2930 my $default_CD_modifiers = OpenSRF::Utils::SettingsClient->new->config_value(
2931 apps => 'open-ils.search' => app_settings => 'default_CD_modifiers'
2934 # Protect against empty / missing default_CD_modifiers setting
2935 if ($default_CD_modifiers and !ref($default_CD_modifiers)) {
2936 $args{query} = "$default_CD_modifiers $args{query}";
2939 my $simple_plan = $args{_simple_plan};
2940 # remove bad chunks of the %args hash
2941 for my $bad ( grep { /^_/ } keys(%args)) {
2942 delete($args{$bad});
2946 # parse the query and supply any query-level %arg-based defaults
2947 # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2948 my $query = $parser->new( %args )->parse;
2950 my $config = OpenSRF::Utils::SettingsClient->new();
2952 # set the locale-based default preferred location
2953 if (!$query->parse_tree->find_filter('preferred_language')) {
2954 $parser->default_preferred_language( $args{preferred_language} );
2956 if (!$parser->default_preferred_language) {
2957 my $ses_locale = $client->session ? $client->session->session_locale : '';
2958 $parser->default_preferred_language( $locale_map{ lc($ses_locale) } );
2961 if (!$parser->default_preferred_language) { # still nothing...
2962 my $tmp_dpl = $config->config_value(
2963 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2964 ) || $config->config_value(
2965 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2968 $parser->default_preferred_language( $tmp_dpl )
2973 # set the global default language multiplier
2974 if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2977 if ($tmp_dplw = $args{preferred_language_weight} || $args{preferred_language_multiplier} ) {
2978 $parser->default_preferred_language_multiplier($tmp_dplw);
2981 $tmp_dplw = $config->config_value(
2982 apps => 'open-ils.search' => app_settings => 'default_preferred_language_weight'
2983 ) || $config->config_value(
2984 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2987 $parser->default_preferred_language_multiplier( $tmp_dplw );
2991 # gather the site, if one is specified, defaulting to the in-query version
2992 my $ou = $args{org_unit};
2993 if (my ($filter) = $query->parse_tree->find_filter('site')) {
2994 $ou = $filter->args->[0] if (@{$filter->args});
2996 $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^(-)?\d+$/);
2998 # gather lasso, as with $ou
2999 my $lasso = $args{lasso};
3000 if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
3001 $lasso = $filter->args->[0] if (@{$filter->args});
3003 $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
3004 $lasso = -$lasso if ($lasso);
3007 # # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
3008 # # gather user lasso, as with $ou and lasso
3009 # my $mylasso = $args{my_lasso};
3010 # if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
3011 # $mylasso = $filter->args->[0] if (@{$filter->args});
3013 # $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
3016 # if we have a lasso, go with that, otherwise ... ou
3017 $ou = $lasso if ($lasso);
3019 # gather the preferred OU, if one is specified, as with $ou
3020 my $pref_ou = $args{pref_ou};
3021 $log->info("pref_ou = $pref_ou");
3022 if (my ($filter) = $query->parse_tree->find_filter('pref_ou')) {
3023 $pref_ou = $filter->args->[0] if (@{$filter->args});
3025 $pref_ou = actor::org_unit->search( { shortname => $pref_ou } )->next->id if ($pref_ou and $pref_ou !~ /^(-)?\d+$/);
3027 # get the default $ou if we have nothing
3028 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
3031 # 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
3032 # gather the depth, if one is specified, defaulting to the in-query version
3033 my $depth = $args{depth};
3034 if (my ($filter) = $query->parse_tree->find_filter('depth')) {
3035 $depth = $filter->args->[0] if (@{$filter->args});
3037 $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
3040 # gather the limit or default to 10
3041 my $limit = $args{check_limit} || 'NULL';
3042 if (my ($filter) = $query->parse_tree->find_filter('limit')) {
3043 $limit = $filter->args->[0] if (@{$filter->args});
3045 if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
3046 $limit = $filter->args->[0] if (@{$filter->args});
3050 # gather the offset or default to 0
3051 my $offset = $args{skip_check} || $args{offset} || 0;
3052 if (my ($filter) = $query->parse_tree->find_filter('offset')) {
3053 $offset = $filter->args->[0] if (@{$filter->args});
3055 if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
3056 $offset = $filter->args->[0] if (@{$filter->args});
3060 # gather the estimation strategy or default to inclusion
3061 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
3062 if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
3063 $estimation_strategy = $filter->args->[0] if (@{$filter->args});
3067 # gather the estimation strategy or default to inclusion
3068 my $core_limit = $args{core_limit};
3069 if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
3070 $core_limit = $filter->args->[0] if (@{$filter->args});
3074 # gather statuses, and then forget those if we have an #available modifier
3076 if (my ($filter) = $query->parse_tree->find_filter('statuses')) {
3077 @statuses = @{$filter->args} if (@{$filter->args});
3079 @statuses = (0,7,12) if ($query->parse_tree->find_modifier('available'));
3084 if (my ($filter) = $query->parse_tree->find_filter('locations')) {
3085 @location = @{$filter->args} if (@{$filter->args});
3088 # gather location_groups
3089 if (my ($filter) = $query->parse_tree->find_filter('location_groups')) {
3090 my @loc_groups = @{$filter->args} if (@{$filter->args});
3092 # collect the mapped locations and add them to the locations() filter
3095 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3096 my $maps = $cstore->request(
3097 'open-ils.cstore.direct.asset.copy_location_group_map.search.atomic',
3098 {lgroup => \@loc_groups})->gather(1);
3100 push(@location, $_->location) for @$maps;
3105 my $param_check = $limit || $query->superpage_size || 'NULL';
3106 my $param_offset = $offset || 'NULL';
3107 my $param_limit = $core_limit || 'NULL';
3109 my $sp = $query->superpage || 1;
3111 $param_offset = ($sp - 1) * $sp_size;
3114 my $param_search_ou = $ou;
3115 my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
3116 my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
3117 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
3118 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
3119 my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
3120 my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
3121 my $param_pref_ou = $pref_ou || 'NULL';
3123 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3124 SELECT * -- bib search: $args{query}
3125 FROM search.query_parser_fts(
3126 $param_search_ou\:\:INT,
3127 $param_depth\:\:INT,
3128 $param_core_query\:\:TEXT,
3129 $param_statuses\:\:INT[],
3130 $param_locations\:\:INT[],
3131 $param_offset\:\:INT,
3132 $param_check\:\:INT,
3133 $param_limit\:\:INT,
3134 $metarecord\:\:BOOL,
3136 $param_pref_ou\:\:INT
3142 my $recs = $sth->fetchall_arrayref({});
3143 my $summary_row = pop @$recs;
3145 my $total = $$summary_row{total};
3146 my $checked = $$summary_row{checked};
3147 my $visible = $$summary_row{visible};
3148 my $deleted = $$summary_row{deleted};
3149 my $excluded = $$summary_row{excluded};
3151 my $estimate = $visible;
3152 if ( $total > $checked && $checked ) {
3154 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
3155 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
3159 delete $$summary_row{id};
3160 delete $$summary_row{rel};
3161 delete $$summary_row{record};
3163 if (defined($simple_plan)) {
3164 $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3166 $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3169 $client->respond( $summary_row );
3171 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
3173 for my $rec (@$recs) {
3174 delete $$rec{checked};
3175 delete $$rec{visible};
3176 delete $$rec{excluded};
3177 delete $$rec{deleted};
3178 delete $$rec{total};
3179 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3181 $client->respond( $rec );
3185 __PACKAGE__->register_method(
3186 api_name => "open-ils.storage.query_parser_search",
3187 method => 'query_parser_fts',
3193 sub query_parser_fts_wrapper {
3198 $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3199 # grab the query parser and initialize it
3200 my $parser = $OpenILS::Application::Storage::QParser;
3203 if (!$parser->initialization_complete) {
3204 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3205 $parser->initialize(
3206 config_record_attr_index_norm_map =>
3208 'open-ils.cstore.direct.config.record_attr_index_norm_map.search.atomic',
3209 { id => { "!=" => undef } },
3210 { flesh => 1, flesh_fields => { crainm => [qw/norm/] }, order_by => [{ class => "crainm", field => "pos" }] }
3212 search_relevance_adjustment =>
3214 'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
3215 { id => { "!=" => undef } }
3217 config_metabib_field =>
3219 'open-ils.cstore.direct.config.metabib_field.search.atomic',
3220 { id => { "!=" => undef } }
3222 config_metabib_search_alias =>
3224 'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
3225 { alias => { "!=" => undef } }
3227 config_metabib_field_index_norm_map =>
3229 'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
3230 { id => { "!=" => undef } },
3231 { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
3233 config_record_attr_definition =>
3235 'open-ils.cstore.direct.config.record_attr_definition.search.atomic',
3236 { name => { "!=" => undef } }
3240 $cstore->disconnect;
3241 die("Cannot initialize $parser!") unless ($parser->initialization_complete);
3244 if (! scalar( keys %{$args{searches}} )) {
3245 die "No search arguments were passed to ".$self->api_name;
3248 $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3249 my $base_query = '';
3250 for my $sclass ( keys %{$args{searches}} ) {
3251 $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3252 $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3255 my $query = $base_query;
3256 $log->debug("Full base query: $base_query", DEBUG);
3258 $query = "$args{facets} $query" if ($args{facets});
3260 if (!$locale_map{COMPLETE}) {
3262 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3263 for my $locale ( @locales ) {
3264 $locale_map{lc($locale->code)} = $locale->marc_code;
3266 $locale_map{COMPLETE} = 1;
3270 my $base_plan = $parser->new( query => $base_query )->parse;
3272 $query = "$query preferred_language($args{preferred_language})"
3273 if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3274 $query = "$query preferred_language_weight($args{preferred_language_weight})"
3275 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'));
3278 # we add these to the end of the query (last-wins) because in wrapper mode we want to retain the behaviour
3279 # of separately specified options taking precidenc -- IOW, the user should not be able to cause a change in,
3280 # say, superpage size by adjusting the query string.
3281 $query = "$query estimation_strategy($args{estimation_strategy})" if ($args{estimation_strategy});
3282 $query = "$query site($args{org_unit})" if ($args{org_unit});
3283 $query = "$query depth($args{depth})" if (defined($args{depth}));
3284 $query = "$query sort($args{sort})" if ($args{sort});
3285 $query = "$query limit($args{limit})" if ($args{limit});
3286 $query = "$query core_limit($args{core_limit})" if ($args{core_limit});
3287 $query = "$query skip_check($args{skip_check})" if ($args{skip_check});
3288 $query = "$query superpage($args{superpage})" if ($args{superpage});
3289 $query = "$query offset($args{offset})" if ($args{offset});
3290 $query = "$query #metarecord" if ($self->api_name =~ /metabib/);
3291 $query = "$query #available" if ($args{available});
3292 $query = "$query #descending" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3293 $query = "$query #staff" if ($self->api_name =~ /staff/);
3294 $query = "$query before($args{before})" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3295 $query = "$query after($args{after})" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3296 $query = "$query during($args{during})" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3297 $query = "$query between($args{between}[0],$args{between}[1])"
3298 if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3301 my (@between,@statuses,@locations,@location_groups,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3303 # XXX legacy format and item type support
3304 if ($args{format}) {
3305 my ($t, $f) = split '-', $args{format};
3306 $args{item_type} = [ split '', $t ];
3307 $args{item_form} = [ split '', $f ];
3310 for my $filter ( qw/locations location_groups statuses between audience language lit_form item_form item_type bib_level vr_format/ ) {
3311 if (my $s = $args{$filter}) {
3312 $s = [$s] if (!ref($s));
3314 my @filter_list = @$s;
3316 next if ($filter eq 'between' and scalar(@filter_list) != 2);
3317 next if (@filter_list == 0);
3319 my $filter_string = join ',', @filter_list;
3320 $query = "$query $filter($filter_string)";
3324 $log->debug("Full QueryParser query: $query", DEBUG);
3326 return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan );
3328 __PACKAGE__->register_method(
3329 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
3330 method => 'query_parser_fts_wrapper',
3335 __PACKAGE__->register_method(
3336 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3337 method => 'query_parser_fts_wrapper',
3342 __PACKAGE__->register_method(
3343 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
3344 method => 'query_parser_fts_wrapper',
3349 __PACKAGE__->register_method(
3350 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3351 method => 'query_parser_fts_wrapper',