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/;
14 my $log = 'OpenSRF::Utils::Logger';
18 sub ordered_records_from_metarecord {
26 my (@types,@forms,@blvl);
29 my ($t, $f, $b) = split '-', $formats;
30 @types = split '', $t;
31 @forms = split '', $f;
37 "actor.org_unit_descendants($org, $depth)" :
38 "actor.org_unit_descendants($org)" ;
41 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';
42 $copies_visible = '' if ($self->api_name =~ /staff/o);
44 my $sm_table = metabib::metarecord_source_map->table;
45 my $rd_table = metabib::record_descriptor->table;
46 my $fr_table = metabib::full_rec->table;
47 my $cn_table = asset::call_number->table;
48 my $cl_table = asset::copy_location->table;
49 my $cp_table = asset::copy->table;
50 my $cs_table = config::copy_status->table;
51 my $src_table = config::bib_source->table;
52 my $out_table = actor::org_unit_type->table;
53 my $br_table = biblio::record_entry->table;
60 FIRST(COALESCE(LTRIM(SUBSTR( value, COALESCE(SUBSTRING(ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'zzzzzzzz')) AS title
72 if ($copies_visible) {
78 WHERE rd.record = sm.source
79 AND fr.record = sm.source
82 AND (EXISTS ((SELECT 1
84 JOIN $cn_table cn ON (cp.call_number = cn.id)
85 JOIN $cs_table cs ON (cp.status = cs.id)
86 JOIN $cl_table cl ON (cp.location = cl.id)
87 JOIN $descendants d ON (cp.circ_lib = d.id)
88 WHERE cn.record = sm.source
94 WHERE src.id = br.source
95 AND src.transcendant IS TRUE))
102 JOIN $br_table br ON (sm.source = br.id)
103 JOIN $fr_table fr ON (fr.record = br.id)
104 JOIN $rd_table rd ON (rd.record = br.id)
105 WHERE sm.metarecord = ?
111 WHERE cn.record = br.id
112 AND cn.deleted = FALSE
113 AND cp.deleted = FALSE
114 AND cp.circ_lib = d.id
115 AND cn.id = cp.call_number
121 WHERE cn.record = br.id
122 AND cn.deleted = FALSE
123 AND cp.deleted = FALSE
124 AND cn.id = cp.call_number
130 WHERE src.id = br.source
131 AND src.transcendant IS TRUE))
137 $sql .= ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
141 $sql .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
145 $sql .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
155 GROUP BY record, item_type, item_form, quality
158 WHEN item_type IS NULL -- default
160 WHEN item_type = '' -- default
162 WHEN item_type IN ('a','t') -- books
164 WHEN item_type = 'g' -- movies
166 WHEN item_type IN ('i','j') -- sound recordings
168 WHEN item_type = 'm' -- software
170 WHEN item_type = 'k' -- images
172 WHEN item_type IN ('e','f') -- maps
174 WHEN item_type IN ('o','p') -- mixed
176 WHEN item_type IN ('c','d') -- music
178 WHEN item_type = 'r' -- 3d
185 my $ids = metabib::metarecord_source_map->db_Main->selectcol_arrayref($sql, {}, "$mr", @types, @forms, @blvl);
186 return $ids if ($self->api_name =~ /atomic$/o);
188 $client->respond( $_ ) for ( @$ids );
192 __PACKAGE__->register_method(
193 api_name => 'open-ils.storage.ordered.metabib.metarecord.records',
194 method => 'ordered_records_from_metarecord',
198 __PACKAGE__->register_method(
199 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff',
200 method => 'ordered_records_from_metarecord',
205 __PACKAGE__->register_method(
206 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.atomic',
207 method => 'ordered_records_from_metarecord',
211 __PACKAGE__->register_method(
212 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic',
213 method => 'ordered_records_from_metarecord',
218 # XXX: this subroutine and its two registered methods are marked for
219 # deprecation, as they do not work properly in 2.x (these tags are no longer
220 # normalized in mfr) and are not in known use
224 my $isxn = lc(shift());
228 $isxn =~ s/-//o if ($self->api_name =~ /isbn/o);
230 my $tag = ($self->api_name =~ /isbn/o) ? "'020' OR f.tag = '024'" : "'022'";
232 my $fr_table = metabib::full_rec->table;
233 my $bib_table = biblio::record_entry->table;
236 SELECT DISTINCT f.record
238 JOIN $bib_table b ON (b.id = f.record)
241 AND b.deleted IS FALSE
244 my $list = metabib::full_rec->db_Main->selectcol_arrayref($sql, {}, "$isxn%");
245 $client->respond($_) for (@$list);
248 __PACKAGE__->register_method(
249 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
250 method => 'isxn_search',
254 __PACKAGE__->register_method(
255 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.issn',
256 method => 'isxn_search',
261 sub metarecord_copy_count {
267 my $sm_table = metabib::metarecord_source_map->table;
268 my $rd_table = metabib::record_descriptor->table;
269 my $cn_table = asset::call_number->table;
270 my $cp_table = asset::copy->table;
271 my $br_table = biblio::record_entry->table;
272 my $src_table = config::bib_source->table;
273 my $cl_table = asset::copy_location->table;
274 my $cs_table = config::copy_status->table;
275 my $out_table = actor::org_unit_type->table;
277 my $descendants = "actor.org_unit_descendants(u.id)";
278 my $ancestors = "actor.org_unit_ancestors(?) u JOIN $out_table t ON (u.ou_type = t.id)";
280 if ($args{org_unit} < 0) {
281 $args{org_unit} *= -1;
282 $ancestors = "(select org_unit as id from actor.org_lasso_map where lasso = ?) u CROSS JOIN (SELECT -1 AS depth) t";
285 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';
286 $copies_visible = '' if ($self->api_name =~ /staff/o);
288 my (@types,@forms,@blvl);
289 my ($t_filter, $f_filter, $b_filter) = ('','','');
292 my ($t, $f, $b) = split '-', $args{format};
293 @types = split '', $t;
294 @forms = split '', $f;
295 @blvl = split '', $b;
298 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
302 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
306 $b_filter .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
316 JOIN $cn_table cn ON (cn.record = r.source)
317 JOIN $rd_table rd ON (cn.record = rd.record)
318 JOIN $cp_table cp ON (cn.id = cp.call_number)
319 JOIN $cs_table cs ON (cp.status = cs.id)
320 JOIN $cl_table cl ON (cp.location = cl.id)
321 JOIN $descendants a ON (cp.circ_lib = a.id)
322 WHERE r.metarecord = ?
323 AND cn.deleted IS FALSE
324 AND cp.deleted IS FALSE
334 JOIN $cn_table cn ON (cn.record = r.source)
335 JOIN $rd_table rd ON (cn.record = rd.record)
336 JOIN $cp_table cp ON (cn.id = cp.call_number)
337 JOIN $cs_table cs ON (cp.status = cs.id)
338 JOIN $cl_table cl ON (cp.location = cl.id)
339 JOIN $descendants a ON (cp.circ_lib = a.id)
340 WHERE r.metarecord = ?
341 AND cp.status IN (0,7,12)
342 AND cn.deleted IS FALSE
343 AND cp.deleted IS FALSE
353 JOIN $cn_table cn ON (cn.record = r.source)
354 JOIN $rd_table rd ON (cn.record = rd.record)
355 JOIN $cp_table cp ON (cn.id = cp.call_number)
356 JOIN $cs_table cs ON (cp.status = cs.id)
357 JOIN $cl_table cl ON (cp.location = cl.id)
358 WHERE r.metarecord = ?
359 AND cn.deleted IS FALSE
360 AND cp.deleted IS FALSE
361 AND cp.opac_visible IS TRUE
362 AND cs.opac_visible IS TRUE
363 AND cl.opac_visible IS TRUE
372 JOIN $br_table br ON (br.id = r.source)
373 JOIN $src_table src ON (src.id = br.source)
374 WHERE r.metarecord = ?
375 AND src.transcendant IS TRUE
383 my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
384 $sth->execute( ''.$args{metarecord},
388 ''.$args{metarecord},
392 ''.$args{metarecord},
396 ''.$args{metarecord},
400 while ( my $row = $sth->fetchrow_hashref ) {
401 $client->respond( $row );
405 __PACKAGE__->register_method(
406 api_name => 'open-ils.storage.metabib.metarecord.copy_count',
407 method => 'metarecord_copy_count',
412 __PACKAGE__->register_method(
413 api_name => 'open-ils.storage.metabib.metarecord.copy_count.staff',
414 method => 'metarecord_copy_count',
420 sub biblio_multi_search_full_rec {
425 my $class_join = $args{class_join} || 'AND';
426 my $limit = $args{limit} || 100;
427 my $offset = $args{offset} || 0;
428 my $sort = $args{'sort'};
429 my $sort_dir = $args{sort_dir} || 'DESC';
434 for my $arg (@{ $args{searches} }) {
435 my $term = $$arg{term};
436 my $limiters = $$arg{restrict};
438 my ($index_col) = metabib::full_rec->columns('FTS');
439 $index_col ||= 'value';
440 my $search_table = metabib::full_rec->table;
442 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
444 my $fts_where = $fts->sql_where_clause();
445 my @fts_ranks = $fts->fts_rank;
447 my $rank = join(' + ', @fts_ranks);
450 for my $limit (@$limiters) {
451 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
452 # MARC control field; mfr.subfield is NULL
453 push @wheres, "( tag = ? AND $fts_where )";
454 push @binds, $$limit{tag};
455 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
457 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
458 push @binds, $$limit{tag}, $$limit{subfield};
459 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
462 my $where = join(' OR ', @wheres);
464 push @selects, "SELECT record, AVG($rank) as sum FROM $search_table WHERE $where GROUP BY record";
468 my $descendants = defined($args{depth}) ?
469 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
470 "actor.org_unit_descendants($args{org_unit})" ;
473 my $metabib_record_descriptor = metabib::record_descriptor->table;
474 my $metabib_full_rec = metabib::full_rec->table;
475 my $asset_call_number_table = asset::call_number->table;
476 my $asset_copy_table = asset::copy->table;
477 my $cs_table = config::copy_status->table;
478 my $cl_table = asset::copy_location->table;
479 my $br_table = biblio::record_entry->table;
481 my $cj = 'HAVING COUNT(x.record) = ' . scalar(@selects) if ($class_join eq 'AND');
483 '(SELECT x.record, sum(x.sum) FROM (('.
484 join(') UNION ALL (', @selects).
485 ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
487 my $has_vols = 'AND cn.owning_lib = d.id';
488 my $has_copies = 'AND cp.call_number = cn.id';
489 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';
491 if ($self->api_name =~ /staff/o) {
492 $copies_visible = '';
493 $has_copies = '' if ($ou_type == 0);
494 $has_vols = '' if ($ou_type == 0);
497 my ($t_filter, $f_filter) = ('','');
498 my ($a_filter, $l_filter, $lf_filter) = ('','','');
500 if (my $a = $args{audience}) {
501 $a = [$a] if (!ref($a));
504 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
508 if (my $l = $args{language}) {
509 $l = [$l] if (!ref($l));
512 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
516 if (my $f = $args{lit_form}) {
517 $f = [$f] if (!ref($f));
520 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
521 push @binds, @lit_form;
524 if (my $f = $args{item_form}) {
525 $f = [$f] if (!ref($f));
528 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
532 if (my $t = $args{item_type}) {
533 $t = [$t] if (!ref($t));
536 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
542 my ($t, $f) = split '-', $args{format};
543 my @types = split '', $t;
544 my @forms = split '', $f;
546 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
550 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
552 push @binds, @types, @forms;
555 my $relevance = 'sum(f.sum)';
556 $relevance = 1 if (!$copies_visible);
558 my $rank = $relevance;
559 if (lc($sort) eq 'pubdate') {
562 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'9999')::INT
563 FROM $metabib_full_rec frp
564 WHERE frp.record = f.record
566 AND frp.subfield = 'c'
570 } elsif (lc($sort) eq 'create_date') {
572 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
574 } elsif (lc($sort) eq 'edit_date') {
576 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
578 } elsif (lc($sort) eq 'title') {
581 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'zzzzzzzz')
582 FROM $metabib_full_rec frt
583 WHERE frt.record = f.record
585 AND frt.subfield = 'a'
589 } elsif (lc($sort) eq 'author') {
592 SELECT COALESCE(LTRIM(fra.value),'zzzzzzzz')
593 FROM $metabib_full_rec fra
594 WHERE fra.record = f.record
595 AND fra.tag LIKE '1%'
596 AND fra.subfield = 'a'
597 ORDER BY fra.tag::text::int
606 if ($copies_visible) {
608 SELECT f.record, $relevance, count(DISTINCT cp.id), $rank
609 FROM $search_table f,
610 $asset_call_number_table cn,
611 $asset_copy_table cp,
615 $metabib_record_descriptor rd,
617 WHERE br.id = f.record
618 AND cn.record = f.record
619 AND rd.record = f.record
620 AND cp.status = cs.id
621 AND cp.location = cl.id
622 AND br.deleted IS FALSE
623 AND cn.deleted IS FALSE
624 AND cp.deleted IS FALSE
633 GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
634 ORDER BY 4 $sort_dir,3 DESC
638 SELECT f.record, 1, 1, $rank
639 FROM $search_table f,
641 $metabib_record_descriptor rd
642 WHERE br.id = f.record
643 AND rd.record = f.record
644 AND br.deleted IS FALSE
656 $log->debug("Search SQL :: [$select]",DEBUG);
658 my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
659 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
662 $max = 1 if (!@$recs);
664 $max = $$_[1] if ($$_[1] > $max);
668 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
669 next unless ($$rec[0]);
670 my ($rid,$rank,$junk,$skip) = @$rec;
671 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
675 __PACKAGE__->register_method(
676 api_name => 'open-ils.storage.biblio.full_rec.multi_search',
677 method => 'biblio_multi_search_full_rec',
682 __PACKAGE__->register_method(
683 api_name => 'open-ils.storage.biblio.full_rec.multi_search.staff',
684 method => 'biblio_multi_search_full_rec',
690 sub search_full_rec {
696 my $term = $args{term};
697 my $limiters = $args{restrict};
699 my ($index_col) = metabib::full_rec->columns('FTS');
700 $index_col ||= 'value';
701 my $search_table = metabib::full_rec->table;
703 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
705 my $fts_where = $fts->sql_where_clause();
706 my @fts_ranks = $fts->fts_rank;
708 my $rank = join(' + ', @fts_ranks);
712 for my $limit (@$limiters) {
713 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
714 # MARC control field; mfr.subfield is NULL
715 push @wheres, "( tag = ? AND $fts_where )";
716 push @binds, $$limit{tag};
717 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
719 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
720 push @binds, $$limit{tag}, $$limit{subfield};
721 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
724 my $where = join(' OR ', @wheres);
726 my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
728 $log->debug("Search SQL :: [$select]",DEBUG);
730 my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
731 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
733 $client->respond($_) for (@$recs);
736 __PACKAGE__->register_method(
737 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
738 method => 'search_full_rec',
743 __PACKAGE__->register_method(
744 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
745 method => 'search_full_rec',
752 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
753 sub search_class_fts {
758 my $term = $args{term};
759 my $ou = $args{org_unit};
760 my $ou_type = $args{depth};
761 my $limit = $args{limit};
762 my $offset = $args{offset};
764 my $limit_clause = '';
765 my $offset_clause = '';
767 $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
768 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
771 my ($t_filter, $f_filter) = ('','');
774 my ($t, $f) = split '-', $args{format};
775 @types = split '', $t;
776 @forms = split '', $f;
778 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
782 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
788 my $descendants = defined($ou_type) ?
789 "actor.org_unit_descendants($ou, $ou_type)" :
790 "actor.org_unit_descendants($ou)";
792 my $class = $self->{cdbi};
793 my $search_table = $class->table;
795 my $metabib_record_descriptor = metabib::record_descriptor->table;
796 my $metabib_metarecord = metabib::metarecord->table;
797 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
798 my $asset_call_number_table = asset::call_number->table;
799 my $asset_copy_table = asset::copy->table;
800 my $cs_table = config::copy_status->table;
801 my $cl_table = asset::copy_location->table;
803 my ($index_col) = $class->columns('FTS');
804 $index_col ||= 'value';
806 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
807 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
809 my $fts_where = $fts->sql_where_clause;
810 my @fts_ranks = $fts->fts_rank;
812 my $rank = join(' + ', @fts_ranks);
814 my $has_vols = 'AND cn.owning_lib = d.id';
815 my $has_copies = 'AND cp.call_number = cn.id';
816 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';
818 my $visible_count = ', count(DISTINCT cp.id)';
819 my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
821 if ($self->api_name =~ /staff/o) {
822 $copies_visible = '';
823 $visible_count_test = '';
824 $has_copies = '' if ($ou_type == 0);
825 $has_vols = '' if ($ou_type == 0);
828 my $rank_calc = <<" RANK";
830 * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
831 * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
832 * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
833 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
836 $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
838 if ($copies_visible) {
840 SELECT m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
841 FROM $search_table f,
842 $metabib_metarecord_source_map_table m,
843 $asset_call_number_table cn,
844 $asset_copy_table cp,
847 $metabib_record_descriptor rd,
850 AND m.source = f.source
851 AND cn.record = m.source
852 AND rd.record = m.source
853 AND cp.status = cs.id
854 AND cp.location = cl.id
860 GROUP BY 1 $visible_count_test
862 $limit_clause $offset_clause
866 SELECT m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
867 FROM $search_table f,
868 $metabib_metarecord_source_map_table m,
869 $metabib_record_descriptor rd
871 AND m.source = f.source
872 AND rd.record = m.source
877 $limit_clause $offset_clause
881 $log->debug("Field Search SQL :: [$select]",DEBUG);
883 my $SQLstring = join('%',$fts->words);
884 my $REstring = join('\\s+',$fts->words);
885 my $first_word = ($fts->words)[0].'%';
886 my $recs = ($self->api_name =~ /unordered/o) ?
887 $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
888 $class->db_Main->selectall_arrayref($select, {},
889 '%'.lc($SQLstring).'%', # phrase order match
890 lc($first_word), # first word match
891 '^\\s*'.lc($REstring).'\\s*/?\s*$', # full exact match
895 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
897 $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
901 for my $class ( qw/title author subject keyword series identifier/ ) {
902 __PACKAGE__->register_method(
903 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord",
904 method => 'search_class_fts',
907 cdbi => "metabib::${class}_field_entry",
910 __PACKAGE__->register_method(
911 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
912 method => 'search_class_fts',
915 cdbi => "metabib::${class}_field_entry",
918 __PACKAGE__->register_method(
919 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
920 method => 'search_class_fts',
923 cdbi => "metabib::${class}_field_entry",
926 __PACKAGE__->register_method(
927 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
928 method => 'search_class_fts',
931 cdbi => "metabib::${class}_field_entry",
936 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
937 sub search_class_fts_count {
942 my $term = $args{term};
943 my $ou = $args{org_unit};
944 my $ou_type = $args{depth};
945 my $limit = $args{limit} || 100;
946 my $offset = $args{offset} || 0;
948 my $descendants = defined($ou_type) ?
949 "actor.org_unit_descendants($ou, $ou_type)" :
950 "actor.org_unit_descendants($ou)";
953 my ($t_filter, $f_filter) = ('','');
956 my ($t, $f) = split '-', $args{format};
957 @types = split '', $t;
958 @forms = split '', $f;
960 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
964 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
969 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
971 my $class = $self->{cdbi};
972 my $search_table = $class->table;
974 my $metabib_record_descriptor = metabib::record_descriptor->table;
975 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
976 my $asset_call_number_table = asset::call_number->table;
977 my $asset_copy_table = asset::copy->table;
978 my $cs_table = config::copy_status->table;
979 my $cl_table = asset::copy_location->table;
981 my ($index_col) = $class->columns('FTS');
982 $index_col ||= 'value';
984 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
986 my $fts_where = $fts->sql_where_clause;
988 my $has_vols = 'AND cn.owning_lib = d.id';
989 my $has_copies = 'AND cp.call_number = cn.id';
990 my $copies_visible = 'AND d.opac_visible IS TRUE AND cp.opac_visible IS TRUE AND cs.opac_visible IS TRUE AND cl.opac_visible IS TRUE';
991 if ($self->api_name =~ /staff/o) {
992 $copies_visible = '';
993 $has_vols = '' if ($ou_type == 0);
994 $has_copies = '' if ($ou_type == 0);
997 # XXX test an "EXISTS version of descendant checking...
999 if ($copies_visible) {
1001 SELECT count(distinct m.metarecord)
1002 FROM $search_table f,
1003 $metabib_metarecord_source_map_table m,
1004 $metabib_metarecord_source_map_table mr,
1005 $asset_call_number_table cn,
1006 $asset_copy_table cp,
1009 $metabib_record_descriptor rd,
1012 AND mr.source = f.source
1013 AND mr.metarecord = m.metarecord
1014 AND cn.record = m.source
1015 AND rd.record = m.source
1016 AND cp.status = cs.id
1017 AND cp.location = cl.id
1026 SELECT count(distinct m.metarecord)
1027 FROM $search_table f,
1028 $metabib_metarecord_source_map_table m,
1029 $metabib_metarecord_source_map_table mr,
1030 $metabib_record_descriptor rd
1032 AND mr.source = f.source
1033 AND mr.metarecord = m.metarecord
1034 AND rd.record = m.source
1040 $log->debug("Field Search Count SQL :: [$select]",DEBUG);
1042 my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1044 $log->debug("Count Search yielded $recs results.",DEBUG);
1049 for my $class ( qw/title author subject keyword series identifier/ ) {
1050 __PACKAGE__->register_method(
1051 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
1052 method => 'search_class_fts_count',
1055 cdbi => "metabib::${class}_field_entry",
1058 __PACKAGE__->register_method(
1059 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
1060 method => 'search_class_fts_count',
1063 cdbi => "metabib::${class}_field_entry",
1069 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1070 sub postfilter_search_class_fts {
1075 my $term = $args{term};
1076 my $sort = $args{'sort'};
1077 my $sort_dir = $args{sort_dir} || 'DESC';
1078 my $ou = $args{org_unit};
1079 my $ou_type = $args{depth};
1080 my $limit = $args{limit} || 10;
1081 my $visibility_limit = $args{visibility_limit} || 5000;
1082 my $offset = $args{offset} || 0;
1084 my $outer_limit = 1000;
1086 my $limit_clause = '';
1087 my $offset_clause = '';
1089 $limit_clause = "LIMIT $outer_limit";
1090 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1092 my (@types,@forms,@lang,@aud,@lit_form);
1093 my ($t_filter, $f_filter) = ('','');
1094 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1095 my ($ot_filter, $of_filter) = ('','');
1096 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1098 if (my $a = $args{audience}) {
1099 $a = [$a] if (!ref($a));
1102 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1103 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1106 if (my $l = $args{language}) {
1107 $l = [$l] if (!ref($l));
1110 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1111 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1114 if (my $f = $args{lit_form}) {
1115 $f = [$f] if (!ref($f));
1118 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1119 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1122 if ($args{format}) {
1123 my ($t, $f) = split '-', $args{format};
1124 @types = split '', $t;
1125 @forms = split '', $f;
1127 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1128 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1132 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1133 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1138 my $descendants = defined($ou_type) ?
1139 "actor.org_unit_descendants($ou, $ou_type)" :
1140 "actor.org_unit_descendants($ou)";
1142 my $class = $self->{cdbi};
1143 my $search_table = $class->table;
1145 my $metabib_full_rec = metabib::full_rec->table;
1146 my $metabib_record_descriptor = metabib::record_descriptor->table;
1147 my $metabib_metarecord = metabib::metarecord->table;
1148 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1149 my $asset_call_number_table = asset::call_number->table;
1150 my $asset_copy_table = asset::copy->table;
1151 my $cs_table = config::copy_status->table;
1152 my $cl_table = asset::copy_location->table;
1153 my $br_table = biblio::record_entry->table;
1155 my ($index_col) = $class->columns('FTS');
1156 $index_col ||= 'value';
1158 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1160 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1162 my $SQLstring = join('%',map { lc($_) } $fts->words);
1163 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1164 my $first_word = lc(($fts->words)[0]).'%';
1166 my $fts_where = $fts->sql_where_clause;
1167 my @fts_ranks = $fts->fts_rank;
1170 $bonus{'metabib::identifier_field_entry'} =
1171 $bonus{'metabib::keyword_field_entry'} = [
1172 { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring }
1175 $bonus{'metabib::title_field_entry'} =
1176 $bonus{'metabib::series_field_entry'} = [
1177 { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1178 { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1179 @{ $bonus{'metabib::keyword_field_entry'} }
1182 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1183 $bonus_list ||= '1';
1185 my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1187 my $relevance = join(' + ', @fts_ranks);
1188 $relevance = <<" RANK";
1189 (SUM( ( $relevance ) * ( $bonus_list ) )/COUNT(m.source))
1192 my $string_default_sort = 'zzzz';
1193 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1195 my $number_default_sort = '9999';
1196 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1198 my $rank = $relevance;
1199 if (lc($sort) eq 'pubdate') {
1202 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1203 FROM $metabib_full_rec frp
1204 WHERE frp.record = mr.master_record
1206 AND frp.subfield = 'c'
1210 } elsif (lc($sort) eq 'create_date') {
1212 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1214 } elsif (lc($sort) eq 'edit_date') {
1216 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1218 } elsif (lc($sort) eq 'title') {
1221 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1222 FROM $metabib_full_rec frt
1223 WHERE frt.record = mr.master_record
1225 AND frt.subfield = 'a'
1229 } elsif (lc($sort) eq 'author') {
1232 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1233 FROM $metabib_full_rec fra
1234 WHERE fra.record = mr.master_record
1235 AND fra.tag LIKE '1%'
1236 AND fra.subfield = 'a'
1237 ORDER BY fra.tag::text::int
1245 my $select = <<" SQL";
1246 SELECT m.metarecord,
1248 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1250 FROM $search_table f,
1251 $metabib_metarecord_source_map_table m,
1252 $metabib_metarecord_source_map_table smrs,
1253 $metabib_metarecord mr,
1254 $metabib_record_descriptor rd
1256 AND smrs.metarecord = mr.id
1257 AND m.source = f.source
1258 AND m.metarecord = mr.id
1259 AND rd.record = smrs.source
1265 GROUP BY m.metarecord
1266 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1267 LIMIT $visibility_limit
1274 FROM $asset_call_number_table cn,
1275 $metabib_metarecord_source_map_table mrs,
1276 $asset_copy_table cp,
1281 $metabib_record_descriptor ord,
1283 WHERE mrs.metarecord = s.metarecord
1284 AND br.id = mrs.source
1285 AND cn.record = mrs.source
1286 AND cp.status = cs.id
1287 AND cp.location = cl.id
1288 AND cn.owning_lib = d.id
1289 AND cp.call_number = cn.id
1290 AND cp.opac_visible IS TRUE
1291 AND cs.opac_visible IS TRUE
1292 AND cl.opac_visible IS TRUE
1293 AND d.opac_visible IS TRUE
1294 AND br.active IS TRUE
1295 AND br.deleted IS FALSE
1296 AND ord.record = mrs.source
1302 ORDER BY 4 $sort_dir
1304 } elsif ($self->api_name !~ /staff/o) {
1311 FROM $asset_call_number_table cn,
1312 $metabib_metarecord_source_map_table mrs,
1313 $asset_copy_table cp,
1318 $metabib_record_descriptor ord
1320 WHERE mrs.metarecord = s.metarecord
1321 AND br.id = mrs.source
1322 AND cn.record = mrs.source
1323 AND cp.status = cs.id
1324 AND cp.location = cl.id
1325 AND cp.circ_lib = d.id
1326 AND cp.call_number = cn.id
1327 AND cp.opac_visible IS TRUE
1328 AND cs.opac_visible IS TRUE
1329 AND cl.opac_visible IS TRUE
1330 AND d.opac_visible IS TRUE
1331 AND br.active IS TRUE
1332 AND br.deleted IS FALSE
1333 AND ord.record = mrs.source
1341 ORDER BY 4 $sort_dir
1350 FROM $asset_call_number_table cn,
1351 $asset_copy_table cp,
1352 $metabib_metarecord_source_map_table mrs,
1355 $metabib_record_descriptor ord
1357 WHERE mrs.metarecord = s.metarecord
1358 AND br.id = mrs.source
1359 AND cn.record = mrs.source
1360 AND cn.id = cp.call_number
1361 AND br.deleted IS FALSE
1362 AND cn.deleted IS FALSE
1363 AND ord.record = mrs.source
1364 AND ( cn.owning_lib = d.id
1365 OR ( cp.circ_lib = d.id
1366 AND cp.deleted IS FALSE
1378 FROM $asset_call_number_table cn,
1379 $metabib_metarecord_source_map_table mrs,
1380 $metabib_record_descriptor ord
1381 WHERE mrs.metarecord = s.metarecord
1382 AND cn.record = mrs.source
1383 AND ord.record = mrs.source
1391 ORDER BY 4 $sort_dir
1396 $log->debug("Field Search SQL :: [$select]",DEBUG);
1398 my $recs = $class->db_Main->selectall_arrayref(
1400 (@bonus_values > 0 ? @bonus_values : () ),
1401 ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1402 @types, @forms, @aud, @lang, @lit_form,
1403 @types, @forms, @aud, @lang, @lit_form,
1404 ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1406 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1409 $max = 1 if (!@$recs);
1411 $max = $$_[1] if ($$_[1] > $max);
1414 my $count = scalar(@$recs);
1415 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1416 my ($mrid,$rank,$skip) = @$rec;
1417 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1422 for my $class ( qw/title author subject keyword series identifier/ ) {
1423 __PACKAGE__->register_method(
1424 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1425 method => 'postfilter_search_class_fts',
1428 cdbi => "metabib::${class}_field_entry",
1431 __PACKAGE__->register_method(
1432 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1433 method => 'postfilter_search_class_fts',
1436 cdbi => "metabib::${class}_field_entry",
1443 my $_cdbi = { title => "metabib::title_field_entry",
1444 author => "metabib::author_field_entry",
1445 subject => "metabib::subject_field_entry",
1446 keyword => "metabib::keyword_field_entry",
1447 series => "metabib::series_field_entry",
1448 identifier => "metabib::identifier_field_entry",
1451 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1452 sub postfilter_search_multi_class_fts {
1457 my $sort = $args{'sort'};
1458 my $sort_dir = $args{sort_dir} || 'DESC';
1459 my $ou = $args{org_unit};
1460 my $ou_type = $args{depth};
1461 my $limit = $args{limit} || 10;
1462 my $offset = $args{offset} || 0;
1463 my $visibility_limit = $args{visibility_limit} || 5000;
1466 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1469 if (!defined($args{org_unit})) {
1470 die "No target organizational unit passed to ".$self->api_name;
1473 if (! scalar( keys %{$args{searches}} )) {
1474 die "No search arguments were passed to ".$self->api_name;
1477 my $outer_limit = 1000;
1479 my $limit_clause = '';
1480 my $offset_clause = '';
1482 $limit_clause = "LIMIT $outer_limit";
1483 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1485 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1486 my ($t_filter, $f_filter, $v_filter) = ('','','');
1487 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1488 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1489 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1491 if ($args{available}) {
1492 $avail_filter = ' AND cp.status IN (0,7,12)';
1495 if (my $a = $args{audience}) {
1496 $a = [$a] if (!ref($a));
1499 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1500 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1503 if (my $l = $args{language}) {
1504 $l = [$l] if (!ref($l));
1507 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1508 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1511 if (my $f = $args{lit_form}) {
1512 $f = [$f] if (!ref($f));
1515 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1516 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1519 if (my $f = $args{item_form}) {
1520 $f = [$f] if (!ref($f));
1523 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1524 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1527 if (my $t = $args{item_type}) {
1528 $t = [$t] if (!ref($t));
1531 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1532 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1535 if (my $v = $args{vr_format}) {
1536 $v = [$v] if (!ref($v));
1539 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1540 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1544 # XXX legacy format and item type support
1545 if ($args{format}) {
1546 my ($t, $f) = split '-', $args{format};
1547 @types = split '', $t;
1548 @forms = split '', $f;
1550 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1551 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1555 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1556 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1562 my $descendants = defined($ou_type) ?
1563 "actor.org_unit_descendants($ou, $ou_type)" :
1564 "actor.org_unit_descendants($ou)";
1566 my $search_table_list = '';
1568 my $join_table_list = '';
1571 my $field_table = config::metabib_field->table;
1575 my $prev_search_group;
1576 my $curr_search_group;
1580 for my $search_group (sort keys %{$args{searches}}) {
1581 (my $search_group_name = $search_group) =~ s/\|/_/gso;
1582 ($search_class,$search_field) = split /\|/, $search_group;
1583 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1585 if ($search_field) {
1586 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1587 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1592 $prev_search_group = $curr_search_group if ($curr_search_group);
1594 $curr_search_group = $search_group_name;
1596 my $class = $_cdbi->{$search_class};
1597 my $search_table = $class->table;
1599 my ($index_col) = $class->columns('FTS');
1600 $index_col ||= 'value';
1603 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1605 my $fts_where = $fts->sql_where_clause;
1606 my @fts_ranks = $fts->fts_rank;
1608 my $SQLstring = join('%',map { lc($_) } $fts->words);
1609 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1610 my $first_word = lc(($fts->words)[0]).'%';
1612 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1613 my $rank = join(' + ', @fts_ranks);
1616 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1617 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1619 $bonus{'series'} = [
1620 { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1621 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1624 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1626 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1627 $bonus_list ||= '1';
1629 push @bonus_lists, $bonus_list;
1630 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1633 #---------------------
1635 $search_table_list .= "$search_table $search_group_name, ";
1636 push @rank_list,$rank;
1637 $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1639 if ($metabib_field) {
1640 $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1641 $metabib_field = undef;
1644 if ($prev_search_group) {
1645 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1649 my $metabib_record_descriptor = metabib::record_descriptor->table;
1650 my $metabib_full_rec = metabib::full_rec->table;
1651 my $metabib_metarecord = metabib::metarecord->table;
1652 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1653 my $asset_call_number_table = asset::call_number->table;
1654 my $asset_copy_table = asset::copy->table;
1655 my $cs_table = config::copy_status->table;
1656 my $cl_table = asset::copy_location->table;
1657 my $br_table = biblio::record_entry->table;
1658 my $source_table = config::bib_source->table;
1660 my $bonuses = join (' * ', @bonus_lists);
1661 my $relevance = join (' + ', @rank_list);
1662 $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1664 my $string_default_sort = 'zzzz';
1665 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1667 my $number_default_sort = '9999';
1668 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1672 my $secondary_sort = <<" SORT";
1674 SELECT COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1675 FROM $metabib_full_rec sfrt,
1676 $metabib_metarecord mr
1677 WHERE sfrt.record = mr.master_record
1678 AND sfrt.tag = '245'
1679 AND sfrt.subfield = 'a'
1684 my $rank = $relevance;
1685 if (lc($sort) eq 'pubdate') {
1688 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1689 FROM $metabib_full_rec frp
1690 WHERE frp.record = mr.master_record
1692 AND frp.subfield = 'c'
1696 } elsif (lc($sort) eq 'create_date') {
1698 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1700 } elsif (lc($sort) eq 'edit_date') {
1702 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1704 } elsif (lc($sort) eq 'title') {
1707 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1708 FROM $metabib_full_rec frt
1709 WHERE frt.record = mr.master_record
1711 AND frt.subfield = 'a'
1715 $secondary_sort = <<" SORT";
1717 SELECT COALESCE(SUBSTRING(sfrp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1718 FROM $metabib_full_rec sfrp
1719 WHERE sfrp.record = mr.master_record
1720 AND sfrp.tag = '260'
1721 AND sfrp.subfield = 'c'
1725 } elsif (lc($sort) eq 'author') {
1728 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1729 FROM $metabib_full_rec fra
1730 WHERE fra.record = mr.master_record
1731 AND fra.tag LIKE '1%'
1732 AND fra.subfield = 'a'
1733 ORDER BY fra.tag::text::int
1738 push @bonus_values, @bonus_values;
1743 my $select = <<" SQL";
1744 SELECT m.metarecord,
1746 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1749 FROM $search_table_list
1750 $metabib_metarecord mr,
1751 $metabib_metarecord_source_map_table m,
1752 $metabib_metarecord_source_map_table smrs
1753 WHERE m.metarecord = smrs.metarecord
1754 AND mr.id = m.metarecord
1757 GROUP BY m.metarecord
1758 -- ORDER BY 4 $sort_dir
1759 LIMIT $visibility_limit
1762 if ($self->api_name !~ /staff/o) {
1769 FROM $asset_call_number_table cn,
1770 $metabib_metarecord_source_map_table mrs,
1771 $asset_copy_table cp,
1776 $metabib_record_descriptor ord
1777 WHERE mrs.metarecord = s.metarecord
1778 AND br.id = mrs.source
1779 AND cn.record = mrs.source
1780 AND cp.status = cs.id
1781 AND cp.location = cl.id
1782 AND cp.circ_lib = d.id
1783 AND cp.call_number = cn.id
1784 AND cp.opac_visible IS TRUE
1785 AND cs.opac_visible IS TRUE
1786 AND cl.opac_visible IS TRUE
1787 AND d.opac_visible IS TRUE
1788 AND br.active IS TRUE
1789 AND br.deleted IS FALSE
1790 AND cp.deleted IS FALSE
1791 AND cn.deleted IS FALSE
1792 AND ord.record = mrs.source
1805 $metabib_metarecord_source_map_table mrs,
1806 $metabib_record_descriptor ord,
1808 WHERE mrs.metarecord = s.metarecord
1809 AND ord.record = mrs.source
1810 AND br.id = mrs.source
1811 AND br.source = src.id
1812 AND src.transcendant IS TRUE
1820 ORDER BY 4 $sort_dir, 5
1827 $metabib_metarecord_source_map_table omrs,
1828 $metabib_record_descriptor ord
1829 WHERE omrs.metarecord = s.metarecord
1830 AND ord.record = omrs.source
1833 FROM $asset_call_number_table cn,
1834 $asset_copy_table cp,
1837 WHERE br.id = omrs.source
1838 AND cn.record = omrs.source
1839 AND br.deleted IS FALSE
1840 AND cn.deleted IS FALSE
1841 AND cp.call_number = cn.id
1842 AND ( cn.owning_lib = d.id
1843 OR ( cp.circ_lib = d.id
1844 AND cp.deleted IS FALSE
1852 FROM $asset_call_number_table cn
1853 WHERE cn.record = omrs.source
1854 AND cn.deleted IS FALSE
1860 $metabib_metarecord_source_map_table mrs,
1861 $metabib_record_descriptor ord,
1863 WHERE mrs.metarecord = s.metarecord
1864 AND br.id = mrs.source
1865 AND br.source = src.id
1866 AND src.transcendant IS TRUE
1882 ORDER BY 4 $sort_dir, 5
1887 $log->debug("Field Search SQL :: [$select]",DEBUG);
1889 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1892 @types, @forms, @vformats, @aud, @lang, @lit_form,
1893 @types, @forms, @vformats, @aud, @lang, @lit_form,
1894 # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1897 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1900 $max = 1 if (!@$recs);
1902 $max = $$_[1] if ($$_[1] > $max);
1905 my $count = scalar(@$recs);
1906 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1907 next unless ($$rec[0]);
1908 my ($mrid,$rank,$skip) = @$rec;
1909 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1914 __PACKAGE__->register_method(
1915 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1916 method => 'postfilter_search_multi_class_fts',
1921 __PACKAGE__->register_method(
1922 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1923 method => 'postfilter_search_multi_class_fts',
1929 __PACKAGE__->register_method(
1930 api_name => "open-ils.storage.metabib.multiclass.search_fts",
1931 method => 'postfilter_search_multi_class_fts',
1936 __PACKAGE__->register_method(
1937 api_name => "open-ils.storage.metabib.multiclass.search_fts.staff",
1938 method => 'postfilter_search_multi_class_fts',
1944 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1945 sub biblio_search_multi_class_fts {
1950 my $sort = $args{'sort'};
1951 my $sort_dir = $args{sort_dir} || 'DESC';
1952 my $ou = $args{org_unit};
1953 my $ou_type = $args{depth};
1954 my $limit = $args{limit} || 10;
1955 my $offset = $args{offset} || 0;
1956 my $pref_lang = $args{prefered_language} || 'eng';
1957 my $visibility_limit = $args{visibility_limit} || 5000;
1960 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1963 if (! scalar( keys %{$args{searches}} )) {
1964 die "No search arguments were passed to ".$self->api_name;
1967 my $outer_limit = 1000;
1969 my $limit_clause = '';
1970 my $offset_clause = '';
1972 $limit_clause = "LIMIT $outer_limit";
1973 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1975 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1976 my ($t_filter, $f_filter, $v_filter) = ('','','');
1977 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1978 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1979 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1981 if ($args{available}) {
1982 $avail_filter = ' AND cp.status IN (0,7,12)';
1985 if (my $a = $args{audience}) {
1986 $a = [$a] if (!ref($a));
1989 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1990 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1993 if (my $l = $args{language}) {
1994 $l = [$l] if (!ref($l));
1997 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1998 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
2001 if (my $f = $args{lit_form}) {
2002 $f = [$f] if (!ref($f));
2005 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2006 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2009 if (my $f = $args{item_form}) {
2010 $f = [$f] if (!ref($f));
2013 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2014 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2017 if (my $t = $args{item_type}) {
2018 $t = [$t] if (!ref($t));
2021 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2022 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2025 if (my $v = $args{vr_format}) {
2026 $v = [$v] if (!ref($v));
2029 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
2030 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
2033 # XXX legacy format and item type support
2034 if ($args{format}) {
2035 my ($t, $f) = split '-', $args{format};
2036 @types = split '', $t;
2037 @forms = split '', $f;
2039 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2040 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2044 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2045 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2050 my $descendants = defined($ou_type) ?
2051 "actor.org_unit_descendants($ou, $ou_type)" :
2052 "actor.org_unit_descendants($ou)";
2054 my $search_table_list = '';
2056 my $join_table_list = '';
2059 my $field_table = config::metabib_field->table;
2063 my $prev_search_group;
2064 my $curr_search_group;
2068 for my $search_group (sort keys %{$args{searches}}) {
2069 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2070 ($search_class,$search_field) = split /\|/, $search_group;
2071 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2073 if ($search_field) {
2074 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2075 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2080 $prev_search_group = $curr_search_group if ($curr_search_group);
2082 $curr_search_group = $search_group_name;
2084 my $class = $_cdbi->{$search_class};
2085 my $search_table = $class->table;
2087 my ($index_col) = $class->columns('FTS');
2088 $index_col ||= 'value';
2091 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2093 my $fts_where = $fts->sql_where_clause;
2094 my @fts_ranks = $fts->fts_rank;
2096 my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2097 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2098 my $first_word = lc(($fts->words)[0]).'%';
2100 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2101 my $rank = join(' + ', @fts_ranks);
2104 $bonus{'subject'} = [];
2105 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2107 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2109 $bonus{'series'} = [
2110 { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2111 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2114 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2117 push @{ $bonus{'title'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2118 push @{ $bonus{'author'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2119 push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2120 push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2121 push @{ $bonus{'series'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2124 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2125 $bonus_list ||= '1';
2127 push @bonus_lists, $bonus_list;
2128 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2130 #---------------------
2132 $search_table_list .= "$search_table $search_group_name, ";
2133 push @rank_list,$rank;
2134 $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2136 if ($metabib_field) {
2137 $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2138 $metabib_field = undef;
2141 if ($prev_search_group) {
2142 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2146 my $metabib_record_descriptor = metabib::record_descriptor->table;
2147 my $metabib_full_rec = metabib::full_rec->table;
2148 my $metabib_metarecord = metabib::metarecord->table;
2149 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2150 my $asset_call_number_table = asset::call_number->table;
2151 my $asset_copy_table = asset::copy->table;
2152 my $cs_table = config::copy_status->table;
2153 my $cl_table = asset::copy_location->table;
2154 my $br_table = biblio::record_entry->table;
2155 my $source_table = config::bib_source->table;
2158 my $bonuses = join (' * ', @bonus_lists);
2159 my $relevance = join (' + ', @rank_list);
2160 $relevance = "AVG( ($relevance) * ($bonuses) )";
2162 my $string_default_sort = 'zzzz';
2163 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2165 my $number_default_sort = '9999';
2166 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2168 my $rank = $relevance;
2169 if (lc($sort) eq 'pubdate') {
2172 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d{4}'),'$number_default_sort')::INT
2173 FROM $metabib_full_rec frp
2174 WHERE frp.record = b.id
2176 AND frp.subfield = 'c'
2180 } elsif (lc($sort) eq 'create_date') {
2182 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2184 } elsif (lc($sort) eq 'edit_date') {
2186 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2188 } elsif (lc($sort) eq 'title') {
2191 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2192 FROM $metabib_full_rec frt
2193 WHERE frt.record = b.id
2195 AND frt.subfield = 'a'
2199 } elsif (lc($sort) eq 'author') {
2202 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
2203 FROM $metabib_full_rec fra
2204 WHERE fra.record = b.id
2205 AND fra.tag LIKE '1%'
2206 AND fra.subfield = 'a'
2207 ORDER BY fra.tag::text::int
2212 push @bonus_values, @bonus_values;
2217 my $select = <<" SQL";
2222 FROM $search_table_list
2223 $metabib_record_descriptor rd,
2226 WHERE rd.record = b.id
2227 AND b.active IS TRUE
2228 AND b.deleted IS FALSE
2237 GROUP BY b.id, b.source
2238 ORDER BY 3 $sort_dir
2239 LIMIT $visibility_limit
2242 if ($self->api_name !~ /staff/o) {
2247 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2250 FROM $asset_call_number_table cn,
2251 $asset_copy_table cp,
2255 WHERE cn.record = s.id
2256 AND cp.status = cs.id
2257 AND cp.location = cl.id
2258 AND cp.call_number = cn.id
2259 AND cp.opac_visible IS TRUE
2260 AND cs.opac_visible IS TRUE
2261 AND cl.opac_visible IS TRUE
2262 AND d.opac_visible IS TRUE
2263 AND cp.deleted IS FALSE
2264 AND cn.deleted IS FALSE
2265 AND cp.circ_lib = d.id
2269 OR src.transcendant IS TRUE
2270 ORDER BY 3 $sort_dir
2277 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2280 FROM $asset_call_number_table cn,
2281 $asset_copy_table cp,
2283 WHERE cn.record = s.id
2284 AND cp.call_number = cn.id
2285 AND cn.deleted IS FALSE
2286 AND cp.circ_lib = d.id
2287 AND cp.deleted IS FALSE
2293 FROM $asset_call_number_table cn
2294 WHERE cn.record = s.id
2297 OR src.transcendant IS TRUE
2298 ORDER BY 3 $sort_dir
2303 $log->debug("Field Search SQL :: [$select]",DEBUG);
2305 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2307 @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2310 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2312 my $count = scalar(@$recs);
2313 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2314 next unless ($$rec[0]);
2315 my ($mrid,$rank) = @$rec;
2316 $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2321 __PACKAGE__->register_method(
2322 api_name => "open-ils.storage.biblio.multiclass.search_fts.record",
2323 method => 'biblio_search_multi_class_fts',
2328 __PACKAGE__->register_method(
2329 api_name => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2330 method => 'biblio_search_multi_class_fts',
2335 __PACKAGE__->register_method(
2336 api_name => "open-ils.storage.biblio.multiclass.search_fts",
2337 method => 'biblio_search_multi_class_fts',
2342 __PACKAGE__->register_method(
2343 api_name => "open-ils.storage.biblio.multiclass.search_fts.staff",
2344 method => 'biblio_search_multi_class_fts',
2352 my $default_preferred_language;
2353 my $default_preferred_language_weight;
2355 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2361 if (!$locale_map{COMPLETE}) {
2363 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2364 for my $locale ( @locales ) {
2365 $locale_map{lc($locale->code)} = $locale->marc_code;
2367 $locale_map{COMPLETE} = 1;
2371 my $config = OpenSRF::Utils::SettingsClient->new();
2373 if (!$default_preferred_language) {
2375 $default_preferred_language = $config->config_value(
2376 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2377 ) || $config->config_value(
2378 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2383 if (!$default_preferred_language_weight) {
2385 $default_preferred_language_weight = $config->config_value(
2386 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2387 ) || $config->config_value(
2388 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2392 # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2393 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2395 my $ou = $args{org_unit};
2396 my $limit = $args{limit} || 10;
2397 my $offset = $args{offset} || 0;
2400 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2403 if (! scalar( keys %{$args{searches}} )) {
2404 die "No search arguments were passed to ".$self->api_name;
2407 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2409 if (!defined($args{preferred_language})) {
2410 my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2411 $args{preferred_language} =
2412 $locale_map{ lc($ses_locale) } || 'eng';
2415 if (!defined($args{preferred_language_weight})) {
2416 $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2419 if ($args{available}) {
2420 @statuses = (0,7,12);
2423 if (my $s = $args{locations}) {
2424 $s = [$s] if (!ref($s));
2428 if (my $b = $args{between}) {
2429 if (ref($b) && @$b == 2) {
2434 if (my $s = $args{statuses}) {
2435 $s = [$s] if (!ref($s));
2439 if (my $a = $args{audience}) {
2440 $a = [$a] if (!ref($a));
2444 if (my $l = $args{language}) {
2445 $l = [$l] if (!ref($l));
2449 if (my $f = $args{lit_form}) {
2450 $f = [$f] if (!ref($f));
2454 if (my $f = $args{item_form}) {
2455 $f = [$f] if (!ref($f));
2459 if (my $t = $args{item_type}) {
2460 $t = [$t] if (!ref($t));
2464 if (my $b = $args{bib_level}) {
2465 $b = [$b] if (!ref($b));
2469 if (my $v = $args{vr_format}) {
2470 $v = [$v] if (!ref($v));
2474 # XXX legacy format and item type support
2475 if ($args{format}) {
2476 my ($t, $f) = split '-', $args{format};
2477 @types = split '', $t;
2478 @forms = split '', $f;
2481 my %stored_proc_search_args;
2482 for my $search_group (sort keys %{$args{searches}}) {
2483 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2484 my ($search_class,$search_field) = split /\|/, $search_group;
2485 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2487 if ($search_field) {
2488 unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2489 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2494 my $class = $_cdbi->{$search_class};
2495 my $search_table = $class->table;
2497 my ($index_col) = $class->columns('FTS');
2498 $index_col ||= 'value';
2501 my $fts = OpenILS::Application::Storage::FTS->compile(
2502 $search_class => $args{searches}{$search_group}{term},
2503 $search_group_name.'.value',
2504 "$search_group_name.$index_col"
2506 $fts->sql_where_clause; # this builds the ranks for us
2508 my @fts_ranks = $fts->fts_rank;
2509 my @fts_queries = $fts->fts_query;
2510 my @phrases = map { lc($_) } $fts->phrases;
2511 my @words = map { lc($_) } $fts->words;
2513 $stored_proc_search_args{$search_group} = {
2514 fts_rank => \@fts_ranks,
2515 fts_query => \@fts_queries,
2516 phrase => \@phrases,
2522 my $param_search_ou = $ou;
2523 my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2524 my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2525 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2526 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2527 my $param_audience = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud ) . '}$$';
2528 my $param_language = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang ) . '}$$';
2529 my $param_lit_form = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2530 my $param_types = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types ) . '}$$';
2531 my $param_forms = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms ) . '}$$';
2532 my $param_vformats = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2533 my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2534 my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2535 my $param_after = $args{after} ; $param_after = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2536 my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2537 my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2538 my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2539 my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2540 my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2541 my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2542 my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2543 my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2544 my $param_rel_limit = $args{core_limit}; $param_rel_limit ||= 'NULL';
2545 my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2546 my $param_skip_chk = $args{skip_check}; $param_skip_chk ||= 'NULL';
2548 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2550 FROM search.staged_fts(
2551 $param_search_ou\:\:INT,
2552 $param_depth\:\:INT,
2553 $param_searches\:\:TEXT,
2554 $param_statuses\:\:INT[],
2555 $param_locations\:\:INT[],
2556 $param_audience\:\:TEXT[],
2557 $param_language\:\:TEXT[],
2558 $param_lit_form\:\:TEXT[],
2559 $param_types\:\:TEXT[],
2560 $param_forms\:\:TEXT[],
2561 $param_vformats\:\:TEXT[],
2562 $param_bib_level\:\:TEXT[],
2563 $param_before\:\:TEXT,
2564 $param_after\:\:TEXT,
2565 $param_during\:\:TEXT,
2566 $param_between\:\:TEXT[],
2567 $param_pref_lang\:\:TEXT,
2568 $param_pref_lang_multiplier\:\:REAL,
2569 $param_sort\:\:TEXT,
2570 $param_sort_desc\:\:BOOL,
2571 $metarecord\:\:BOOL,
2573 $param_rel_limit\:\:INT,
2574 $param_chk_limit\:\:INT,
2575 $param_skip_chk\:\:INT
2581 my $recs = $sth->fetchall_arrayref({});
2582 my $summary_row = pop @$recs;
2584 my $total = $$summary_row{total};
2585 my $checked = $$summary_row{checked};
2586 my $visible = $$summary_row{visible};
2587 my $deleted = $$summary_row{deleted};
2588 my $excluded = $$summary_row{excluded};
2590 my $estimate = $visible;
2591 if ( $total > $checked && $checked ) {
2593 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2594 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2598 delete $$summary_row{id};
2599 delete $$summary_row{rel};
2600 delete $$summary_row{record};
2602 $client->respond( $summary_row );
2604 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2606 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2607 delete $$rec{checked};
2608 delete $$rec{visible};
2609 delete $$rec{excluded};
2610 delete $$rec{deleted};
2611 delete $$rec{total};
2612 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2614 $client->respond( $rec );
2618 __PACKAGE__->register_method(
2619 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
2620 method => 'staged_fts',
2625 __PACKAGE__->register_method(
2626 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2627 method => 'staged_fts',
2632 __PACKAGE__->register_method(
2633 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
2634 method => 'staged_fts',
2639 __PACKAGE__->register_method(
2640 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2641 method => 'staged_fts',
2647 sub FTS_paging_estimate {
2651 my $checked = shift;
2652 my $visible = shift;
2653 my $excluded = shift;
2654 my $deleted = shift;
2657 my $deleted_ratio = $deleted / $checked;
2658 my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2660 my $exclusion_ratio = $excluded / $checked;
2661 my $delete_adjusted_exclusion_ratio = $checked - $deleted ? $excluded / ($checked - $deleted) : 1;
2663 my $inclusion_ratio = $visible / $checked;
2664 my $delete_adjusted_inclusion_ratio = $checked - $deleted ? $visible / ($checked - $deleted) : 0;
2667 exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2668 inclusion => int($delete_adjusted_total * $inclusion_ratio),
2669 delete_adjusted_exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2670 delete_adjusted_inclusion => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2673 __PACKAGE__->register_method(
2674 api_name => "open-ils.storage.fts_paging_estimate",
2675 method => 'FTS_paging_estimate',
2681 Hash of estimation values based on four variant estimation strategies:
2682 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2683 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2684 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2685 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2688 Helper method used to determin the approximate number of
2689 hits for a search that spans multiple superpages. For
2690 sparse superpages, the inclusion estimate will likely be the
2691 best estimate. The exclusion strategy is the original, but
2692 inclusion is the default.
2695 { name => 'checked',
2696 desc => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2699 { name => 'visible',
2700 desc => 'Number of records visible to the search location on the current superpage.',
2703 { name => 'excluded',
2704 desc => 'Number of records excluded from the search location on the current superpage.',
2707 { name => 'deleted',
2708 desc => 'Number of deleted records on the current superpage.',
2712 desc => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2725 my $term = $$args{term};
2726 my $limit = $$args{max} || 1;
2727 my $min = $$args{min} || 1;
2728 my @classes = @{$$args{class}};
2730 $limit = $min if ($min > $limit);
2733 @classes = ( qw/ title author subject series keyword / );
2737 my $bre_table = biblio::record_entry->table;
2738 my $cn_table = asset::call_number->table;
2739 my $cp_table = asset::copy->table;
2741 for my $search_class ( @classes ) {
2743 my $class = $_cdbi->{$search_class};
2744 my $search_table = $class->table;
2746 my ($index_col) = $class->columns('FTS');
2747 $index_col ||= 'value';
2750 my $where = OpenILS::Application::Storage::FTS
2751 ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2755 SELECT COUNT(DISTINCT X.source)
2756 FROM (SELECT $search_class.source
2757 FROM $search_table $search_class
2758 JOIN $bre_table b ON (b.id = $search_class.source)
2763 HAVING COUNT(DISTINCT X.source) >= $min;
2766 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2767 $matches{$search_class} = $res ? $res->[0] : 0;
2772 __PACKAGE__->register_method(
2773 api_name => "open-ils.storage.search.xref",
2774 method => 'xref_count',
2778 sub query_parser_fts {
2784 # grab the query parser and initialize it
2785 my $parser = $OpenILS::Application::Storage::QParser;
2788 if (!$parser->initialization_complete) {
2789 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
2790 $parser->initialize(
2791 config_record_attr_index_norm_map =>
2793 'open-ils.cstore.direct.config.record_attr_index_norm_map.search.atomic',
2794 { id => { "!=" => undef } },
2795 { flesh => 1, flesh_fields => { crainm => [qw/norm/] }, order_by => [{ class => "crainm", field => "pos" }] }
2797 search_relevance_adjustment =>
2799 'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
2800 { id => { "!=" => undef } }
2802 config_metabib_field =>
2804 'open-ils.cstore.direct.config.metabib_field.search.atomic',
2805 { id => { "!=" => undef } }
2807 config_metabib_search_alias =>
2809 'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
2810 { alias => { "!=" => undef } }
2812 config_metabib_field_index_norm_map =>
2814 'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
2815 { id => { "!=" => undef } },
2816 { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
2818 config_record_attr_definition =>
2820 'open-ils.cstore.direct.config.record_attr_definition.search.atomic',
2821 { name => { "!=" => undef } }
2825 $cstore->disconnect;
2826 die("Cannot initialize $parser!") unless ($parser->initialization_complete);
2830 # populate the locale/language map
2831 if (!$locale_map{COMPLETE}) {
2833 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2834 for my $locale ( @locales ) {
2835 $locale_map{lc($locale->code)} = $locale->marc_code;
2837 $locale_map{COMPLETE} = 1;
2841 # I hope we have a query!
2842 if (! $args{query} ) {
2843 die "No query was passed to ".$self->api_name;
2846 my $default_CD_modifiers = OpenSRF::Utils::SettingsClient->new->config_value(
2847 apps => 'open-ils.search' => app_settings => 'default_CD_modifiers'
2849 $args{query} = "$default_CD_modifiers $args{query}" if ($default_CD_modifiers);
2852 my $simple_plan = $args{_simple_plan};
2853 # remove bad chunks of the %args hash
2854 for my $bad ( grep { /^_/ } keys(%args)) {
2855 delete($args{$bad});
2859 # parse the query and supply any query-level %arg-based defaults
2860 # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2861 my $query = $parser->new( %args )->parse;
2863 my $config = OpenSRF::Utils::SettingsClient->new();
2865 # set the locale-based default prefered location
2866 if (!$query->parse_tree->find_filter('preferred_language')) {
2867 $parser->default_preferred_language( $args{preferred_language} );
2869 if (!$parser->default_preferred_language) {
2870 my $ses_locale = $client->session ? $client->session->session_locale : '';
2871 $parser->default_preferred_language( $locale_map{ lc($ses_locale) } );
2874 if (!$parser->default_preferred_language) { # still nothing...
2875 my $tmp_dpl = $config->config_value(
2876 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2877 ) || $config->config_value(
2878 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2881 $parser->default_preferred_language( $tmp_dpl )
2886 # set the global default language multiplier
2887 if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2890 if ($tmp_dplw = $args{preferred_language_weight} || $args{preferred_language_multiplier} ) {
2891 $parser->default_preferred_language_multiplier($tmp_dplw);
2894 $tmp_dplw = $config->config_value(
2895 apps => 'open-ils.search' => app_settings => 'default_preferred_language_weight'
2896 ) || $config->config_value(
2897 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2900 $parser->default_preferred_language_multiplier( $tmp_dplw );
2904 # gather the site, if one is specified, defaulting to the in-query version
2905 my $ou = $args{org_unit};
2906 if (my ($filter) = $query->parse_tree->find_filter('site')) {
2907 $ou = $filter->args->[0] if (@{$filter->args});
2909 $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^(-)?\d+$/);
2912 # gather lasso, as with $ou
2913 my $lasso = $args{lasso};
2914 if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
2915 $lasso = $filter->args->[0] if (@{$filter->args});
2917 $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
2918 $lasso = -$lasso if ($lasso);
2921 # # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
2922 # # gather user lasso, as with $ou and lasso
2923 # my $mylasso = $args{my_lasso};
2924 # if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
2925 # $mylasso = $filter->args->[0] if (@{$filter->args});
2927 # $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
2930 # if we have a lasso, go with that, otherwise ... ou
2931 $ou = $lasso if ($lasso);
2934 # get the default $ou if we have nothing
2935 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
2938 # 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
2939 # gather the depth, if one is specified, defaulting to the in-query version
2940 my $depth = $args{depth};
2941 if (my ($filter) = $query->parse_tree->find_filter('depth')) {
2942 $depth = $filter->args->[0] if (@{$filter->args});
2944 $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
2947 # gather the limit or default to 10
2948 my $limit = $args{check_limit} || 'NULL';
2949 if (my ($filter) = $query->parse_tree->find_filter('limit')) {
2950 $limit = $filter->args->[0] if (@{$filter->args});
2952 if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
2953 $limit = $filter->args->[0] if (@{$filter->args});
2957 # gather the offset or default to 0
2958 my $offset = $args{skip_check} || $args{offset} || 0;
2959 if (my ($filter) = $query->parse_tree->find_filter('offset')) {
2960 $offset = $filter->args->[0] if (@{$filter->args});
2962 if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
2963 $offset = $filter->args->[0] if (@{$filter->args});
2967 # gather the estimation strategy or default to inclusion
2968 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2969 if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
2970 $estimation_strategy = $filter->args->[0] if (@{$filter->args});
2974 # gather the estimation strategy or default to inclusion
2975 my $core_limit = $args{core_limit};
2976 if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
2977 $core_limit = $filter->args->[0] if (@{$filter->args});
2981 # gather statuses, and then forget those if we have an #available modifier
2983 if (my ($filter) = $query->parse_tree->find_filter('statuses')) {
2984 @statuses = @{$filter->args} if (@{$filter->args});
2986 @statuses = (0,7,12) if ($query->parse_tree->find_modifier('available'));
2991 if (my ($filter) = $query->parse_tree->find_filter('locations')) {
2992 @location = @{$filter->args} if (@{$filter->args});
2996 my $param_check = $limit || $query->superpage_size || 'NULL';
2997 my $param_offset = $offset || 'NULL';
2998 my $param_limit = $core_limit || 'NULL';
3000 my $sp = $query->superpage || 1;
3002 $param_offset = ($sp - 1) * $sp_size;
3005 my $param_search_ou = $ou;
3006 my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
3007 my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
3008 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
3009 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
3010 my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
3011 my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
3013 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3014 SELECT * -- bib search: $args{query}
3015 FROM search.query_parser_fts(
3016 $param_search_ou\:\:INT,
3017 $param_depth\:\:INT,
3018 $param_core_query\:\:TEXT,
3019 $param_statuses\:\:INT[],
3020 $param_locations\:\:INT[],
3021 $param_offset\:\:INT,
3022 $param_check\:\:INT,
3023 $param_limit\:\:INT,
3024 $metarecord\:\:BOOL,
3031 my $recs = $sth->fetchall_arrayref({});
3032 my $summary_row = pop @$recs;
3034 my $total = $$summary_row{total};
3035 my $checked = $$summary_row{checked};
3036 my $visible = $$summary_row{visible};
3037 my $deleted = $$summary_row{deleted};
3038 my $excluded = $$summary_row{excluded};
3040 my $estimate = $visible;
3041 if ( $total > $checked && $checked ) {
3043 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
3044 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
3048 delete $$summary_row{id};
3049 delete $$summary_row{rel};
3050 delete $$summary_row{record};
3052 if (defined($simple_plan)) {
3053 $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3055 $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3058 $client->respond( $summary_row );
3060 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
3062 for my $rec (@$recs) {
3063 delete $$rec{checked};
3064 delete $$rec{visible};
3065 delete $$rec{excluded};
3066 delete $$rec{deleted};
3067 delete $$rec{total};
3068 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3070 $client->respond( $rec );
3074 __PACKAGE__->register_method(
3075 api_name => "open-ils.storage.query_parser_search",
3076 method => 'query_parser_fts',
3082 sub query_parser_fts_wrapper {
3087 $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3088 # grab the query parser and initialize it
3089 my $parser = $OpenILS::Application::Storage::QParser;
3092 if (!$parser->initialization_complete) {
3093 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3094 $parser->initialize(
3095 config_record_attr_index_norm_map =>
3097 'open-ils.cstore.direct.config.record_attr_index_norm_map.search.atomic',
3098 { id => { "!=" => undef } },
3099 { flesh => 1, flesh_fields => { crainm => [qw/norm/] }, order_by => [{ class => "crainm", field => "pos" }] }
3101 search_relevance_adjustment =>
3103 'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
3104 { id => { "!=" => undef } }
3106 config_metabib_field =>
3108 'open-ils.cstore.direct.config.metabib_field.search.atomic',
3109 { id => { "!=" => undef } }
3111 config_metabib_search_alias =>
3113 'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
3114 { alias => { "!=" => undef } }
3116 config_metabib_field_index_norm_map =>
3118 'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
3119 { id => { "!=" => undef } },
3120 { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
3122 config_record_attr_definition =>
3124 'open-ils.cstore.direct.config.record_attr_definition.search.atomic',
3125 { name => { "!=" => undef } }
3129 $cstore->disconnect;
3130 die("Cannot initialize $parser!") unless ($parser->initialization_complete);
3133 if (! scalar( keys %{$args{searches}} )) {
3134 die "No search arguments were passed to ".$self->api_name;
3137 $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3138 my $base_query = '';
3139 for my $sclass ( keys %{$args{searches}} ) {
3140 $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3141 $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3144 my $query = $base_query;
3145 $log->debug("Full base query: $base_query", DEBUG);
3147 $query = "$args{facets} $query" if ($args{facets});
3149 if (!$locale_map{COMPLETE}) {
3151 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3152 for my $locale ( @locales ) {
3153 $locale_map{lc($locale->code)} = $locale->marc_code;
3155 $locale_map{COMPLETE} = 1;
3159 my $base_plan = $parser->new( query => $base_query )->parse;
3161 $query = "preferred_language($args{preferred_language}) $query"
3162 if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3163 $query = "preferred_language_weight($args{preferred_language_weight}) $query"
3164 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'));
3166 $query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
3167 $query = "site($args{org_unit}) $query" if ($args{org_unit});
3168 $query = "depth($args{depth}) $query" if (defined($args{depth}));
3169 $query = "sort($args{sort}) $query" if ($args{sort});
3170 $query = "limit($args{limit}) $query" if ($args{limit});
3171 $query = "core_limit($args{core_limit}) $query" if ($args{core_limit});
3172 $query = "skip_check($args{skip_check}) $query" if ($args{skip_check});
3173 $query = "superpage($args{superpage}) $query" if ($args{superpage});
3174 $query = "offset($args{offset}) $query" if ($args{offset});
3175 $query = "#metarecord $query" if ($self->api_name =~ /metabib/);
3176 $query = "#available $query" if ($args{available});
3177 $query = "#descending $query" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3178 $query = "#staff $query" if ($self->api_name =~ /staff/);
3179 $query = "before($args{before}) $query" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3180 $query = "after($args{after}) $query" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3181 $query = "during($args{during}) $query" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3182 $query = "between($args{between}[0],$args{between}[1]) $query"
3183 if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3186 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3188 # XXX legacy format and item type support
3189 if ($args{format}) {
3190 my ($t, $f) = split '-', $args{format};
3191 $args{item_type} = [ split '', $t ];
3192 $args{item_form} = [ split '', $f ];
3195 for my $filter ( qw/locations statuses between audience language lit_form item_form item_type bib_level vr_format/ ) {
3196 if (my $s = $args{$filter}) {
3197 $s = [$s] if (!ref($s));
3199 my @filter_list = @$s;
3201 next if ($filter eq 'between' and scalar(@filter_list) != 2);
3202 next if (@filter_list == 0);
3204 my $filter_string = join ',', @filter_list;
3205 $query = "$filter($filter_string) $query";
3209 $log->debug("Full QueryParser query: $query", DEBUG);
3211 return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan );
3213 __PACKAGE__->register_method(
3214 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
3215 method => 'query_parser_fts_wrapper',
3220 __PACKAGE__->register_method(
3221 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3222 method => 'query_parser_fts_wrapper',
3227 __PACKAGE__->register_method(
3228 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
3229 method => 'query_parser_fts_wrapper',
3234 __PACKAGE__->register_method(
3235 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3236 method => 'query_parser_fts_wrapper',