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 '\\\\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',
221 my $isxn = lc(shift());
225 $isxn =~ s/-//o if ($self->api_name =~ /isbn/o);
227 my $tag = ($self->api_name =~ /isbn/o) ? "'020' OR f.tag = '024'" : "'022'";
229 my $fr_table = metabib::full_rec->table;
230 my $bib_table = biblio::record_entry->table;
233 SELECT DISTINCT f.record
235 JOIN $bib_table b ON (b.id = f.record)
238 AND b.deleted IS FALSE
241 my $list = metabib::full_rec->db_Main->selectcol_arrayref($sql, {}, "$isxn%");
242 $client->respond($_) for (@$list);
245 __PACKAGE__->register_method(
246 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
247 method => 'isxn_search',
251 __PACKAGE__->register_method(
252 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.issn',
253 method => 'isxn_search',
258 sub metarecord_copy_count {
264 my $sm_table = metabib::metarecord_source_map->table;
265 my $rd_table = metabib::record_descriptor->table;
266 my $cn_table = asset::call_number->table;
267 my $cp_table = asset::copy->table;
268 my $br_table = biblio::record_entry->table;
269 my $src_table = config::bib_source->table;
270 my $cl_table = asset::copy_location->table;
271 my $cs_table = config::copy_status->table;
272 my $out_table = actor::org_unit_type->table;
274 my $descendants = "actor.org_unit_descendants(u.id)";
275 my $ancestors = "actor.org_unit_ancestors(?) u JOIN $out_table t ON (u.ou_type = t.id)";
277 if ($args{org_unit} < 0) {
278 $args{org_unit} *= -1;
279 $ancestors = "(select org_unit as id from actor.org_lasso_map where lasso = ?) u CROSS JOIN (SELECT -1 AS depth) t";
282 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';
283 $copies_visible = '' if ($self->api_name =~ /staff/o);
285 my (@types,@forms,@blvl);
286 my ($t_filter, $f_filter, $b_filter) = ('','','');
289 my ($t, $f, $b) = split '-', $args{format};
290 @types = split '', $t;
291 @forms = split '', $f;
292 @blvl = split '', $b;
295 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
299 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
303 $b_filter .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
313 JOIN $cn_table cn ON (cn.record = r.source)
314 JOIN $rd_table rd ON (cn.record = rd.record)
315 JOIN $cp_table cp ON (cn.id = cp.call_number)
316 JOIN $cs_table cs ON (cp.status = cs.id)
317 JOIN $cl_table cl ON (cp.location = cl.id)
318 JOIN $descendants a ON (cp.circ_lib = a.id)
319 WHERE r.metarecord = ?
320 AND cn.deleted IS FALSE
321 AND cp.deleted IS FALSE
331 JOIN $cn_table cn ON (cn.record = r.source)
332 JOIN $rd_table rd ON (cn.record = rd.record)
333 JOIN $cp_table cp ON (cn.id = cp.call_number)
334 JOIN $cs_table cs ON (cp.status = cs.id)
335 JOIN $cl_table cl ON (cp.location = cl.id)
336 JOIN $descendants a ON (cp.circ_lib = a.id)
337 WHERE r.metarecord = ?
338 AND cp.status IN (0,7,12)
339 AND cn.deleted IS FALSE
340 AND cp.deleted IS FALSE
350 JOIN $cn_table cn ON (cn.record = r.source)
351 JOIN $rd_table rd ON (cn.record = rd.record)
352 JOIN $cp_table cp ON (cn.id = cp.call_number)
353 JOIN $cs_table cs ON (cp.status = cs.id)
354 JOIN $cl_table cl ON (cp.location = cl.id)
355 WHERE r.metarecord = ?
356 AND cn.deleted IS FALSE
357 AND cp.deleted IS FALSE
358 AND cp.opac_visible IS TRUE
359 AND cs.opac_visible IS TRUE
360 AND cl.opac_visible IS TRUE
369 JOIN $br_table br ON (br.id = r.source)
370 JOIN $src_table src ON (src.id = br.source)
371 WHERE r.metarecord = ?
372 AND src.transcendant IS TRUE
380 my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
381 $sth->execute( ''.$args{metarecord},
385 ''.$args{metarecord},
389 ''.$args{metarecord},
393 ''.$args{metarecord},
397 while ( my $row = $sth->fetchrow_hashref ) {
398 $client->respond( $row );
402 __PACKAGE__->register_method(
403 api_name => 'open-ils.storage.metabib.metarecord.copy_count',
404 method => 'metarecord_copy_count',
409 __PACKAGE__->register_method(
410 api_name => 'open-ils.storage.metabib.metarecord.copy_count.staff',
411 method => 'metarecord_copy_count',
417 sub biblio_multi_search_full_rec {
422 my $class_join = $args{class_join} || 'AND';
423 my $limit = $args{limit} || 100;
424 my $offset = $args{offset} || 0;
425 my $sort = $args{'sort'};
426 my $sort_dir = $args{sort_dir} || 'DESC';
431 for my $arg (@{ $args{searches} }) {
432 my $term = $$arg{term};
433 my $limiters = $$arg{restrict};
435 my ($index_col) = metabib::full_rec->columns('FTS') || 'value';
436 my $search_table = metabib::full_rec->table;
438 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
440 my $fts_where = $fts->sql_where_clause();
441 my @fts_ranks = $fts->fts_rank;
443 my $rank = join(' + ', @fts_ranks);
446 for my $limit (@$limiters) {
447 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
448 push @binds, $$limit{tag}, $$limit{subfield};
449 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
451 my $where = join(' OR ', @wheres);
453 push @selects, "SELECT id, record, $rank as sum FROM $search_table WHERE $where";
457 my $descendants = defined($args{depth}) ?
458 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
459 "actor.org_unit_descendants($args{org_unit})" ;
462 my $metabib_record_descriptor = metabib::record_descriptor->table;
463 my $metabib_full_rec = metabib::full_rec->table;
464 my $asset_call_number_table = asset::call_number->table;
465 my $asset_copy_table = asset::copy->table;
466 my $cs_table = config::copy_status->table;
467 my $cl_table = asset::copy_location->table;
468 my $br_table = biblio::record_entry->table;
470 my $cj = 'HAVING COUNT(x.id) = ' . scalar(@selects) if ($class_join eq 'AND');
472 '(SELECT x.record, sum(x.sum) FROM (('.
473 join(') UNION ALL (', @selects).
474 ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
476 my $has_vols = 'AND cn.owning_lib = d.id';
477 my $has_copies = 'AND cp.call_number = cn.id';
478 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';
480 if ($self->api_name =~ /staff/o) {
481 $copies_visible = '';
482 $has_copies = '' if ($ou_type == 0);
483 $has_vols = '' if ($ou_type == 0);
486 my ($t_filter, $f_filter) = ('','');
487 my ($a_filter, $l_filter, $lf_filter) = ('','','');
489 if (my $a = $args{audience}) {
490 $a = [$a] if (!ref($a));
493 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
497 if (my $l = $args{language}) {
498 $l = [$l] if (!ref($l));
501 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
505 if (my $f = $args{lit_form}) {
506 $f = [$f] if (!ref($f));
509 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
510 push @binds, @lit_form;
513 if (my $f = $args{item_form}) {
514 $f = [$f] if (!ref($f));
517 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
521 if (my $t = $args{item_type}) {
522 $t = [$t] if (!ref($t));
525 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
531 my ($t, $f) = split '-', $args{format};
532 my @types = split '', $t;
533 my @forms = split '', $f;
535 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
539 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
541 push @binds, @types, @forms;
544 my $relevance = 'sum(f.sum)';
545 $relevance = 1 if (!$copies_visible);
547 my $rank = $relevance;
548 if (lc($sort) eq 'pubdate') {
551 SELECT COALESCE(SUBSTRING(frp.value FROM '\\\\d+'),'9999')::INT
552 FROM $metabib_full_rec frp
553 WHERE frp.record = f.record
555 AND frp.subfield = 'c'
559 } elsif (lc($sort) eq 'create_date') {
561 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
563 } elsif (lc($sort) eq 'edit_date') {
565 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
567 } elsif (lc($sort) eq 'title') {
570 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM '\\\\d+'),'0')::INT + 1 )),'zzzzzzzz')
571 FROM $metabib_full_rec frt
572 WHERE frt.record = f.record
574 AND frt.subfield = 'a'
578 } elsif (lc($sort) eq 'author') {
581 SELECT COALESCE(LTRIM(fra.value),'zzzzzzzz')
582 FROM $metabib_full_rec fra
583 WHERE fra.record = f.record
584 AND fra.tag LIKE '1%'
585 AND fra.subfield = 'a'
586 ORDER BY fra.tag::text::int
595 if ($copies_visible) {
597 SELECT f.record, $relevance, count(DISTINCT cp.id), $rank
598 FROM $search_table f,
599 $asset_call_number_table cn,
600 $asset_copy_table cp,
604 $metabib_record_descriptor rd,
606 WHERE br.id = f.record
607 AND cn.record = f.record
608 AND rd.record = f.record
609 AND cp.status = cs.id
610 AND cp.location = cl.id
611 AND br.deleted IS FALSE
612 AND cn.deleted IS FALSE
613 AND cp.deleted IS FALSE
622 GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
623 ORDER BY 4 $sort_dir,3 DESC
627 SELECT f.record, 1, 1, $rank
628 FROM $search_table f,
630 $metabib_record_descriptor rd
631 WHERE br.id = f.record
632 AND rd.record = f.record
633 AND br.deleted IS FALSE
645 $log->debug("Search SQL :: [$select]",DEBUG);
647 my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
648 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
651 $max = 1 if (!@$recs);
653 $max = $$_[1] if ($$_[1] > $max);
657 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
658 next unless ($$rec[0]);
659 my ($rid,$rank,$junk,$skip) = @$rec;
660 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
664 __PACKAGE__->register_method(
665 api_name => 'open-ils.storage.biblio.full_rec.multi_search',
666 method => 'biblio_multi_search_full_rec',
671 __PACKAGE__->register_method(
672 api_name => 'open-ils.storage.biblio.full_rec.multi_search.staff',
673 method => 'biblio_multi_search_full_rec',
679 sub search_full_rec {
685 my $term = $args{term};
686 my $limiters = $args{restrict};
688 my ($index_col) = metabib::full_rec->columns('FTS');
689 $index_col ||= 'value';
690 my $search_table = metabib::full_rec->table;
692 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
694 my $fts_where = $fts->sql_where_clause();
695 my @fts_ranks = $fts->fts_rank;
697 my $rank = join(' + ', @fts_ranks);
701 for my $limit (@$limiters) {
702 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
703 push @binds, $$limit{tag}, $$limit{subfield};
704 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
706 my $where = join(' OR ', @wheres);
708 my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
710 $log->debug("Search SQL :: [$select]",DEBUG);
712 my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
713 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
715 $client->respond($_) for (@$recs);
718 __PACKAGE__->register_method(
719 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
720 method => 'search_full_rec',
725 __PACKAGE__->register_method(
726 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
727 method => 'search_full_rec',
734 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
735 sub search_class_fts {
740 my $term = $args{term};
741 my $ou = $args{org_unit};
742 my $ou_type = $args{depth};
743 my $limit = $args{limit};
744 my $offset = $args{offset};
746 my $limit_clause = '';
747 my $offset_clause = '';
749 $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
750 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
753 my ($t_filter, $f_filter) = ('','');
756 my ($t, $f) = split '-', $args{format};
757 @types = split '', $t;
758 @forms = split '', $f;
760 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
764 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
770 my $descendants = defined($ou_type) ?
771 "actor.org_unit_descendants($ou, $ou_type)" :
772 "actor.org_unit_descendants($ou)";
774 my $class = $self->{cdbi};
775 my $search_table = $class->table;
777 my $metabib_record_descriptor = metabib::record_descriptor->table;
778 my $metabib_metarecord = metabib::metarecord->table;
779 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
780 my $asset_call_number_table = asset::call_number->table;
781 my $asset_copy_table = asset::copy->table;
782 my $cs_table = config::copy_status->table;
783 my $cl_table = asset::copy_location->table;
785 my ($index_col) = $class->columns('FTS');
786 $index_col ||= 'value';
788 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
789 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
791 my $fts_where = $fts->sql_where_clause;
792 my @fts_ranks = $fts->fts_rank;
794 my $rank = join(' + ', @fts_ranks);
796 my $has_vols = 'AND cn.owning_lib = d.id';
797 my $has_copies = 'AND cp.call_number = cn.id';
798 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';
800 my $visible_count = ', count(DISTINCT cp.id)';
801 my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
803 if ($self->api_name =~ /staff/o) {
804 $copies_visible = '';
805 $visible_count_test = '';
806 $has_copies = '' if ($ou_type == 0);
807 $has_vols = '' if ($ou_type == 0);
810 my $rank_calc = <<" RANK";
812 * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
813 * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
814 * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
815 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
818 $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
820 if ($copies_visible) {
822 SELECT m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
823 FROM $search_table f,
824 $metabib_metarecord_source_map_table m,
825 $asset_call_number_table cn,
826 $asset_copy_table cp,
829 $metabib_record_descriptor rd,
832 AND m.source = f.source
833 AND cn.record = m.source
834 AND rd.record = m.source
835 AND cp.status = cs.id
836 AND cp.location = cl.id
842 GROUP BY 1 $visible_count_test
844 $limit_clause $offset_clause
848 SELECT m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
849 FROM $search_table f,
850 $metabib_metarecord_source_map_table m,
851 $metabib_record_descriptor rd
853 AND m.source = f.source
854 AND rd.record = m.source
859 $limit_clause $offset_clause
863 $log->debug("Field Search SQL :: [$select]",DEBUG);
865 my $SQLstring = join('%',$fts->words);
866 my $REstring = join('\\s+',$fts->words);
867 my $first_word = ($fts->words)[0].'%';
868 my $recs = ($self->api_name =~ /unordered/o) ?
869 $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
870 $class->db_Main->selectall_arrayref($select, {},
871 '%'.lc($SQLstring).'%', # phrase order match
872 lc($first_word), # first word match
873 '^\\s*'.lc($REstring).'\\s*/?\s*$', # full exact match
877 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
879 $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
883 for my $class ( qw/title author subject keyword series identifier/ ) {
884 __PACKAGE__->register_method(
885 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord",
886 method => 'search_class_fts',
889 cdbi => "metabib::${class}_field_entry",
892 __PACKAGE__->register_method(
893 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
894 method => 'search_class_fts',
897 cdbi => "metabib::${class}_field_entry",
900 __PACKAGE__->register_method(
901 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
902 method => 'search_class_fts',
905 cdbi => "metabib::${class}_field_entry",
908 __PACKAGE__->register_method(
909 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
910 method => 'search_class_fts',
913 cdbi => "metabib::${class}_field_entry",
918 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
919 sub search_class_fts_count {
924 my $term = $args{term};
925 my $ou = $args{org_unit};
926 my $ou_type = $args{depth};
927 my $limit = $args{limit} || 100;
928 my $offset = $args{offset} || 0;
930 my $descendants = defined($ou_type) ?
931 "actor.org_unit_descendants($ou, $ou_type)" :
932 "actor.org_unit_descendants($ou)";
935 my ($t_filter, $f_filter) = ('','');
938 my ($t, $f) = split '-', $args{format};
939 @types = split '', $t;
940 @forms = split '', $f;
942 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
946 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
951 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
953 my $class = $self->{cdbi};
954 my $search_table = $class->table;
956 my $metabib_record_descriptor = metabib::record_descriptor->table;
957 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
958 my $asset_call_number_table = asset::call_number->table;
959 my $asset_copy_table = asset::copy->table;
960 my $cs_table = config::copy_status->table;
961 my $cl_table = asset::copy_location->table;
963 my ($index_col) = $class->columns('FTS');
964 $index_col ||= 'value';
966 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
968 my $fts_where = $fts->sql_where_clause;
970 my $has_vols = 'AND cn.owning_lib = d.id';
971 my $has_copies = 'AND cp.call_number = cn.id';
972 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';
973 if ($self->api_name =~ /staff/o) {
974 $copies_visible = '';
975 $has_vols = '' if ($ou_type == 0);
976 $has_copies = '' if ($ou_type == 0);
979 # XXX test an "EXISTS version of descendant checking...
981 if ($copies_visible) {
983 SELECT count(distinct m.metarecord)
984 FROM $search_table f,
985 $metabib_metarecord_source_map_table m,
986 $metabib_metarecord_source_map_table mr,
987 $asset_call_number_table cn,
988 $asset_copy_table cp,
991 $metabib_record_descriptor rd,
994 AND mr.source = f.source
995 AND mr.metarecord = m.metarecord
996 AND cn.record = m.source
997 AND rd.record = m.source
998 AND cp.status = cs.id
999 AND cp.location = cl.id
1008 SELECT count(distinct m.metarecord)
1009 FROM $search_table f,
1010 $metabib_metarecord_source_map_table m,
1011 $metabib_metarecord_source_map_table mr,
1012 $metabib_record_descriptor rd
1014 AND mr.source = f.source
1015 AND mr.metarecord = m.metarecord
1016 AND rd.record = m.source
1022 $log->debug("Field Search Count SQL :: [$select]",DEBUG);
1024 my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1026 $log->debug("Count Search yielded $recs results.",DEBUG);
1031 for my $class ( qw/title author subject keyword series identifier/ ) {
1032 __PACKAGE__->register_method(
1033 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
1034 method => 'search_class_fts_count',
1037 cdbi => "metabib::${class}_field_entry",
1040 __PACKAGE__->register_method(
1041 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
1042 method => 'search_class_fts_count',
1045 cdbi => "metabib::${class}_field_entry",
1051 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1052 sub postfilter_search_class_fts {
1057 my $term = $args{term};
1058 my $sort = $args{'sort'};
1059 my $sort_dir = $args{sort_dir} || 'DESC';
1060 my $ou = $args{org_unit};
1061 my $ou_type = $args{depth};
1062 my $limit = $args{limit} || 10;
1063 my $visibility_limit = $args{visibility_limit} || 5000;
1064 my $offset = $args{offset} || 0;
1066 my $outer_limit = 1000;
1068 my $limit_clause = '';
1069 my $offset_clause = '';
1071 $limit_clause = "LIMIT $outer_limit";
1072 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1074 my (@types,@forms,@lang,@aud,@lit_form);
1075 my ($t_filter, $f_filter) = ('','');
1076 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1077 my ($ot_filter, $of_filter) = ('','');
1078 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1080 if (my $a = $args{audience}) {
1081 $a = [$a] if (!ref($a));
1084 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1085 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1088 if (my $l = $args{language}) {
1089 $l = [$l] if (!ref($l));
1092 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1093 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1096 if (my $f = $args{lit_form}) {
1097 $f = [$f] if (!ref($f));
1100 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1101 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1104 if ($args{format}) {
1105 my ($t, $f) = split '-', $args{format};
1106 @types = split '', $t;
1107 @forms = split '', $f;
1109 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1110 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1114 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1115 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1120 my $descendants = defined($ou_type) ?
1121 "actor.org_unit_descendants($ou, $ou_type)" :
1122 "actor.org_unit_descendants($ou)";
1124 my $class = $self->{cdbi};
1125 my $search_table = $class->table;
1127 my $metabib_full_rec = metabib::full_rec->table;
1128 my $metabib_record_descriptor = metabib::record_descriptor->table;
1129 my $metabib_metarecord = metabib::metarecord->table;
1130 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1131 my $asset_call_number_table = asset::call_number->table;
1132 my $asset_copy_table = asset::copy->table;
1133 my $cs_table = config::copy_status->table;
1134 my $cl_table = asset::copy_location->table;
1135 my $br_table = biblio::record_entry->table;
1137 my ($index_col) = $class->columns('FTS');
1138 $index_col ||= 'value';
1140 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1142 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1144 my $SQLstring = join('%',map { lc($_) } $fts->words);
1145 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1146 my $first_word = lc(($fts->words)[0]).'%';
1148 my $fts_where = $fts->sql_where_clause;
1149 my @fts_ranks = $fts->fts_rank;
1152 $bonus{'metabib::identifier_field_entry'} =
1153 $bonus{'metabib::keyword_field_entry'} = [
1154 { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring }
1157 $bonus{'metabib::title_field_entry'} =
1158 $bonus{'metabib::series_field_entry'} = [
1159 { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1160 { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1161 @{ $bonus{'metabib::keyword_field_entry'} }
1164 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1165 $bonus_list ||= '1';
1167 my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1169 my $relevance = join(' + ', @fts_ranks);
1170 $relevance = <<" RANK";
1171 (SUM( ( $relevance ) * ( $bonus_list ) )/COUNT(m.source))
1174 my $string_default_sort = 'zzzz';
1175 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1177 my $number_default_sort = '9999';
1178 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1180 my $rank = $relevance;
1181 if (lc($sort) eq 'pubdate') {
1184 SELECT COALESCE(SUBSTRING(frp.value FROM '\\\\d+'),'$number_default_sort')::INT
1185 FROM $metabib_full_rec frp
1186 WHERE frp.record = mr.master_record
1188 AND frp.subfield = 'c'
1192 } elsif (lc($sort) eq 'create_date') {
1194 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1196 } elsif (lc($sort) eq 'edit_date') {
1198 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1200 } elsif (lc($sort) eq 'title') {
1203 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM '\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1204 FROM $metabib_full_rec frt
1205 WHERE frt.record = mr.master_record
1207 AND frt.subfield = 'a'
1211 } elsif (lc($sort) eq 'author') {
1214 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1215 FROM $metabib_full_rec fra
1216 WHERE fra.record = mr.master_record
1217 AND fra.tag LIKE '1%'
1218 AND fra.subfield = 'a'
1219 ORDER BY fra.tag::text::int
1227 my $select = <<" SQL";
1228 SELECT m.metarecord,
1230 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1232 FROM $search_table f,
1233 $metabib_metarecord_source_map_table m,
1234 $metabib_metarecord_source_map_table smrs,
1235 $metabib_metarecord mr,
1236 $metabib_record_descriptor rd
1238 AND smrs.metarecord = mr.id
1239 AND m.source = f.source
1240 AND m.metarecord = mr.id
1241 AND rd.record = smrs.source
1247 GROUP BY m.metarecord
1248 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1249 LIMIT $visibility_limit
1256 FROM $asset_call_number_table cn,
1257 $metabib_metarecord_source_map_table mrs,
1258 $asset_copy_table cp,
1263 $metabib_record_descriptor ord,
1265 WHERE mrs.metarecord = s.metarecord
1266 AND br.id = mrs.source
1267 AND cn.record = mrs.source
1268 AND cp.status = cs.id
1269 AND cp.location = cl.id
1270 AND cn.owning_lib = d.id
1271 AND cp.call_number = cn.id
1272 AND cp.opac_visible IS TRUE
1273 AND cs.opac_visible IS TRUE
1274 AND cl.opac_visible IS TRUE
1275 AND d.opac_visible IS TRUE
1276 AND br.active IS TRUE
1277 AND br.deleted IS FALSE
1278 AND ord.record = mrs.source
1284 ORDER BY 4 $sort_dir
1286 } elsif ($self->api_name !~ /staff/o) {
1293 FROM $asset_call_number_table cn,
1294 $metabib_metarecord_source_map_table mrs,
1295 $asset_copy_table cp,
1300 $metabib_record_descriptor ord
1302 WHERE mrs.metarecord = s.metarecord
1303 AND br.id = mrs.source
1304 AND cn.record = mrs.source
1305 AND cp.status = cs.id
1306 AND cp.location = cl.id
1307 AND cp.circ_lib = d.id
1308 AND cp.call_number = cn.id
1309 AND cp.opac_visible IS TRUE
1310 AND cs.opac_visible IS TRUE
1311 AND cl.opac_visible IS TRUE
1312 AND d.opac_visible IS TRUE
1313 AND br.active IS TRUE
1314 AND br.deleted IS FALSE
1315 AND ord.record = mrs.source
1323 ORDER BY 4 $sort_dir
1332 FROM $asset_call_number_table cn,
1333 $asset_copy_table cp,
1334 $metabib_metarecord_source_map_table mrs,
1337 $metabib_record_descriptor ord
1339 WHERE mrs.metarecord = s.metarecord
1340 AND br.id = mrs.source
1341 AND cn.record = mrs.source
1342 AND cn.id = cp.call_number
1343 AND br.deleted IS FALSE
1344 AND cn.deleted IS FALSE
1345 AND ord.record = mrs.source
1346 AND ( cn.owning_lib = d.id
1347 OR ( cp.circ_lib = d.id
1348 AND cp.deleted IS FALSE
1360 FROM $asset_call_number_table cn,
1361 $metabib_metarecord_source_map_table mrs,
1362 $metabib_record_descriptor ord
1363 WHERE mrs.metarecord = s.metarecord
1364 AND cn.record = mrs.source
1365 AND ord.record = mrs.source
1373 ORDER BY 4 $sort_dir
1378 $log->debug("Field Search SQL :: [$select]",DEBUG);
1380 my $recs = $class->db_Main->selectall_arrayref(
1382 (@bonus_values > 0 ? @bonus_values : () ),
1383 ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1384 @types, @forms, @aud, @lang, @lit_form,
1385 @types, @forms, @aud, @lang, @lit_form,
1386 ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1388 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1391 $max = 1 if (!@$recs);
1393 $max = $$_[1] if ($$_[1] > $max);
1396 my $count = scalar(@$recs);
1397 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1398 my ($mrid,$rank,$skip) = @$rec;
1399 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1404 for my $class ( qw/title author subject keyword series identifier/ ) {
1405 __PACKAGE__->register_method(
1406 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1407 method => 'postfilter_search_class_fts',
1410 cdbi => "metabib::${class}_field_entry",
1413 __PACKAGE__->register_method(
1414 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1415 method => 'postfilter_search_class_fts',
1418 cdbi => "metabib::${class}_field_entry",
1425 my $_cdbi = { title => "metabib::title_field_entry",
1426 author => "metabib::author_field_entry",
1427 subject => "metabib::subject_field_entry",
1428 keyword => "metabib::keyword_field_entry",
1429 series => "metabib::series_field_entry",
1430 identifier => "metabib::identifier_field_entry",
1433 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1434 sub postfilter_search_multi_class_fts {
1439 my $sort = $args{'sort'};
1440 my $sort_dir = $args{sort_dir} || 'DESC';
1441 my $ou = $args{org_unit};
1442 my $ou_type = $args{depth};
1443 my $limit = $args{limit} || 10;
1444 my $offset = $args{offset} || 0;
1445 my $visibility_limit = $args{visibility_limit} || 5000;
1448 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1451 if (!defined($args{org_unit})) {
1452 die "No target organizational unit passed to ".$self->api_name;
1455 if (! scalar( keys %{$args{searches}} )) {
1456 die "No search arguments were passed to ".$self->api_name;
1459 my $outer_limit = 1000;
1461 my $limit_clause = '';
1462 my $offset_clause = '';
1464 $limit_clause = "LIMIT $outer_limit";
1465 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1467 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1468 my ($t_filter, $f_filter, $v_filter) = ('','','');
1469 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1470 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1471 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1473 if ($args{available}) {
1474 $avail_filter = ' AND cp.status IN (0,7,12)';
1477 if (my $a = $args{audience}) {
1478 $a = [$a] if (!ref($a));
1481 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1482 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1485 if (my $l = $args{language}) {
1486 $l = [$l] if (!ref($l));
1489 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1490 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1493 if (my $f = $args{lit_form}) {
1494 $f = [$f] if (!ref($f));
1497 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1498 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1501 if (my $f = $args{item_form}) {
1502 $f = [$f] if (!ref($f));
1505 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1506 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1509 if (my $t = $args{item_type}) {
1510 $t = [$t] if (!ref($t));
1513 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1514 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1517 if (my $v = $args{vr_format}) {
1518 $v = [$v] if (!ref($v));
1521 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1522 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1526 # XXX legacy format and item type support
1527 if ($args{format}) {
1528 my ($t, $f) = split '-', $args{format};
1529 @types = split '', $t;
1530 @forms = split '', $f;
1532 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1533 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1537 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1538 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1544 my $descendants = defined($ou_type) ?
1545 "actor.org_unit_descendants($ou, $ou_type)" :
1546 "actor.org_unit_descendants($ou)";
1548 my $search_table_list = '';
1550 my $join_table_list = '';
1553 my $field_table = config::metabib_field->table;
1557 my $prev_search_group;
1558 my $curr_search_group;
1562 for my $search_group (sort keys %{$args{searches}}) {
1563 (my $search_group_name = $search_group) =~ s/\|/_/gso;
1564 ($search_class,$search_field) = split /\|/, $search_group;
1565 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1567 if ($search_field) {
1568 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1569 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1574 $prev_search_group = $curr_search_group if ($curr_search_group);
1576 $curr_search_group = $search_group_name;
1578 my $class = $_cdbi->{$search_class};
1579 my $search_table = $class->table;
1581 my ($index_col) = $class->columns('FTS');
1582 $index_col ||= 'value';
1585 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1587 my $fts_where = $fts->sql_where_clause;
1588 my @fts_ranks = $fts->fts_rank;
1590 my $SQLstring = join('%',map { lc($_) } $fts->words);
1591 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1592 my $first_word = lc(($fts->words)[0]).'%';
1594 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1595 my $rank = join(' + ', @fts_ranks);
1598 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1599 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1601 $bonus{'series'} = [
1602 { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1603 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1606 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1608 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1609 $bonus_list ||= '1';
1611 push @bonus_lists, $bonus_list;
1612 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1615 #---------------------
1617 $search_table_list .= "$search_table $search_group_name, ";
1618 push @rank_list,$rank;
1619 $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1621 if ($metabib_field) {
1622 $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1623 $metabib_field = undef;
1626 if ($prev_search_group) {
1627 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1631 my $metabib_record_descriptor = metabib::record_descriptor->table;
1632 my $metabib_full_rec = metabib::full_rec->table;
1633 my $metabib_metarecord = metabib::metarecord->table;
1634 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1635 my $asset_call_number_table = asset::call_number->table;
1636 my $asset_copy_table = asset::copy->table;
1637 my $cs_table = config::copy_status->table;
1638 my $cl_table = asset::copy_location->table;
1639 my $br_table = biblio::record_entry->table;
1640 my $source_table = config::bib_source->table;
1642 my $bonuses = join (' * ', @bonus_lists);
1643 my $relevance = join (' + ', @rank_list);
1644 $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1646 my $string_default_sort = 'zzzz';
1647 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1649 my $number_default_sort = '9999';
1650 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1654 my $secondary_sort = <<" SORT";
1656 SELECT COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM '\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1657 FROM $metabib_full_rec sfrt,
1658 $metabib_metarecord mr
1659 WHERE sfrt.record = mr.master_record
1660 AND sfrt.tag = '245'
1661 AND sfrt.subfield = 'a'
1666 my $rank = $relevance;
1667 if (lc($sort) eq 'pubdate') {
1670 SELECT COALESCE(SUBSTRING(frp.value FROM '\\\\d+'),'$number_default_sort')::INT
1671 FROM $metabib_full_rec frp
1672 WHERE frp.record = mr.master_record
1674 AND frp.subfield = 'c'
1678 } elsif (lc($sort) eq 'create_date') {
1680 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1682 } elsif (lc($sort) eq 'edit_date') {
1684 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1686 } elsif (lc($sort) eq 'title') {
1689 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM '\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1690 FROM $metabib_full_rec frt
1691 WHERE frt.record = mr.master_record
1693 AND frt.subfield = 'a'
1697 $secondary_sort = <<" SORT";
1699 SELECT COALESCE(SUBSTRING(sfrp.value FROM '\\\\d+'),'$number_default_sort')::INT
1700 FROM $metabib_full_rec sfrp
1701 WHERE sfrp.record = mr.master_record
1702 AND sfrp.tag = '260'
1703 AND sfrp.subfield = 'c'
1707 } elsif (lc($sort) eq 'author') {
1710 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1711 FROM $metabib_full_rec fra
1712 WHERE fra.record = mr.master_record
1713 AND fra.tag LIKE '1%'
1714 AND fra.subfield = 'a'
1715 ORDER BY fra.tag::text::int
1720 push @bonus_values, @bonus_values;
1725 my $select = <<" SQL";
1726 SELECT m.metarecord,
1728 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1731 FROM $search_table_list
1732 $metabib_metarecord mr,
1733 $metabib_metarecord_source_map_table m,
1734 $metabib_metarecord_source_map_table smrs
1735 WHERE m.metarecord = smrs.metarecord
1736 AND mr.id = m.metarecord
1739 GROUP BY m.metarecord
1740 -- ORDER BY 4 $sort_dir
1741 LIMIT $visibility_limit
1744 if ($self->api_name !~ /staff/o) {
1751 FROM $asset_call_number_table cn,
1752 $metabib_metarecord_source_map_table mrs,
1753 $asset_copy_table cp,
1758 $metabib_record_descriptor ord
1759 WHERE mrs.metarecord = s.metarecord
1760 AND br.id = mrs.source
1761 AND cn.record = mrs.source
1762 AND cp.status = cs.id
1763 AND cp.location = cl.id
1764 AND cp.circ_lib = d.id
1765 AND cp.call_number = cn.id
1766 AND cp.opac_visible IS TRUE
1767 AND cs.opac_visible IS TRUE
1768 AND cl.opac_visible IS TRUE
1769 AND d.opac_visible IS TRUE
1770 AND br.active IS TRUE
1771 AND br.deleted IS FALSE
1772 AND cp.deleted IS FALSE
1773 AND cn.deleted IS FALSE
1774 AND ord.record = mrs.source
1787 $metabib_metarecord_source_map_table mrs,
1788 $metabib_record_descriptor ord,
1790 WHERE mrs.metarecord = s.metarecord
1791 AND ord.record = mrs.source
1792 AND br.id = mrs.source
1793 AND br.source = src.id
1794 AND src.transcendant IS TRUE
1802 ORDER BY 4 $sort_dir, 5
1809 $metabib_metarecord_source_map_table omrs,
1810 $metabib_record_descriptor ord
1811 WHERE omrs.metarecord = s.metarecord
1812 AND ord.record = omrs.source
1815 FROM $asset_call_number_table cn,
1816 $asset_copy_table cp,
1819 WHERE br.id = omrs.source
1820 AND cn.record = omrs.source
1821 AND br.deleted IS FALSE
1822 AND cn.deleted IS FALSE
1823 AND cp.call_number = cn.id
1824 AND ( cn.owning_lib = d.id
1825 OR ( cp.circ_lib = d.id
1826 AND cp.deleted IS FALSE
1834 FROM $asset_call_number_table cn
1835 WHERE cn.record = omrs.source
1836 AND cn.deleted IS FALSE
1842 $metabib_metarecord_source_map_table mrs,
1843 $metabib_record_descriptor ord,
1845 WHERE mrs.metarecord = s.metarecord
1846 AND br.id = mrs.source
1847 AND br.source = src.id
1848 AND src.transcendant IS TRUE
1864 ORDER BY 4 $sort_dir, 5
1869 $log->debug("Field Search SQL :: [$select]",DEBUG);
1871 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1874 @types, @forms, @vformats, @aud, @lang, @lit_form,
1875 @types, @forms, @vformats, @aud, @lang, @lit_form,
1876 # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1879 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1882 $max = 1 if (!@$recs);
1884 $max = $$_[1] if ($$_[1] > $max);
1887 my $count = scalar(@$recs);
1888 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1889 next unless ($$rec[0]);
1890 my ($mrid,$rank,$skip) = @$rec;
1891 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1896 __PACKAGE__->register_method(
1897 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1898 method => 'postfilter_search_multi_class_fts',
1903 __PACKAGE__->register_method(
1904 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1905 method => 'postfilter_search_multi_class_fts',
1911 __PACKAGE__->register_method(
1912 api_name => "open-ils.storage.metabib.multiclass.search_fts",
1913 method => 'postfilter_search_multi_class_fts',
1918 __PACKAGE__->register_method(
1919 api_name => "open-ils.storage.metabib.multiclass.search_fts.staff",
1920 method => 'postfilter_search_multi_class_fts',
1926 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1927 sub biblio_search_multi_class_fts {
1932 my $sort = $args{'sort'};
1933 my $sort_dir = $args{sort_dir} || 'DESC';
1934 my $ou = $args{org_unit};
1935 my $ou_type = $args{depth};
1936 my $limit = $args{limit} || 10;
1937 my $offset = $args{offset} || 0;
1938 my $pref_lang = $args{prefered_language} || 'eng';
1939 my $visibility_limit = $args{visibility_limit} || 5000;
1942 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1945 if (! scalar( keys %{$args{searches}} )) {
1946 die "No search arguments were passed to ".$self->api_name;
1949 my $outer_limit = 1000;
1951 my $limit_clause = '';
1952 my $offset_clause = '';
1954 $limit_clause = "LIMIT $outer_limit";
1955 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1957 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1958 my ($t_filter, $f_filter, $v_filter) = ('','','');
1959 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1960 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1961 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1963 if ($args{available}) {
1964 $avail_filter = ' AND cp.status IN (0,7,12)';
1967 if (my $a = $args{audience}) {
1968 $a = [$a] if (!ref($a));
1971 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1972 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1975 if (my $l = $args{language}) {
1976 $l = [$l] if (!ref($l));
1979 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1980 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1983 if (my $f = $args{lit_form}) {
1984 $f = [$f] if (!ref($f));
1987 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1988 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1991 if (my $f = $args{item_form}) {
1992 $f = [$f] if (!ref($f));
1995 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1996 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1999 if (my $t = $args{item_type}) {
2000 $t = [$t] if (!ref($t));
2003 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2004 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2007 if (my $v = $args{vr_format}) {
2008 $v = [$v] if (!ref($v));
2011 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
2012 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
2015 # XXX legacy format and item type support
2016 if ($args{format}) {
2017 my ($t, $f) = split '-', $args{format};
2018 @types = split '', $t;
2019 @forms = split '', $f;
2021 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2022 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2026 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2027 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2032 my $descendants = defined($ou_type) ?
2033 "actor.org_unit_descendants($ou, $ou_type)" :
2034 "actor.org_unit_descendants($ou)";
2036 my $search_table_list = '';
2038 my $join_table_list = '';
2041 my $field_table = config::metabib_field->table;
2045 my $prev_search_group;
2046 my $curr_search_group;
2050 for my $search_group (sort keys %{$args{searches}}) {
2051 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2052 ($search_class,$search_field) = split /\|/, $search_group;
2053 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2055 if ($search_field) {
2056 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2057 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2062 $prev_search_group = $curr_search_group if ($curr_search_group);
2064 $curr_search_group = $search_group_name;
2066 my $class = $_cdbi->{$search_class};
2067 my $search_table = $class->table;
2069 my ($index_col) = $class->columns('FTS');
2070 $index_col ||= 'value';
2073 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2075 my $fts_where = $fts->sql_where_clause;
2076 my @fts_ranks = $fts->fts_rank;
2078 my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2079 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2080 my $first_word = lc(($fts->words)[0]).'%';
2082 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2083 my $rank = join(' + ', @fts_ranks);
2086 $bonus{'subject'} = [];
2087 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2089 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2091 $bonus{'series'} = [
2092 { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2093 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2096 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2099 push @{ $bonus{'title'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2100 push @{ $bonus{'author'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2101 push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2102 push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2103 push @{ $bonus{'series'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2106 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2107 $bonus_list ||= '1';
2109 push @bonus_lists, $bonus_list;
2110 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2112 #---------------------
2114 $search_table_list .= "$search_table $search_group_name, ";
2115 push @rank_list,$rank;
2116 $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2118 if ($metabib_field) {
2119 $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2120 $metabib_field = undef;
2123 if ($prev_search_group) {
2124 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2128 my $metabib_record_descriptor = metabib::record_descriptor->table;
2129 my $metabib_full_rec = metabib::full_rec->table;
2130 my $metabib_metarecord = metabib::metarecord->table;
2131 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2132 my $asset_call_number_table = asset::call_number->table;
2133 my $asset_copy_table = asset::copy->table;
2134 my $cs_table = config::copy_status->table;
2135 my $cl_table = asset::copy_location->table;
2136 my $br_table = biblio::record_entry->table;
2137 my $source_table = config::bib_source->table;
2140 my $bonuses = join (' * ', @bonus_lists);
2141 my $relevance = join (' + ', @rank_list);
2142 $relevance = "AVG( ($relevance) * ($bonuses) )";
2144 my $string_default_sort = 'zzzz';
2145 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2147 my $number_default_sort = '9999';
2148 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2150 my $rank = $relevance;
2151 if (lc($sort) eq 'pubdate') {
2154 SELECT COALESCE(SUBSTRING(frp.value FROM '\\\\d{4}'),'$number_default_sort')::INT
2155 FROM $metabib_full_rec frp
2156 WHERE frp.record = b.id
2158 AND frp.subfield = 'c'
2162 } elsif (lc($sort) eq 'create_date') {
2164 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2166 } elsif (lc($sort) eq 'edit_date') {
2168 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2170 } elsif (lc($sort) eq 'title') {
2173 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM '\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2174 FROM $metabib_full_rec frt
2175 WHERE frt.record = b.id
2177 AND frt.subfield = 'a'
2181 } elsif (lc($sort) eq 'author') {
2184 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
2185 FROM $metabib_full_rec fra
2186 WHERE fra.record = b.id
2187 AND fra.tag LIKE '1%'
2188 AND fra.subfield = 'a'
2189 ORDER BY fra.tag::text::int
2194 push @bonus_values, @bonus_values;
2199 my $select = <<" SQL";
2204 FROM $search_table_list
2205 $metabib_record_descriptor rd,
2208 WHERE rd.record = b.id
2209 AND b.active IS TRUE
2210 AND b.deleted IS FALSE
2219 GROUP BY b.id, b.source
2220 ORDER BY 3 $sort_dir
2221 LIMIT $visibility_limit
2224 if ($self->api_name !~ /staff/o) {
2229 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2232 FROM $asset_call_number_table cn,
2233 $asset_copy_table cp,
2237 WHERE cn.record = s.id
2238 AND cp.status = cs.id
2239 AND cp.location = cl.id
2240 AND cp.call_number = cn.id
2241 AND cp.opac_visible IS TRUE
2242 AND cs.opac_visible IS TRUE
2243 AND cl.opac_visible IS TRUE
2244 AND d.opac_visible IS TRUE
2245 AND cp.deleted IS FALSE
2246 AND cn.deleted IS FALSE
2247 AND cp.circ_lib = d.id
2251 OR src.transcendant IS TRUE
2252 ORDER BY 3 $sort_dir
2259 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2262 FROM $asset_call_number_table cn,
2263 $asset_copy_table cp,
2265 WHERE cn.record = s.id
2266 AND cp.call_number = cn.id
2267 AND cn.deleted IS FALSE
2268 AND cp.circ_lib = d.id
2269 AND cp.deleted IS FALSE
2275 FROM $asset_call_number_table cn
2276 WHERE cn.record = s.id
2279 OR src.transcendant IS TRUE
2280 ORDER BY 3 $sort_dir
2285 $log->debug("Field Search SQL :: [$select]",DEBUG);
2287 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2289 @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2292 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2294 my $count = scalar(@$recs);
2295 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2296 next unless ($$rec[0]);
2297 my ($mrid,$rank) = @$rec;
2298 $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2303 __PACKAGE__->register_method(
2304 api_name => "open-ils.storage.biblio.multiclass.search_fts.record",
2305 method => 'biblio_search_multi_class_fts',
2310 __PACKAGE__->register_method(
2311 api_name => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2312 method => 'biblio_search_multi_class_fts',
2317 __PACKAGE__->register_method(
2318 api_name => "open-ils.storage.biblio.multiclass.search_fts",
2319 method => 'biblio_search_multi_class_fts',
2324 __PACKAGE__->register_method(
2325 api_name => "open-ils.storage.biblio.multiclass.search_fts.staff",
2326 method => 'biblio_search_multi_class_fts',
2334 my $default_preferred_language;
2335 my $default_preferred_language_weight;
2337 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2343 if (!$locale_map{COMPLETE}) {
2345 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2346 for my $locale ( @locales ) {
2347 $locale_map{$locale->code} = $locale->marc_code;
2349 $locale_map{COMPLETE} = 1;
2353 if (!$default_preferred_language) {
2355 $default_preferred_language = OpenSRF::Utils::SettingsClient
2358 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2363 if (!$default_preferred_language_weight) {
2365 $default_preferred_language_weight = OpenSRF::Utils::SettingsClient
2368 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2373 # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2374 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2376 my $ou = $args{org_unit};
2377 my $limit = $args{limit} || 10;
2378 my $offset = $args{offset} || 0;
2381 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2384 if (! scalar( keys %{$args{searches}} )) {
2385 die "No search arguments were passed to ".$self->api_name;
2388 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2390 if (!defined($args{preferred_language})) {
2391 my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2392 $args{preferred_language} =
2393 $locale_map{ $ses_locale } || 'eng';
2396 if (!defined($args{preferred_language_weight})) {
2397 $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2400 if ($args{available}) {
2401 @statuses = (0,7,12);
2404 if (my $s = $args{locations}) {
2405 $s = [$s] if (!ref($s));
2409 if (my $b = $args{between}) {
2410 if (ref($b) && @$b == 2) {
2415 if (my $s = $args{statuses}) {
2416 $s = [$s] if (!ref($s));
2420 if (my $a = $args{audience}) {
2421 $a = [$a] if (!ref($a));
2425 if (my $l = $args{language}) {
2426 $l = [$l] if (!ref($l));
2430 if (my $f = $args{lit_form}) {
2431 $f = [$f] if (!ref($f));
2435 if (my $f = $args{item_form}) {
2436 $f = [$f] if (!ref($f));
2440 if (my $t = $args{item_type}) {
2441 $t = [$t] if (!ref($t));
2445 if (my $b = $args{bib_level}) {
2446 $b = [$b] if (!ref($b));
2450 if (my $v = $args{vr_format}) {
2451 $v = [$v] if (!ref($v));
2455 # XXX legacy format and item type support
2456 if ($args{format}) {
2457 my ($t, $f) = split '-', $args{format};
2458 @types = split '', $t;
2459 @forms = split '', $f;
2462 my %stored_proc_search_args;
2463 for my $search_group (sort keys %{$args{searches}}) {
2464 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2465 my ($search_class,$search_field) = split /\|/, $search_group;
2466 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2468 if ($search_field) {
2469 unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2470 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2475 my $class = $_cdbi->{$search_class};
2476 my $search_table = $class->table;
2478 my ($index_col) = $class->columns('FTS');
2479 $index_col ||= 'value';
2482 my $fts = OpenILS::Application::Storage::FTS->compile(
2483 $search_class => $args{searches}{$search_group}{term},
2484 $search_group_name.'.value',
2485 "$search_group_name.$index_col"
2487 $fts->sql_where_clause; # this builds the ranks for us
2489 my @fts_ranks = $fts->fts_rank;
2490 my @fts_queries = $fts->fts_query;
2491 my @phrases = map { lc($_) } $fts->phrases;
2492 my @words = map { lc($_) } $fts->words;
2494 $stored_proc_search_args{$search_group} = {
2495 fts_rank => \@fts_ranks,
2496 fts_query => \@fts_queries,
2497 phrase => \@phrases,
2503 my $param_search_ou = $ou;
2504 my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2505 my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2506 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2507 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2508 my $param_audience = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud ) . '}$$';
2509 my $param_language = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang ) . '}$$';
2510 my $param_lit_form = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2511 my $param_types = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types ) . '}$$';
2512 my $param_forms = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms ) . '}$$';
2513 my $param_vformats = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2514 my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2515 my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2516 my $param_after = $args{after} ; $param_after = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2517 my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2518 my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2519 my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2520 my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2521 my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2522 my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2523 my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2524 my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2525 my $param_rel_limit = $args{core_limit}; $param_rel_limit ||= 'NULL';
2526 my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2527 my $param_skip_chk = $args{skip_check}; $param_skip_chk ||= 'NULL';
2529 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2531 FROM search.staged_fts(
2532 $param_search_ou\:\:INT,
2533 $param_depth\:\:INT,
2534 $param_searches\:\:TEXT,
2535 $param_statuses\:\:INT[],
2536 $param_locations\:\:INT[],
2537 $param_audience\:\:TEXT[],
2538 $param_language\:\:TEXT[],
2539 $param_lit_form\:\:TEXT[],
2540 $param_types\:\:TEXT[],
2541 $param_forms\:\:TEXT[],
2542 $param_vformats\:\:TEXT[],
2543 $param_bib_level\:\:TEXT[],
2544 $param_before\:\:TEXT,
2545 $param_after\:\:TEXT,
2546 $param_during\:\:TEXT,
2547 $param_between\:\:TEXT[],
2548 $param_pref_lang\:\:TEXT,
2549 $param_pref_lang_multiplier\:\:REAL,
2550 $param_sort\:\:TEXT,
2551 $param_sort_desc\:\:BOOL,
2552 $metarecord\:\:BOOL,
2554 $param_rel_limit\:\:INT,
2555 $param_chk_limit\:\:INT,
2556 $param_skip_chk\:\:INT
2562 my $recs = $sth->fetchall_arrayref({});
2563 my $summary_row = pop @$recs;
2565 my $total = $$summary_row{total};
2566 my $checked = $$summary_row{checked};
2567 my $visible = $$summary_row{visible};
2568 my $deleted = $$summary_row{deleted};
2569 my $excluded = $$summary_row{excluded};
2571 my $estimate = $visible;
2572 if ( $total > $checked && $checked ) {
2574 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2575 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2579 delete $$summary_row{id};
2580 delete $$summary_row{rel};
2581 delete $$summary_row{record};
2583 $client->respond( $summary_row );
2585 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2587 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2588 delete $$rec{checked};
2589 delete $$rec{visible};
2590 delete $$rec{excluded};
2591 delete $$rec{deleted};
2592 delete $$rec{total};
2593 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2595 $client->respond( $rec );
2599 __PACKAGE__->register_method(
2600 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
2601 method => 'staged_fts',
2606 __PACKAGE__->register_method(
2607 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2608 method => 'staged_fts',
2613 __PACKAGE__->register_method(
2614 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
2615 method => 'staged_fts',
2620 __PACKAGE__->register_method(
2621 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2622 method => 'staged_fts',
2628 sub FTS_paging_estimate {
2632 my $checked = shift;
2633 my $visible = shift;
2634 my $excluded = shift;
2635 my $deleted = shift;
2638 my $deleted_ratio = $deleted / $checked;
2639 my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2641 my $exclusion_ratio = $excluded / $checked;
2642 my $delete_adjusted_exclusion_ratio = $excluded / ($checked - $deleted);
2644 my $inclusion_ratio = $visible / $checked;
2645 my $delete_adjusted_inclusion_ratio = $visible / ($checked - $deleted);
2648 exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2649 inclusion => int($delete_adjusted_total * $inclusion_ratio),
2650 delete_adjusted_exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2651 delete_adjusted_inclusion => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2654 __PACKAGE__->register_method(
2655 api_name => "open-ils.storage.fts_paging_estimate",
2656 method => 'FTS_paging_estimate',
2662 Hash of estimation values based on four variant estimation strategies:
2663 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2664 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2665 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2666 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2669 Helper method used to determin the approximate number of
2670 hits for a search that spans multiple superpages. For
2671 sparse superpages, the inclusion estimate will likely be the
2672 best estimate. The exclusion strategy is the original, but
2673 inclusion is the default.
2676 { name => 'checked',
2677 desc => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2680 { name => 'visible',
2681 desc => 'Number of records visible to the search location on the current superpage.',
2684 { name => 'excluded',
2685 desc => 'Number of records excluded from the search location on the current superpage.',
2688 { name => 'deleted',
2689 desc => 'Number of deleted records on the current superpage.',
2693 desc => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2706 my $term = $$args{term};
2707 my $limit = $$args{max} || 1;
2708 my $min = $$args{min} || 1;
2709 my @classes = @{$$args{class}};
2711 $limit = $min if ($min > $limit);
2714 @classes = ( qw/ title author subject series keyword / );
2718 my $bre_table = biblio::record_entry->table;
2719 my $cn_table = asset::call_number->table;
2720 my $cp_table = asset::copy->table;
2722 for my $search_class ( @classes ) {
2724 my $class = $_cdbi->{$search_class};
2725 my $search_table = $class->table;
2727 my ($index_col) = $class->columns('FTS');
2728 $index_col ||= 'value';
2731 my $where = OpenILS::Application::Storage::FTS
2732 ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2736 SELECT COUNT(DISTINCT X.source)
2737 FROM (SELECT $search_class.source
2738 FROM $search_table $search_class
2739 JOIN $bre_table b ON (b.id = $search_class.source)
2744 HAVING COUNT(DISTINCT X.source) >= $min;
2747 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2748 $matches{$search_class} = $res ? $res->[0] : 0;
2753 __PACKAGE__->register_method(
2754 api_name => "open-ils.storage.search.xref",
2755 method => 'xref_count',
2759 sub query_parser_fts {
2765 # grab the query parser and initialize it
2766 my $parser = $OpenILS::Application::Storage::QParser;
2769 if (!$parser->initialization_complete) {
2770 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
2771 $parser->initialize(
2772 config_metabib_field_index_norm_map =>
2774 'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
2775 { id => { "!=" => undef } },
2776 { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
2778 search_relevance_adjustment =>
2780 'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
2781 { id => { "!=" => undef } }
2783 config_metabib_field =>
2785 'open-ils.cstore.direct.config.metabib_field.search.atomic',
2786 { id => { "!=" => undef } }
2788 config_metabib_search_alias =>
2790 'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
2791 { alias => { "!=" => undef } }
2795 $cstore->disconnect;
2796 die("Cannot initialize $parser!") unless ($parser->initialization_complete);
2800 # populate the locale/language map
2801 if (!$locale_map{COMPLETE}) {
2803 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2804 for my $locale ( @locales ) {
2805 $locale_map{$locale->code} = $locale->marc_code;
2807 $locale_map{COMPLETE} = 1;
2811 # I hope we have a query!
2812 if (! $args{query} ) {
2813 die "No query was passed to ".$self->api_name;
2817 my $simple_plan = $args{_simple_plan};
2818 # remove bad chunks of the %args hash
2819 for my $bad ( grep { /^_/ } keys(%args)) {
2820 delete($args{$bad});
2824 # parse the query and supply any query-level %arg-based defaults
2825 # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2826 my $query = $parser->new( %args )->parse;
2829 # set the locale-based default prefered location
2830 if (!$query->parse_tree->find_filter('preferred_language')) {
2831 $parser->default_preferred_language( $args{preferred_language} );
2832 if (!$parser->default_preferred_language) {
2833 my $ses_locale = $client->session ? $client->session->session_locale : '';
2834 $parser->default_preferred_language( $locale_map{ $ses_locale } );
2836 $parser->default_preferred_language(
2837 OpenSRF::Utils::SettingsClient->new->config_value(
2838 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2840 ) if (!$parser->default_preferred_language);
2844 # set the global default language multiplier
2845 if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2846 $parser->default_preferred_language_multiplier($args{preferred_language_weight});
2847 $parser->default_preferred_language_multiplier($args{preferred_language_multiplier});
2848 $parser->default_preferred_language_multiplier(
2849 OpenSRF::Utils::SettingsClient->new->config_value(
2850 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2852 ) if (!$parser->default_preferred_language_multiplier);
2855 # gather the site, if one is specified, defaulting to the in-query version
2856 my $ou = $args{org_unit};
2857 if (my ($filter) = $query->parse_tree->find_filter('site')) {
2858 $ou = $filter->args->[0] if (@{$filter->args});
2860 $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^\d+$/);
2863 # gather lasso, as with $ou
2864 my $lasso = $args{lasso};
2865 if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
2866 $lasso = $filter->args->[0] if (@{$filter->args});
2868 $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
2869 $lasso = -$lasso if ($lasso);
2872 # # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
2873 # # gather user lasso, as with $ou and lasso
2874 # my $mylasso = $args{my_lasso};
2875 # if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
2876 # $mylasso = $filter->args->[0] if (@{$filter->args});
2878 # $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
2881 # if we have a lasso, go with that, otherwise ... ou
2882 $ou = $lasso if ($lasso);
2885 # get the default $ou if we have nothing
2886 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
2889 # 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
2890 # gather the depth, if one is specified, defaulting to the in-query version
2891 my $depth = $args{depth};
2892 if (my ($filter) = $query->parse_tree->find_filter('depth')) {
2893 $depth = $filter->args->[0] if (@{$filter->args});
2895 $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
2898 # gather the limit or default to 10
2899 my $limit = $args{check_limit} || 'NULL';
2900 if (my ($filter) = $query->parse_tree->find_filter('limit')) {
2901 $limit = $filter->args->[0] if (@{$filter->args});
2903 if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
2904 $limit = $filter->args->[0] if (@{$filter->args});
2908 # gather the offset or default to 0
2909 my $offset = $args{skip_check} || $args{offset} || 0;
2910 if (my ($filter) = $query->parse_tree->find_filter('offset')) {
2911 $offset = $filter->args->[0] if (@{$filter->args});
2913 if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
2914 $offset = $filter->args->[0] if (@{$filter->args});
2918 # gather the estimation strategy or default to inclusion
2919 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2920 if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
2921 $estimation_strategy = $filter->args->[0] if (@{$filter->args});
2925 # gather the estimation strategy or default to inclusion
2926 my $core_limit = $args{core_limit};
2927 if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
2928 $core_limit = $filter->args->[0] if (@{$filter->args});
2932 # gather statuses, and then forget those if we have an #available modifier
2934 if (my ($filter) = $query->parse_tree->find_filter('statuses')) {
2935 @statuses = @{$filter->args} if (@{$filter->args});
2937 @statuses = (0,7,12) if ($query->parse_tree->find_modifier('available'));
2942 if (my ($filter) = $query->parse_tree->find_filter('locations')) {
2943 @location = @{$filter->args} if (@{$filter->args});
2947 my $param_check = $limit || $query->superpage_size || 'NULL';
2948 my $param_offset = $offset || 'NULL';
2949 my $param_limit = $core_limit || 'NULL';
2951 my $sp = $query->superpage || 1;
2953 $param_offset = ($sp - 1) * $sp_size;
2956 my $param_search_ou = $ou;
2957 my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
2958 my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
2959 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
2960 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
2961 my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
2962 my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
2964 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2966 FROM search.query_parser_fts(
2967 $param_search_ou\:\:INT,
2968 $param_depth\:\:INT,
2969 $param_core_query\:\:TEXT,
2970 $param_statuses\:\:INT[],
2971 $param_locations\:\:INT[],
2972 $param_offset\:\:INT,
2973 $param_check\:\:INT,
2974 $param_limit\:\:INT,
2975 $metarecord\:\:BOOL,
2982 my $recs = $sth->fetchall_arrayref({});
2983 my $summary_row = pop @$recs;
2985 my $total = $$summary_row{total};
2986 my $checked = $$summary_row{checked};
2987 my $visible = $$summary_row{visible};
2988 my $deleted = $$summary_row{deleted};
2989 my $excluded = $$summary_row{excluded};
2991 my $estimate = $visible;
2992 if ( $total > $checked && $checked ) {
2994 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2995 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2999 delete $$summary_row{id};
3000 delete $$summary_row{rel};
3001 delete $$summary_row{record};
3003 if (defined($simple_plan)) {
3004 $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3006 $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3009 $client->respond( $summary_row );
3011 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
3013 for my $rec (@$recs) {
3014 delete $$rec{checked};
3015 delete $$rec{visible};
3016 delete $$rec{excluded};
3017 delete $$rec{deleted};
3018 delete $$rec{total};
3019 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3021 $client->respond( $rec );
3026 sub query_parser_fts_wrapper {
3031 $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3032 # grab the query parser and initialize it
3033 my $parser = $OpenILS::Application::Storage::QParser;
3036 if (!$parser->initialization_complete) {
3037 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3038 $parser->initialize(
3039 config_metabib_field_index_norm_map =>
3041 'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
3042 { id => { "!=" => undef } },
3043 { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
3045 search_relevance_adjustment =>
3047 'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
3048 { id => { "!=" => undef } }
3050 config_metabib_field =>
3052 'open-ils.cstore.direct.config.metabib_field.search.atomic',
3053 { id => { "!=" => undef } }
3055 config_metabib_search_alias =>
3057 'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
3058 { alias => { "!=" => undef } }
3062 $cstore->disconnect;
3063 die("Cannot initialize $parser!") unless ($parser->initialization_complete);
3066 if (! scalar( keys %{$args{searches}} )) {
3067 die "No search arguments were passed to ".$self->api_name;
3070 $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3071 my $base_query = '';
3072 for my $sclass ( keys %{$args{searches}} ) {
3073 $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3074 $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3077 my $query = $base_query;
3078 $log->debug("Full base query: $base_query", DEBUG);
3080 if (!$locale_map{COMPLETE}) {
3082 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3083 for my $locale ( @locales ) {
3084 $locale_map{$locale->code} = $locale->marc_code;
3086 $locale_map{COMPLETE} = 1;
3090 my $base_plan = $parser->new( query => $base_query )->parse;
3092 $query = "preferred_language($args{preferred_language}) $query"
3093 if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3094 $query = "preferred_language_weight($args{preferred_language_weight}) $query"
3095 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'));
3097 $query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
3098 $query = "site($args{org_unit}) $query" if ($args{org_unit});
3099 $query = "sort($args{sort}) $query" if ($args{sort});
3100 $query = "limit($args{limit}) $query" if ($args{limit});
3101 $query = "core_limit($args{core_limit}) $query" if ($args{core_limit});
3102 $query = "skip_check($args{skip_check}) $query" if ($args{skip_check});
3103 $query = "superpage($args{superpage}) $query" if ($args{superpage});
3104 $query = "offset($args{offset}) $query" if ($args{offset});
3105 $query = "#metarecord $query" if ($self->api_name =~ /metabib/);
3106 $query = "#available $query" if ($args{available});
3107 $query = "#descending $query" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3108 $query = "#staff $query" if ($self->api_name =~ /staff/);
3109 $query = "before($args{before}) $query" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3110 $query = "after($args{after}) $query" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3111 $query = "during($args{during}) $query" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3112 $query = "between($args{between}[0],$args{between}[1]) $query"
3113 if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3116 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3118 # XXX legacy format and item type support
3119 if ($args{format}) {
3120 my ($t, $f) = split '-', $args{format};
3121 $args{item_type} = [ split '', $t ];
3122 $args{item_form} = [ split '', $f ];
3125 for my $filter ( qw/locations statuses between audience language lit_form item_form item_type bib_level vr_format/ ) {
3126 if (my $s = $args{$filter}) {
3127 $s = [$s] if (!ref($s));
3129 my @filter_list = @$s;
3131 next if ($filter eq 'between' and scalar(@filter_list) != 2);
3132 next if (@filter_list == 0);
3134 my $filter_string = join ',', @filter_list;
3135 $query = "$filter($filter_string) $query";
3139 $log->debug("Full QueryParser query: $query", DEBUG);
3141 return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan );
3143 __PACKAGE__->register_method(
3144 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
3145 method => 'query_parser_fts_wrapper',
3150 __PACKAGE__->register_method(
3151 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3152 method => 'query_parser_fts_wrapper',
3157 __PACKAGE__->register_method(
3158 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
3159 method => 'query_parser_fts_wrapper',
3164 __PACKAGE__->register_method(
3165 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3166 method => 'query_parser_fts_wrapper',