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 } }
59 die("Cannot initialize $parser!") unless ($parser->initialization_complete);
62 sub ordered_records_from_metarecord {
70 my (@types,@forms,@blvl);
73 my ($t, $f, $b) = split '-', $formats;
74 @types = split '', $t;
75 @forms = split '', $f;
81 "actor.org_unit_descendants($org, $depth)" :
82 "actor.org_unit_descendants($org)" ;
85 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';
86 $copies_visible = '' if ($self->api_name =~ /staff/o);
88 my $sm_table = metabib::metarecord_source_map->table;
89 my $rd_table = metabib::record_descriptor->table;
90 my $fr_table = metabib::full_rec->table;
91 my $cn_table = asset::call_number->table;
92 my $cl_table = asset::copy_location->table;
93 my $cp_table = asset::copy->table;
94 my $cs_table = config::copy_status->table;
95 my $src_table = config::bib_source->table;
96 my $out_table = actor::org_unit_type->table;
97 my $br_table = biblio::record_entry->table;
104 FIRST(COALESCE(LTRIM(SUBSTR( value, COALESCE(SUBSTRING(ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'zzzzzzzz')) AS title
116 if ($copies_visible) {
122 WHERE rd.record = sm.source
123 AND fr.record = sm.source
124 AND br.id = sm.source
125 AND sm.metarecord = ?
126 AND (EXISTS ((SELECT 1
128 JOIN $cn_table cn ON (cp.call_number = cn.id)
129 JOIN $cs_table cs ON (cp.status = cs.id)
130 JOIN $cl_table cl ON (cp.location = cl.id)
131 JOIN $descendants d ON (cp.circ_lib = d.id)
132 WHERE cn.record = sm.source
138 WHERE src.id = br.source
139 AND src.transcendant IS TRUE))
146 JOIN $br_table br ON (sm.source = br.id)
147 JOIN $fr_table fr ON (fr.record = br.id)
148 JOIN $rd_table rd ON (rd.record = br.id)
149 WHERE sm.metarecord = ?
155 WHERE cn.record = br.id
156 AND cn.deleted = FALSE
157 AND cp.deleted = FALSE
158 AND cp.circ_lib = d.id
159 AND cn.id = cp.call_number
165 WHERE cn.record = br.id
166 AND cn.deleted = FALSE
167 AND cp.deleted = FALSE
168 AND cn.id = cp.call_number
174 WHERE src.id = br.source
175 AND src.transcendant IS TRUE))
181 $sql .= ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
185 $sql .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
189 $sql .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
199 GROUP BY record, item_type, item_form, quality
202 WHEN item_type IS NULL -- default
204 WHEN item_type = '' -- default
206 WHEN item_type IN ('a','t') -- books
208 WHEN item_type = 'g' -- movies
210 WHEN item_type IN ('i','j') -- sound recordings
212 WHEN item_type = 'm' -- software
214 WHEN item_type = 'k' -- images
216 WHEN item_type IN ('e','f') -- maps
218 WHEN item_type IN ('o','p') -- mixed
220 WHEN item_type IN ('c','d') -- music
222 WHEN item_type = 'r' -- 3d
229 my $ids = metabib::metarecord_source_map->db_Main->selectcol_arrayref($sql, {}, "$mr", @types, @forms, @blvl);
230 return $ids if ($self->api_name =~ /atomic$/o);
232 $client->respond( $_ ) for ( @$ids );
236 __PACKAGE__->register_method(
237 api_name => 'open-ils.storage.ordered.metabib.metarecord.records',
238 method => 'ordered_records_from_metarecord',
242 __PACKAGE__->register_method(
243 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff',
244 method => 'ordered_records_from_metarecord',
249 __PACKAGE__->register_method(
250 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.atomic',
251 method => 'ordered_records_from_metarecord',
255 __PACKAGE__->register_method(
256 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic',
257 method => 'ordered_records_from_metarecord',
262 # XXX: this subroutine and its two registered methods are marked for
263 # deprecation, as they do not work properly in 2.x (these tags are no longer
264 # normalized in mfr) and are not in known use
268 my $isxn = lc(shift());
272 $isxn =~ s/-//o if ($self->api_name =~ /isbn/o);
274 my $tag = ($self->api_name =~ /isbn/o) ? "'020' OR f.tag = '024'" : "'022'";
276 my $fr_table = metabib::full_rec->table;
277 my $bib_table = biblio::record_entry->table;
280 SELECT DISTINCT f.record
282 JOIN $bib_table b ON (b.id = f.record)
285 AND b.deleted IS FALSE
288 my $list = metabib::full_rec->db_Main->selectcol_arrayref($sql, {}, "$isxn%");
289 $client->respond($_) for (@$list);
292 __PACKAGE__->register_method(
293 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
294 method => 'isxn_search',
298 __PACKAGE__->register_method(
299 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.issn',
300 method => 'isxn_search',
305 sub metarecord_copy_count {
311 my $sm_table = metabib::metarecord_source_map->table;
312 my $rd_table = metabib::record_descriptor->table;
313 my $cn_table = asset::call_number->table;
314 my $cp_table = asset::copy->table;
315 my $br_table = biblio::record_entry->table;
316 my $src_table = config::bib_source->table;
317 my $cl_table = asset::copy_location->table;
318 my $cs_table = config::copy_status->table;
319 my $out_table = actor::org_unit_type->table;
321 my $descendants = "actor.org_unit_descendants(u.id)";
322 my $ancestors = "actor.org_unit_ancestors(?) u JOIN $out_table t ON (u.ou_type = t.id)";
324 if ($args{org_unit} < 0) {
325 $args{org_unit} *= -1;
326 $ancestors = "(select org_unit as id from actor.org_lasso_map where lasso = ?) u CROSS JOIN (SELECT -1 AS depth) t";
329 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';
330 $copies_visible = '' if ($self->api_name =~ /staff/o);
332 my (@types,@forms,@blvl);
333 my ($t_filter, $f_filter, $b_filter) = ('','','');
336 my ($t, $f, $b) = split '-', $args{format};
337 @types = split '', $t;
338 @forms = split '', $f;
339 @blvl = split '', $b;
342 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
346 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
350 $b_filter .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
360 JOIN $cn_table cn ON (cn.record = r.source)
361 JOIN $rd_table rd ON (cn.record = rd.record)
362 JOIN $cp_table cp ON (cn.id = cp.call_number)
363 JOIN $cs_table cs ON (cp.status = cs.id)
364 JOIN $cl_table cl ON (cp.location = cl.id)
365 JOIN $descendants a ON (cp.circ_lib = a.id)
366 WHERE r.metarecord = ?
367 AND cn.deleted IS FALSE
368 AND cp.deleted IS FALSE
378 JOIN $cn_table cn ON (cn.record = r.source)
379 JOIN $rd_table rd ON (cn.record = rd.record)
380 JOIN $cp_table cp ON (cn.id = cp.call_number)
381 JOIN $cs_table cs ON (cp.status = cs.id)
382 JOIN $cl_table cl ON (cp.location = cl.id)
383 JOIN $descendants a ON (cp.circ_lib = a.id)
384 WHERE r.metarecord = ?
385 AND cp.status IN (0,7,12)
386 AND cn.deleted IS FALSE
387 AND cp.deleted IS FALSE
397 JOIN $cn_table cn ON (cn.record = r.source)
398 JOIN $rd_table rd ON (cn.record = rd.record)
399 JOIN $cp_table cp ON (cn.id = cp.call_number)
400 JOIN $cs_table cs ON (cp.status = cs.id)
401 JOIN $cl_table cl ON (cp.location = cl.id)
402 WHERE r.metarecord = ?
403 AND cn.deleted IS FALSE
404 AND cp.deleted IS FALSE
405 AND cp.opac_visible IS TRUE
406 AND cs.opac_visible IS TRUE
407 AND cl.opac_visible IS TRUE
416 JOIN $br_table br ON (br.id = r.source)
417 JOIN $src_table src ON (src.id = br.source)
418 WHERE r.metarecord = ?
419 AND src.transcendant IS TRUE
427 my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
428 $sth->execute( ''.$args{metarecord},
432 ''.$args{metarecord},
436 ''.$args{metarecord},
440 ''.$args{metarecord},
444 while ( my $row = $sth->fetchrow_hashref ) {
445 $client->respond( $row );
449 __PACKAGE__->register_method(
450 api_name => 'open-ils.storage.metabib.metarecord.copy_count',
451 method => 'metarecord_copy_count',
456 __PACKAGE__->register_method(
457 api_name => 'open-ils.storage.metabib.metarecord.copy_count.staff',
458 method => 'metarecord_copy_count',
464 sub biblio_multi_search_full_rec {
469 my $class_join = $args{class_join} || 'AND';
470 my $limit = $args{limit} || 100;
471 my $offset = $args{offset} || 0;
472 my $sort = $args{'sort'};
473 my $sort_dir = $args{sort_dir} || 'DESC';
478 for my $arg (@{ $args{searches} }) {
479 my $term = $$arg{term};
480 my $limiters = $$arg{restrict};
482 my ($index_col) = metabib::full_rec->columns('FTS');
483 $index_col ||= 'value';
484 my $search_table = metabib::full_rec->table;
486 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
488 my $fts_where = $fts->sql_where_clause();
489 my @fts_ranks = $fts->fts_rank;
491 my $rank = join(' + ', @fts_ranks);
494 for my $limit (@$limiters) {
495 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
496 # MARC control field; mfr.subfield is NULL
497 push @wheres, "( tag = ? AND $fts_where )";
498 push @binds, $$limit{tag};
499 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
501 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
502 push @binds, $$limit{tag}, $$limit{subfield};
503 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
506 my $where = join(' OR ', @wheres);
508 push @selects, "SELECT record, AVG($rank) as sum FROM $search_table WHERE $where GROUP BY record";
512 my $descendants = defined($args{depth}) ?
513 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
514 "actor.org_unit_descendants($args{org_unit})" ;
517 my $metabib_record_descriptor = metabib::record_descriptor->table;
518 my $metabib_full_rec = metabib::full_rec->table;
519 my $asset_call_number_table = asset::call_number->table;
520 my $asset_copy_table = asset::copy->table;
521 my $cs_table = config::copy_status->table;
522 my $cl_table = asset::copy_location->table;
523 my $br_table = biblio::record_entry->table;
525 my $cj = 'HAVING COUNT(x.record) = ' . scalar(@selects) if ($class_join eq 'AND');
527 '(SELECT x.record, sum(x.sum) FROM (('.
528 join(') UNION ALL (', @selects).
529 ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
531 my $has_vols = 'AND cn.owning_lib = d.id';
532 my $has_copies = 'AND cp.call_number = cn.id';
533 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';
535 if ($self->api_name =~ /staff/o) {
536 $copies_visible = '';
537 $has_copies = '' if ($ou_type == 0);
538 $has_vols = '' if ($ou_type == 0);
541 my ($t_filter, $f_filter) = ('','');
542 my ($a_filter, $l_filter, $lf_filter) = ('','','');
544 if (my $a = $args{audience}) {
545 $a = [$a] if (!ref($a));
548 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
552 if (my $l = $args{language}) {
553 $l = [$l] if (!ref($l));
556 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
560 if (my $f = $args{lit_form}) {
561 $f = [$f] if (!ref($f));
564 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
565 push @binds, @lit_form;
568 if (my $f = $args{item_form}) {
569 $f = [$f] if (!ref($f));
572 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
576 if (my $t = $args{item_type}) {
577 $t = [$t] if (!ref($t));
580 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
586 my ($t, $f) = split '-', $args{format};
587 my @types = split '', $t;
588 my @forms = split '', $f;
590 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
594 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
596 push @binds, @types, @forms;
599 my $relevance = 'sum(f.sum)';
600 $relevance = 1 if (!$copies_visible);
602 my $rank = $relevance;
603 if (lc($sort) eq 'pubdate') {
606 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'9999')::INT
607 FROM $metabib_full_rec frp
608 WHERE frp.record = f.record
610 AND frp.subfield = 'c'
614 } elsif (lc($sort) eq 'create_date') {
616 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
618 } elsif (lc($sort) eq 'edit_date') {
620 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
622 } elsif (lc($sort) eq 'title') {
625 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'zzzzzzzz')
626 FROM $metabib_full_rec frt
627 WHERE frt.record = f.record
629 AND frt.subfield = 'a'
633 } elsif (lc($sort) eq 'author') {
636 SELECT COALESCE(LTRIM(fra.value),'zzzzzzzz')
637 FROM $metabib_full_rec fra
638 WHERE fra.record = f.record
639 AND fra.tag LIKE '1%'
640 AND fra.subfield = 'a'
641 ORDER BY fra.tag::text::int
650 if ($copies_visible) {
652 SELECT f.record, $relevance, count(DISTINCT cp.id), $rank
653 FROM $search_table f,
654 $asset_call_number_table cn,
655 $asset_copy_table cp,
659 $metabib_record_descriptor rd,
661 WHERE br.id = f.record
662 AND cn.record = f.record
663 AND rd.record = f.record
664 AND cp.status = cs.id
665 AND cp.location = cl.id
666 AND br.deleted IS FALSE
667 AND cn.deleted IS FALSE
668 AND cp.deleted IS FALSE
677 GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
678 ORDER BY 4 $sort_dir,3 DESC
682 SELECT f.record, 1, 1, $rank
683 FROM $search_table f,
685 $metabib_record_descriptor rd
686 WHERE br.id = f.record
687 AND rd.record = f.record
688 AND br.deleted IS FALSE
700 $log->debug("Search SQL :: [$select]",DEBUG);
702 my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
703 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
706 $max = 1 if (!@$recs);
708 $max = $$_[1] if ($$_[1] > $max);
712 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
713 next unless ($$rec[0]);
714 my ($rid,$rank,$junk,$skip) = @$rec;
715 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
719 __PACKAGE__->register_method(
720 api_name => 'open-ils.storage.biblio.full_rec.multi_search',
721 method => 'biblio_multi_search_full_rec',
726 __PACKAGE__->register_method(
727 api_name => 'open-ils.storage.biblio.full_rec.multi_search.staff',
728 method => 'biblio_multi_search_full_rec',
734 sub search_full_rec {
740 my $term = $args{term};
741 my $limiters = $args{restrict};
743 my ($index_col) = metabib::full_rec->columns('FTS');
744 $index_col ||= 'value';
745 my $search_table = metabib::full_rec->table;
747 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
749 my $fts_where = $fts->sql_where_clause();
750 my @fts_ranks = $fts->fts_rank;
752 my $rank = join(' + ', @fts_ranks);
756 for my $limit (@$limiters) {
757 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
758 # MARC control field; mfr.subfield is NULL
759 push @wheres, "( tag = ? AND $fts_where )";
760 push @binds, $$limit{tag};
761 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
763 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
764 push @binds, $$limit{tag}, $$limit{subfield};
765 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
768 my $where = join(' OR ', @wheres);
770 my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
772 $log->debug("Search SQL :: [$select]",DEBUG);
774 my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
775 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
777 $client->respond($_) for (@$recs);
780 __PACKAGE__->register_method(
781 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
782 method => 'search_full_rec',
787 __PACKAGE__->register_method(
788 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
789 method => 'search_full_rec',
796 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
797 sub search_class_fts {
802 my $term = $args{term};
803 my $ou = $args{org_unit};
804 my $ou_type = $args{depth};
805 my $limit = $args{limit};
806 my $offset = $args{offset};
808 my $limit_clause = '';
809 my $offset_clause = '';
811 $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
812 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
815 my ($t_filter, $f_filter) = ('','');
818 my ($t, $f) = split '-', $args{format};
819 @types = split '', $t;
820 @forms = split '', $f;
822 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
826 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
832 my $descendants = defined($ou_type) ?
833 "actor.org_unit_descendants($ou, $ou_type)" :
834 "actor.org_unit_descendants($ou)";
836 my $class = $self->{cdbi};
837 my $search_table = $class->table;
839 my $metabib_record_descriptor = metabib::record_descriptor->table;
840 my $metabib_metarecord = metabib::metarecord->table;
841 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
842 my $asset_call_number_table = asset::call_number->table;
843 my $asset_copy_table = asset::copy->table;
844 my $cs_table = config::copy_status->table;
845 my $cl_table = asset::copy_location->table;
847 my ($index_col) = $class->columns('FTS');
848 $index_col ||= 'value';
850 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
851 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
853 my $fts_where = $fts->sql_where_clause;
854 my @fts_ranks = $fts->fts_rank;
856 my $rank = join(' + ', @fts_ranks);
858 my $has_vols = 'AND cn.owning_lib = d.id';
859 my $has_copies = 'AND cp.call_number = cn.id';
860 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';
862 my $visible_count = ', count(DISTINCT cp.id)';
863 my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
865 if ($self->api_name =~ /staff/o) {
866 $copies_visible = '';
867 $visible_count_test = '';
868 $has_copies = '' if ($ou_type == 0);
869 $has_vols = '' if ($ou_type == 0);
872 my $rank_calc = <<" RANK";
874 * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
875 * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
876 * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
877 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
880 $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
882 if ($copies_visible) {
884 SELECT m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
885 FROM $search_table f,
886 $metabib_metarecord_source_map_table m,
887 $asset_call_number_table cn,
888 $asset_copy_table cp,
891 $metabib_record_descriptor rd,
894 AND m.source = f.source
895 AND cn.record = m.source
896 AND rd.record = m.source
897 AND cp.status = cs.id
898 AND cp.location = cl.id
904 GROUP BY 1 $visible_count_test
906 $limit_clause $offset_clause
910 SELECT m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
911 FROM $search_table f,
912 $metabib_metarecord_source_map_table m,
913 $metabib_record_descriptor rd
915 AND m.source = f.source
916 AND rd.record = m.source
921 $limit_clause $offset_clause
925 $log->debug("Field Search SQL :: [$select]",DEBUG);
927 my $SQLstring = join('%',$fts->words);
928 my $REstring = join('\\s+',$fts->words);
929 my $first_word = ($fts->words)[0].'%';
930 my $recs = ($self->api_name =~ /unordered/o) ?
931 $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
932 $class->db_Main->selectall_arrayref($select, {},
933 '%'.lc($SQLstring).'%', # phrase order match
934 lc($first_word), # first word match
935 '^\\s*'.lc($REstring).'\\s*/?\s*$', # full exact match
939 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
941 $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
945 for my $class ( qw/title author subject keyword series identifier/ ) {
946 __PACKAGE__->register_method(
947 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord",
948 method => 'search_class_fts',
951 cdbi => "metabib::${class}_field_entry",
954 __PACKAGE__->register_method(
955 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
956 method => 'search_class_fts',
959 cdbi => "metabib::${class}_field_entry",
962 __PACKAGE__->register_method(
963 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
964 method => 'search_class_fts',
967 cdbi => "metabib::${class}_field_entry",
970 __PACKAGE__->register_method(
971 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
972 method => 'search_class_fts',
975 cdbi => "metabib::${class}_field_entry",
980 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
981 sub search_class_fts_count {
986 my $term = $args{term};
987 my $ou = $args{org_unit};
988 my $ou_type = $args{depth};
989 my $limit = $args{limit} || 100;
990 my $offset = $args{offset} || 0;
992 my $descendants = defined($ou_type) ?
993 "actor.org_unit_descendants($ou, $ou_type)" :
994 "actor.org_unit_descendants($ou)";
997 my ($t_filter, $f_filter) = ('','');
1000 my ($t, $f) = split '-', $args{format};
1001 @types = split '', $t;
1002 @forms = split '', $f;
1004 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1008 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1013 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
1015 my $class = $self->{cdbi};
1016 my $search_table = $class->table;
1018 my $metabib_record_descriptor = metabib::record_descriptor->table;
1019 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1020 my $asset_call_number_table = asset::call_number->table;
1021 my $asset_copy_table = asset::copy->table;
1022 my $cs_table = config::copy_status->table;
1023 my $cl_table = asset::copy_location->table;
1025 my ($index_col) = $class->columns('FTS');
1026 $index_col ||= 'value';
1028 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
1030 my $fts_where = $fts->sql_where_clause;
1032 my $has_vols = 'AND cn.owning_lib = d.id';
1033 my $has_copies = 'AND cp.call_number = cn.id';
1034 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';
1035 if ($self->api_name =~ /staff/o) {
1036 $copies_visible = '';
1037 $has_vols = '' if ($ou_type == 0);
1038 $has_copies = '' if ($ou_type == 0);
1041 # XXX test an "EXISTS version of descendant checking...
1043 if ($copies_visible) {
1045 SELECT count(distinct m.metarecord)
1046 FROM $search_table f,
1047 $metabib_metarecord_source_map_table m,
1048 $metabib_metarecord_source_map_table mr,
1049 $asset_call_number_table cn,
1050 $asset_copy_table cp,
1053 $metabib_record_descriptor rd,
1056 AND mr.source = f.source
1057 AND mr.metarecord = m.metarecord
1058 AND cn.record = m.source
1059 AND rd.record = m.source
1060 AND cp.status = cs.id
1061 AND cp.location = cl.id
1070 SELECT count(distinct m.metarecord)
1071 FROM $search_table f,
1072 $metabib_metarecord_source_map_table m,
1073 $metabib_metarecord_source_map_table mr,
1074 $metabib_record_descriptor rd
1076 AND mr.source = f.source
1077 AND mr.metarecord = m.metarecord
1078 AND rd.record = m.source
1084 $log->debug("Field Search Count SQL :: [$select]",DEBUG);
1086 my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1088 $log->debug("Count Search yielded $recs results.",DEBUG);
1093 for my $class ( qw/title author subject keyword series identifier/ ) {
1094 __PACKAGE__->register_method(
1095 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",
1104 method => 'search_class_fts_count',
1107 cdbi => "metabib::${class}_field_entry",
1113 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1114 sub postfilter_search_class_fts {
1119 my $term = $args{term};
1120 my $sort = $args{'sort'};
1121 my $sort_dir = $args{sort_dir} || 'DESC';
1122 my $ou = $args{org_unit};
1123 my $ou_type = $args{depth};
1124 my $limit = $args{limit} || 10;
1125 my $visibility_limit = $args{visibility_limit} || 5000;
1126 my $offset = $args{offset} || 0;
1128 my $outer_limit = 1000;
1130 my $limit_clause = '';
1131 my $offset_clause = '';
1133 $limit_clause = "LIMIT $outer_limit";
1134 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1136 my (@types,@forms,@lang,@aud,@lit_form);
1137 my ($t_filter, $f_filter) = ('','');
1138 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1139 my ($ot_filter, $of_filter) = ('','');
1140 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1142 if (my $a = $args{audience}) {
1143 $a = [$a] if (!ref($a));
1146 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1147 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1150 if (my $l = $args{language}) {
1151 $l = [$l] if (!ref($l));
1154 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1155 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1158 if (my $f = $args{lit_form}) {
1159 $f = [$f] if (!ref($f));
1162 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1163 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1166 if ($args{format}) {
1167 my ($t, $f) = split '-', $args{format};
1168 @types = split '', $t;
1169 @forms = split '', $f;
1171 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1172 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1176 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1177 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1182 my $descendants = defined($ou_type) ?
1183 "actor.org_unit_descendants($ou, $ou_type)" :
1184 "actor.org_unit_descendants($ou)";
1186 my $class = $self->{cdbi};
1187 my $search_table = $class->table;
1189 my $metabib_full_rec = metabib::full_rec->table;
1190 my $metabib_record_descriptor = metabib::record_descriptor->table;
1191 my $metabib_metarecord = metabib::metarecord->table;
1192 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1193 my $asset_call_number_table = asset::call_number->table;
1194 my $asset_copy_table = asset::copy->table;
1195 my $cs_table = config::copy_status->table;
1196 my $cl_table = asset::copy_location->table;
1197 my $br_table = biblio::record_entry->table;
1199 my ($index_col) = $class->columns('FTS');
1200 $index_col ||= 'value';
1202 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1204 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1206 my $SQLstring = join('%',map { lc($_) } $fts->words);
1207 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1208 my $first_word = lc(($fts->words)[0]).'%';
1210 my $fts_where = $fts->sql_where_clause;
1211 my @fts_ranks = $fts->fts_rank;
1214 $bonus{'metabib::identifier_field_entry'} =
1215 $bonus{'metabib::keyword_field_entry'} = [
1216 { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring }
1219 $bonus{'metabib::title_field_entry'} =
1220 $bonus{'metabib::series_field_entry'} = [
1221 { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1222 { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1223 @{ $bonus{'metabib::keyword_field_entry'} }
1226 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1227 $bonus_list ||= '1';
1229 my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1231 my $relevance = join(' + ', @fts_ranks);
1232 $relevance = <<" RANK";
1233 (SUM( ( $relevance ) * ( $bonus_list ) )/COUNT(m.source))
1236 my $string_default_sort = 'zzzz';
1237 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1239 my $number_default_sort = '9999';
1240 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1242 my $rank = $relevance;
1243 if (lc($sort) eq 'pubdate') {
1246 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1247 FROM $metabib_full_rec frp
1248 WHERE frp.record = mr.master_record
1250 AND frp.subfield = 'c'
1254 } elsif (lc($sort) eq 'create_date') {
1256 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1258 } elsif (lc($sort) eq 'edit_date') {
1260 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1262 } elsif (lc($sort) eq 'title') {
1265 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1266 FROM $metabib_full_rec frt
1267 WHERE frt.record = mr.master_record
1269 AND frt.subfield = 'a'
1273 } elsif (lc($sort) eq 'author') {
1276 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1277 FROM $metabib_full_rec fra
1278 WHERE fra.record = mr.master_record
1279 AND fra.tag LIKE '1%'
1280 AND fra.subfield = 'a'
1281 ORDER BY fra.tag::text::int
1289 my $select = <<" SQL";
1290 SELECT m.metarecord,
1292 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1294 FROM $search_table f,
1295 $metabib_metarecord_source_map_table m,
1296 $metabib_metarecord_source_map_table smrs,
1297 $metabib_metarecord mr,
1298 $metabib_record_descriptor rd
1300 AND smrs.metarecord = mr.id
1301 AND m.source = f.source
1302 AND m.metarecord = mr.id
1303 AND rd.record = smrs.source
1309 GROUP BY m.metarecord
1310 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1311 LIMIT $visibility_limit
1318 FROM $asset_call_number_table cn,
1319 $metabib_metarecord_source_map_table mrs,
1320 $asset_copy_table cp,
1325 $metabib_record_descriptor ord,
1327 WHERE mrs.metarecord = s.metarecord
1328 AND br.id = mrs.source
1329 AND cn.record = mrs.source
1330 AND cp.status = cs.id
1331 AND cp.location = cl.id
1332 AND cn.owning_lib = d.id
1333 AND cp.call_number = cn.id
1334 AND cp.opac_visible IS TRUE
1335 AND cs.opac_visible IS TRUE
1336 AND cl.opac_visible IS TRUE
1337 AND d.opac_visible IS TRUE
1338 AND br.active IS TRUE
1339 AND br.deleted IS FALSE
1340 AND ord.record = mrs.source
1346 ORDER BY 4 $sort_dir
1348 } elsif ($self->api_name !~ /staff/o) {
1355 FROM $asset_call_number_table cn,
1356 $metabib_metarecord_source_map_table mrs,
1357 $asset_copy_table cp,
1362 $metabib_record_descriptor ord
1364 WHERE mrs.metarecord = s.metarecord
1365 AND br.id = mrs.source
1366 AND cn.record = mrs.source
1367 AND cp.status = cs.id
1368 AND cp.location = cl.id
1369 AND cp.circ_lib = d.id
1370 AND cp.call_number = cn.id
1371 AND cp.opac_visible IS TRUE
1372 AND cs.opac_visible IS TRUE
1373 AND cl.opac_visible IS TRUE
1374 AND d.opac_visible IS TRUE
1375 AND br.active IS TRUE
1376 AND br.deleted IS FALSE
1377 AND ord.record = mrs.source
1385 ORDER BY 4 $sort_dir
1394 FROM $asset_call_number_table cn,
1395 $asset_copy_table cp,
1396 $metabib_metarecord_source_map_table mrs,
1399 $metabib_record_descriptor ord
1401 WHERE mrs.metarecord = s.metarecord
1402 AND br.id = mrs.source
1403 AND cn.record = mrs.source
1404 AND cn.id = cp.call_number
1405 AND br.deleted IS FALSE
1406 AND cn.deleted IS FALSE
1407 AND ord.record = mrs.source
1408 AND ( cn.owning_lib = d.id
1409 OR ( cp.circ_lib = d.id
1410 AND cp.deleted IS FALSE
1422 FROM $asset_call_number_table cn,
1423 $metabib_metarecord_source_map_table mrs,
1424 $metabib_record_descriptor ord
1425 WHERE mrs.metarecord = s.metarecord
1426 AND cn.record = mrs.source
1427 AND ord.record = mrs.source
1435 ORDER BY 4 $sort_dir
1440 $log->debug("Field Search SQL :: [$select]",DEBUG);
1442 my $recs = $class->db_Main->selectall_arrayref(
1444 (@bonus_values > 0 ? @bonus_values : () ),
1445 ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1446 @types, @forms, @aud, @lang, @lit_form,
1447 @types, @forms, @aud, @lang, @lit_form,
1448 ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1450 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1453 $max = 1 if (!@$recs);
1455 $max = $$_[1] if ($$_[1] > $max);
1458 my $count = scalar(@$recs);
1459 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1460 my ($mrid,$rank,$skip) = @$rec;
1461 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1466 for my $class ( qw/title author subject keyword series identifier/ ) {
1467 __PACKAGE__->register_method(
1468 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1469 method => 'postfilter_search_class_fts',
1472 cdbi => "metabib::${class}_field_entry",
1475 __PACKAGE__->register_method(
1476 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1477 method => 'postfilter_search_class_fts',
1480 cdbi => "metabib::${class}_field_entry",
1487 my $_cdbi = { title => "metabib::title_field_entry",
1488 author => "metabib::author_field_entry",
1489 subject => "metabib::subject_field_entry",
1490 keyword => "metabib::keyword_field_entry",
1491 series => "metabib::series_field_entry",
1492 identifier => "metabib::identifier_field_entry",
1495 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1496 sub postfilter_search_multi_class_fts {
1501 my $sort = $args{'sort'};
1502 my $sort_dir = $args{sort_dir} || 'DESC';
1503 my $ou = $args{org_unit};
1504 my $ou_type = $args{depth};
1505 my $limit = $args{limit} || 10;
1506 my $offset = $args{offset} || 0;
1507 my $visibility_limit = $args{visibility_limit} || 5000;
1510 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1513 if (!defined($args{org_unit})) {
1514 die "No target organizational unit passed to ".$self->api_name;
1517 if (! scalar( keys %{$args{searches}} )) {
1518 die "No search arguments were passed to ".$self->api_name;
1521 my $outer_limit = 1000;
1523 my $limit_clause = '';
1524 my $offset_clause = '';
1526 $limit_clause = "LIMIT $outer_limit";
1527 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1529 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1530 my ($t_filter, $f_filter, $v_filter) = ('','','');
1531 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1532 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1533 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1535 if ($args{available}) {
1536 $avail_filter = ' AND cp.status IN (0,7,12)';
1539 if (my $a = $args{audience}) {
1540 $a = [$a] if (!ref($a));
1543 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1544 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1547 if (my $l = $args{language}) {
1548 $l = [$l] if (!ref($l));
1551 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1552 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1555 if (my $f = $args{lit_form}) {
1556 $f = [$f] if (!ref($f));
1559 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1560 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1563 if (my $f = $args{item_form}) {
1564 $f = [$f] if (!ref($f));
1567 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1568 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1571 if (my $t = $args{item_type}) {
1572 $t = [$t] if (!ref($t));
1575 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1576 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1579 if (my $v = $args{vr_format}) {
1580 $v = [$v] if (!ref($v));
1583 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1584 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1588 # XXX legacy format and item type support
1589 if ($args{format}) {
1590 my ($t, $f) = split '-', $args{format};
1591 @types = split '', $t;
1592 @forms = split '', $f;
1594 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1595 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1599 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1600 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1606 my $descendants = defined($ou_type) ?
1607 "actor.org_unit_descendants($ou, $ou_type)" :
1608 "actor.org_unit_descendants($ou)";
1610 my $search_table_list = '';
1612 my $join_table_list = '';
1615 my $field_table = config::metabib_field->table;
1619 my $prev_search_group;
1620 my $curr_search_group;
1624 for my $search_group (sort keys %{$args{searches}}) {
1625 (my $search_group_name = $search_group) =~ s/\|/_/gso;
1626 ($search_class,$search_field) = split /\|/, $search_group;
1627 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1629 if ($search_field) {
1630 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1631 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1636 $prev_search_group = $curr_search_group if ($curr_search_group);
1638 $curr_search_group = $search_group_name;
1640 my $class = $_cdbi->{$search_class};
1641 my $search_table = $class->table;
1643 my ($index_col) = $class->columns('FTS');
1644 $index_col ||= 'value';
1647 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1649 my $fts_where = $fts->sql_where_clause;
1650 my @fts_ranks = $fts->fts_rank;
1652 my $SQLstring = join('%',map { lc($_) } $fts->words);
1653 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1654 my $first_word = lc(($fts->words)[0]).'%';
1656 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1657 my $rank = join(' + ', @fts_ranks);
1660 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1661 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1663 $bonus{'series'} = [
1664 { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1665 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1668 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1670 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1671 $bonus_list ||= '1';
1673 push @bonus_lists, $bonus_list;
1674 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1677 #---------------------
1679 $search_table_list .= "$search_table $search_group_name, ";
1680 push @rank_list,$rank;
1681 $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1683 if ($metabib_field) {
1684 $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1685 $metabib_field = undef;
1688 if ($prev_search_group) {
1689 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1693 my $metabib_record_descriptor = metabib::record_descriptor->table;
1694 my $metabib_full_rec = metabib::full_rec->table;
1695 my $metabib_metarecord = metabib::metarecord->table;
1696 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1697 my $asset_call_number_table = asset::call_number->table;
1698 my $asset_copy_table = asset::copy->table;
1699 my $cs_table = config::copy_status->table;
1700 my $cl_table = asset::copy_location->table;
1701 my $br_table = biblio::record_entry->table;
1702 my $source_table = config::bib_source->table;
1704 my $bonuses = join (' * ', @bonus_lists);
1705 my $relevance = join (' + ', @rank_list);
1706 $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1708 my $string_default_sort = 'zzzz';
1709 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1711 my $number_default_sort = '9999';
1712 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1716 my $secondary_sort = <<" SORT";
1718 SELECT COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1719 FROM $metabib_full_rec sfrt,
1720 $metabib_metarecord mr
1721 WHERE sfrt.record = mr.master_record
1722 AND sfrt.tag = '245'
1723 AND sfrt.subfield = 'a'
1728 my $rank = $relevance;
1729 if (lc($sort) eq 'pubdate') {
1732 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1733 FROM $metabib_full_rec frp
1734 WHERE frp.record = mr.master_record
1736 AND frp.subfield = 'c'
1740 } elsif (lc($sort) eq 'create_date') {
1742 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1744 } elsif (lc($sort) eq 'edit_date') {
1746 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1748 } elsif (lc($sort) eq 'title') {
1751 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1752 FROM $metabib_full_rec frt
1753 WHERE frt.record = mr.master_record
1755 AND frt.subfield = 'a'
1759 $secondary_sort = <<" SORT";
1761 SELECT COALESCE(SUBSTRING(sfrp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1762 FROM $metabib_full_rec sfrp
1763 WHERE sfrp.record = mr.master_record
1764 AND sfrp.tag = '260'
1765 AND sfrp.subfield = 'c'
1769 } elsif (lc($sort) eq 'author') {
1772 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1773 FROM $metabib_full_rec fra
1774 WHERE fra.record = mr.master_record
1775 AND fra.tag LIKE '1%'
1776 AND fra.subfield = 'a'
1777 ORDER BY fra.tag::text::int
1782 push @bonus_values, @bonus_values;
1787 my $select = <<" SQL";
1788 SELECT m.metarecord,
1790 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1793 FROM $search_table_list
1794 $metabib_metarecord mr,
1795 $metabib_metarecord_source_map_table m,
1796 $metabib_metarecord_source_map_table smrs
1797 WHERE m.metarecord = smrs.metarecord
1798 AND mr.id = m.metarecord
1801 GROUP BY m.metarecord
1802 -- ORDER BY 4 $sort_dir
1803 LIMIT $visibility_limit
1806 if ($self->api_name !~ /staff/o) {
1813 FROM $asset_call_number_table cn,
1814 $metabib_metarecord_source_map_table mrs,
1815 $asset_copy_table cp,
1820 $metabib_record_descriptor ord
1821 WHERE mrs.metarecord = s.metarecord
1822 AND br.id = mrs.source
1823 AND cn.record = mrs.source
1824 AND cp.status = cs.id
1825 AND cp.location = cl.id
1826 AND cp.circ_lib = d.id
1827 AND cp.call_number = cn.id
1828 AND cp.opac_visible IS TRUE
1829 AND cs.opac_visible IS TRUE
1830 AND cl.opac_visible IS TRUE
1831 AND d.opac_visible IS TRUE
1832 AND br.active IS TRUE
1833 AND br.deleted IS FALSE
1834 AND cp.deleted IS FALSE
1835 AND cn.deleted IS FALSE
1836 AND ord.record = mrs.source
1849 $metabib_metarecord_source_map_table mrs,
1850 $metabib_record_descriptor ord,
1852 WHERE mrs.metarecord = s.metarecord
1853 AND ord.record = mrs.source
1854 AND br.id = mrs.source
1855 AND br.source = src.id
1856 AND src.transcendant IS TRUE
1864 ORDER BY 4 $sort_dir, 5
1871 $metabib_metarecord_source_map_table omrs,
1872 $metabib_record_descriptor ord
1873 WHERE omrs.metarecord = s.metarecord
1874 AND ord.record = omrs.source
1877 FROM $asset_call_number_table cn,
1878 $asset_copy_table cp,
1881 WHERE br.id = omrs.source
1882 AND cn.record = omrs.source
1883 AND br.deleted IS FALSE
1884 AND cn.deleted IS FALSE
1885 AND cp.call_number = cn.id
1886 AND ( cn.owning_lib = d.id
1887 OR ( cp.circ_lib = d.id
1888 AND cp.deleted IS FALSE
1896 FROM $asset_call_number_table cn
1897 WHERE cn.record = omrs.source
1898 AND cn.deleted IS FALSE
1904 $metabib_metarecord_source_map_table mrs,
1905 $metabib_record_descriptor ord,
1907 WHERE mrs.metarecord = s.metarecord
1908 AND br.id = mrs.source
1909 AND br.source = src.id
1910 AND src.transcendant IS TRUE
1926 ORDER BY 4 $sort_dir, 5
1931 $log->debug("Field Search SQL :: [$select]",DEBUG);
1933 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1936 @types, @forms, @vformats, @aud, @lang, @lit_form,
1937 @types, @forms, @vformats, @aud, @lang, @lit_form,
1938 # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1941 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1944 $max = 1 if (!@$recs);
1946 $max = $$_[1] if ($$_[1] > $max);
1949 my $count = scalar(@$recs);
1950 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1951 next unless ($$rec[0]);
1952 my ($mrid,$rank,$skip) = @$rec;
1953 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1958 __PACKAGE__->register_method(
1959 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1960 method => 'postfilter_search_multi_class_fts',
1965 __PACKAGE__->register_method(
1966 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1967 method => 'postfilter_search_multi_class_fts',
1973 __PACKAGE__->register_method(
1974 api_name => "open-ils.storage.metabib.multiclass.search_fts",
1975 method => 'postfilter_search_multi_class_fts',
1980 __PACKAGE__->register_method(
1981 api_name => "open-ils.storage.metabib.multiclass.search_fts.staff",
1982 method => 'postfilter_search_multi_class_fts',
1988 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1989 sub biblio_search_multi_class_fts {
1994 my $sort = $args{'sort'};
1995 my $sort_dir = $args{sort_dir} || 'DESC';
1996 my $ou = $args{org_unit};
1997 my $ou_type = $args{depth};
1998 my $limit = $args{limit} || 10;
1999 my $offset = $args{offset} || 0;
2000 my $pref_lang = $args{preferred_language} || 'eng';
2001 my $visibility_limit = $args{visibility_limit} || 5000;
2004 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2007 if (! scalar( keys %{$args{searches}} )) {
2008 die "No search arguments were passed to ".$self->api_name;
2011 my $outer_limit = 1000;
2013 my $limit_clause = '';
2014 my $offset_clause = '';
2016 $limit_clause = "LIMIT $outer_limit";
2017 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
2019 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
2020 my ($t_filter, $f_filter, $v_filter) = ('','','');
2021 my ($a_filter, $l_filter, $lf_filter) = ('','','');
2022 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
2023 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
2025 if ($args{available}) {
2026 $avail_filter = ' AND cp.status IN (0,7,12)';
2029 if (my $a = $args{audience}) {
2030 $a = [$a] if (!ref($a));
2033 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
2034 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
2037 if (my $l = $args{language}) {
2038 $l = [$l] if (!ref($l));
2041 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
2042 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
2045 if (my $f = $args{lit_form}) {
2046 $f = [$f] if (!ref($f));
2049 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2050 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2053 if (my $f = $args{item_form}) {
2054 $f = [$f] if (!ref($f));
2057 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2058 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2061 if (my $t = $args{item_type}) {
2062 $t = [$t] if (!ref($t));
2065 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2066 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2069 if (my $v = $args{vr_format}) {
2070 $v = [$v] if (!ref($v));
2073 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
2074 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
2077 # XXX legacy format and item type support
2078 if ($args{format}) {
2079 my ($t, $f) = split '-', $args{format};
2080 @types = split '', $t;
2081 @forms = split '', $f;
2083 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2084 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2088 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2089 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2094 my $descendants = defined($ou_type) ?
2095 "actor.org_unit_descendants($ou, $ou_type)" :
2096 "actor.org_unit_descendants($ou)";
2098 my $search_table_list = '';
2100 my $join_table_list = '';
2103 my $field_table = config::metabib_field->table;
2107 my $prev_search_group;
2108 my $curr_search_group;
2112 for my $search_group (sort keys %{$args{searches}}) {
2113 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2114 ($search_class,$search_field) = split /\|/, $search_group;
2115 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2117 if ($search_field) {
2118 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2119 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2124 $prev_search_group = $curr_search_group if ($curr_search_group);
2126 $curr_search_group = $search_group_name;
2128 my $class = $_cdbi->{$search_class};
2129 my $search_table = $class->table;
2131 my ($index_col) = $class->columns('FTS');
2132 $index_col ||= 'value';
2135 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2137 my $fts_where = $fts->sql_where_clause;
2138 my @fts_ranks = $fts->fts_rank;
2140 my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2141 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2142 my $first_word = lc(($fts->words)[0]).'%';
2144 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2145 my $rank = join(' + ', @fts_ranks);
2148 $bonus{'subject'} = [];
2149 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2151 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2153 $bonus{'series'} = [
2154 { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2155 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2158 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2161 push @{ $bonus{'title'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2162 push @{ $bonus{'author'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2163 push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2164 push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2165 push @{ $bonus{'series'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2168 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2169 $bonus_list ||= '1';
2171 push @bonus_lists, $bonus_list;
2172 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2174 #---------------------
2176 $search_table_list .= "$search_table $search_group_name, ";
2177 push @rank_list,$rank;
2178 $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2180 if ($metabib_field) {
2181 $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2182 $metabib_field = undef;
2185 if ($prev_search_group) {
2186 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2190 my $metabib_record_descriptor = metabib::record_descriptor->table;
2191 my $metabib_full_rec = metabib::full_rec->table;
2192 my $metabib_metarecord = metabib::metarecord->table;
2193 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2194 my $asset_call_number_table = asset::call_number->table;
2195 my $asset_copy_table = asset::copy->table;
2196 my $cs_table = config::copy_status->table;
2197 my $cl_table = asset::copy_location->table;
2198 my $br_table = biblio::record_entry->table;
2199 my $source_table = config::bib_source->table;
2202 my $bonuses = join (' * ', @bonus_lists);
2203 my $relevance = join (' + ', @rank_list);
2204 $relevance = "AVG( ($relevance) * ($bonuses) )";
2206 my $string_default_sort = 'zzzz';
2207 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2209 my $number_default_sort = '9999';
2210 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2212 my $rank = $relevance;
2213 if (lc($sort) eq 'pubdate') {
2216 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d{4}'),'$number_default_sort')::INT
2217 FROM $metabib_full_rec frp
2218 WHERE frp.record = b.id
2220 AND frp.subfield = 'c'
2224 } elsif (lc($sort) eq 'create_date') {
2226 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2228 } elsif (lc($sort) eq 'edit_date') {
2230 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2232 } elsif (lc($sort) eq 'title') {
2235 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2236 FROM $metabib_full_rec frt
2237 WHERE frt.record = b.id
2239 AND frt.subfield = 'a'
2243 } elsif (lc($sort) eq 'author') {
2246 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
2247 FROM $metabib_full_rec fra
2248 WHERE fra.record = b.id
2249 AND fra.tag LIKE '1%'
2250 AND fra.subfield = 'a'
2251 ORDER BY fra.tag::text::int
2256 push @bonus_values, @bonus_values;
2261 my $select = <<" SQL";
2266 FROM $search_table_list
2267 $metabib_record_descriptor rd,
2270 WHERE rd.record = b.id
2271 AND b.active IS TRUE
2272 AND b.deleted IS FALSE
2281 GROUP BY b.id, b.source
2282 ORDER BY 3 $sort_dir
2283 LIMIT $visibility_limit
2286 if ($self->api_name !~ /staff/o) {
2291 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2294 FROM $asset_call_number_table cn,
2295 $asset_copy_table cp,
2299 WHERE cn.record = s.id
2300 AND cp.status = cs.id
2301 AND cp.location = cl.id
2302 AND cp.call_number = cn.id
2303 AND cp.opac_visible IS TRUE
2304 AND cs.opac_visible IS TRUE
2305 AND cl.opac_visible IS TRUE
2306 AND d.opac_visible IS TRUE
2307 AND cp.deleted IS FALSE
2308 AND cn.deleted IS FALSE
2309 AND cp.circ_lib = d.id
2313 OR src.transcendant IS TRUE
2314 ORDER BY 3 $sort_dir
2321 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2324 FROM $asset_call_number_table cn,
2325 $asset_copy_table cp,
2327 WHERE cn.record = s.id
2328 AND cp.call_number = cn.id
2329 AND cn.deleted IS FALSE
2330 AND cp.circ_lib = d.id
2331 AND cp.deleted IS FALSE
2337 FROM $asset_call_number_table cn
2338 WHERE cn.record = s.id
2341 OR src.transcendant IS TRUE
2342 ORDER BY 3 $sort_dir
2347 $log->debug("Field Search SQL :: [$select]",DEBUG);
2349 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2351 @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2354 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2356 my $count = scalar(@$recs);
2357 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2358 next unless ($$rec[0]);
2359 my ($mrid,$rank) = @$rec;
2360 $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2365 __PACKAGE__->register_method(
2366 api_name => "open-ils.storage.biblio.multiclass.search_fts.record",
2367 method => 'biblio_search_multi_class_fts',
2372 __PACKAGE__->register_method(
2373 api_name => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2374 method => 'biblio_search_multi_class_fts',
2379 __PACKAGE__->register_method(
2380 api_name => "open-ils.storage.biblio.multiclass.search_fts",
2381 method => 'biblio_search_multi_class_fts',
2386 __PACKAGE__->register_method(
2387 api_name => "open-ils.storage.biblio.multiclass.search_fts.staff",
2388 method => 'biblio_search_multi_class_fts',
2396 my $default_preferred_language;
2397 my $default_preferred_language_weight;
2399 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2405 if (!$locale_map{COMPLETE}) {
2407 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2408 for my $locale ( @locales ) {
2409 $locale_map{lc($locale->code)} = $locale->marc_code;
2411 $locale_map{COMPLETE} = 1;
2415 my $config = OpenSRF::Utils::SettingsClient->new();
2417 if (!$default_preferred_language) {
2419 $default_preferred_language = $config->config_value(
2420 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2421 ) || $config->config_value(
2422 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2427 if (!$default_preferred_language_weight) {
2429 $default_preferred_language_weight = $config->config_value(
2430 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2431 ) || $config->config_value(
2432 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2436 # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2437 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2439 my $ou = $args{org_unit};
2440 my $limit = $args{limit} || 10;
2441 my $offset = $args{offset} || 0;
2444 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2447 if (! scalar( keys %{$args{searches}} )) {
2448 die "No search arguments were passed to ".$self->api_name;
2451 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2453 if (!defined($args{preferred_language})) {
2454 my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2455 $args{preferred_language} =
2456 $locale_map{ lc($ses_locale) } || 'eng';
2459 if (!defined($args{preferred_language_weight})) {
2460 $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2463 if ($args{available}) {
2464 @statuses = (0,7,12);
2467 if (my $s = $args{locations}) {
2468 $s = [$s] if (!ref($s));
2472 if (my $b = $args{between}) {
2473 if (ref($b) && @$b == 2) {
2478 if (my $s = $args{statuses}) {
2479 $s = [$s] if (!ref($s));
2483 if (my $a = $args{audience}) {
2484 $a = [$a] if (!ref($a));
2488 if (my $l = $args{language}) {
2489 $l = [$l] if (!ref($l));
2493 if (my $f = $args{lit_form}) {
2494 $f = [$f] if (!ref($f));
2498 if (my $f = $args{item_form}) {
2499 $f = [$f] if (!ref($f));
2503 if (my $t = $args{item_type}) {
2504 $t = [$t] if (!ref($t));
2508 if (my $b = $args{bib_level}) {
2509 $b = [$b] if (!ref($b));
2513 if (my $v = $args{vr_format}) {
2514 $v = [$v] if (!ref($v));
2518 # XXX legacy format and item type support
2519 if ($args{format}) {
2520 my ($t, $f) = split '-', $args{format};
2521 @types = split '', $t;
2522 @forms = split '', $f;
2525 my %stored_proc_search_args;
2526 for my $search_group (sort keys %{$args{searches}}) {
2527 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2528 my ($search_class,$search_field) = split /\|/, $search_group;
2529 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2531 if ($search_field) {
2532 unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2533 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2538 my $class = $_cdbi->{$search_class};
2539 my $search_table = $class->table;
2541 my ($index_col) = $class->columns('FTS');
2542 $index_col ||= 'value';
2545 my $fts = OpenILS::Application::Storage::FTS->compile(
2546 $search_class => $args{searches}{$search_group}{term},
2547 $search_group_name.'.value',
2548 "$search_group_name.$index_col"
2550 $fts->sql_where_clause; # this builds the ranks for us
2552 my @fts_ranks = $fts->fts_rank;
2553 my @fts_queries = $fts->fts_query;
2554 my @phrases = map { lc($_) } $fts->phrases;
2555 my @words = map { lc($_) } $fts->words;
2557 $stored_proc_search_args{$search_group} = {
2558 fts_rank => \@fts_ranks,
2559 fts_query => \@fts_queries,
2560 phrase => \@phrases,
2566 my $param_search_ou = $ou;
2567 my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2568 my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2569 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2570 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2571 my $param_audience = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud ) . '}$$';
2572 my $param_language = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang ) . '}$$';
2573 my $param_lit_form = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2574 my $param_types = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types ) . '}$$';
2575 my $param_forms = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms ) . '}$$';
2576 my $param_vformats = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2577 my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2578 my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2579 my $param_after = $args{after} ; $param_after = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2580 my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2581 my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2582 my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2583 my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2584 my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2585 my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2586 my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2587 my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2588 my $param_rel_limit = $args{core_limit}; $param_rel_limit ||= 'NULL';
2589 my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2590 my $param_skip_chk = $args{skip_check}; $param_skip_chk ||= 'NULL';
2592 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2594 FROM search.staged_fts(
2595 $param_search_ou\:\:INT,
2596 $param_depth\:\:INT,
2597 $param_searches\:\:TEXT,
2598 $param_statuses\:\:INT[],
2599 $param_locations\:\:INT[],
2600 $param_audience\:\:TEXT[],
2601 $param_language\:\:TEXT[],
2602 $param_lit_form\:\:TEXT[],
2603 $param_types\:\:TEXT[],
2604 $param_forms\:\:TEXT[],
2605 $param_vformats\:\:TEXT[],
2606 $param_bib_level\:\:TEXT[],
2607 $param_before\:\:TEXT,
2608 $param_after\:\:TEXT,
2609 $param_during\:\:TEXT,
2610 $param_between\:\:TEXT[],
2611 $param_pref_lang\:\:TEXT,
2612 $param_pref_lang_multiplier\:\:REAL,
2613 $param_sort\:\:TEXT,
2614 $param_sort_desc\:\:BOOL,
2615 $metarecord\:\:BOOL,
2617 $param_rel_limit\:\:INT,
2618 $param_chk_limit\:\:INT,
2619 $param_skip_chk\:\:INT
2625 my $recs = $sth->fetchall_arrayref({});
2626 my $summary_row = pop @$recs;
2628 my $total = $$summary_row{total};
2629 my $checked = $$summary_row{checked};
2630 my $visible = $$summary_row{visible};
2631 my $deleted = $$summary_row{deleted};
2632 my $excluded = $$summary_row{excluded};
2634 my $estimate = $visible;
2635 if ( $total > $checked && $checked ) {
2637 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2638 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2642 delete $$summary_row{id};
2643 delete $$summary_row{rel};
2644 delete $$summary_row{record};
2646 $client->respond( $summary_row );
2648 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2650 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2651 delete $$rec{checked};
2652 delete $$rec{visible};
2653 delete $$rec{excluded};
2654 delete $$rec{deleted};
2655 delete $$rec{total};
2656 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2658 $client->respond( $rec );
2662 __PACKAGE__->register_method(
2663 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
2664 method => 'staged_fts',
2669 __PACKAGE__->register_method(
2670 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2671 method => 'staged_fts',
2676 __PACKAGE__->register_method(
2677 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
2678 method => 'staged_fts',
2683 __PACKAGE__->register_method(
2684 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2685 method => 'staged_fts',
2691 sub FTS_paging_estimate {
2695 my $checked = shift;
2696 my $visible = shift;
2697 my $excluded = shift;
2698 my $deleted = shift;
2701 my $deleted_ratio = $deleted / $checked;
2702 my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2704 my $exclusion_ratio = $excluded / $checked;
2705 my $delete_adjusted_exclusion_ratio = $checked - $deleted ? $excluded / ($checked - $deleted) : 1;
2707 my $inclusion_ratio = $visible / $checked;
2708 my $delete_adjusted_inclusion_ratio = $checked - $deleted ? $visible / ($checked - $deleted) : 0;
2711 exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2712 inclusion => int($delete_adjusted_total * $inclusion_ratio),
2713 delete_adjusted_exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2714 delete_adjusted_inclusion => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2717 __PACKAGE__->register_method(
2718 api_name => "open-ils.storage.fts_paging_estimate",
2719 method => 'FTS_paging_estimate',
2725 Hash of estimation values based on four variant estimation strategies:
2726 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2727 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2728 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2729 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2732 Helper method used to determin the approximate number of
2733 hits for a search that spans multiple superpages. For
2734 sparse superpages, the inclusion estimate will likely be the
2735 best estimate. The exclusion strategy is the original, but
2736 inclusion is the default.
2739 { name => 'checked',
2740 desc => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2743 { name => 'visible',
2744 desc => 'Number of records visible to the search location on the current superpage.',
2747 { name => 'excluded',
2748 desc => 'Number of records excluded from the search location on the current superpage.',
2751 { name => 'deleted',
2752 desc => 'Number of deleted records on the current superpage.',
2756 desc => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2769 my $term = $$args{term};
2770 my $limit = $$args{max} || 1;
2771 my $min = $$args{min} || 1;
2772 my @classes = @{$$args{class}};
2774 $limit = $min if ($min > $limit);
2777 @classes = ( qw/ title author subject series keyword / );
2781 my $bre_table = biblio::record_entry->table;
2782 my $cn_table = asset::call_number->table;
2783 my $cp_table = asset::copy->table;
2785 for my $search_class ( @classes ) {
2787 my $class = $_cdbi->{$search_class};
2788 my $search_table = $class->table;
2790 my ($index_col) = $class->columns('FTS');
2791 $index_col ||= 'value';
2794 my $where = OpenILS::Application::Storage::FTS
2795 ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2799 SELECT COUNT(DISTINCT X.source)
2800 FROM (SELECT $search_class.source
2801 FROM $search_table $search_class
2802 JOIN $bre_table b ON (b.id = $search_class.source)
2807 HAVING COUNT(DISTINCT X.source) >= $min;
2810 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2811 $matches{$search_class} = $res ? $res->[0] : 0;
2816 __PACKAGE__->register_method(
2817 api_name => "open-ils.storage.search.xref",
2818 method => 'xref_count',
2822 # Takes an abstract query object and recursively turns it back into a string
2824 sub abstract_query2str {
2825 my ($self, $conn, $query) = @_;
2827 return QueryParser::Canonicalize::abstract_query2str_impl($query, 0);
2830 __PACKAGE__->register_method(
2831 api_name => "open-ils.storage.query_parser.abstract_query.canonicalize",
2832 method => "abstract_query2str",
2837 Abstract query parser object, with complete config data. For example input,
2838 see the 'abstract_query' part of the output of an API call like
2839 open-ils.search.biblio.multiclass.query, when called with the return_abstract
2843 return => { type => "string", desc => "String representation of abstract query object" }
2847 sub str2abstract_query {
2848 my ($self, $conn, $query, $qp_opts, $with_config) = @_;
2850 my %use_opts = ( # reasonable defaults? should these even be hardcoded here?
2852 superpage_size => 1000,
2853 core_limit => 25000,
2855 (ref $opts eq 'HASH' ? %$opts : ())
2860 # grab the query parser and initialize it
2861 my $parser = $OpenILS::Application::Storage::QParser;
2864 _initialize_parser($parser) unless $parser->initialization_complete;
2866 my $query = $parser->new(%use_opts)->parse;
2868 return $query->parse_tree->to_abstract_query(with_config => $with_config);
2871 __PACKAGE__->register_method(
2872 api_name => "open-ils.storage.query_parser.abstract_query.from_string",
2873 method => "str2abstract_query",
2877 {desc => "Query", type => "string"},
2878 {desc => q/Arguments for initializing QueryParser (optional)/,
2880 {desc => q/Flag enabling inclusion of QP config in returned object (optional, default false)/,
2883 return => { type => "object", desc => "abstract representation of query parser query" }
2887 sub query_parser_fts {
2893 # grab the query parser and initialize it
2894 my $parser = $OpenILS::Application::Storage::QParser;
2897 _initialize_parser($parser) unless $parser->initialization_complete;
2899 # populate the locale/language map
2900 if (!$locale_map{COMPLETE}) {
2902 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2903 for my $locale ( @locales ) {
2904 $locale_map{lc($locale->code)} = $locale->marc_code;
2906 $locale_map{COMPLETE} = 1;
2910 # I hope we have a query!
2911 if (! $args{query} ) {
2912 die "No query was passed to ".$self->api_name;
2915 my $default_CD_modifiers = OpenSRF::Utils::SettingsClient->new->config_value(
2916 apps => 'open-ils.search' => app_settings => 'default_CD_modifiers'
2919 # Protect against empty / missing default_CD_modifiers setting
2920 if ($default_CD_modifiers and !ref($default_CD_modifiers)) {
2921 $args{query} = "$default_CD_modifiers $args{query}";
2924 my $simple_plan = $args{_simple_plan};
2925 # remove bad chunks of the %args hash
2926 for my $bad ( grep { /^_/ } keys(%args)) {
2927 delete($args{$bad});
2931 # parse the query and supply any query-level %arg-based defaults
2932 # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2933 my $query = $parser->new( %args )->parse;
2935 my $config = OpenSRF::Utils::SettingsClient->new();
2937 # set the locale-based default preferred location
2938 if (!$query->parse_tree->find_filter('preferred_language')) {
2939 $parser->default_preferred_language( $args{preferred_language} );
2941 if (!$parser->default_preferred_language) {
2942 my $ses_locale = $client->session ? $client->session->session_locale : '';
2943 $parser->default_preferred_language( $locale_map{ lc($ses_locale) } );
2946 if (!$parser->default_preferred_language) { # still nothing...
2947 my $tmp_dpl = $config->config_value(
2948 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2949 ) || $config->config_value(
2950 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2953 $parser->default_preferred_language( $tmp_dpl )
2958 # set the global default language multiplier
2959 if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2962 if ($tmp_dplw = $args{preferred_language_weight} || $args{preferred_language_multiplier} ) {
2963 $parser->default_preferred_language_multiplier($tmp_dplw);
2966 $tmp_dplw = $config->config_value(
2967 apps => 'open-ils.search' => app_settings => 'default_preferred_language_weight'
2968 ) || $config->config_value(
2969 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2972 $parser->default_preferred_language_multiplier( $tmp_dplw );
2976 # gather the site, if one is specified, defaulting to the in-query version
2977 my $ou = $args{org_unit};
2978 if (my ($filter) = $query->parse_tree->find_filter('site')) {
2979 $ou = $filter->args->[0] if (@{$filter->args});
2981 $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^(-)?\d+$/);
2983 # gather lasso, as with $ou
2984 my $lasso = $args{lasso};
2985 if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
2986 $lasso = $filter->args->[0] if (@{$filter->args});
2988 $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
2989 $lasso = -$lasso if ($lasso);
2992 # # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
2993 # # gather user lasso, as with $ou and lasso
2994 # my $mylasso = $args{my_lasso};
2995 # if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
2996 # $mylasso = $filter->args->[0] if (@{$filter->args});
2998 # $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
3001 # if we have a lasso, go with that, otherwise ... ou
3002 $ou = $lasso if ($lasso);
3004 # gather the preferred OU, if one is specified, as with $ou
3005 my $pref_ou = $args{pref_ou};
3006 $log->info("pref_ou = $pref_ou");
3007 if (my ($filter) = $query->parse_tree->find_filter('pref_ou')) {
3008 $pref_ou = $filter->args->[0] if (@{$filter->args});
3010 $pref_ou = actor::org_unit->search( { shortname => $pref_ou } )->next->id if ($pref_ou and $pref_ou !~ /^(-)?\d+$/);
3012 # get the default $ou if we have nothing
3013 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
3016 # 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
3017 # gather the depth, if one is specified, defaulting to the in-query version
3018 my $depth = $args{depth};
3019 if (my ($filter) = $query->parse_tree->find_filter('depth')) {
3020 $depth = $filter->args->[0] if (@{$filter->args});
3022 $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
3025 # gather the limit or default to 10
3026 my $limit = $args{check_limit} || 'NULL';
3027 if (my ($filter) = $query->parse_tree->find_filter('limit')) {
3028 $limit = $filter->args->[0] if (@{$filter->args});
3030 if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
3031 $limit = $filter->args->[0] if (@{$filter->args});
3035 # gather the offset or default to 0
3036 my $offset = $args{skip_check} || $args{offset} || 0;
3037 if (my ($filter) = $query->parse_tree->find_filter('offset')) {
3038 $offset = $filter->args->[0] if (@{$filter->args});
3040 if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
3041 $offset = $filter->args->[0] if (@{$filter->args});
3045 # gather the estimation strategy or default to inclusion
3046 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
3047 if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
3048 $estimation_strategy = $filter->args->[0] if (@{$filter->args});
3052 # gather the estimation strategy or default to inclusion
3053 my $core_limit = $args{core_limit};
3054 if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
3055 $core_limit = $filter->args->[0] if (@{$filter->args});
3059 # gather statuses, and then forget those if we have an #available modifier
3061 if (my ($filter) = $query->parse_tree->find_filter('statuses')) {
3062 @statuses = @{$filter->args} if (@{$filter->args});
3064 @statuses = (0,7,12) if ($query->parse_tree->find_modifier('available'));
3069 if (my ($filter) = $query->parse_tree->find_filter('locations')) {
3070 @location = @{$filter->args} if (@{$filter->args});
3073 # gather location_groups
3074 if (my ($filter) = $query->parse_tree->find_filter('location_groups')) {
3075 my @loc_groups = @{$filter->args} if (@{$filter->args});
3077 # collect the mapped locations and add them to the locations() filter
3080 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3081 my $maps = $cstore->request(
3082 'open-ils.cstore.direct.asset.copy_location_group_map.search.atomic',
3083 {lgroup => \@loc_groups})->gather(1);
3085 push(@location, $_->location) for @$maps;
3090 my $param_check = $limit || $query->superpage_size || 'NULL';
3091 my $param_offset = $offset || 'NULL';
3092 my $param_limit = $core_limit || 'NULL';
3094 my $sp = $query->superpage || 1;
3096 $param_offset = ($sp - 1) * $sp_size;
3099 my $param_search_ou = $ou;
3100 my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
3101 my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
3102 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
3103 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
3104 my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
3105 my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
3106 my $param_pref_ou = $pref_ou || 'NULL';
3108 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3109 SELECT * -- bib search: $args{query}
3110 FROM search.query_parser_fts(
3111 $param_search_ou\:\:INT,
3112 $param_depth\:\:INT,
3113 $param_core_query\:\:TEXT,
3114 $param_statuses\:\:INT[],
3115 $param_locations\:\:INT[],
3116 $param_offset\:\:INT,
3117 $param_check\:\:INT,
3118 $param_limit\:\:INT,
3119 $metarecord\:\:BOOL,
3121 $param_pref_ou\:\:INT
3127 my $recs = $sth->fetchall_arrayref({});
3128 my $summary_row = pop @$recs;
3130 my $total = $$summary_row{total};
3131 my $checked = $$summary_row{checked};
3132 my $visible = $$summary_row{visible};
3133 my $deleted = $$summary_row{deleted};
3134 my $excluded = $$summary_row{excluded};
3136 my $estimate = $visible;
3137 if ( $total > $checked && $checked ) {
3139 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
3140 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
3144 delete $$summary_row{id};
3145 delete $$summary_row{rel};
3146 delete $$summary_row{record};
3148 if (defined($simple_plan)) {
3149 $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3151 $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3154 $client->respond( $summary_row );
3156 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
3158 for my $rec (@$recs) {
3159 delete $$rec{checked};
3160 delete $$rec{visible};
3161 delete $$rec{excluded};
3162 delete $$rec{deleted};
3163 delete $$rec{total};
3164 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3166 $client->respond( $rec );
3170 __PACKAGE__->register_method(
3171 api_name => "open-ils.storage.query_parser_search",
3172 method => 'query_parser_fts',
3178 sub query_parser_fts_wrapper {
3183 $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3184 # grab the query parser and initialize it
3185 my $parser = $OpenILS::Application::Storage::QParser;
3188 if (!$parser->initialization_complete) {
3189 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3190 $parser->initialize(
3191 config_record_attr_index_norm_map =>
3193 'open-ils.cstore.direct.config.record_attr_index_norm_map.search.atomic',
3194 { id => { "!=" => undef } },
3195 { flesh => 1, flesh_fields => { crainm => [qw/norm/] }, order_by => [{ class => "crainm", field => "pos" }] }
3197 search_relevance_adjustment =>
3199 'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
3200 { id => { "!=" => undef } }
3202 config_metabib_field =>
3204 'open-ils.cstore.direct.config.metabib_field.search.atomic',
3205 { id => { "!=" => undef } }
3207 config_metabib_search_alias =>
3209 'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
3210 { alias => { "!=" => undef } }
3212 config_metabib_field_index_norm_map =>
3214 'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
3215 { id => { "!=" => undef } },
3216 { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
3218 config_record_attr_definition =>
3220 'open-ils.cstore.direct.config.record_attr_definition.search.atomic',
3221 { name => { "!=" => undef } }
3225 $cstore->disconnect;
3226 die("Cannot initialize $parser!") unless ($parser->initialization_complete);
3229 if (! scalar( keys %{$args{searches}} )) {
3230 die "No search arguments were passed to ".$self->api_name;
3233 $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3234 my $base_query = '';
3235 for my $sclass ( keys %{$args{searches}} ) {
3236 $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3237 $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3240 my $query = $base_query;
3241 $log->debug("Full base query: $base_query", DEBUG);
3243 $query = "$args{facets} $query" if ($args{facets});
3245 if (!$locale_map{COMPLETE}) {
3247 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3248 for my $locale ( @locales ) {
3249 $locale_map{lc($locale->code)} = $locale->marc_code;
3251 $locale_map{COMPLETE} = 1;
3255 my $base_plan = $parser->new( query => $base_query )->parse;
3257 $query = "$query preferred_language($args{preferred_language})"
3258 if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3259 $query = "$query preferred_language_weight($args{preferred_language_weight})"
3260 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'));
3263 # we add these to the end of the query (last-wins) because in wrapper mode we want to retain the behaviour
3264 # of separately specified options taking precidenc -- IOW, the user should not be able to cause a change in,
3265 # say, superpage size by adjusting the query string.
3266 $query = "$query estimation_strategy($args{estimation_strategy})" if ($args{estimation_strategy});
3267 $query = "$query site($args{org_unit})" if ($args{org_unit});
3268 $query = "$query depth($args{depth})" if (defined($args{depth}));
3269 $query = "$query sort($args{sort})" if ($args{sort});
3270 $query = "$query limit($args{limit})" if ($args{limit});
3271 $query = "$query core_limit($args{core_limit})" if ($args{core_limit});
3272 $query = "$query skip_check($args{skip_check})" if ($args{skip_check});
3273 $query = "$query superpage($args{superpage})" if ($args{superpage});
3274 $query = "$query offset($args{offset})" if ($args{offset});
3275 $query = "$query #metarecord" if ($self->api_name =~ /metabib/);
3276 $query = "$query #available" if ($args{available});
3277 $query = "$query #descending" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3278 $query = "$query #staff" if ($self->api_name =~ /staff/);
3279 $query = "$query before($args{before})" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3280 $query = "$query after($args{after})" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3281 $query = "$query during($args{during})" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3282 $query = "$query between($args{between}[0],$args{between}[1])"
3283 if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3286 my (@between,@statuses,@locations,@location_groups,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3288 # XXX legacy format and item type support
3289 if ($args{format}) {
3290 my ($t, $f) = split '-', $args{format};
3291 $args{item_type} = [ split '', $t ];
3292 $args{item_form} = [ split '', $f ];
3295 for my $filter ( qw/locations location_groups statuses between audience language lit_form item_form item_type bib_level vr_format/ ) {
3296 if (my $s = $args{$filter}) {
3297 $s = [$s] if (!ref($s));
3299 my @filter_list = @$s;
3301 next if ($filter eq 'between' and scalar(@filter_list) != 2);
3302 next if (@filter_list == 0);
3304 my $filter_string = join ',', @filter_list;
3305 $query = "$query $filter($filter_string)";
3309 $log->debug("Full QueryParser query: $query", DEBUG);
3311 return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan );
3313 __PACKAGE__->register_method(
3314 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
3315 method => 'query_parser_fts_wrapper',
3320 __PACKAGE__->register_method(
3321 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3322 method => 'query_parser_fts_wrapper',
3327 __PACKAGE__->register_method(
3328 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
3329 method => 'query_parser_fts_wrapper',
3334 __PACKAGE__->register_method(
3335 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3336 method => 'query_parser_fts_wrapper',