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/ ) {
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/ ) {
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::keyword_field_entry'} = [ { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring } ];
1153 $bonus{'metabib::title_field_entry'} =
1154 $bonus{'metabib::series_field_entry'} = [
1155 { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1156 { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1157 @{ $bonus{'metabib::keyword_field_entry'} }
1160 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1161 $bonus_list ||= '1';
1163 my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1165 my $relevance = join(' + ', @fts_ranks);
1166 $relevance = <<" RANK";
1167 (SUM( ( $relevance ) * ( $bonus_list ) )/COUNT(m.source))
1170 my $string_default_sort = 'zzzz';
1171 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1173 my $number_default_sort = '9999';
1174 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1176 my $rank = $relevance;
1177 if (lc($sort) eq 'pubdate') {
1180 SELECT COALESCE(SUBSTRING(frp.value FROM '\\\\d+'),'$number_default_sort')::INT
1181 FROM $metabib_full_rec frp
1182 WHERE frp.record = mr.master_record
1184 AND frp.subfield = 'c'
1188 } elsif (lc($sort) eq 'create_date') {
1190 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1192 } elsif (lc($sort) eq 'edit_date') {
1194 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1196 } elsif (lc($sort) eq 'title') {
1199 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM '\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1200 FROM $metabib_full_rec frt
1201 WHERE frt.record = mr.master_record
1203 AND frt.subfield = 'a'
1207 } elsif (lc($sort) eq 'author') {
1210 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1211 FROM $metabib_full_rec fra
1212 WHERE fra.record = mr.master_record
1213 AND fra.tag LIKE '1%'
1214 AND fra.subfield = 'a'
1215 ORDER BY fra.tag::text::int
1223 my $select = <<" SQL";
1224 SELECT m.metarecord,
1226 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1228 FROM $search_table f,
1229 $metabib_metarecord_source_map_table m,
1230 $metabib_metarecord_source_map_table smrs,
1231 $metabib_metarecord mr,
1232 $metabib_record_descriptor rd
1234 AND smrs.metarecord = mr.id
1235 AND m.source = f.source
1236 AND m.metarecord = mr.id
1237 AND rd.record = smrs.source
1243 GROUP BY m.metarecord
1244 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1245 LIMIT $visibility_limit
1252 FROM $asset_call_number_table cn,
1253 $metabib_metarecord_source_map_table mrs,
1254 $asset_copy_table cp,
1259 $metabib_record_descriptor ord,
1261 WHERE mrs.metarecord = s.metarecord
1262 AND br.id = mrs.source
1263 AND cn.record = mrs.source
1264 AND cp.status = cs.id
1265 AND cp.location = cl.id
1266 AND cn.owning_lib = d.id
1267 AND cp.call_number = cn.id
1268 AND cp.opac_visible IS TRUE
1269 AND cs.opac_visible IS TRUE
1270 AND cl.opac_visible IS TRUE
1271 AND d.opac_visible IS TRUE
1272 AND br.active IS TRUE
1273 AND br.deleted IS FALSE
1274 AND ord.record = mrs.source
1280 ORDER BY 4 $sort_dir
1282 } elsif ($self->api_name !~ /staff/o) {
1289 FROM $asset_call_number_table cn,
1290 $metabib_metarecord_source_map_table mrs,
1291 $asset_copy_table cp,
1296 $metabib_record_descriptor ord
1298 WHERE mrs.metarecord = s.metarecord
1299 AND br.id = mrs.source
1300 AND cn.record = mrs.source
1301 AND cp.status = cs.id
1302 AND cp.location = cl.id
1303 AND cp.circ_lib = d.id
1304 AND cp.call_number = cn.id
1305 AND cp.opac_visible IS TRUE
1306 AND cs.opac_visible IS TRUE
1307 AND cl.opac_visible IS TRUE
1308 AND d.opac_visible IS TRUE
1309 AND br.active IS TRUE
1310 AND br.deleted IS FALSE
1311 AND ord.record = mrs.source
1319 ORDER BY 4 $sort_dir
1328 FROM $asset_call_number_table cn,
1329 $asset_copy_table cp,
1330 $metabib_metarecord_source_map_table mrs,
1333 $metabib_record_descriptor ord
1335 WHERE mrs.metarecord = s.metarecord
1336 AND br.id = mrs.source
1337 AND cn.record = mrs.source
1338 AND cn.id = cp.call_number
1339 AND br.deleted IS FALSE
1340 AND cn.deleted IS FALSE
1341 AND ord.record = mrs.source
1342 AND ( cn.owning_lib = d.id
1343 OR ( cp.circ_lib = d.id
1344 AND cp.deleted IS FALSE
1356 FROM $asset_call_number_table cn,
1357 $metabib_metarecord_source_map_table mrs,
1358 $metabib_record_descriptor ord
1359 WHERE mrs.metarecord = s.metarecord
1360 AND cn.record = mrs.source
1361 AND ord.record = mrs.source
1369 ORDER BY 4 $sort_dir
1374 $log->debug("Field Search SQL :: [$select]",DEBUG);
1376 my $recs = $class->db_Main->selectall_arrayref(
1378 (@bonus_values > 0 ? @bonus_values : () ),
1379 ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1380 @types, @forms, @aud, @lang, @lit_form,
1381 @types, @forms, @aud, @lang, @lit_form,
1382 ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1384 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1387 $max = 1 if (!@$recs);
1389 $max = $$_[1] if ($$_[1] > $max);
1392 my $count = scalar(@$recs);
1393 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1394 my ($mrid,$rank,$skip) = @$rec;
1395 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1400 for my $class ( qw/title author subject keyword series/ ) {
1401 __PACKAGE__->register_method(
1402 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1403 method => 'postfilter_search_class_fts',
1406 cdbi => "metabib::${class}_field_entry",
1409 __PACKAGE__->register_method(
1410 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1411 method => 'postfilter_search_class_fts',
1414 cdbi => "metabib::${class}_field_entry",
1421 my $_cdbi = { title => "metabib::title_field_entry",
1422 author => "metabib::author_field_entry",
1423 subject => "metabib::subject_field_entry",
1424 keyword => "metabib::keyword_field_entry",
1425 series => "metabib::series_field_entry",
1428 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1429 sub postfilter_search_multi_class_fts {
1434 my $sort = $args{'sort'};
1435 my $sort_dir = $args{sort_dir} || 'DESC';
1436 my $ou = $args{org_unit};
1437 my $ou_type = $args{depth};
1438 my $limit = $args{limit} || 10;
1439 my $offset = $args{offset} || 0;
1440 my $visibility_limit = $args{visibility_limit} || 5000;
1443 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1446 if (!defined($args{org_unit})) {
1447 die "No target organizational unit passed to ".$self->api_name;
1450 if (! scalar( keys %{$args{searches}} )) {
1451 die "No search arguments were passed to ".$self->api_name;
1454 my $outer_limit = 1000;
1456 my $limit_clause = '';
1457 my $offset_clause = '';
1459 $limit_clause = "LIMIT $outer_limit";
1460 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1462 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1463 my ($t_filter, $f_filter, $v_filter) = ('','','');
1464 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1465 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1466 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1468 if ($args{available}) {
1469 $avail_filter = ' AND cp.status IN (0,7,12)';
1472 if (my $a = $args{audience}) {
1473 $a = [$a] if (!ref($a));
1476 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1477 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1480 if (my $l = $args{language}) {
1481 $l = [$l] if (!ref($l));
1484 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1485 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1488 if (my $f = $args{lit_form}) {
1489 $f = [$f] if (!ref($f));
1492 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1493 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1496 if (my $f = $args{item_form}) {
1497 $f = [$f] if (!ref($f));
1500 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1501 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1504 if (my $t = $args{item_type}) {
1505 $t = [$t] if (!ref($t));
1508 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1509 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1512 if (my $v = $args{vr_format}) {
1513 $v = [$v] if (!ref($v));
1516 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1517 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1521 # XXX legacy format and item type support
1522 if ($args{format}) {
1523 my ($t, $f) = split '-', $args{format};
1524 @types = split '', $t;
1525 @forms = split '', $f;
1527 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1528 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1532 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1533 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1539 my $descendants = defined($ou_type) ?
1540 "actor.org_unit_descendants($ou, $ou_type)" :
1541 "actor.org_unit_descendants($ou)";
1543 my $search_table_list = '';
1545 my $join_table_list = '';
1548 my $field_table = config::metabib_field->table;
1552 my $prev_search_group;
1553 my $curr_search_group;
1557 for my $search_group (sort keys %{$args{searches}}) {
1558 (my $search_group_name = $search_group) =~ s/\|/_/gso;
1559 ($search_class,$search_field) = split /\|/, $search_group;
1560 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1562 if ($search_field) {
1563 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1564 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1569 $prev_search_group = $curr_search_group if ($curr_search_group);
1571 $curr_search_group = $search_group_name;
1573 my $class = $_cdbi->{$search_class};
1574 my $search_table = $class->table;
1576 my ($index_col) = $class->columns('FTS');
1577 $index_col ||= 'value';
1580 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1582 my $fts_where = $fts->sql_where_clause;
1583 my @fts_ranks = $fts->fts_rank;
1585 my $SQLstring = join('%',map { lc($_) } $fts->words);
1586 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1587 my $first_word = lc(($fts->words)[0]).'%';
1589 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1590 my $rank = join(' + ', @fts_ranks);
1593 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1594 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1596 $bonus{'series'} = [
1597 { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1598 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1601 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1603 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1604 $bonus_list ||= '1';
1606 push @bonus_lists, $bonus_list;
1607 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1610 #---------------------
1612 $search_table_list .= "$search_table $search_group_name, ";
1613 push @rank_list,$rank;
1614 $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1616 if ($metabib_field) {
1617 $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1618 $metabib_field = undef;
1621 if ($prev_search_group) {
1622 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1626 my $metabib_record_descriptor = metabib::record_descriptor->table;
1627 my $metabib_full_rec = metabib::full_rec->table;
1628 my $metabib_metarecord = metabib::metarecord->table;
1629 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1630 my $asset_call_number_table = asset::call_number->table;
1631 my $asset_copy_table = asset::copy->table;
1632 my $cs_table = config::copy_status->table;
1633 my $cl_table = asset::copy_location->table;
1634 my $br_table = biblio::record_entry->table;
1635 my $source_table = config::bib_source->table;
1637 my $bonuses = join (' * ', @bonus_lists);
1638 my $relevance = join (' + ', @rank_list);
1639 $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1641 my $string_default_sort = 'zzzz';
1642 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1644 my $number_default_sort = '9999';
1645 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1649 my $secondary_sort = <<" SORT";
1651 SELECT COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM '\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1652 FROM $metabib_full_rec sfrt,
1653 $metabib_metarecord mr
1654 WHERE sfrt.record = mr.master_record
1655 AND sfrt.tag = '245'
1656 AND sfrt.subfield = 'a'
1661 my $rank = $relevance;
1662 if (lc($sort) eq 'pubdate') {
1665 SELECT COALESCE(SUBSTRING(frp.value FROM '\\\\d+'),'$number_default_sort')::INT
1666 FROM $metabib_full_rec frp
1667 WHERE frp.record = mr.master_record
1669 AND frp.subfield = 'c'
1673 } elsif (lc($sort) eq 'create_date') {
1675 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1677 } elsif (lc($sort) eq 'edit_date') {
1679 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1681 } elsif (lc($sort) eq 'title') {
1684 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM '\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1685 FROM $metabib_full_rec frt
1686 WHERE frt.record = mr.master_record
1688 AND frt.subfield = 'a'
1692 $secondary_sort = <<" SORT";
1694 SELECT COALESCE(SUBSTRING(sfrp.value FROM '\\\\d+'),'$number_default_sort')::INT
1695 FROM $metabib_full_rec sfrp
1696 WHERE sfrp.record = mr.master_record
1697 AND sfrp.tag = '260'
1698 AND sfrp.subfield = 'c'
1702 } elsif (lc($sort) eq 'author') {
1705 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1706 FROM $metabib_full_rec fra
1707 WHERE fra.record = mr.master_record
1708 AND fra.tag LIKE '1%'
1709 AND fra.subfield = 'a'
1710 ORDER BY fra.tag::text::int
1715 push @bonus_values, @bonus_values;
1720 my $select = <<" SQL";
1721 SELECT m.metarecord,
1723 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1726 FROM $search_table_list
1727 $metabib_metarecord mr,
1728 $metabib_metarecord_source_map_table m,
1729 $metabib_metarecord_source_map_table smrs
1730 WHERE m.metarecord = smrs.metarecord
1731 AND mr.id = m.metarecord
1734 GROUP BY m.metarecord
1735 -- ORDER BY 4 $sort_dir
1736 LIMIT $visibility_limit
1739 if ($self->api_name !~ /staff/o) {
1746 FROM $asset_call_number_table cn,
1747 $metabib_metarecord_source_map_table mrs,
1748 $asset_copy_table cp,
1753 $metabib_record_descriptor ord
1754 WHERE mrs.metarecord = s.metarecord
1755 AND br.id = mrs.source
1756 AND cn.record = mrs.source
1757 AND cp.status = cs.id
1758 AND cp.location = cl.id
1759 AND cp.circ_lib = d.id
1760 AND cp.call_number = cn.id
1761 AND cp.opac_visible IS TRUE
1762 AND cs.opac_visible IS TRUE
1763 AND cl.opac_visible IS TRUE
1764 AND d.opac_visible IS TRUE
1765 AND br.active IS TRUE
1766 AND br.deleted IS FALSE
1767 AND cp.deleted IS FALSE
1768 AND cn.deleted IS FALSE
1769 AND ord.record = mrs.source
1782 $metabib_metarecord_source_map_table mrs,
1783 $metabib_record_descriptor ord,
1785 WHERE mrs.metarecord = s.metarecord
1786 AND ord.record = mrs.source
1787 AND br.id = mrs.source
1788 AND br.source = src.id
1789 AND src.transcendant IS TRUE
1797 ORDER BY 4 $sort_dir, 5
1804 $metabib_metarecord_source_map_table omrs,
1805 $metabib_record_descriptor ord
1806 WHERE omrs.metarecord = s.metarecord
1807 AND ord.record = omrs.source
1810 FROM $asset_call_number_table cn,
1811 $asset_copy_table cp,
1814 WHERE br.id = omrs.source
1815 AND cn.record = omrs.source
1816 AND br.deleted IS FALSE
1817 AND cn.deleted IS FALSE
1818 AND cp.call_number = cn.id
1819 AND ( cn.owning_lib = d.id
1820 OR ( cp.circ_lib = d.id
1821 AND cp.deleted IS FALSE
1829 FROM $asset_call_number_table cn
1830 WHERE cn.record = omrs.source
1831 AND cn.deleted IS FALSE
1837 $metabib_metarecord_source_map_table mrs,
1838 $metabib_record_descriptor ord,
1840 WHERE mrs.metarecord = s.metarecord
1841 AND br.id = mrs.source
1842 AND br.source = src.id
1843 AND src.transcendant IS TRUE
1859 ORDER BY 4 $sort_dir, 5
1864 $log->debug("Field Search SQL :: [$select]",DEBUG);
1866 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1869 @types, @forms, @vformats, @aud, @lang, @lit_form,
1870 @types, @forms, @vformats, @aud, @lang, @lit_form,
1871 # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1874 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1877 $max = 1 if (!@$recs);
1879 $max = $$_[1] if ($$_[1] > $max);
1882 my $count = scalar(@$recs);
1883 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1884 next unless ($$rec[0]);
1885 my ($mrid,$rank,$skip) = @$rec;
1886 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1891 __PACKAGE__->register_method(
1892 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1893 method => 'postfilter_search_multi_class_fts',
1898 __PACKAGE__->register_method(
1899 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1900 method => 'postfilter_search_multi_class_fts',
1906 __PACKAGE__->register_method(
1907 api_name => "open-ils.storage.metabib.multiclass.search_fts",
1908 method => 'postfilter_search_multi_class_fts',
1913 __PACKAGE__->register_method(
1914 api_name => "open-ils.storage.metabib.multiclass.search_fts.staff",
1915 method => 'postfilter_search_multi_class_fts',
1921 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1922 sub biblio_search_multi_class_fts {
1927 my $sort = $args{'sort'};
1928 my $sort_dir = $args{sort_dir} || 'DESC';
1929 my $ou = $args{org_unit};
1930 my $ou_type = $args{depth};
1931 my $limit = $args{limit} || 10;
1932 my $offset = $args{offset} || 0;
1933 my $pref_lang = $args{prefered_language} || 'eng';
1934 my $visibility_limit = $args{visibility_limit} || 5000;
1937 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1940 if (! scalar( keys %{$args{searches}} )) {
1941 die "No search arguments were passed to ".$self->api_name;
1944 my $outer_limit = 1000;
1946 my $limit_clause = '';
1947 my $offset_clause = '';
1949 $limit_clause = "LIMIT $outer_limit";
1950 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1952 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1953 my ($t_filter, $f_filter, $v_filter) = ('','','');
1954 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1955 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1956 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1958 if ($args{available}) {
1959 $avail_filter = ' AND cp.status IN (0,7,12)';
1962 if (my $a = $args{audience}) {
1963 $a = [$a] if (!ref($a));
1966 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1967 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1970 if (my $l = $args{language}) {
1971 $l = [$l] if (!ref($l));
1974 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1975 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1978 if (my $f = $args{lit_form}) {
1979 $f = [$f] if (!ref($f));
1982 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1983 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1986 if (my $f = $args{item_form}) {
1987 $f = [$f] if (!ref($f));
1990 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1991 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1994 if (my $t = $args{item_type}) {
1995 $t = [$t] if (!ref($t));
1998 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1999 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2002 if (my $v = $args{vr_format}) {
2003 $v = [$v] if (!ref($v));
2006 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
2007 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
2010 # XXX legacy format and item type support
2011 if ($args{format}) {
2012 my ($t, $f) = split '-', $args{format};
2013 @types = split '', $t;
2014 @forms = split '', $f;
2016 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2017 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2021 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2022 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2027 my $descendants = defined($ou_type) ?
2028 "actor.org_unit_descendants($ou, $ou_type)" :
2029 "actor.org_unit_descendants($ou)";
2031 my $search_table_list = '';
2033 my $join_table_list = '';
2036 my $field_table = config::metabib_field->table;
2040 my $prev_search_group;
2041 my $curr_search_group;
2045 for my $search_group (sort keys %{$args{searches}}) {
2046 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2047 ($search_class,$search_field) = split /\|/, $search_group;
2048 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2050 if ($search_field) {
2051 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2052 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2057 $prev_search_group = $curr_search_group if ($curr_search_group);
2059 $curr_search_group = $search_group_name;
2061 my $class = $_cdbi->{$search_class};
2062 my $search_table = $class->table;
2064 my ($index_col) = $class->columns('FTS');
2065 $index_col ||= 'value';
2068 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2070 my $fts_where = $fts->sql_where_clause;
2071 my @fts_ranks = $fts->fts_rank;
2073 my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2074 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2075 my $first_word = lc(($fts->words)[0]).'%';
2077 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2078 my $rank = join(' + ', @fts_ranks);
2081 $bonus{'subject'} = [];
2082 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2084 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2086 $bonus{'series'} = [
2087 { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2088 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2091 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2094 push @{ $bonus{'title'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2095 push @{ $bonus{'author'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2096 push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2097 push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2098 push @{ $bonus{'series'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2101 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2102 $bonus_list ||= '1';
2104 push @bonus_lists, $bonus_list;
2105 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2107 #---------------------
2109 $search_table_list .= "$search_table $search_group_name, ";
2110 push @rank_list,$rank;
2111 $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2113 if ($metabib_field) {
2114 $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2115 $metabib_field = undef;
2118 if ($prev_search_group) {
2119 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2123 my $metabib_record_descriptor = metabib::record_descriptor->table;
2124 my $metabib_full_rec = metabib::full_rec->table;
2125 my $metabib_metarecord = metabib::metarecord->table;
2126 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2127 my $asset_call_number_table = asset::call_number->table;
2128 my $asset_copy_table = asset::copy->table;
2129 my $cs_table = config::copy_status->table;
2130 my $cl_table = asset::copy_location->table;
2131 my $br_table = biblio::record_entry->table;
2132 my $source_table = config::bib_source->table;
2135 my $bonuses = join (' * ', @bonus_lists);
2136 my $relevance = join (' + ', @rank_list);
2137 $relevance = "AVG( ($relevance) * ($bonuses) )";
2139 my $string_default_sort = 'zzzz';
2140 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2142 my $number_default_sort = '9999';
2143 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2145 my $rank = $relevance;
2146 if (lc($sort) eq 'pubdate') {
2149 SELECT COALESCE(SUBSTRING(frp.value FROM '\\\\d{4}'),'$number_default_sort')::INT
2150 FROM $metabib_full_rec frp
2151 WHERE frp.record = b.id
2153 AND frp.subfield = 'c'
2157 } elsif (lc($sort) eq 'create_date') {
2159 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2161 } elsif (lc($sort) eq 'edit_date') {
2163 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2165 } elsif (lc($sort) eq 'title') {
2168 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM '\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2169 FROM $metabib_full_rec frt
2170 WHERE frt.record = b.id
2172 AND frt.subfield = 'a'
2176 } elsif (lc($sort) eq 'author') {
2179 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
2180 FROM $metabib_full_rec fra
2181 WHERE fra.record = b.id
2182 AND fra.tag LIKE '1%'
2183 AND fra.subfield = 'a'
2184 ORDER BY fra.tag::text::int
2189 push @bonus_values, @bonus_values;
2194 my $select = <<" SQL";
2199 FROM $search_table_list
2200 $metabib_record_descriptor rd,
2203 WHERE rd.record = b.id
2204 AND b.active IS TRUE
2205 AND b.deleted IS FALSE
2214 GROUP BY b.id, b.source
2215 ORDER BY 3 $sort_dir
2216 LIMIT $visibility_limit
2219 if ($self->api_name !~ /staff/o) {
2224 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2227 FROM $asset_call_number_table cn,
2228 $asset_copy_table cp,
2232 WHERE cn.record = s.id
2233 AND cp.status = cs.id
2234 AND cp.location = cl.id
2235 AND cp.call_number = cn.id
2236 AND cp.opac_visible IS TRUE
2237 AND cs.opac_visible IS TRUE
2238 AND cl.opac_visible IS TRUE
2239 AND d.opac_visible IS TRUE
2240 AND cp.deleted IS FALSE
2241 AND cn.deleted IS FALSE
2242 AND cp.circ_lib = d.id
2246 OR src.transcendant IS TRUE
2247 ORDER BY 3 $sort_dir
2254 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2257 FROM $asset_call_number_table cn,
2258 $asset_copy_table cp,
2260 WHERE cn.record = s.id
2261 AND cp.call_number = cn.id
2262 AND cn.deleted IS FALSE
2263 AND cp.circ_lib = d.id
2264 AND cp.deleted IS FALSE
2270 FROM $asset_call_number_table cn
2271 WHERE cn.record = s.id
2274 OR src.transcendant IS TRUE
2275 ORDER BY 3 $sort_dir
2280 $log->debug("Field Search SQL :: [$select]",DEBUG);
2282 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2284 @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2287 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2289 my $count = scalar(@$recs);
2290 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2291 next unless ($$rec[0]);
2292 my ($mrid,$rank) = @$rec;
2293 $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2298 __PACKAGE__->register_method(
2299 api_name => "open-ils.storage.biblio.multiclass.search_fts.record",
2300 method => 'biblio_search_multi_class_fts',
2305 __PACKAGE__->register_method(
2306 api_name => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2307 method => 'biblio_search_multi_class_fts',
2312 __PACKAGE__->register_method(
2313 api_name => "open-ils.storage.biblio.multiclass.search_fts",
2314 method => 'biblio_search_multi_class_fts',
2319 __PACKAGE__->register_method(
2320 api_name => "open-ils.storage.biblio.multiclass.search_fts.staff",
2321 method => 'biblio_search_multi_class_fts',
2329 my $default_preferred_language;
2330 my $default_preferred_language_weight;
2332 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2338 if (!$locale_map{COMPLETE}) {
2340 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2341 for my $locale ( @locales ) {
2342 $locale_map{$locale->code} = $locale->marc_code;
2344 $locale_map{COMPLETE} = 1;
2348 if (!$default_preferred_language) {
2350 $default_preferred_language = OpenSRF::Utils::SettingsClient
2353 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2358 if (!$default_preferred_language_weight) {
2360 $default_preferred_language_weight = OpenSRF::Utils::SettingsClient
2363 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2368 # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2369 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2371 my $ou = $args{org_unit};
2372 my $limit = $args{limit} || 10;
2373 my $offset = $args{offset} || 0;
2376 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2379 if (! scalar( keys %{$args{searches}} )) {
2380 die "No search arguments were passed to ".$self->api_name;
2383 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2385 if (!defined($args{preferred_language})) {
2386 my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2387 $args{preferred_language} =
2388 $locale_map{ $ses_locale } || 'eng';
2391 if (!defined($args{preferred_language_weight})) {
2392 $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2395 if ($args{available}) {
2396 @statuses = (0,7,12);
2399 if (my $s = $args{locations}) {
2400 $s = [$s] if (!ref($s));
2404 if (my $b = $args{between}) {
2405 if (ref($b) && @$b == 2) {
2410 if (my $s = $args{statuses}) {
2411 $s = [$s] if (!ref($s));
2415 if (my $a = $args{audience}) {
2416 $a = [$a] if (!ref($a));
2420 if (my $l = $args{language}) {
2421 $l = [$l] if (!ref($l));
2425 if (my $f = $args{lit_form}) {
2426 $f = [$f] if (!ref($f));
2430 if (my $f = $args{item_form}) {
2431 $f = [$f] if (!ref($f));
2435 if (my $t = $args{item_type}) {
2436 $t = [$t] if (!ref($t));
2440 if (my $b = $args{bib_level}) {
2441 $b = [$b] if (!ref($b));
2445 if (my $v = $args{vr_format}) {
2446 $v = [$v] if (!ref($v));
2450 # XXX legacy format and item type support
2451 if ($args{format}) {
2452 my ($t, $f) = split '-', $args{format};
2453 @types = split '', $t;
2454 @forms = split '', $f;
2457 my %stored_proc_search_args;
2458 for my $search_group (sort keys %{$args{searches}}) {
2459 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2460 my ($search_class,$search_field) = split /\|/, $search_group;
2461 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2463 if ($search_field) {
2464 unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2465 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2470 my $class = $_cdbi->{$search_class};
2471 my $search_table = $class->table;
2473 my ($index_col) = $class->columns('FTS');
2474 $index_col ||= 'value';
2477 my $fts = OpenILS::Application::Storage::FTS->compile(
2478 $search_class => $args{searches}{$search_group}{term},
2479 $search_group_name.'.value',
2480 "$search_group_name.$index_col"
2482 $fts->sql_where_clause; # this builds the ranks for us
2484 my @fts_ranks = $fts->fts_rank;
2485 my @fts_queries = $fts->fts_query;
2486 my @phrases = map { lc($_) } $fts->phrases;
2487 my @words = map { lc($_) } $fts->words;
2489 $stored_proc_search_args{$search_group} = {
2490 fts_rank => \@fts_ranks,
2491 fts_query => \@fts_queries,
2492 phrase => \@phrases,
2498 my $param_search_ou = $ou;
2499 my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2500 my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2501 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2502 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2503 my $param_audience = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud ) . '}$$';
2504 my $param_language = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang ) . '}$$';
2505 my $param_lit_form = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2506 my $param_types = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types ) . '}$$';
2507 my $param_forms = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms ) . '}$$';
2508 my $param_vformats = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2509 my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2510 my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2511 my $param_after = $args{after} ; $param_after = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2512 my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2513 my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2514 my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2515 my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2516 my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2517 my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2518 my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2519 my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2520 my $param_rel_limit = $args{core_limit}; $param_rel_limit ||= 'NULL';
2521 my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2522 my $param_skip_chk = $args{skip_check}; $param_skip_chk ||= 'NULL';
2524 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2526 FROM search.staged_fts(
2527 $param_search_ou\:\:INT,
2528 $param_depth\:\:INT,
2529 $param_searches\:\:TEXT,
2530 $param_statuses\:\:INT[],
2531 $param_locations\:\:INT[],
2532 $param_audience\:\:TEXT[],
2533 $param_language\:\:TEXT[],
2534 $param_lit_form\:\:TEXT[],
2535 $param_types\:\:TEXT[],
2536 $param_forms\:\:TEXT[],
2537 $param_vformats\:\:TEXT[],
2538 $param_bib_level\:\:TEXT[],
2539 $param_before\:\:TEXT,
2540 $param_after\:\:TEXT,
2541 $param_during\:\:TEXT,
2542 $param_between\:\:TEXT[],
2543 $param_pref_lang\:\:TEXT,
2544 $param_pref_lang_multiplier\:\:REAL,
2545 $param_sort\:\:TEXT,
2546 $param_sort_desc\:\:BOOL,
2547 $metarecord\:\:BOOL,
2549 $param_rel_limit\:\:INT,
2550 $param_chk_limit\:\:INT,
2551 $param_skip_chk\:\:INT
2557 my $recs = $sth->fetchall_arrayref({});
2558 my $summary_row = pop @$recs;
2560 my $total = $$summary_row{total};
2561 my $checked = $$summary_row{checked};
2562 my $visible = $$summary_row{visible};
2563 my $deleted = $$summary_row{deleted};
2564 my $excluded = $$summary_row{excluded};
2566 my $estimate = $visible;
2567 if ( $total > $checked && $checked ) {
2569 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2570 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2574 delete $$summary_row{id};
2575 delete $$summary_row{rel};
2576 delete $$summary_row{record};
2578 $client->respond( $summary_row );
2580 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2582 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2583 delete $$rec{checked};
2584 delete $$rec{visible};
2585 delete $$rec{excluded};
2586 delete $$rec{deleted};
2587 delete $$rec{total};
2588 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2590 $client->respond( $rec );
2594 __PACKAGE__->register_method(
2595 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
2596 method => 'staged_fts',
2601 __PACKAGE__->register_method(
2602 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2603 method => 'staged_fts',
2608 __PACKAGE__->register_method(
2609 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
2610 method => 'staged_fts',
2615 __PACKAGE__->register_method(
2616 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2617 method => 'staged_fts',
2623 sub FTS_paging_estimate {
2627 my $checked = shift;
2628 my $visible = shift;
2629 my $excluded = shift;
2630 my $deleted = shift;
2633 my $deleted_ratio = $deleted / $checked;
2634 my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2636 my $exclusion_ratio = $excluded / $checked;
2637 my $delete_adjusted_exclusion_ratio = $excluded / ($checked - $deleted);
2639 my $inclusion_ratio = $visible / $checked;
2640 my $delete_adjusted_inclusion_ratio = $visible / ($checked - $deleted);
2643 exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2644 inclusion => int($delete_adjusted_total * $inclusion_ratio),
2645 delete_adjusted_exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2646 delete_adjusted_inclusion => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2649 __PACKAGE__->register_method(
2650 api_name => "open-ils.storage.fts_paging_estimate",
2651 method => 'FTS_paging_estimate',
2657 Hash of estimation values based on four variant estimation strategies:
2658 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2659 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2660 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2661 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2664 Helper method used to determin the approximate number of
2665 hits for a search that spans multiple superpages. For
2666 sparse superpages, the inclusion estimate will likely be the
2667 best estimate. The exclusion strategy is the original, but
2668 inclusion is the default.
2671 { name => 'checked',
2672 desc => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2675 { name => 'visible',
2676 desc => 'Number of records visible to the search location on the current superpage.',
2679 { name => 'excluded',
2680 desc => 'Number of records excluded from the search location on the current superpage.',
2683 { name => 'deleted',
2684 desc => 'Number of deleted records on the current superpage.',
2688 desc => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2701 my $term = $$args{term};
2702 my $limit = $$args{max} || 1;
2703 my $min = $$args{min} || 1;
2704 my @classes = @{$$args{class}};
2706 $limit = $min if ($min > $limit);
2709 @classes = ( qw/ title author subject series keyword / );
2713 my $bre_table = biblio::record_entry->table;
2714 my $cn_table = asset::call_number->table;
2715 my $cp_table = asset::copy->table;
2717 for my $search_class ( @classes ) {
2719 my $class = $_cdbi->{$search_class};
2720 my $search_table = $class->table;
2722 my ($index_col) = $class->columns('FTS');
2723 $index_col ||= 'value';
2726 my $where = OpenILS::Application::Storage::FTS
2727 ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2731 SELECT COUNT(DISTINCT X.source)
2732 FROM (SELECT $search_class.source
2733 FROM $search_table $search_class
2734 JOIN $bre_table b ON (b.id = $search_class.source)
2739 HAVING COUNT(DISTINCT X.source) >= $min;
2742 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2743 $matches{$search_class} = $res ? $res->[0] : 0;
2748 __PACKAGE__->register_method(
2749 api_name => "open-ils.storage.search.xref",
2750 method => 'xref_count',
2754 sub query_parser_fts {
2760 # grab the query parser and initialize it
2761 my $parser = $OpenILS::Application::Storage::QParser;
2764 if (!$parser->initialization_complete) {
2765 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
2766 $parser->initialize(
2767 config_metabib_field_index_norm_map =>
2769 'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
2770 { id => { "!=" => undef } },
2771 { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
2773 search_relevance_adjustment =>
2775 'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
2776 { id => { "!=" => undef } }
2778 config_metabib_field =>
2780 'open-ils.cstore.direct.config.metabib_field.search.atomic',
2781 { id => { "!=" => undef } }
2783 config_metabib_search_alias =>
2785 'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
2786 { alias => { "!=" => undef } }
2790 $cstore->disconnect;
2791 die("Cannot initialize $parser!") unless ($parser->initialization_complete);
2795 # populate the locale/language map
2796 if (!$locale_map{COMPLETE}) {
2798 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2799 for my $locale ( @locales ) {
2800 $locale_map{$locale->code} = $locale->marc_code;
2802 $locale_map{COMPLETE} = 1;
2806 # I hope we have a query!
2807 if (! $args{query} ) {
2808 die "No query was passed to ".$self->api_name;
2812 my $simple_plan = $args{_simple_plan};
2813 # remove bad chunks of the %args hash
2814 for my $bad ( grep { /^_/ } keys(%args)) {
2815 delete($args{$bad});
2819 # parse the query and supply any query-level %arg-based defaults
2820 # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2821 my $query = $parser->new( %args )->parse;
2824 # set the locale-based default prefered location
2825 if (!$query->parse_tree->find_filter('preferred_language')) {
2826 $parser->default_preferred_language( $args{preferred_language} );
2827 if (!$parser->default_preferred_language) {
2828 my $ses_locale = $client->session ? $client->session->session_locale : '';
2829 $parser->default_preferred_language( $locale_map{ $ses_locale } );
2831 $parser->default_preferred_language(
2832 OpenSRF::Utils::SettingsClient->new->config_value(
2833 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2835 ) if (!$parser->default_preferred_language);
2839 # set the global default language multiplier
2840 if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2841 $parser->default_preferred_language_multiplier($args{preferred_language_weight});
2842 $parser->default_preferred_language_multiplier($args{preferred_language_multiplier});
2843 $parser->default_preferred_language_multiplier(
2844 OpenSRF::Utils::SettingsClient->new->config_value(
2845 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2847 ) if (!$parser->default_preferred_language_multiplier);
2850 # gather the site, if one is specified, defaulting to the in-query version
2851 my $ou = $args{org_unit};
2852 if (my ($filter) = $query->parse_tree->find_filter('site')) {
2853 $ou = $filter->args->[0] if (@{$filter->args});
2855 $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^\d+$/);
2858 # gather lasso, as with $ou
2859 my $lasso = $args{lasso};
2860 if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
2861 $lasso = $filter->args->[0] if (@{$filter->args});
2863 $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
2864 $lasso = -$lasso if ($lasso);
2867 # # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
2868 # # gather user lasso, as with $ou and lasso
2869 # my $mylasso = $args{my_lasso};
2870 # if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
2871 # $mylasso = $filter->args->[0] if (@{$filter->args});
2873 # $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
2876 # if we have a lasso, go with that, otherwise ... ou
2877 $ou = $lasso if ($lasso);
2880 # get the default $ou if we have nothing
2881 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
2884 # 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
2885 # gather the depth, if one is specified, defaulting to the in-query version
2886 my $depth = $args{depth};
2887 if (my ($filter) = $query->parse_tree->find_filter('depth')) {
2888 $depth = $filter->args->[0] if (@{$filter->args});
2890 $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
2893 # gather the limit or default to 10
2894 my $limit = $args{check_limit} || 'NULL';
2895 if (my ($filter) = $query->parse_tree->find_filter('limit')) {
2896 $limit = $filter->args->[0] if (@{$filter->args});
2898 if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
2899 $limit = $filter->args->[0] if (@{$filter->args});
2903 # gather the offset or default to 0
2904 my $offset = $args{skip_check} || $args{offset} || 0;
2905 if (my ($filter) = $query->parse_tree->find_filter('offset')) {
2906 $offset = $filter->args->[0] if (@{$filter->args});
2908 if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
2909 $offset = $filter->args->[0] if (@{$filter->args});
2913 # gather the estimation strategy or default to inclusion
2914 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2915 if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
2916 $estimation_strategy = $filter->args->[0] if (@{$filter->args});
2920 # gather the estimation strategy or default to inclusion
2921 my $core_limit = $args{core_limit};
2922 if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
2923 $core_limit = $filter->args->[0] if (@{$filter->args});
2927 # gather statuses, and then forget those if we have an #available modifier
2929 if (my ($filter) = $query->parse_tree->find_filter('statuses')) {
2930 @statuses = @{$filter->args} if (@{$filter->args});
2932 @statuses = (0,7,12) if ($query->parse_tree->find_modifier('available'));
2937 if (my ($filter) = $query->parse_tree->find_filter('locations')) {
2938 @location = @{$filter->args} if (@{$filter->args});
2942 my $param_check = $limit || $query->superpage_size || 'NULL';
2943 my $param_offset = $offset || 'NULL';
2944 my $param_limit = $core_limit || 'NULL';
2946 my $sp = $query->superpage || 1;
2948 $param_offset = ($sp - 1) * $sp_size;
2951 my $param_search_ou = $ou;
2952 my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
2953 my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
2954 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
2955 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
2956 my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
2957 my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
2959 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2961 FROM search.query_parser_fts(
2962 $param_search_ou\:\:INT,
2963 $param_depth\:\:INT,
2964 $param_core_query\:\:TEXT,
2965 $param_statuses\:\:INT[],
2966 $param_locations\:\:INT[],
2967 $param_offset\:\:INT,
2968 $param_check\:\:INT,
2969 $param_limit\:\:INT,
2977 my $recs = $sth->fetchall_arrayref({});
2978 my $summary_row = pop @$recs;
2980 my $total = $$summary_row{total};
2981 my $checked = $$summary_row{checked};
2982 my $visible = $$summary_row{visible};
2983 my $deleted = $$summary_row{deleted};
2984 my $excluded = $$summary_row{excluded};
2986 my $estimate = $visible;
2987 if ( $total > $checked && $checked ) {
2989 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2990 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2994 delete $$summary_row{id};
2995 delete $$summary_row{rel};
2996 delete $$summary_row{record};
2998 if (defined($simple_plan)) {
2999 $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3001 $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3004 $client->respond( $summary_row );
3006 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
3008 for my $rec (@$recs) {
3009 delete $$rec{checked};
3010 delete $$rec{visible};
3011 delete $$rec{excluded};
3012 delete $$rec{deleted};
3013 delete $$rec{total};
3014 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3016 $client->respond( $rec );
3021 sub query_parser_fts_wrapper {
3026 $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3027 # grab the query parser and initialize it
3028 my $parser = $OpenILS::Application::Storage::QParser;
3031 if (!$parser->initialization_complete) {
3032 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3033 $parser->initialize(
3034 config_metabib_field_index_norm_map =>
3036 'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
3037 { id => { "!=" => undef } },
3038 { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
3040 search_relevance_adjustment =>
3042 'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
3043 { id => { "!=" => undef } }
3045 config_metabib_field =>
3047 'open-ils.cstore.direct.config.metabib_field.search.atomic',
3048 { id => { "!=" => undef } }
3050 config_metabib_search_alias =>
3052 'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
3053 { alias => { "!=" => undef } }
3057 $cstore->disconnect;
3058 die("Cannot initialize $parser!") unless ($parser->initialization_complete);
3061 if (! scalar( keys %{$args{searches}} )) {
3062 die "No search arguments were passed to ".$self->api_name;
3065 $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3066 my $base_query = '';
3067 for my $sclass ( keys %{$args{searches}} ) {
3068 $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3069 $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3072 my $query = $base_query;
3073 $log->debug("Full base query: $base_query", DEBUG);
3075 if (!$locale_map{COMPLETE}) {
3077 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3078 for my $locale ( @locales ) {
3079 $locale_map{$locale->code} = $locale->marc_code;
3081 $locale_map{COMPLETE} = 1;
3085 my $base_plan = $parser->new( query => $base_query )->parse;
3087 $query = "preferred_language($args{preferred_language}) $query"
3088 if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3089 $query = "preferred_language_weight($args{preferred_language_weight}) $query"
3090 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'));
3092 $query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
3093 $query = "site($args{org_unit}) $query" if ($args{org_unit});
3094 $query = "sort($args{sort}) $query" if ($args{sort});
3095 $query = "limit($args{limit}) $query" if ($args{limit});
3096 $query = "core_limit($args{core_limit}) $query" if ($args{core_limit});
3097 $query = "skip_check($args{skip_check}) $query" if ($args{skip_check});
3098 $query = "superpage($args{superpage}) $query" if ($args{superpage});
3099 $query = "offset($args{offset}) $query" if ($args{offset});
3100 $query = "#available $query" if ($args{available});
3101 $query = "#descending $query" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3102 $query = "#staff $query" if ($self->api_name =~ /staff/);
3103 $query = "before($args{before}) $query" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3104 $query = "after($args{after}) $query" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3105 $query = "during($args{during}) $query" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3106 $query = "between($args{between}[0],$args{between}[1]) $query"
3107 if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3110 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3112 # XXX legacy format and item type support
3113 if ($args{format}) {
3114 my ($t, $f) = split '-', $args{format};
3115 $args{item_type} = [ split '', $t ];
3116 $args{item_form} = [ split '', $f ];
3119 for my $filter ( qw/locations statuses between audience language lit_form item_form item_type bib_level vr_format/ ) {
3120 if (my $s = $args{$filter}) {
3121 $s = [$s] if (!ref($s));
3123 my @filter_list = @$s;
3125 next if ($filter eq 'between' and scalar(@filter_list) != 2);
3126 next if (@filter_list == 0);
3128 my $filter_string = join ',', @filter_list;
3129 $query = "$filter($filter_string) $query";
3133 $log->debug("Full QueryParser query: $query", DEBUG);
3135 return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan );
3137 __PACKAGE__->register_method(
3138 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
3139 method => 'query_parser_fts_wrapper',
3144 __PACKAGE__->register_method(
3145 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3146 method => 'query_parser_fts_wrapper',
3151 __PACKAGE__->register_method(
3152 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
3153 method => 'query_parser_fts_wrapper',
3158 __PACKAGE__->register_method(
3159 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3160 method => 'query_parser_fts_wrapper',