1 package OpenILS::Application::Storage::Publisher::metabib;
2 use base qw/OpenILS::Application::Storage::Publisher/;
4 use OpenSRF::EX qw/:try/;
5 use OpenILS::Application::Storage::FTS;
6 use OpenILS::Utils::Fieldmapper;
7 use OpenSRF::Utils::Logger qw/:level/;
8 use OpenSRF::Utils::Cache;
9 use OpenSRF::Utils::JSON;
11 use Digest::MD5 qw/md5_hex/;
14 my $log = 'OpenSRF::Utils::Logger';
18 sub ordered_records_from_metarecord {
26 my (@types,@forms,@blvl);
29 my ($t, $f, $b) = split '-', $formats;
30 @types = split '', $t;
31 @forms = split '', $f;
37 "actor.org_unit_descendants($org, $depth)" :
38 "actor.org_unit_descendants($org)" ;
41 my $copies_visible = 'AND d.opac_visible IS TRUE AND cp.opac_visible IS TRUE AND cs.opac_visible IS TRUE AND cl.opac_visible IS TRUE';
42 $copies_visible = '' if ($self->api_name =~ /staff/o);
44 my $sm_table = metabib::metarecord_source_map->table;
45 my $rd_table = metabib::record_descriptor->table;
46 my $fr_table = metabib::full_rec->table;
47 my $cn_table = asset::call_number->table;
48 my $cl_table = asset::copy_location->table;
49 my $cp_table = asset::copy->table;
50 my $cs_table = config::copy_status->table;
51 my $src_table = config::bib_source->table;
52 my $out_table = actor::org_unit_type->table;
53 my $br_table = biblio::record_entry->table;
60 FIRST(COALESCE(LTRIM(SUBSTR( value, COALESCE(SUBSTRING(ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'zzzzzzzz')) AS title
72 if ($copies_visible) {
78 WHERE rd.record = sm.source
79 AND fr.record = sm.source
82 AND (EXISTS ((SELECT 1
84 JOIN $cn_table cn ON (cp.call_number = cn.id)
85 JOIN $cs_table cs ON (cp.status = cs.id)
86 JOIN $cl_table cl ON (cp.location = cl.id)
87 JOIN $descendants d ON (cp.circ_lib = d.id)
88 WHERE cn.record = sm.source
94 WHERE src.id = br.source
95 AND src.transcendant IS TRUE))
102 JOIN $br_table br ON (sm.source = br.id)
103 JOIN $fr_table fr ON (fr.record = br.id)
104 JOIN $rd_table rd ON (rd.record = br.id)
105 WHERE sm.metarecord = ?
111 WHERE cn.record = br.id
112 AND cn.deleted = FALSE
113 AND cp.deleted = FALSE
114 AND cp.circ_lib = d.id
115 AND cn.id = cp.call_number
121 WHERE cn.record = br.id
122 AND cn.deleted = FALSE
123 AND cp.deleted = FALSE
124 AND cn.id = cp.call_number
130 WHERE src.id = br.source
131 AND src.transcendant IS TRUE))
137 $sql .= ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
141 $sql .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
145 $sql .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
155 GROUP BY record, item_type, item_form, quality
158 WHEN item_type IS NULL -- default
160 WHEN item_type = '' -- default
162 WHEN item_type IN ('a','t') -- books
164 WHEN item_type = 'g' -- movies
166 WHEN item_type IN ('i','j') -- sound recordings
168 WHEN item_type = 'm' -- software
170 WHEN item_type = 'k' -- images
172 WHEN item_type IN ('e','f') -- maps
174 WHEN item_type IN ('o','p') -- mixed
176 WHEN item_type IN ('c','d') -- music
178 WHEN item_type = 'r' -- 3d
185 my $ids = metabib::metarecord_source_map->db_Main->selectcol_arrayref($sql, {}, "$mr", @types, @forms, @blvl);
186 return $ids if ($self->api_name =~ /atomic$/o);
188 $client->respond( $_ ) for ( @$ids );
192 __PACKAGE__->register_method(
193 api_name => 'open-ils.storage.ordered.metabib.metarecord.records',
194 method => 'ordered_records_from_metarecord',
198 __PACKAGE__->register_method(
199 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff',
200 method => 'ordered_records_from_metarecord',
205 __PACKAGE__->register_method(
206 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.atomic',
207 method => 'ordered_records_from_metarecord',
211 __PACKAGE__->register_method(
212 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic',
213 method => 'ordered_records_from_metarecord',
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');
436 $index_col ||= 'value';
437 my $search_table = metabib::full_rec->table;
439 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
441 my $fts_where = $fts->sql_where_clause();
442 my @fts_ranks = $fts->fts_rank;
444 my $rank = join(' + ', @fts_ranks);
447 for my $limit (@$limiters) {
448 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
449 # MARC control field; mfr.subfield is NULL
450 push @wheres, "( tag = ? AND $fts_where )";
451 push @binds, $$limit{tag};
452 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
454 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
455 push @binds, $$limit{tag}, $$limit{subfield};
456 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
459 my $where = join(' OR ', @wheres);
461 push @selects, "SELECT id, record, $rank as sum FROM $search_table WHERE $where";
465 my $descendants = defined($args{depth}) ?
466 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
467 "actor.org_unit_descendants($args{org_unit})" ;
470 my $metabib_record_descriptor = metabib::record_descriptor->table;
471 my $metabib_full_rec = metabib::full_rec->table;
472 my $asset_call_number_table = asset::call_number->table;
473 my $asset_copy_table = asset::copy->table;
474 my $cs_table = config::copy_status->table;
475 my $cl_table = asset::copy_location->table;
476 my $br_table = biblio::record_entry->table;
478 my $cj = 'HAVING COUNT(x.id) = ' . scalar(@selects) if ($class_join eq 'AND');
480 '(SELECT x.record, sum(x.sum) FROM (('.
481 join(') UNION ALL (', @selects).
482 ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
484 my $has_vols = 'AND cn.owning_lib = d.id';
485 my $has_copies = 'AND cp.call_number = cn.id';
486 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';
488 if ($self->api_name =~ /staff/o) {
489 $copies_visible = '';
490 $has_copies = '' if ($ou_type == 0);
491 $has_vols = '' if ($ou_type == 0);
494 my ($t_filter, $f_filter) = ('','');
495 my ($a_filter, $l_filter, $lf_filter) = ('','','');
497 if (my $a = $args{audience}) {
498 $a = [$a] if (!ref($a));
501 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
505 if (my $l = $args{language}) {
506 $l = [$l] if (!ref($l));
509 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
513 if (my $f = $args{lit_form}) {
514 $f = [$f] if (!ref($f));
517 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
518 push @binds, @lit_form;
521 if (my $f = $args{item_form}) {
522 $f = [$f] if (!ref($f));
525 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
529 if (my $t = $args{item_type}) {
530 $t = [$t] if (!ref($t));
533 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
539 my ($t, $f) = split '-', $args{format};
540 my @types = split '', $t;
541 my @forms = split '', $f;
543 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
547 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
549 push @binds, @types, @forms;
552 my $relevance = 'sum(f.sum)';
553 $relevance = 1 if (!$copies_visible);
555 my $rank = $relevance;
556 if (lc($sort) eq 'pubdate') {
559 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'9999')::INT
560 FROM $metabib_full_rec frp
561 WHERE frp.record = f.record
563 AND frp.subfield = 'c'
567 } elsif (lc($sort) eq 'create_date') {
569 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
571 } elsif (lc($sort) eq 'edit_date') {
573 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
575 } elsif (lc($sort) eq 'title') {
578 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'zzzzzzzz')
579 FROM $metabib_full_rec frt
580 WHERE frt.record = f.record
582 AND frt.subfield = 'a'
586 } elsif (lc($sort) eq 'author') {
589 SELECT COALESCE(LTRIM(fra.value),'zzzzzzzz')
590 FROM $metabib_full_rec fra
591 WHERE fra.record = f.record
592 AND fra.tag LIKE '1%'
593 AND fra.subfield = 'a'
594 ORDER BY fra.tag::text::int
603 if ($copies_visible) {
605 SELECT f.record, $relevance, count(DISTINCT cp.id), $rank
606 FROM $search_table f,
607 $asset_call_number_table cn,
608 $asset_copy_table cp,
612 $metabib_record_descriptor rd,
614 WHERE br.id = f.record
615 AND cn.record = f.record
616 AND rd.record = f.record
617 AND cp.status = cs.id
618 AND cp.location = cl.id
619 AND br.deleted IS FALSE
620 AND cn.deleted IS FALSE
621 AND cp.deleted IS FALSE
630 GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
631 ORDER BY 4 $sort_dir,3 DESC
635 SELECT f.record, 1, 1, $rank
636 FROM $search_table f,
638 $metabib_record_descriptor rd
639 WHERE br.id = f.record
640 AND rd.record = f.record
641 AND br.deleted IS FALSE
653 $log->debug("Search SQL :: [$select]",DEBUG);
655 my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
656 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
659 $max = 1 if (!@$recs);
661 $max = $$_[1] if ($$_[1] > $max);
665 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
666 next unless ($$rec[0]);
667 my ($rid,$rank,$junk,$skip) = @$rec;
668 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
672 __PACKAGE__->register_method(
673 api_name => 'open-ils.storage.biblio.full_rec.multi_search',
674 method => 'biblio_multi_search_full_rec',
679 __PACKAGE__->register_method(
680 api_name => 'open-ils.storage.biblio.full_rec.multi_search.staff',
681 method => 'biblio_multi_search_full_rec',
687 sub search_full_rec {
693 my $term = $args{term};
694 my $limiters = $args{restrict};
696 my ($index_col) = metabib::full_rec->columns('FTS');
697 $index_col ||= 'value';
698 my $search_table = metabib::full_rec->table;
700 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
702 my $fts_where = $fts->sql_where_clause();
703 my @fts_ranks = $fts->fts_rank;
705 my $rank = join(' + ', @fts_ranks);
709 for my $limit (@$limiters) {
710 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
711 # MARC control field; mfr.subfield is NULL
712 push @wheres, "( tag = ? AND $fts_where )";
713 push @binds, $$limit{tag};
714 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
716 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
717 push @binds, $$limit{tag}, $$limit{subfield};
718 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
721 my $where = join(' OR ', @wheres);
723 my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
725 $log->debug("Search SQL :: [$select]",DEBUG);
727 my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
728 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
730 $client->respond($_) for (@$recs);
733 __PACKAGE__->register_method(
734 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
735 method => 'search_full_rec',
740 __PACKAGE__->register_method(
741 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
742 method => 'search_full_rec',
749 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
750 sub search_class_fts {
755 my $term = $args{term};
756 my $ou = $args{org_unit};
757 my $ou_type = $args{depth};
758 my $limit = $args{limit};
759 my $offset = $args{offset};
761 my $limit_clause = '';
762 my $offset_clause = '';
764 $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
765 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
768 my ($t_filter, $f_filter) = ('','');
771 my ($t, $f) = split '-', $args{format};
772 @types = split '', $t;
773 @forms = split '', $f;
775 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
779 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
785 my $descendants = defined($ou_type) ?
786 "actor.org_unit_descendants($ou, $ou_type)" :
787 "actor.org_unit_descendants($ou)";
789 my $class = $self->{cdbi};
790 my $search_table = $class->table;
792 my $metabib_record_descriptor = metabib::record_descriptor->table;
793 my $metabib_metarecord = metabib::metarecord->table;
794 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
795 my $asset_call_number_table = asset::call_number->table;
796 my $asset_copy_table = asset::copy->table;
797 my $cs_table = config::copy_status->table;
798 my $cl_table = asset::copy_location->table;
800 my ($index_col) = $class->columns('FTS');
801 $index_col ||= 'value';
803 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
804 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
806 my $fts_where = $fts->sql_where_clause;
807 my @fts_ranks = $fts->fts_rank;
809 my $rank = join(' + ', @fts_ranks);
811 my $has_vols = 'AND cn.owning_lib = d.id';
812 my $has_copies = 'AND cp.call_number = cn.id';
813 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';
815 my $visible_count = ', count(DISTINCT cp.id)';
816 my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
818 if ($self->api_name =~ /staff/o) {
819 $copies_visible = '';
820 $visible_count_test = '';
821 $has_copies = '' if ($ou_type == 0);
822 $has_vols = '' if ($ou_type == 0);
825 my $rank_calc = <<" RANK";
827 * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
828 * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
829 * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
830 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
833 $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
835 if ($copies_visible) {
837 SELECT m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
838 FROM $search_table f,
839 $metabib_metarecord_source_map_table m,
840 $asset_call_number_table cn,
841 $asset_copy_table cp,
844 $metabib_record_descriptor rd,
847 AND m.source = f.source
848 AND cn.record = m.source
849 AND rd.record = m.source
850 AND cp.status = cs.id
851 AND cp.location = cl.id
857 GROUP BY 1 $visible_count_test
859 $limit_clause $offset_clause
863 SELECT m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
864 FROM $search_table f,
865 $metabib_metarecord_source_map_table m,
866 $metabib_record_descriptor rd
868 AND m.source = f.source
869 AND rd.record = m.source
874 $limit_clause $offset_clause
878 $log->debug("Field Search SQL :: [$select]",DEBUG);
880 my $SQLstring = join('%',$fts->words);
881 my $REstring = join('\\s+',$fts->words);
882 my $first_word = ($fts->words)[0].'%';
883 my $recs = ($self->api_name =~ /unordered/o) ?
884 $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
885 $class->db_Main->selectall_arrayref($select, {},
886 '%'.lc($SQLstring).'%', # phrase order match
887 lc($first_word), # first word match
888 '^\\s*'.lc($REstring).'\\s*/?\s*$', # full exact match
892 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
894 $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
898 for my $class ( qw/title author subject keyword series identifier/ ) {
899 __PACKAGE__->register_method(
900 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord",
901 method => 'search_class_fts',
904 cdbi => "metabib::${class}_field_entry",
907 __PACKAGE__->register_method(
908 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
909 method => 'search_class_fts',
912 cdbi => "metabib::${class}_field_entry",
915 __PACKAGE__->register_method(
916 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
917 method => 'search_class_fts',
920 cdbi => "metabib::${class}_field_entry",
923 __PACKAGE__->register_method(
924 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
925 method => 'search_class_fts',
928 cdbi => "metabib::${class}_field_entry",
933 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
934 sub search_class_fts_count {
939 my $term = $args{term};
940 my $ou = $args{org_unit};
941 my $ou_type = $args{depth};
942 my $limit = $args{limit} || 100;
943 my $offset = $args{offset} || 0;
945 my $descendants = defined($ou_type) ?
946 "actor.org_unit_descendants($ou, $ou_type)" :
947 "actor.org_unit_descendants($ou)";
950 my ($t_filter, $f_filter) = ('','');
953 my ($t, $f) = split '-', $args{format};
954 @types = split '', $t;
955 @forms = split '', $f;
957 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
961 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
966 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
968 my $class = $self->{cdbi};
969 my $search_table = $class->table;
971 my $metabib_record_descriptor = metabib::record_descriptor->table;
972 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
973 my $asset_call_number_table = asset::call_number->table;
974 my $asset_copy_table = asset::copy->table;
975 my $cs_table = config::copy_status->table;
976 my $cl_table = asset::copy_location->table;
978 my ($index_col) = $class->columns('FTS');
979 $index_col ||= 'value';
981 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
983 my $fts_where = $fts->sql_where_clause;
985 my $has_vols = 'AND cn.owning_lib = d.id';
986 my $has_copies = 'AND cp.call_number = cn.id';
987 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';
988 if ($self->api_name =~ /staff/o) {
989 $copies_visible = '';
990 $has_vols = '' if ($ou_type == 0);
991 $has_copies = '' if ($ou_type == 0);
994 # XXX test an "EXISTS version of descendant checking...
996 if ($copies_visible) {
998 SELECT count(distinct m.metarecord)
999 FROM $search_table f,
1000 $metabib_metarecord_source_map_table m,
1001 $metabib_metarecord_source_map_table mr,
1002 $asset_call_number_table cn,
1003 $asset_copy_table cp,
1006 $metabib_record_descriptor rd,
1009 AND mr.source = f.source
1010 AND mr.metarecord = m.metarecord
1011 AND cn.record = m.source
1012 AND rd.record = m.source
1013 AND cp.status = cs.id
1014 AND cp.location = cl.id
1023 SELECT count(distinct m.metarecord)
1024 FROM $search_table f,
1025 $metabib_metarecord_source_map_table m,
1026 $metabib_metarecord_source_map_table mr,
1027 $metabib_record_descriptor rd
1029 AND mr.source = f.source
1030 AND mr.metarecord = m.metarecord
1031 AND rd.record = m.source
1037 $log->debug("Field Search Count SQL :: [$select]",DEBUG);
1039 my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1041 $log->debug("Count Search yielded $recs results.",DEBUG);
1046 for my $class ( qw/title author subject keyword series identifier/ ) {
1047 __PACKAGE__->register_method(
1048 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
1049 method => 'search_class_fts_count',
1052 cdbi => "metabib::${class}_field_entry",
1055 __PACKAGE__->register_method(
1056 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
1057 method => 'search_class_fts_count',
1060 cdbi => "metabib::${class}_field_entry",
1066 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1067 sub postfilter_search_class_fts {
1072 my $term = $args{term};
1073 my $sort = $args{'sort'};
1074 my $sort_dir = $args{sort_dir} || 'DESC';
1075 my $ou = $args{org_unit};
1076 my $ou_type = $args{depth};
1077 my $limit = $args{limit} || 10;
1078 my $visibility_limit = $args{visibility_limit} || 5000;
1079 my $offset = $args{offset} || 0;
1081 my $outer_limit = 1000;
1083 my $limit_clause = '';
1084 my $offset_clause = '';
1086 $limit_clause = "LIMIT $outer_limit";
1087 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1089 my (@types,@forms,@lang,@aud,@lit_form);
1090 my ($t_filter, $f_filter) = ('','');
1091 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1092 my ($ot_filter, $of_filter) = ('','');
1093 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1095 if (my $a = $args{audience}) {
1096 $a = [$a] if (!ref($a));
1099 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1100 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1103 if (my $l = $args{language}) {
1104 $l = [$l] if (!ref($l));
1107 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1108 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1111 if (my $f = $args{lit_form}) {
1112 $f = [$f] if (!ref($f));
1115 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1116 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1119 if ($args{format}) {
1120 my ($t, $f) = split '-', $args{format};
1121 @types = split '', $t;
1122 @forms = split '', $f;
1124 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1125 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1129 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1130 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1135 my $descendants = defined($ou_type) ?
1136 "actor.org_unit_descendants($ou, $ou_type)" :
1137 "actor.org_unit_descendants($ou)";
1139 my $class = $self->{cdbi};
1140 my $search_table = $class->table;
1142 my $metabib_full_rec = metabib::full_rec->table;
1143 my $metabib_record_descriptor = metabib::record_descriptor->table;
1144 my $metabib_metarecord = metabib::metarecord->table;
1145 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1146 my $asset_call_number_table = asset::call_number->table;
1147 my $asset_copy_table = asset::copy->table;
1148 my $cs_table = config::copy_status->table;
1149 my $cl_table = asset::copy_location->table;
1150 my $br_table = biblio::record_entry->table;
1152 my ($index_col) = $class->columns('FTS');
1153 $index_col ||= 'value';
1155 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1157 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1159 my $SQLstring = join('%',map { lc($_) } $fts->words);
1160 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1161 my $first_word = lc(($fts->words)[0]).'%';
1163 my $fts_where = $fts->sql_where_clause;
1164 my @fts_ranks = $fts->fts_rank;
1167 $bonus{'metabib::identifier_field_entry'} =
1168 $bonus{'metabib::keyword_field_entry'} = [
1169 { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring }
1172 $bonus{'metabib::title_field_entry'} =
1173 $bonus{'metabib::series_field_entry'} = [
1174 { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1175 { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1176 @{ $bonus{'metabib::keyword_field_entry'} }
1179 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1180 $bonus_list ||= '1';
1182 my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1184 my $relevance = join(' + ', @fts_ranks);
1185 $relevance = <<" RANK";
1186 (SUM( ( $relevance ) * ( $bonus_list ) )/COUNT(m.source))
1189 my $string_default_sort = 'zzzz';
1190 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1192 my $number_default_sort = '9999';
1193 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1195 my $rank = $relevance;
1196 if (lc($sort) eq 'pubdate') {
1199 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1200 FROM $metabib_full_rec frp
1201 WHERE frp.record = mr.master_record
1203 AND frp.subfield = 'c'
1207 } elsif (lc($sort) eq 'create_date') {
1209 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1211 } elsif (lc($sort) eq 'edit_date') {
1213 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1215 } elsif (lc($sort) eq 'title') {
1218 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1219 FROM $metabib_full_rec frt
1220 WHERE frt.record = mr.master_record
1222 AND frt.subfield = 'a'
1226 } elsif (lc($sort) eq 'author') {
1229 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1230 FROM $metabib_full_rec fra
1231 WHERE fra.record = mr.master_record
1232 AND fra.tag LIKE '1%'
1233 AND fra.subfield = 'a'
1234 ORDER BY fra.tag::text::int
1242 my $select = <<" SQL";
1243 SELECT m.metarecord,
1245 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1247 FROM $search_table f,
1248 $metabib_metarecord_source_map_table m,
1249 $metabib_metarecord_source_map_table smrs,
1250 $metabib_metarecord mr,
1251 $metabib_record_descriptor rd
1253 AND smrs.metarecord = mr.id
1254 AND m.source = f.source
1255 AND m.metarecord = mr.id
1256 AND rd.record = smrs.source
1262 GROUP BY m.metarecord
1263 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1264 LIMIT $visibility_limit
1271 FROM $asset_call_number_table cn,
1272 $metabib_metarecord_source_map_table mrs,
1273 $asset_copy_table cp,
1278 $metabib_record_descriptor ord,
1280 WHERE mrs.metarecord = s.metarecord
1281 AND br.id = mrs.source
1282 AND cn.record = mrs.source
1283 AND cp.status = cs.id
1284 AND cp.location = cl.id
1285 AND cn.owning_lib = d.id
1286 AND cp.call_number = cn.id
1287 AND cp.opac_visible IS TRUE
1288 AND cs.opac_visible IS TRUE
1289 AND cl.opac_visible IS TRUE
1290 AND d.opac_visible IS TRUE
1291 AND br.active IS TRUE
1292 AND br.deleted IS FALSE
1293 AND ord.record = mrs.source
1299 ORDER BY 4 $sort_dir
1301 } elsif ($self->api_name !~ /staff/o) {
1308 FROM $asset_call_number_table cn,
1309 $metabib_metarecord_source_map_table mrs,
1310 $asset_copy_table cp,
1315 $metabib_record_descriptor ord
1317 WHERE mrs.metarecord = s.metarecord
1318 AND br.id = mrs.source
1319 AND cn.record = mrs.source
1320 AND cp.status = cs.id
1321 AND cp.location = cl.id
1322 AND cp.circ_lib = d.id
1323 AND cp.call_number = cn.id
1324 AND cp.opac_visible IS TRUE
1325 AND cs.opac_visible IS TRUE
1326 AND cl.opac_visible IS TRUE
1327 AND d.opac_visible IS TRUE
1328 AND br.active IS TRUE
1329 AND br.deleted IS FALSE
1330 AND ord.record = mrs.source
1338 ORDER BY 4 $sort_dir
1347 FROM $asset_call_number_table cn,
1348 $asset_copy_table cp,
1349 $metabib_metarecord_source_map_table mrs,
1352 $metabib_record_descriptor ord
1354 WHERE mrs.metarecord = s.metarecord
1355 AND br.id = mrs.source
1356 AND cn.record = mrs.source
1357 AND cn.id = cp.call_number
1358 AND br.deleted IS FALSE
1359 AND cn.deleted IS FALSE
1360 AND ord.record = mrs.source
1361 AND ( cn.owning_lib = d.id
1362 OR ( cp.circ_lib = d.id
1363 AND cp.deleted IS FALSE
1375 FROM $asset_call_number_table cn,
1376 $metabib_metarecord_source_map_table mrs,
1377 $metabib_record_descriptor ord
1378 WHERE mrs.metarecord = s.metarecord
1379 AND cn.record = mrs.source
1380 AND ord.record = mrs.source
1388 ORDER BY 4 $sort_dir
1393 $log->debug("Field Search SQL :: [$select]",DEBUG);
1395 my $recs = $class->db_Main->selectall_arrayref(
1397 (@bonus_values > 0 ? @bonus_values : () ),
1398 ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1399 @types, @forms, @aud, @lang, @lit_form,
1400 @types, @forms, @aud, @lang, @lit_form,
1401 ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1403 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1406 $max = 1 if (!@$recs);
1408 $max = $$_[1] if ($$_[1] > $max);
1411 my $count = scalar(@$recs);
1412 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1413 my ($mrid,$rank,$skip) = @$rec;
1414 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1419 for my $class ( qw/title author subject keyword series identifier/ ) {
1420 __PACKAGE__->register_method(
1421 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1422 method => 'postfilter_search_class_fts',
1425 cdbi => "metabib::${class}_field_entry",
1428 __PACKAGE__->register_method(
1429 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1430 method => 'postfilter_search_class_fts',
1433 cdbi => "metabib::${class}_field_entry",
1440 my $_cdbi = { title => "metabib::title_field_entry",
1441 author => "metabib::author_field_entry",
1442 subject => "metabib::subject_field_entry",
1443 keyword => "metabib::keyword_field_entry",
1444 series => "metabib::series_field_entry",
1445 identifier => "metabib::identifier_field_entry",
1448 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1449 sub postfilter_search_multi_class_fts {
1454 my $sort = $args{'sort'};
1455 my $sort_dir = $args{sort_dir} || 'DESC';
1456 my $ou = $args{org_unit};
1457 my $ou_type = $args{depth};
1458 my $limit = $args{limit} || 10;
1459 my $offset = $args{offset} || 0;
1460 my $visibility_limit = $args{visibility_limit} || 5000;
1463 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1466 if (!defined($args{org_unit})) {
1467 die "No target organizational unit passed to ".$self->api_name;
1470 if (! scalar( keys %{$args{searches}} )) {
1471 die "No search arguments were passed to ".$self->api_name;
1474 my $outer_limit = 1000;
1476 my $limit_clause = '';
1477 my $offset_clause = '';
1479 $limit_clause = "LIMIT $outer_limit";
1480 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1482 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1483 my ($t_filter, $f_filter, $v_filter) = ('','','');
1484 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1485 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1486 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1488 if ($args{available}) {
1489 $avail_filter = ' AND cp.status IN (0,7,12)';
1492 if (my $a = $args{audience}) {
1493 $a = [$a] if (!ref($a));
1496 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1497 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1500 if (my $l = $args{language}) {
1501 $l = [$l] if (!ref($l));
1504 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1505 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1508 if (my $f = $args{lit_form}) {
1509 $f = [$f] if (!ref($f));
1512 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1513 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1516 if (my $f = $args{item_form}) {
1517 $f = [$f] if (!ref($f));
1520 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1521 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1524 if (my $t = $args{item_type}) {
1525 $t = [$t] if (!ref($t));
1528 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1529 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1532 if (my $v = $args{vr_format}) {
1533 $v = [$v] if (!ref($v));
1536 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1537 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1541 # XXX legacy format and item type support
1542 if ($args{format}) {
1543 my ($t, $f) = split '-', $args{format};
1544 @types = split '', $t;
1545 @forms = split '', $f;
1547 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1548 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1552 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1553 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1559 my $descendants = defined($ou_type) ?
1560 "actor.org_unit_descendants($ou, $ou_type)" :
1561 "actor.org_unit_descendants($ou)";
1563 my $search_table_list = '';
1565 my $join_table_list = '';
1568 my $field_table = config::metabib_field->table;
1572 my $prev_search_group;
1573 my $curr_search_group;
1577 for my $search_group (sort keys %{$args{searches}}) {
1578 (my $search_group_name = $search_group) =~ s/\|/_/gso;
1579 ($search_class,$search_field) = split /\|/, $search_group;
1580 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1582 if ($search_field) {
1583 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1584 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1589 $prev_search_group = $curr_search_group if ($curr_search_group);
1591 $curr_search_group = $search_group_name;
1593 my $class = $_cdbi->{$search_class};
1594 my $search_table = $class->table;
1596 my ($index_col) = $class->columns('FTS');
1597 $index_col ||= 'value';
1600 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1602 my $fts_where = $fts->sql_where_clause;
1603 my @fts_ranks = $fts->fts_rank;
1605 my $SQLstring = join('%',map { lc($_) } $fts->words);
1606 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1607 my $first_word = lc(($fts->words)[0]).'%';
1609 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1610 my $rank = join(' + ', @fts_ranks);
1613 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1614 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1616 $bonus{'series'} = [
1617 { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1618 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1621 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1623 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1624 $bonus_list ||= '1';
1626 push @bonus_lists, $bonus_list;
1627 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1630 #---------------------
1632 $search_table_list .= "$search_table $search_group_name, ";
1633 push @rank_list,$rank;
1634 $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1636 if ($metabib_field) {
1637 $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1638 $metabib_field = undef;
1641 if ($prev_search_group) {
1642 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1646 my $metabib_record_descriptor = metabib::record_descriptor->table;
1647 my $metabib_full_rec = metabib::full_rec->table;
1648 my $metabib_metarecord = metabib::metarecord->table;
1649 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1650 my $asset_call_number_table = asset::call_number->table;
1651 my $asset_copy_table = asset::copy->table;
1652 my $cs_table = config::copy_status->table;
1653 my $cl_table = asset::copy_location->table;
1654 my $br_table = biblio::record_entry->table;
1655 my $source_table = config::bib_source->table;
1657 my $bonuses = join (' * ', @bonus_lists);
1658 my $relevance = join (' + ', @rank_list);
1659 $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1661 my $string_default_sort = 'zzzz';
1662 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1664 my $number_default_sort = '9999';
1665 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1669 my $secondary_sort = <<" SORT";
1671 SELECT COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1672 FROM $metabib_full_rec sfrt,
1673 $metabib_metarecord mr
1674 WHERE sfrt.record = mr.master_record
1675 AND sfrt.tag = '245'
1676 AND sfrt.subfield = 'a'
1681 my $rank = $relevance;
1682 if (lc($sort) eq 'pubdate') {
1685 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1686 FROM $metabib_full_rec frp
1687 WHERE frp.record = mr.master_record
1689 AND frp.subfield = 'c'
1693 } elsif (lc($sort) eq 'create_date') {
1695 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1697 } elsif (lc($sort) eq 'edit_date') {
1699 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1701 } elsif (lc($sort) eq 'title') {
1704 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1705 FROM $metabib_full_rec frt
1706 WHERE frt.record = mr.master_record
1708 AND frt.subfield = 'a'
1712 $secondary_sort = <<" SORT";
1714 SELECT COALESCE(SUBSTRING(sfrp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1715 FROM $metabib_full_rec sfrp
1716 WHERE sfrp.record = mr.master_record
1717 AND sfrp.tag = '260'
1718 AND sfrp.subfield = 'c'
1722 } elsif (lc($sort) eq 'author') {
1725 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1726 FROM $metabib_full_rec fra
1727 WHERE fra.record = mr.master_record
1728 AND fra.tag LIKE '1%'
1729 AND fra.subfield = 'a'
1730 ORDER BY fra.tag::text::int
1735 push @bonus_values, @bonus_values;
1740 my $select = <<" SQL";
1741 SELECT m.metarecord,
1743 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1746 FROM $search_table_list
1747 $metabib_metarecord mr,
1748 $metabib_metarecord_source_map_table m,
1749 $metabib_metarecord_source_map_table smrs
1750 WHERE m.metarecord = smrs.metarecord
1751 AND mr.id = m.metarecord
1754 GROUP BY m.metarecord
1755 -- ORDER BY 4 $sort_dir
1756 LIMIT $visibility_limit
1759 if ($self->api_name !~ /staff/o) {
1766 FROM $asset_call_number_table cn,
1767 $metabib_metarecord_source_map_table mrs,
1768 $asset_copy_table cp,
1773 $metabib_record_descriptor ord
1774 WHERE mrs.metarecord = s.metarecord
1775 AND br.id = mrs.source
1776 AND cn.record = mrs.source
1777 AND cp.status = cs.id
1778 AND cp.location = cl.id
1779 AND cp.circ_lib = d.id
1780 AND cp.call_number = cn.id
1781 AND cp.opac_visible IS TRUE
1782 AND cs.opac_visible IS TRUE
1783 AND cl.opac_visible IS TRUE
1784 AND d.opac_visible IS TRUE
1785 AND br.active IS TRUE
1786 AND br.deleted IS FALSE
1787 AND cp.deleted IS FALSE
1788 AND cn.deleted IS FALSE
1789 AND ord.record = mrs.source
1802 $metabib_metarecord_source_map_table mrs,
1803 $metabib_record_descriptor ord,
1805 WHERE mrs.metarecord = s.metarecord
1806 AND ord.record = mrs.source
1807 AND br.id = mrs.source
1808 AND br.source = src.id
1809 AND src.transcendant IS TRUE
1817 ORDER BY 4 $sort_dir, 5
1824 $metabib_metarecord_source_map_table omrs,
1825 $metabib_record_descriptor ord
1826 WHERE omrs.metarecord = s.metarecord
1827 AND ord.record = omrs.source
1830 FROM $asset_call_number_table cn,
1831 $asset_copy_table cp,
1834 WHERE br.id = omrs.source
1835 AND cn.record = omrs.source
1836 AND br.deleted IS FALSE
1837 AND cn.deleted IS FALSE
1838 AND cp.call_number = cn.id
1839 AND ( cn.owning_lib = d.id
1840 OR ( cp.circ_lib = d.id
1841 AND cp.deleted IS FALSE
1849 FROM $asset_call_number_table cn
1850 WHERE cn.record = omrs.source
1851 AND cn.deleted IS FALSE
1857 $metabib_metarecord_source_map_table mrs,
1858 $metabib_record_descriptor ord,
1860 WHERE mrs.metarecord = s.metarecord
1861 AND br.id = mrs.source
1862 AND br.source = src.id
1863 AND src.transcendant IS TRUE
1879 ORDER BY 4 $sort_dir, 5
1884 $log->debug("Field Search SQL :: [$select]",DEBUG);
1886 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1889 @types, @forms, @vformats, @aud, @lang, @lit_form,
1890 @types, @forms, @vformats, @aud, @lang, @lit_form,
1891 # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1894 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1897 $max = 1 if (!@$recs);
1899 $max = $$_[1] if ($$_[1] > $max);
1902 my $count = scalar(@$recs);
1903 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1904 next unless ($$rec[0]);
1905 my ($mrid,$rank,$skip) = @$rec;
1906 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1911 __PACKAGE__->register_method(
1912 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1913 method => 'postfilter_search_multi_class_fts',
1918 __PACKAGE__->register_method(
1919 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1920 method => 'postfilter_search_multi_class_fts',
1926 __PACKAGE__->register_method(
1927 api_name => "open-ils.storage.metabib.multiclass.search_fts",
1928 method => 'postfilter_search_multi_class_fts',
1933 __PACKAGE__->register_method(
1934 api_name => "open-ils.storage.metabib.multiclass.search_fts.staff",
1935 method => 'postfilter_search_multi_class_fts',
1941 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1942 sub biblio_search_multi_class_fts {
1947 my $sort = $args{'sort'};
1948 my $sort_dir = $args{sort_dir} || 'DESC';
1949 my $ou = $args{org_unit};
1950 my $ou_type = $args{depth};
1951 my $limit = $args{limit} || 10;
1952 my $offset = $args{offset} || 0;
1953 my $pref_lang = $args{prefered_language} || 'eng';
1954 my $visibility_limit = $args{visibility_limit} || 5000;
1957 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1960 if (! scalar( keys %{$args{searches}} )) {
1961 die "No search arguments were passed to ".$self->api_name;
1964 my $outer_limit = 1000;
1966 my $limit_clause = '';
1967 my $offset_clause = '';
1969 $limit_clause = "LIMIT $outer_limit";
1970 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1972 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1973 my ($t_filter, $f_filter, $v_filter) = ('','','');
1974 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1975 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1976 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1978 if ($args{available}) {
1979 $avail_filter = ' AND cp.status IN (0,7,12)';
1982 if (my $a = $args{audience}) {
1983 $a = [$a] if (!ref($a));
1986 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1987 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1990 if (my $l = $args{language}) {
1991 $l = [$l] if (!ref($l));
1994 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1995 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1998 if (my $f = $args{lit_form}) {
1999 $f = [$f] if (!ref($f));
2002 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2003 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2006 if (my $f = $args{item_form}) {
2007 $f = [$f] if (!ref($f));
2010 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2011 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2014 if (my $t = $args{item_type}) {
2015 $t = [$t] if (!ref($t));
2018 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2019 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2022 if (my $v = $args{vr_format}) {
2023 $v = [$v] if (!ref($v));
2026 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
2027 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
2030 # XXX legacy format and item type support
2031 if ($args{format}) {
2032 my ($t, $f) = split '-', $args{format};
2033 @types = split '', $t;
2034 @forms = split '', $f;
2036 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2037 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2041 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2042 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2047 my $descendants = defined($ou_type) ?
2048 "actor.org_unit_descendants($ou, $ou_type)" :
2049 "actor.org_unit_descendants($ou)";
2051 my $search_table_list = '';
2053 my $join_table_list = '';
2056 my $field_table = config::metabib_field->table;
2060 my $prev_search_group;
2061 my $curr_search_group;
2065 for my $search_group (sort keys %{$args{searches}}) {
2066 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2067 ($search_class,$search_field) = split /\|/, $search_group;
2068 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2070 if ($search_field) {
2071 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2072 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2077 $prev_search_group = $curr_search_group if ($curr_search_group);
2079 $curr_search_group = $search_group_name;
2081 my $class = $_cdbi->{$search_class};
2082 my $search_table = $class->table;
2084 my ($index_col) = $class->columns('FTS');
2085 $index_col ||= 'value';
2088 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2090 my $fts_where = $fts->sql_where_clause;
2091 my @fts_ranks = $fts->fts_rank;
2093 my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2094 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2095 my $first_word = lc(($fts->words)[0]).'%';
2097 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2098 my $rank = join(' + ', @fts_ranks);
2101 $bonus{'subject'} = [];
2102 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2104 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2106 $bonus{'series'} = [
2107 { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2108 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2111 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2114 push @{ $bonus{'title'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2115 push @{ $bonus{'author'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2116 push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2117 push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2118 push @{ $bonus{'series'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2121 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2122 $bonus_list ||= '1';
2124 push @bonus_lists, $bonus_list;
2125 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2127 #---------------------
2129 $search_table_list .= "$search_table $search_group_name, ";
2130 push @rank_list,$rank;
2131 $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2133 if ($metabib_field) {
2134 $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2135 $metabib_field = undef;
2138 if ($prev_search_group) {
2139 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2143 my $metabib_record_descriptor = metabib::record_descriptor->table;
2144 my $metabib_full_rec = metabib::full_rec->table;
2145 my $metabib_metarecord = metabib::metarecord->table;
2146 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2147 my $asset_call_number_table = asset::call_number->table;
2148 my $asset_copy_table = asset::copy->table;
2149 my $cs_table = config::copy_status->table;
2150 my $cl_table = asset::copy_location->table;
2151 my $br_table = biblio::record_entry->table;
2152 my $source_table = config::bib_source->table;
2155 my $bonuses = join (' * ', @bonus_lists);
2156 my $relevance = join (' + ', @rank_list);
2157 $relevance = "AVG( ($relevance) * ($bonuses) )";
2159 my $string_default_sort = 'zzzz';
2160 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2162 my $number_default_sort = '9999';
2163 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2165 my $rank = $relevance;
2166 if (lc($sort) eq 'pubdate') {
2169 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d{4}'),'$number_default_sort')::INT
2170 FROM $metabib_full_rec frp
2171 WHERE frp.record = b.id
2173 AND frp.subfield = 'c'
2177 } elsif (lc($sort) eq 'create_date') {
2179 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2181 } elsif (lc($sort) eq 'edit_date') {
2183 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2185 } elsif (lc($sort) eq 'title') {
2188 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2189 FROM $metabib_full_rec frt
2190 WHERE frt.record = b.id
2192 AND frt.subfield = 'a'
2196 } elsif (lc($sort) eq 'author') {
2199 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
2200 FROM $metabib_full_rec fra
2201 WHERE fra.record = b.id
2202 AND fra.tag LIKE '1%'
2203 AND fra.subfield = 'a'
2204 ORDER BY fra.tag::text::int
2209 push @bonus_values, @bonus_values;
2214 my $select = <<" SQL";
2219 FROM $search_table_list
2220 $metabib_record_descriptor rd,
2223 WHERE rd.record = b.id
2224 AND b.active IS TRUE
2225 AND b.deleted IS FALSE
2234 GROUP BY b.id, b.source
2235 ORDER BY 3 $sort_dir
2236 LIMIT $visibility_limit
2239 if ($self->api_name !~ /staff/o) {
2244 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2247 FROM $asset_call_number_table cn,
2248 $asset_copy_table cp,
2252 WHERE cn.record = s.id
2253 AND cp.status = cs.id
2254 AND cp.location = cl.id
2255 AND cp.call_number = cn.id
2256 AND cp.opac_visible IS TRUE
2257 AND cs.opac_visible IS TRUE
2258 AND cl.opac_visible IS TRUE
2259 AND d.opac_visible IS TRUE
2260 AND cp.deleted IS FALSE
2261 AND cn.deleted IS FALSE
2262 AND cp.circ_lib = d.id
2266 OR src.transcendant IS TRUE
2267 ORDER BY 3 $sort_dir
2274 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2277 FROM $asset_call_number_table cn,
2278 $asset_copy_table cp,
2280 WHERE cn.record = s.id
2281 AND cp.call_number = cn.id
2282 AND cn.deleted IS FALSE
2283 AND cp.circ_lib = d.id
2284 AND cp.deleted IS FALSE
2290 FROM $asset_call_number_table cn
2291 WHERE cn.record = s.id
2294 OR src.transcendant IS TRUE
2295 ORDER BY 3 $sort_dir
2300 $log->debug("Field Search SQL :: [$select]",DEBUG);
2302 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2304 @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2307 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2309 my $count = scalar(@$recs);
2310 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2311 next unless ($$rec[0]);
2312 my ($mrid,$rank) = @$rec;
2313 $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2318 __PACKAGE__->register_method(
2319 api_name => "open-ils.storage.biblio.multiclass.search_fts.record",
2320 method => 'biblio_search_multi_class_fts',
2325 __PACKAGE__->register_method(
2326 api_name => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2327 method => 'biblio_search_multi_class_fts',
2332 __PACKAGE__->register_method(
2333 api_name => "open-ils.storage.biblio.multiclass.search_fts",
2334 method => 'biblio_search_multi_class_fts',
2339 __PACKAGE__->register_method(
2340 api_name => "open-ils.storage.biblio.multiclass.search_fts.staff",
2341 method => 'biblio_search_multi_class_fts',
2349 my $default_preferred_language;
2350 my $default_preferred_language_weight;
2352 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2358 if (!$locale_map{COMPLETE}) {
2360 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2361 for my $locale ( @locales ) {
2362 $locale_map{lc($locale->code)} = $locale->marc_code;
2364 $locale_map{COMPLETE} = 1;
2368 if (!$default_preferred_language) {
2370 $default_preferred_language = OpenSRF::Utils::SettingsClient
2373 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2378 if (!$default_preferred_language_weight) {
2380 $default_preferred_language_weight = OpenSRF::Utils::SettingsClient
2383 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2388 # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2389 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2391 my $ou = $args{org_unit};
2392 my $limit = $args{limit} || 10;
2393 my $offset = $args{offset} || 0;
2396 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2399 if (! scalar( keys %{$args{searches}} )) {
2400 die "No search arguments were passed to ".$self->api_name;
2403 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2405 if (!defined($args{preferred_language})) {
2406 my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2407 $args{preferred_language} =
2408 $locale_map{ lc($ses_locale) } || 'eng';
2411 if (!defined($args{preferred_language_weight})) {
2412 $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2415 if ($args{available}) {
2416 @statuses = (0,7,12);
2419 if (my $s = $args{locations}) {
2420 $s = [$s] if (!ref($s));
2424 if (my $b = $args{between}) {
2425 if (ref($b) && @$b == 2) {
2430 if (my $s = $args{statuses}) {
2431 $s = [$s] if (!ref($s));
2435 if (my $a = $args{audience}) {
2436 $a = [$a] if (!ref($a));
2440 if (my $l = $args{language}) {
2441 $l = [$l] if (!ref($l));
2445 if (my $f = $args{lit_form}) {
2446 $f = [$f] if (!ref($f));
2450 if (my $f = $args{item_form}) {
2451 $f = [$f] if (!ref($f));
2455 if (my $t = $args{item_type}) {
2456 $t = [$t] if (!ref($t));
2460 if (my $b = $args{bib_level}) {
2461 $b = [$b] if (!ref($b));
2465 if (my $v = $args{vr_format}) {
2466 $v = [$v] if (!ref($v));
2470 # XXX legacy format and item type support
2471 if ($args{format}) {
2472 my ($t, $f) = split '-', $args{format};
2473 @types = split '', $t;
2474 @forms = split '', $f;
2477 my %stored_proc_search_args;
2478 for my $search_group (sort keys %{$args{searches}}) {
2479 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2480 my ($search_class,$search_field) = split /\|/, $search_group;
2481 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2483 if ($search_field) {
2484 unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2485 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2490 my $class = $_cdbi->{$search_class};
2491 my $search_table = $class->table;
2493 my ($index_col) = $class->columns('FTS');
2494 $index_col ||= 'value';
2497 my $fts = OpenILS::Application::Storage::FTS->compile(
2498 $search_class => $args{searches}{$search_group}{term},
2499 $search_group_name.'.value',
2500 "$search_group_name.$index_col"
2502 $fts->sql_where_clause; # this builds the ranks for us
2504 my @fts_ranks = $fts->fts_rank;
2505 my @fts_queries = $fts->fts_query;
2506 my @phrases = map { lc($_) } $fts->phrases;
2507 my @words = map { lc($_) } $fts->words;
2509 $stored_proc_search_args{$search_group} = {
2510 fts_rank => \@fts_ranks,
2511 fts_query => \@fts_queries,
2512 phrase => \@phrases,
2518 my $param_search_ou = $ou;
2519 my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2520 my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2521 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2522 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2523 my $param_audience = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud ) . '}$$';
2524 my $param_language = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang ) . '}$$';
2525 my $param_lit_form = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2526 my $param_types = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types ) . '}$$';
2527 my $param_forms = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms ) . '}$$';
2528 my $param_vformats = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2529 my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2530 my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2531 my $param_after = $args{after} ; $param_after = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2532 my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2533 my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2534 my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2535 my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2536 my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2537 my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2538 my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2539 my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2540 my $param_rel_limit = $args{core_limit}; $param_rel_limit ||= 'NULL';
2541 my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2542 my $param_skip_chk = $args{skip_check}; $param_skip_chk ||= 'NULL';
2544 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2546 FROM search.staged_fts(
2547 $param_search_ou\:\:INT,
2548 $param_depth\:\:INT,
2549 $param_searches\:\:TEXT,
2550 $param_statuses\:\:INT[],
2551 $param_locations\:\:INT[],
2552 $param_audience\:\:TEXT[],
2553 $param_language\:\:TEXT[],
2554 $param_lit_form\:\:TEXT[],
2555 $param_types\:\:TEXT[],
2556 $param_forms\:\:TEXT[],
2557 $param_vformats\:\:TEXT[],
2558 $param_bib_level\:\:TEXT[],
2559 $param_before\:\:TEXT,
2560 $param_after\:\:TEXT,
2561 $param_during\:\:TEXT,
2562 $param_between\:\:TEXT[],
2563 $param_pref_lang\:\:TEXT,
2564 $param_pref_lang_multiplier\:\:REAL,
2565 $param_sort\:\:TEXT,
2566 $param_sort_desc\:\:BOOL,
2567 $metarecord\:\:BOOL,
2569 $param_rel_limit\:\:INT,
2570 $param_chk_limit\:\:INT,
2571 $param_skip_chk\:\:INT
2577 my $recs = $sth->fetchall_arrayref({});
2578 my $summary_row = pop @$recs;
2580 my $total = $$summary_row{total};
2581 my $checked = $$summary_row{checked};
2582 my $visible = $$summary_row{visible};
2583 my $deleted = $$summary_row{deleted};
2584 my $excluded = $$summary_row{excluded};
2586 my $estimate = $visible;
2587 if ( $total > $checked && $checked ) {
2589 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2590 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2594 delete $$summary_row{id};
2595 delete $$summary_row{rel};
2596 delete $$summary_row{record};
2598 $client->respond( $summary_row );
2600 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2602 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2603 delete $$rec{checked};
2604 delete $$rec{visible};
2605 delete $$rec{excluded};
2606 delete $$rec{deleted};
2607 delete $$rec{total};
2608 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2610 $client->respond( $rec );
2614 __PACKAGE__->register_method(
2615 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
2616 method => 'staged_fts',
2621 __PACKAGE__->register_method(
2622 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2623 method => 'staged_fts',
2628 __PACKAGE__->register_method(
2629 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
2630 method => 'staged_fts',
2635 __PACKAGE__->register_method(
2636 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2637 method => 'staged_fts',
2643 sub FTS_paging_estimate {
2647 my $checked = shift;
2648 my $visible = shift;
2649 my $excluded = shift;
2650 my $deleted = shift;
2653 my $deleted_ratio = $deleted / $checked;
2654 my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2656 my $exclusion_ratio = $excluded / $checked;
2657 my $delete_adjusted_exclusion_ratio = $excluded / ($checked - $deleted);
2659 my $inclusion_ratio = $visible / $checked;
2660 my $delete_adjusted_inclusion_ratio = $visible / ($checked - $deleted);
2663 exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2664 inclusion => int($delete_adjusted_total * $inclusion_ratio),
2665 delete_adjusted_exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2666 delete_adjusted_inclusion => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2669 __PACKAGE__->register_method(
2670 api_name => "open-ils.storage.fts_paging_estimate",
2671 method => 'FTS_paging_estimate',
2677 Hash of estimation values based on four variant estimation strategies:
2678 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2679 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2680 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2681 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2684 Helper method used to determin the approximate number of
2685 hits for a search that spans multiple superpages. For
2686 sparse superpages, the inclusion estimate will likely be the
2687 best estimate. The exclusion strategy is the original, but
2688 inclusion is the default.
2691 { name => 'checked',
2692 desc => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2695 { name => 'visible',
2696 desc => 'Number of records visible to the search location on the current superpage.',
2699 { name => 'excluded',
2700 desc => 'Number of records excluded from the search location on the current superpage.',
2703 { name => 'deleted',
2704 desc => 'Number of deleted records on the current superpage.',
2708 desc => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2721 my $term = $$args{term};
2722 my $limit = $$args{max} || 1;
2723 my $min = $$args{min} || 1;
2724 my @classes = @{$$args{class}};
2726 $limit = $min if ($min > $limit);
2729 @classes = ( qw/ title author subject series keyword / );
2733 my $bre_table = biblio::record_entry->table;
2734 my $cn_table = asset::call_number->table;
2735 my $cp_table = asset::copy->table;
2737 for my $search_class ( @classes ) {
2739 my $class = $_cdbi->{$search_class};
2740 my $search_table = $class->table;
2742 my ($index_col) = $class->columns('FTS');
2743 $index_col ||= 'value';
2746 my $where = OpenILS::Application::Storage::FTS
2747 ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2751 SELECT COUNT(DISTINCT X.source)
2752 FROM (SELECT $search_class.source
2753 FROM $search_table $search_class
2754 JOIN $bre_table b ON (b.id = $search_class.source)
2759 HAVING COUNT(DISTINCT X.source) >= $min;
2762 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2763 $matches{$search_class} = $res ? $res->[0] : 0;
2768 __PACKAGE__->register_method(
2769 api_name => "open-ils.storage.search.xref",
2770 method => 'xref_count',
2774 sub query_parser_fts {
2780 # grab the query parser and initialize it
2781 my $parser = $OpenILS::Application::Storage::QParser;
2784 if (!$parser->initialization_complete) {
2785 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
2786 $parser->initialize(
2787 config_record_attr_index_norm_map =>
2789 'open-ils.cstore.direct.config.record_attr_index_norm_map.search.atomic',
2790 { id => { "!=" => undef } },
2791 { flesh => 1, flesh_fields => { crainm => [qw/norm/] }, order_by => [{ class => "crainm", field => "pos" }] }
2793 search_relevance_adjustment =>
2795 'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
2796 { id => { "!=" => undef } }
2798 config_metabib_field =>
2800 'open-ils.cstore.direct.config.metabib_field.search.atomic',
2801 { id => { "!=" => undef } }
2803 config_metabib_search_alias =>
2805 'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
2806 { alias => { "!=" => undef } }
2808 config_metabib_field_index_norm_map =>
2810 'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
2811 { id => { "!=" => undef } },
2812 { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
2814 config_record_attr_definition =>
2816 'open-ils.cstore.direct.config.record_attr_definition.search.atomic',
2817 { name => { "!=" => undef } }
2821 $cstore->disconnect;
2822 die("Cannot initialize $parser!") unless ($parser->initialization_complete);
2826 # populate the locale/language map
2827 if (!$locale_map{COMPLETE}) {
2829 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2830 for my $locale ( @locales ) {
2831 $locale_map{lc($locale->code)} = $locale->marc_code;
2833 $locale_map{COMPLETE} = 1;
2837 # I hope we have a query!
2838 if (! $args{query} ) {
2839 die "No query was passed to ".$self->api_name;
2843 my $simple_plan = $args{_simple_plan};
2844 # remove bad chunks of the %args hash
2845 for my $bad ( grep { /^_/ } keys(%args)) {
2846 delete($args{$bad});
2850 # parse the query and supply any query-level %arg-based defaults
2851 # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2852 my $query = $parser->new( %args )->parse;
2855 # set the locale-based default prefered location
2856 if (!$query->parse_tree->find_filter('preferred_language')) {
2857 $parser->default_preferred_language( $args{preferred_language} );
2858 if (!$parser->default_preferred_language) {
2859 my $ses_locale = $client->session ? $client->session->session_locale : '';
2860 $parser->default_preferred_language( $locale_map{ lc($ses_locale) } );
2862 $parser->default_preferred_language(
2863 OpenSRF::Utils::SettingsClient->new->config_value(
2864 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2866 ) if (!$parser->default_preferred_language);
2870 # set the global default language multiplier
2871 if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2872 $parser->default_preferred_language_multiplier($args{preferred_language_weight});
2873 $parser->default_preferred_language_multiplier($args{preferred_language_multiplier});
2874 $parser->default_preferred_language_multiplier(
2875 OpenSRF::Utils::SettingsClient->new->config_value(
2876 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2878 ) if (!$parser->default_preferred_language_multiplier);
2881 # gather the site, if one is specified, defaulting to the in-query version
2882 my $ou = $args{org_unit};
2883 if (my ($filter) = $query->parse_tree->find_filter('site')) {
2884 $ou = $filter->args->[0] if (@{$filter->args});
2886 $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^\d+$/);
2889 # gather lasso, as with $ou
2890 my $lasso = $args{lasso};
2891 if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
2892 $lasso = $filter->args->[0] if (@{$filter->args});
2894 $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
2895 $lasso = -$lasso if ($lasso);
2898 # # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
2899 # # gather user lasso, as with $ou and lasso
2900 # my $mylasso = $args{my_lasso};
2901 # if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
2902 # $mylasso = $filter->args->[0] if (@{$filter->args});
2904 # $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
2907 # if we have a lasso, go with that, otherwise ... ou
2908 $ou = $lasso if ($lasso);
2911 # get the default $ou if we have nothing
2912 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
2915 # 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
2916 # gather the depth, if one is specified, defaulting to the in-query version
2917 my $depth = $args{depth};
2918 if (my ($filter) = $query->parse_tree->find_filter('depth')) {
2919 $depth = $filter->args->[0] if (@{$filter->args});
2921 $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
2924 # gather the limit or default to 10
2925 my $limit = $args{check_limit} || 'NULL';
2926 if (my ($filter) = $query->parse_tree->find_filter('limit')) {
2927 $limit = $filter->args->[0] if (@{$filter->args});
2929 if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
2930 $limit = $filter->args->[0] if (@{$filter->args});
2934 # gather the offset or default to 0
2935 my $offset = $args{skip_check} || $args{offset} || 0;
2936 if (my ($filter) = $query->parse_tree->find_filter('offset')) {
2937 $offset = $filter->args->[0] if (@{$filter->args});
2939 if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
2940 $offset = $filter->args->[0] if (@{$filter->args});
2944 # gather the estimation strategy or default to inclusion
2945 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2946 if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
2947 $estimation_strategy = $filter->args->[0] if (@{$filter->args});
2951 # gather the estimation strategy or default to inclusion
2952 my $core_limit = $args{core_limit};
2953 if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
2954 $core_limit = $filter->args->[0] if (@{$filter->args});
2958 # gather statuses, and then forget those if we have an #available modifier
2960 if (my ($filter) = $query->parse_tree->find_filter('statuses')) {
2961 @statuses = @{$filter->args} if (@{$filter->args});
2963 @statuses = (0,7,12) if ($query->parse_tree->find_modifier('available'));
2968 if (my ($filter) = $query->parse_tree->find_filter('locations')) {
2969 @location = @{$filter->args} if (@{$filter->args});
2973 my $param_check = $limit || $query->superpage_size || 'NULL';
2974 my $param_offset = $offset || 'NULL';
2975 my $param_limit = $core_limit || 'NULL';
2977 my $sp = $query->superpage || 1;
2979 $param_offset = ($sp - 1) * $sp_size;
2982 my $param_search_ou = $ou;
2983 my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
2984 my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
2985 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
2986 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
2987 my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
2988 my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
2990 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2991 SELECT * /* bib search */
2992 FROM search.query_parser_fts(
2993 $param_search_ou\:\:INT,
2994 $param_depth\:\:INT,
2995 $param_core_query\:\:TEXT,
2996 $param_statuses\:\:INT[],
2997 $param_locations\:\:INT[],
2998 $param_offset\:\:INT,
2999 $param_check\:\:INT,
3000 $param_limit\:\:INT,
3001 $metarecord\:\:BOOL,
3008 my $recs = $sth->fetchall_arrayref({});
3009 my $summary_row = pop @$recs;
3011 my $total = $$summary_row{total};
3012 my $checked = $$summary_row{checked};
3013 my $visible = $$summary_row{visible};
3014 my $deleted = $$summary_row{deleted};
3015 my $excluded = $$summary_row{excluded};
3017 my $estimate = $visible;
3018 if ( $total > $checked && $checked ) {
3020 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
3021 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
3025 delete $$summary_row{id};
3026 delete $$summary_row{rel};
3027 delete $$summary_row{record};
3029 if (defined($simple_plan)) {
3030 $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3032 $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3035 $client->respond( $summary_row );
3037 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
3039 for my $rec (@$recs) {
3040 delete $$rec{checked};
3041 delete $$rec{visible};
3042 delete $$rec{excluded};
3043 delete $$rec{deleted};
3044 delete $$rec{total};
3045 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3047 $client->respond( $rec );
3051 __PACKAGE__->register_method(
3052 api_name => "open-ils.storage.query_parser_search",
3053 method => 'query_parser_fts',
3059 sub query_parser_fts_wrapper {
3064 $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3065 # grab the query parser and initialize it
3066 my $parser = $OpenILS::Application::Storage::QParser;
3069 if (!$parser->initialization_complete) {
3070 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3071 $parser->initialize(
3072 config_record_attr_index_norm_map =>
3074 'open-ils.cstore.direct.config.record_attr_index_norm_map.search.atomic',
3075 { id => { "!=" => undef } },
3076 { flesh => 1, flesh_fields => { crainm => [qw/norm/] }, order_by => [{ class => "crainm", field => "pos" }] }
3078 search_relevance_adjustment =>
3080 'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
3081 { id => { "!=" => undef } }
3083 config_metabib_field =>
3085 'open-ils.cstore.direct.config.metabib_field.search.atomic',
3086 { id => { "!=" => undef } }
3088 config_metabib_search_alias =>
3090 'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
3091 { alias => { "!=" => undef } }
3093 config_metabib_field_index_norm_map =>
3095 'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
3096 { id => { "!=" => undef } },
3097 { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
3099 config_record_attr_definition =>
3101 'open-ils.cstore.direct.config.record_attr_definition.search.atomic',
3102 { name => { "!=" => undef } }
3106 $cstore->disconnect;
3107 die("Cannot initialize $parser!") unless ($parser->initialization_complete);
3110 if (! scalar( keys %{$args{searches}} )) {
3111 die "No search arguments were passed to ".$self->api_name;
3114 $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3115 my $base_query = '';
3116 for my $sclass ( keys %{$args{searches}} ) {
3117 $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3118 $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3121 my $query = $base_query;
3122 $log->debug("Full base query: $base_query", DEBUG);
3124 $query = "$args{facets} $query" if ($args{facets});
3126 if (!$locale_map{COMPLETE}) {
3128 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3129 for my $locale ( @locales ) {
3130 $locale_map{lc($locale->code)} = $locale->marc_code;
3132 $locale_map{COMPLETE} = 1;
3136 my $base_plan = $parser->new( query => $base_query )->parse;
3138 $query = "preferred_language($args{preferred_language}) $query"
3139 if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3140 $query = "preferred_language_weight($args{preferred_language_weight}) $query"
3141 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'));
3143 $query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
3144 $query = "site($args{org_unit}) $query" if ($args{org_unit});
3145 $query = "depth($args{depth}) $query" if (defined($args{depth}));
3146 $query = "sort($args{sort}) $query" if ($args{sort});
3147 $query = "limit($args{limit}) $query" if ($args{limit});
3148 $query = "core_limit($args{core_limit}) $query" if ($args{core_limit});
3149 $query = "skip_check($args{skip_check}) $query" if ($args{skip_check});
3150 $query = "superpage($args{superpage}) $query" if ($args{superpage});
3151 $query = "offset($args{offset}) $query" if ($args{offset});
3152 $query = "#metarecord $query" if ($self->api_name =~ /metabib/);
3153 $query = "#available $query" if ($args{available});
3154 $query = "#descending $query" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3155 $query = "#staff $query" if ($self->api_name =~ /staff/);
3156 $query = "before($args{before}) $query" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3157 $query = "after($args{after}) $query" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3158 $query = "during($args{during}) $query" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3159 $query = "between($args{between}[0],$args{between}[1]) $query"
3160 if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3163 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3165 # XXX legacy format and item type support
3166 if ($args{format}) {
3167 my ($t, $f) = split '-', $args{format};
3168 $args{item_type} = [ split '', $t ];
3169 $args{item_form} = [ split '', $f ];
3172 for my $filter ( qw/locations statuses between audience language lit_form item_form item_type bib_level vr_format/ ) {
3173 if (my $s = $args{$filter}) {
3174 $s = [$s] if (!ref($s));
3176 my @filter_list = @$s;
3178 next if ($filter eq 'between' and scalar(@filter_list) != 2);
3179 next if (@filter_list == 0);
3181 my $filter_string = join ',', @filter_list;
3182 $query = "$filter($filter_string) $query";
3186 $log->debug("Full QueryParser query: $query", DEBUG);
3188 return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan );
3190 __PACKAGE__->register_method(
3191 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
3192 method => 'query_parser_fts_wrapper',
3197 __PACKAGE__->register_method(
3198 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3199 method => 'query_parser_fts_wrapper',
3204 __PACKAGE__->register_method(
3205 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
3206 method => 'query_parser_fts_wrapper',
3211 __PACKAGE__->register_method(
3212 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3213 method => 'query_parser_fts_wrapper',