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 if (!$default_preferred_language) {
2373 $default_preferred_language = OpenSRF::Utils::SettingsClient
2376 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2381 if (!$default_preferred_language_weight) {
2383 $default_preferred_language_weight = OpenSRF::Utils::SettingsClient
2386 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2391 # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2392 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2394 my $ou = $args{org_unit};
2395 my $limit = $args{limit} || 10;
2396 my $offset = $args{offset} || 0;
2399 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2402 if (! scalar( keys %{$args{searches}} )) {
2403 die "No search arguments were passed to ".$self->api_name;
2406 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2408 if (!defined($args{preferred_language})) {
2409 my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2410 $args{preferred_language} =
2411 $locale_map{ lc($ses_locale) } || 'eng';
2414 if (!defined($args{preferred_language_weight})) {
2415 $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2418 if ($args{available}) {
2419 @statuses = (0,7,12);
2422 if (my $s = $args{locations}) {
2423 $s = [$s] if (!ref($s));
2427 if (my $b = $args{between}) {
2428 if (ref($b) && @$b == 2) {
2433 if (my $s = $args{statuses}) {
2434 $s = [$s] if (!ref($s));
2438 if (my $a = $args{audience}) {
2439 $a = [$a] if (!ref($a));
2443 if (my $l = $args{language}) {
2444 $l = [$l] if (!ref($l));
2448 if (my $f = $args{lit_form}) {
2449 $f = [$f] if (!ref($f));
2453 if (my $f = $args{item_form}) {
2454 $f = [$f] if (!ref($f));
2458 if (my $t = $args{item_type}) {
2459 $t = [$t] if (!ref($t));
2463 if (my $b = $args{bib_level}) {
2464 $b = [$b] if (!ref($b));
2468 if (my $v = $args{vr_format}) {
2469 $v = [$v] if (!ref($v));
2473 # XXX legacy format and item type support
2474 if ($args{format}) {
2475 my ($t, $f) = split '-', $args{format};
2476 @types = split '', $t;
2477 @forms = split '', $f;
2480 my %stored_proc_search_args;
2481 for my $search_group (sort keys %{$args{searches}}) {
2482 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2483 my ($search_class,$search_field) = split /\|/, $search_group;
2484 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2486 if ($search_field) {
2487 unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2488 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2493 my $class = $_cdbi->{$search_class};
2494 my $search_table = $class->table;
2496 my ($index_col) = $class->columns('FTS');
2497 $index_col ||= 'value';
2500 my $fts = OpenILS::Application::Storage::FTS->compile(
2501 $search_class => $args{searches}{$search_group}{term},
2502 $search_group_name.'.value',
2503 "$search_group_name.$index_col"
2505 $fts->sql_where_clause; # this builds the ranks for us
2507 my @fts_ranks = $fts->fts_rank;
2508 my @fts_queries = $fts->fts_query;
2509 my @phrases = map { lc($_) } $fts->phrases;
2510 my @words = map { lc($_) } $fts->words;
2512 $stored_proc_search_args{$search_group} = {
2513 fts_rank => \@fts_ranks,
2514 fts_query => \@fts_queries,
2515 phrase => \@phrases,
2521 my $param_search_ou = $ou;
2522 my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2523 my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2524 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2525 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2526 my $param_audience = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud ) . '}$$';
2527 my $param_language = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang ) . '}$$';
2528 my $param_lit_form = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2529 my $param_types = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types ) . '}$$';
2530 my $param_forms = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms ) . '}$$';
2531 my $param_vformats = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2532 my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2533 my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2534 my $param_after = $args{after} ; $param_after = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2535 my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2536 my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2537 my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2538 my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2539 my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2540 my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2541 my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2542 my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2543 my $param_rel_limit = $args{core_limit}; $param_rel_limit ||= 'NULL';
2544 my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2545 my $param_skip_chk = $args{skip_check}; $param_skip_chk ||= 'NULL';
2547 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2549 FROM search.staged_fts(
2550 $param_search_ou\:\:INT,
2551 $param_depth\:\:INT,
2552 $param_searches\:\:TEXT,
2553 $param_statuses\:\:INT[],
2554 $param_locations\:\:INT[],
2555 $param_audience\:\:TEXT[],
2556 $param_language\:\:TEXT[],
2557 $param_lit_form\:\:TEXT[],
2558 $param_types\:\:TEXT[],
2559 $param_forms\:\:TEXT[],
2560 $param_vformats\:\:TEXT[],
2561 $param_bib_level\:\:TEXT[],
2562 $param_before\:\:TEXT,
2563 $param_after\:\:TEXT,
2564 $param_during\:\:TEXT,
2565 $param_between\:\:TEXT[],
2566 $param_pref_lang\:\:TEXT,
2567 $param_pref_lang_multiplier\:\:REAL,
2568 $param_sort\:\:TEXT,
2569 $param_sort_desc\:\:BOOL,
2570 $metarecord\:\:BOOL,
2572 $param_rel_limit\:\:INT,
2573 $param_chk_limit\:\:INT,
2574 $param_skip_chk\:\:INT
2580 my $recs = $sth->fetchall_arrayref({});
2581 my $summary_row = pop @$recs;
2583 my $total = $$summary_row{total};
2584 my $checked = $$summary_row{checked};
2585 my $visible = $$summary_row{visible};
2586 my $deleted = $$summary_row{deleted};
2587 my $excluded = $$summary_row{excluded};
2589 my $estimate = $visible;
2590 if ( $total > $checked && $checked ) {
2592 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2593 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2597 delete $$summary_row{id};
2598 delete $$summary_row{rel};
2599 delete $$summary_row{record};
2601 $client->respond( $summary_row );
2603 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2605 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2606 delete $$rec{checked};
2607 delete $$rec{visible};
2608 delete $$rec{excluded};
2609 delete $$rec{deleted};
2610 delete $$rec{total};
2611 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2613 $client->respond( $rec );
2617 __PACKAGE__->register_method(
2618 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
2619 method => 'staged_fts',
2624 __PACKAGE__->register_method(
2625 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2626 method => 'staged_fts',
2631 __PACKAGE__->register_method(
2632 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
2633 method => 'staged_fts',
2638 __PACKAGE__->register_method(
2639 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2640 method => 'staged_fts',
2646 sub FTS_paging_estimate {
2650 my $checked = shift;
2651 my $visible = shift;
2652 my $excluded = shift;
2653 my $deleted = shift;
2656 my $deleted_ratio = $deleted / $checked;
2657 my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2659 my $exclusion_ratio = $excluded / $checked;
2660 my $delete_adjusted_exclusion_ratio = $checked - $deleted ? $excluded / ($checked - $deleted) : 1;
2662 my $inclusion_ratio = $visible / $checked;
2663 my $delete_adjusted_inclusion_ratio = $checked - $deleted ? $visible / ($checked - $deleted) : 0;
2666 exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2667 inclusion => int($delete_adjusted_total * $inclusion_ratio),
2668 delete_adjusted_exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2669 delete_adjusted_inclusion => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2672 __PACKAGE__->register_method(
2673 api_name => "open-ils.storage.fts_paging_estimate",
2674 method => 'FTS_paging_estimate',
2680 Hash of estimation values based on four variant estimation strategies:
2681 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2682 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2683 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2684 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2687 Helper method used to determin the approximate number of
2688 hits for a search that spans multiple superpages. For
2689 sparse superpages, the inclusion estimate will likely be the
2690 best estimate. The exclusion strategy is the original, but
2691 inclusion is the default.
2694 { name => 'checked',
2695 desc => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2698 { name => 'visible',
2699 desc => 'Number of records visible to the search location on the current superpage.',
2702 { name => 'excluded',
2703 desc => 'Number of records excluded from the search location on the current superpage.',
2706 { name => 'deleted',
2707 desc => 'Number of deleted records on the current superpage.',
2711 desc => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2724 my $term = $$args{term};
2725 my $limit = $$args{max} || 1;
2726 my $min = $$args{min} || 1;
2727 my @classes = @{$$args{class}};
2729 $limit = $min if ($min > $limit);
2732 @classes = ( qw/ title author subject series keyword / );
2736 my $bre_table = biblio::record_entry->table;
2737 my $cn_table = asset::call_number->table;
2738 my $cp_table = asset::copy->table;
2740 for my $search_class ( @classes ) {
2742 my $class = $_cdbi->{$search_class};
2743 my $search_table = $class->table;
2745 my ($index_col) = $class->columns('FTS');
2746 $index_col ||= 'value';
2749 my $where = OpenILS::Application::Storage::FTS
2750 ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2754 SELECT COUNT(DISTINCT X.source)
2755 FROM (SELECT $search_class.source
2756 FROM $search_table $search_class
2757 JOIN $bre_table b ON (b.id = $search_class.source)
2762 HAVING COUNT(DISTINCT X.source) >= $min;
2765 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2766 $matches{$search_class} = $res ? $res->[0] : 0;
2771 __PACKAGE__->register_method(
2772 api_name => "open-ils.storage.search.xref",
2773 method => 'xref_count',
2777 sub query_parser_fts {
2783 # grab the query parser and initialize it
2784 my $parser = $OpenILS::Application::Storage::QParser;
2787 if (!$parser->initialization_complete) {
2788 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
2789 $parser->initialize(
2790 config_record_attr_index_norm_map =>
2792 'open-ils.cstore.direct.config.record_attr_index_norm_map.search.atomic',
2793 { id => { "!=" => undef } },
2794 { flesh => 1, flesh_fields => { crainm => [qw/norm/] }, order_by => [{ class => "crainm", field => "pos" }] }
2796 search_relevance_adjustment =>
2798 'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
2799 { id => { "!=" => undef } }
2801 config_metabib_field =>
2803 'open-ils.cstore.direct.config.metabib_field.search.atomic',
2804 { id => { "!=" => undef } }
2806 config_metabib_search_alias =>
2808 'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
2809 { alias => { "!=" => undef } }
2811 config_metabib_field_index_norm_map =>
2813 'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
2814 { id => { "!=" => undef } },
2815 { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
2817 config_record_attr_definition =>
2819 'open-ils.cstore.direct.config.record_attr_definition.search.atomic',
2820 { name => { "!=" => undef } }
2824 $cstore->disconnect;
2825 die("Cannot initialize $parser!") unless ($parser->initialization_complete);
2829 # populate the locale/language map
2830 if (!$locale_map{COMPLETE}) {
2832 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2833 for my $locale ( @locales ) {
2834 $locale_map{lc($locale->code)} = $locale->marc_code;
2836 $locale_map{COMPLETE} = 1;
2840 # I hope we have a query!
2841 if (! $args{query} ) {
2842 die "No query was passed to ".$self->api_name;
2846 my $simple_plan = $args{_simple_plan};
2847 # remove bad chunks of the %args hash
2848 for my $bad ( grep { /^_/ } keys(%args)) {
2849 delete($args{$bad});
2853 # parse the query and supply any query-level %arg-based defaults
2854 # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2855 my $query = $parser->new( %args )->parse;
2858 # set the locale-based default prefered location
2859 if (!$query->parse_tree->find_filter('preferred_language')) {
2860 $parser->default_preferred_language( $args{preferred_language} );
2861 if (!$parser->default_preferred_language) {
2862 my $ses_locale = $client->session ? $client->session->session_locale : '';
2863 $parser->default_preferred_language( $locale_map{ lc($ses_locale) } );
2865 $parser->default_preferred_language(
2866 OpenSRF::Utils::SettingsClient->new->config_value(
2867 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2869 ) if (!$parser->default_preferred_language);
2873 # set the global default language multiplier
2874 if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2875 $parser->default_preferred_language_multiplier($args{preferred_language_weight});
2876 $parser->default_preferred_language_multiplier($args{preferred_language_multiplier});
2877 $parser->default_preferred_language_multiplier(
2878 OpenSRF::Utils::SettingsClient->new->config_value(
2879 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2881 ) if (!$parser->default_preferred_language_multiplier);
2884 # gather the site, if one is specified, defaulting to the in-query version
2885 my $ou = $args{org_unit};
2886 if (my ($filter) = $query->parse_tree->find_filter('site')) {
2887 $ou = $filter->args->[0] if (@{$filter->args});
2889 $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^\d+$/);
2892 # gather lasso, as with $ou
2893 my $lasso = $args{lasso};
2894 if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
2895 $lasso = $filter->args->[0] if (@{$filter->args});
2897 $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
2898 $lasso = -$lasso if ($lasso);
2901 # # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
2902 # # gather user lasso, as with $ou and lasso
2903 # my $mylasso = $args{my_lasso};
2904 # if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
2905 # $mylasso = $filter->args->[0] if (@{$filter->args});
2907 # $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
2910 # if we have a lasso, go with that, otherwise ... ou
2911 $ou = $lasso if ($lasso);
2914 # get the default $ou if we have nothing
2915 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
2918 # 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
2919 # gather the depth, if one is specified, defaulting to the in-query version
2920 my $depth = $args{depth};
2921 if (my ($filter) = $query->parse_tree->find_filter('depth')) {
2922 $depth = $filter->args->[0] if (@{$filter->args});
2924 $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
2927 # gather the limit or default to 10
2928 my $limit = $args{check_limit} || 'NULL';
2929 if (my ($filter) = $query->parse_tree->find_filter('limit')) {
2930 $limit = $filter->args->[0] if (@{$filter->args});
2932 if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
2933 $limit = $filter->args->[0] if (@{$filter->args});
2937 # gather the offset or default to 0
2938 my $offset = $args{skip_check} || $args{offset} || 0;
2939 if (my ($filter) = $query->parse_tree->find_filter('offset')) {
2940 $offset = $filter->args->[0] if (@{$filter->args});
2942 if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
2943 $offset = $filter->args->[0] if (@{$filter->args});
2947 # gather the estimation strategy or default to inclusion
2948 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2949 if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
2950 $estimation_strategy = $filter->args->[0] if (@{$filter->args});
2954 # gather the estimation strategy or default to inclusion
2955 my $core_limit = $args{core_limit};
2956 if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
2957 $core_limit = $filter->args->[0] if (@{$filter->args});
2961 # gather statuses, and then forget those if we have an #available modifier
2963 if (my ($filter) = $query->parse_tree->find_filter('statuses')) {
2964 @statuses = @{$filter->args} if (@{$filter->args});
2966 @statuses = (0,7,12) if ($query->parse_tree->find_modifier('available'));
2971 if (my ($filter) = $query->parse_tree->find_filter('locations')) {
2972 @location = @{$filter->args} if (@{$filter->args});
2976 my $param_check = $limit || $query->superpage_size || 'NULL';
2977 my $param_offset = $offset || 'NULL';
2978 my $param_limit = $core_limit || 'NULL';
2980 my $sp = $query->superpage || 1;
2982 $param_offset = ($sp - 1) * $sp_size;
2985 my $param_search_ou = $ou;
2986 my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
2987 my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
2988 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
2989 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
2990 my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
2991 my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
2993 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2994 SELECT * /* bib search */
2995 FROM search.query_parser_fts(
2996 $param_search_ou\:\:INT,
2997 $param_depth\:\:INT,
2998 $param_core_query\:\:TEXT,
2999 $param_statuses\:\:INT[],
3000 $param_locations\:\:INT[],
3001 $param_offset\:\:INT,
3002 $param_check\:\:INT,
3003 $param_limit\:\:INT,
3004 $metarecord\:\:BOOL,
3011 my $recs = $sth->fetchall_arrayref({});
3012 my $summary_row = pop @$recs;
3014 my $total = $$summary_row{total};
3015 my $checked = $$summary_row{checked};
3016 my $visible = $$summary_row{visible};
3017 my $deleted = $$summary_row{deleted};
3018 my $excluded = $$summary_row{excluded};
3020 my $estimate = $visible;
3021 if ( $total > $checked && $checked ) {
3023 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
3024 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
3028 delete $$summary_row{id};
3029 delete $$summary_row{rel};
3030 delete $$summary_row{record};
3032 if (defined($simple_plan)) {
3033 $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3035 $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3038 $client->respond( $summary_row );
3040 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
3042 for my $rec (@$recs) {
3043 delete $$rec{checked};
3044 delete $$rec{visible};
3045 delete $$rec{excluded};
3046 delete $$rec{deleted};
3047 delete $$rec{total};
3048 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3050 $client->respond( $rec );
3054 __PACKAGE__->register_method(
3055 api_name => "open-ils.storage.query_parser_search",
3056 method => 'query_parser_fts',
3062 sub query_parser_fts_wrapper {
3067 $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3068 # grab the query parser and initialize it
3069 my $parser = $OpenILS::Application::Storage::QParser;
3072 if (!$parser->initialization_complete) {
3073 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3074 $parser->initialize(
3075 config_record_attr_index_norm_map =>
3077 'open-ils.cstore.direct.config.record_attr_index_norm_map.search.atomic',
3078 { id => { "!=" => undef } },
3079 { flesh => 1, flesh_fields => { crainm => [qw/norm/] }, order_by => [{ class => "crainm", field => "pos" }] }
3081 search_relevance_adjustment =>
3083 'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
3084 { id => { "!=" => undef } }
3086 config_metabib_field =>
3088 'open-ils.cstore.direct.config.metabib_field.search.atomic',
3089 { id => { "!=" => undef } }
3091 config_metabib_search_alias =>
3093 'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
3094 { alias => { "!=" => undef } }
3096 config_metabib_field_index_norm_map =>
3098 'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
3099 { id => { "!=" => undef } },
3100 { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
3102 config_record_attr_definition =>
3104 'open-ils.cstore.direct.config.record_attr_definition.search.atomic',
3105 { name => { "!=" => undef } }
3109 $cstore->disconnect;
3110 die("Cannot initialize $parser!") unless ($parser->initialization_complete);
3113 if (! scalar( keys %{$args{searches}} )) {
3114 die "No search arguments were passed to ".$self->api_name;
3117 $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3118 my $base_query = '';
3119 for my $sclass ( keys %{$args{searches}} ) {
3120 $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3121 $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3124 my $query = $base_query;
3125 $log->debug("Full base query: $base_query", DEBUG);
3127 $query = "$args{facets} $query" if ($args{facets});
3129 if (!$locale_map{COMPLETE}) {
3131 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3132 for my $locale ( @locales ) {
3133 $locale_map{lc($locale->code)} = $locale->marc_code;
3135 $locale_map{COMPLETE} = 1;
3139 my $base_plan = $parser->new( query => $base_query )->parse;
3141 $query = "preferred_language($args{preferred_language}) $query"
3142 if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3143 $query = "preferred_language_weight($args{preferred_language_weight}) $query"
3144 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'));
3146 $query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
3147 $query = "site($args{org_unit}) $query" if ($args{org_unit});
3148 $query = "depth($args{depth}) $query" if (defined($args{depth}));
3149 $query = "sort($args{sort}) $query" if ($args{sort});
3150 $query = "limit($args{limit}) $query" if ($args{limit});
3151 $query = "core_limit($args{core_limit}) $query" if ($args{core_limit});
3152 $query = "skip_check($args{skip_check}) $query" if ($args{skip_check});
3153 $query = "superpage($args{superpage}) $query" if ($args{superpage});
3154 $query = "offset($args{offset}) $query" if ($args{offset});
3155 $query = "#metarecord $query" if ($self->api_name =~ /metabib/);
3156 $query = "#available $query" if ($args{available});
3157 $query = "#descending $query" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3158 $query = "#staff $query" if ($self->api_name =~ /staff/);
3159 $query = "before($args{before}) $query" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3160 $query = "after($args{after}) $query" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3161 $query = "during($args{during}) $query" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3162 $query = "between($args{between}[0],$args{between}[1]) $query"
3163 if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3166 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3168 # XXX legacy format and item type support
3169 if ($args{format}) {
3170 my ($t, $f) = split '-', $args{format};
3171 $args{item_type} = [ split '', $t ];
3172 $args{item_form} = [ split '', $f ];
3175 for my $filter ( qw/locations statuses between audience language lit_form item_form item_type bib_level vr_format/ ) {
3176 if (my $s = $args{$filter}) {
3177 $s = [$s] if (!ref($s));
3179 my @filter_list = @$s;
3181 next if ($filter eq 'between' and scalar(@filter_list) != 2);
3182 next if (@filter_list == 0);
3184 my $filter_string = join ',', @filter_list;
3185 $query = "$filter($filter_string) $query";
3189 $log->debug("Full QueryParser query: $query", DEBUG);
3191 return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan );
3193 __PACKAGE__->register_method(
3194 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
3195 method => 'query_parser_fts_wrapper',
3200 __PACKAGE__->register_method(
3201 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3202 method => 'query_parser_fts_wrapper',
3207 __PACKAGE__->register_method(
3208 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
3209 method => 'query_parser_fts_wrapper',
3214 __PACKAGE__->register_method(
3215 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3216 method => 'query_parser_fts_wrapper',