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/;
13 use OpenILS::Application::Storage::QueryParser;
15 my $log = 'OpenSRF::Utils::Logger';
19 sub _initialize_parser {
22 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
24 config_record_attr_index_norm_map =>
26 'open-ils.cstore.direct.config.record_attr_index_norm_map.search.atomic',
27 { id => { "!=" => undef } },
28 { flesh => 1, flesh_fields => { crainm => [qw/norm/] }, order_by => [{ class => "crainm", field => "pos" }] }
30 search_relevance_adjustment =>
32 'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
33 { id => { "!=" => undef } }
35 config_metabib_field =>
37 'open-ils.cstore.direct.config.metabib_field.search.atomic',
38 { id => { "!=" => undef } }
40 config_metabib_search_alias =>
42 'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
43 { alias => { "!=" => undef } }
45 config_metabib_field_index_norm_map =>
47 'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
48 { id => { "!=" => undef } },
49 { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
51 config_record_attr_definition =>
53 'open-ils.cstore.direct.config.record_attr_definition.search.atomic',
54 { name => { "!=" => undef } }
56 config_metabib_class_ts_map =>
58 'open-ils.cstore.direct.config.metabib_class_ts_map.search.atomic',
61 config_metabib_field_ts_map =>
63 'open-ils.cstore.direct.config.metabib_field_ts_map.search.atomic',
66 config_metabib_class =>
68 'open-ils.cstore.direct.config.metabib_class.search.atomic',
69 { name => { "!=" => undef } }
74 die("Cannot initialize $parser!") unless ($parser->initialization_complete);
77 sub ordered_records_from_metarecord { # XXX Replace with QP-based search-within-MR
81 my $formats = shift; # dead
85 my $copies_visible = 'LEFT JOIN asset.opac_visible_copies vc ON (br.id = vc.record)';
86 $copies_visible = '' if ($self->api_name =~ /staff/o);
88 my $copies_visible_count = ',COUNT(vc.id)';
89 $copies_visible_count = '' if ($self->api_name =~ /staff/o);
93 $descendants = defined($depth) ?
94 ",actor.org_unit_descendants($org, $depth) d" :
95 ",actor.org_unit_descendants($org) d" ;
102 $copies_visible_count
103 FROM metabib.metarecord_source_map sm
104 JOIN biblio.record_entry br ON (sm.source = br.id AND NOT br.deleted)
105 LEFT JOIN metabib.record_sorter s ON (s.source = br.id AND s.attr = 'titlesort')
106 LEFT JOIN config.bib_source bs ON (br.source = bs.id)
109 WHERE sm.metarecord = ?
113 if ($copies_visible) {
114 $sql .= 'AND (bs.transcendant OR ';
116 $sql .= 'vc.circ_lib = d.id)';
118 $sql .= 'vc.id IS NOT NULL)'
120 $having = 'HAVING COUNT(vc.id) > 0';
128 s.value ASC NULLS LAST
131 my $ids = metabib::metarecord_source_map->db_Main->selectcol_arrayref($sql, {}, "$mr");
132 return $ids if ($self->api_name =~ /atomic$/o);
134 $client->respond( $_ ) for ( @$ids );
138 __PACKAGE__->register_method(
139 api_name => 'open-ils.storage.ordered.metabib.metarecord.records',
140 method => 'ordered_records_from_metarecord',
144 __PACKAGE__->register_method(
145 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff',
146 method => 'ordered_records_from_metarecord',
151 __PACKAGE__->register_method(
152 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.atomic',
153 method => 'ordered_records_from_metarecord',
157 __PACKAGE__->register_method(
158 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic',
159 method => 'ordered_records_from_metarecord',
164 # XXX: this subroutine and its two registered methods are marked for
165 # deprecation, as they do not work properly in 2.x (these tags are no longer
166 # normalized in mfr) and are not in known use
170 my $isxn = lc(shift());
174 $isxn =~ s/-//o if ($self->api_name =~ /isbn/o);
176 my $tag = ($self->api_name =~ /isbn/o) ? "'020' OR f.tag = '024'" : "'022'";
178 my $fr_table = metabib::full_rec->table;
179 my $bib_table = biblio::record_entry->table;
182 SELECT DISTINCT f.record
184 JOIN $bib_table b ON (b.id = f.record)
187 AND b.deleted IS FALSE
190 my $list = metabib::full_rec->db_Main->selectcol_arrayref($sql, {}, "$isxn%");
191 $client->respond($_) for (@$list);
194 __PACKAGE__->register_method(
195 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
196 method => 'isxn_search',
200 __PACKAGE__->register_method(
201 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.issn',
202 method => 'isxn_search',
207 sub metarecord_copy_count {
213 my $sm_table = metabib::metarecord_source_map->table;
214 my $rd_table = metabib::record_descriptor->table;
215 my $cn_table = asset::call_number->table;
216 my $cp_table = asset::copy->table;
217 my $br_table = biblio::record_entry->table;
218 my $src_table = config::bib_source->table;
219 my $cl_table = asset::copy_location->table;
220 my $cs_table = config::copy_status->table;
221 my $out_table = actor::org_unit_type->table;
223 my $descendants = "actor.org_unit_descendants(u.id)";
224 my $ancestors = "actor.org_unit_ancestors(?) u JOIN $out_table t ON (u.ou_type = t.id)";
226 if ($args{org_unit} < 0) {
227 $args{org_unit} *= -1;
228 $ancestors = "(select org_unit as id from actor.org_lasso_map where lasso = ?) u CROSS JOIN (SELECT -1 AS depth) t";
231 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';
232 $copies_visible = '' if ($self->api_name =~ /staff/o);
234 my (@types,@forms,@blvl);
235 my ($t_filter, $f_filter, $b_filter) = ('','','');
238 my ($t, $f, $b) = split '-', $args{format};
239 @types = split '', $t;
240 @forms = split '', $f;
241 @blvl = split '', $b;
244 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
248 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
252 $b_filter .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
262 JOIN $cn_table cn ON (cn.record = r.source)
263 JOIN $rd_table rd ON (cn.record = rd.record)
264 JOIN $cp_table cp ON (cn.id = cp.call_number)
265 JOIN $cs_table cs ON (cp.status = cs.id)
266 JOIN $cl_table cl ON (cp.location = cl.id)
267 JOIN $descendants a ON (cp.circ_lib = a.id)
268 WHERE r.metarecord = ?
269 AND cn.deleted IS FALSE
270 AND cp.deleted IS FALSE
280 JOIN $cn_table cn ON (cn.record = r.source)
281 JOIN $rd_table rd ON (cn.record = rd.record)
282 JOIN $cp_table cp ON (cn.id = cp.call_number)
283 JOIN $cs_table cs ON (cp.status = cs.id)
284 JOIN $cl_table cl ON (cp.location = cl.id)
285 JOIN $descendants a ON (cp.circ_lib = a.id)
286 WHERE r.metarecord = ?
287 AND cp.status IN (0,7,12)
288 AND cn.deleted IS FALSE
289 AND cp.deleted IS FALSE
299 JOIN $cn_table cn ON (cn.record = r.source)
300 JOIN $rd_table rd ON (cn.record = rd.record)
301 JOIN $cp_table cp ON (cn.id = cp.call_number)
302 JOIN $cs_table cs ON (cp.status = cs.id)
303 JOIN $cl_table cl ON (cp.location = cl.id)
304 WHERE r.metarecord = ?
305 AND cn.deleted IS FALSE
306 AND cp.deleted IS FALSE
307 AND cp.opac_visible IS TRUE
308 AND cs.opac_visible IS TRUE
309 AND cl.opac_visible IS TRUE
318 JOIN $br_table br ON (br.id = r.source)
319 JOIN $src_table src ON (src.id = br.source)
320 WHERE r.metarecord = ?
321 AND src.transcendant IS TRUE
329 my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
330 $sth->execute( ''.$args{metarecord},
334 ''.$args{metarecord},
338 ''.$args{metarecord},
342 ''.$args{metarecord},
346 while ( my $row = $sth->fetchrow_hashref ) {
347 $client->respond( $row );
351 __PACKAGE__->register_method(
352 api_name => 'open-ils.storage.metabib.metarecord.copy_count',
353 method => 'metarecord_copy_count',
358 __PACKAGE__->register_method(
359 api_name => 'open-ils.storage.metabib.metarecord.copy_count.staff',
360 method => 'metarecord_copy_count',
366 sub biblio_multi_search_full_rec {
371 my $class_join = $args{class_join} || 'AND';
372 my $limit = $args{limit} || 100;
373 my $offset = $args{offset} || 0;
374 my $sort = $args{'sort'};
375 my $sort_dir = $args{sort_dir} || 'DESC';
380 for my $arg (@{ $args{searches} }) {
381 my $term = $$arg{term};
382 my $limiters = $$arg{restrict};
384 my ($index_col) = metabib::full_rec->columns('FTS');
385 $index_col ||= 'value';
386 my $search_table = metabib::full_rec->table;
388 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
390 my $fts_where = $fts->sql_where_clause();
391 my @fts_ranks = $fts->fts_rank;
393 my $rank = join(' + ', @fts_ranks);
396 for my $limit (@$limiters) {
397 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
398 # MARC control field; mfr.subfield is NULL
399 push @wheres, "( tag = ? AND $fts_where )";
400 push @binds, $$limit{tag};
401 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
403 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
404 push @binds, $$limit{tag}, $$limit{subfield};
405 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
408 my $where = join(' OR ', @wheres);
410 push @selects, "SELECT record, AVG($rank) as sum FROM $search_table WHERE $where GROUP BY record";
414 my $descendants = defined($args{depth}) ?
415 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
416 "actor.org_unit_descendants($args{org_unit})" ;
419 my $metabib_record_descriptor = metabib::record_descriptor->table;
420 my $metabib_full_rec = metabib::full_rec->table;
421 my $asset_call_number_table = asset::call_number->table;
422 my $asset_copy_table = asset::copy->table;
423 my $cs_table = config::copy_status->table;
424 my $cl_table = asset::copy_location->table;
425 my $br_table = biblio::record_entry->table;
427 my $cj = 'HAVING COUNT(x.record) = ' . scalar(@selects) if ($class_join eq 'AND');
429 '(SELECT x.record, sum(x.sum) FROM (('.
430 join(') UNION ALL (', @selects).
431 ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
433 my $has_vols = 'AND cn.owning_lib = d.id';
434 my $has_copies = 'AND cp.call_number = cn.id';
435 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';
437 if ($self->api_name =~ /staff/o) {
438 $copies_visible = '';
439 $has_copies = '' if ($ou_type == 0);
440 $has_vols = '' if ($ou_type == 0);
443 my ($t_filter, $f_filter) = ('','');
444 my ($a_filter, $l_filter, $lf_filter) = ('','','');
447 if (my $a = $args{audience}) {
448 $a = [$a] if (!ref($a));
451 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
456 if (my $l = $args{language}) {
457 $l = [$l] if (!ref($l));
460 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
465 if (my $f = $args{lit_form}) {
466 $f = [$f] if (!ref($f));
469 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
470 push @binds, @lit_form;
474 if (my $f = $args{item_form}) {
475 $f = [$f] if (!ref($f));
478 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
483 if (my $t = $args{item_type}) {
484 $t = [$t] if (!ref($t));
487 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
494 my ($t, $f) = split '-', $args{format};
495 my @types = split '', $t;
496 my @forms = split '', $f;
498 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
503 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
506 push @binds, @types, @forms;
509 my $relevance = 'sum(f.sum)';
510 $relevance = 1 if (!$copies_visible);
512 my $rank = $relevance;
513 if (lc($sort) eq 'pubdate') {
516 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'9999')::INT
517 FROM $metabib_full_rec frp
518 WHERE frp.record = f.record
520 AND frp.subfield = 'c'
524 } elsif (lc($sort) eq 'create_date') {
526 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
528 } elsif (lc($sort) eq 'edit_date') {
530 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
532 } elsif (lc($sort) eq 'title') {
535 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'zzzzzzzz')
536 FROM $metabib_full_rec frt
537 WHERE frt.record = f.record
539 AND frt.subfield = 'a'
543 } elsif (lc($sort) eq 'author') {
546 SELECT COALESCE(LTRIM(fra.value),'zzzzzzzz')
547 FROM $metabib_full_rec fra
548 WHERE fra.record = f.record
549 AND fra.tag LIKE '1%'
550 AND fra.subfield = 'a'
551 ORDER BY fra.tag::text::int
559 my $rd_join = $use_rd ? "$metabib_record_descriptor rd," : '';
560 my $rd_filter = $use_rd ? 'AND rd.record = f.record' : '';
562 if ($copies_visible) {
564 SELECT f.record, $relevance, count(DISTINCT cp.id), $rank
565 FROM $search_table f,
566 $asset_call_number_table cn,
567 $asset_copy_table cp,
573 WHERE br.id = f.record
574 AND cn.record = f.record
575 AND cp.status = cs.id
576 AND cp.location = cl.id
577 AND br.deleted IS FALSE
578 AND cn.deleted IS FALSE
579 AND cp.deleted IS FALSE
589 GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
590 ORDER BY 4 $sort_dir,3 DESC
594 SELECT f.record, 1, 1, $rank
595 FROM $search_table f,
598 WHERE br.id = f.record
599 AND br.deleted IS FALSE
612 $log->debug("Search SQL :: [$select]",DEBUG);
614 my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
615 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
618 $max = 1 if (!@$recs);
620 $max = $$_[1] if ($$_[1] > $max);
624 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
625 next unless ($$rec[0]);
626 my ($rid,$rank,$junk,$skip) = @$rec;
627 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
631 __PACKAGE__->register_method(
632 api_name => 'open-ils.storage.biblio.full_rec.multi_search',
633 method => 'biblio_multi_search_full_rec',
638 __PACKAGE__->register_method(
639 api_name => 'open-ils.storage.biblio.full_rec.multi_search.staff',
640 method => 'biblio_multi_search_full_rec',
646 sub search_full_rec {
652 my $term = $args{term};
653 my $limiters = $args{restrict};
655 my ($index_col) = metabib::full_rec->columns('FTS');
656 $index_col ||= 'value';
657 my $search_table = metabib::full_rec->table;
659 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
661 my $fts_where = $fts->sql_where_clause();
662 my @fts_ranks = $fts->fts_rank;
664 my $rank = join(' + ', @fts_ranks);
668 for my $limit (@$limiters) {
669 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
670 # MARC control field; mfr.subfield is NULL
671 push @wheres, "( tag = ? AND $fts_where )";
672 push @binds, $$limit{tag};
673 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
675 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
676 push @binds, $$limit{tag}, $$limit{subfield};
677 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
680 my $where = join(' OR ', @wheres);
682 my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
684 $log->debug("Search SQL :: [$select]",DEBUG);
686 my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
687 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
689 $client->respond($_) for (@$recs);
692 __PACKAGE__->register_method(
693 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
694 method => 'search_full_rec',
699 __PACKAGE__->register_method(
700 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
701 method => 'search_full_rec',
708 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
709 sub search_class_fts {
714 my $term = $args{term};
715 my $ou = $args{org_unit};
716 my $ou_type = $args{depth};
717 my $limit = $args{limit};
718 my $offset = $args{offset};
720 my $limit_clause = '';
721 my $offset_clause = '';
723 $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
724 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
727 my ($t_filter, $f_filter) = ('','');
730 my ($t, $f) = split '-', $args{format};
731 @types = split '', $t;
732 @forms = split '', $f;
734 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
738 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
744 my $descendants = defined($ou_type) ?
745 "actor.org_unit_descendants($ou, $ou_type)" :
746 "actor.org_unit_descendants($ou)";
748 my $class = $self->{cdbi};
749 my $search_table = $class->table;
751 my $metabib_record_descriptor = metabib::record_descriptor->table;
752 my $metabib_metarecord = metabib::metarecord->table;
753 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
754 my $asset_call_number_table = asset::call_number->table;
755 my $asset_copy_table = asset::copy->table;
756 my $cs_table = config::copy_status->table;
757 my $cl_table = asset::copy_location->table;
759 my ($index_col) = $class->columns('FTS');
760 $index_col ||= 'value';
762 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
763 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
765 my $fts_where = $fts->sql_where_clause;
766 my @fts_ranks = $fts->fts_rank;
768 my $rank = join(' + ', @fts_ranks);
770 my $has_vols = 'AND cn.owning_lib = d.id';
771 my $has_copies = 'AND cp.call_number = cn.id';
772 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';
774 my $visible_count = ', count(DISTINCT cp.id)';
775 my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
777 if ($self->api_name =~ /staff/o) {
778 $copies_visible = '';
779 $visible_count_test = '';
780 $has_copies = '' if ($ou_type == 0);
781 $has_vols = '' if ($ou_type == 0);
784 my $rank_calc = <<" RANK";
786 * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
787 * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
788 * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
789 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
792 $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
794 if ($copies_visible) {
796 SELECT m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
797 FROM $search_table f,
798 $metabib_metarecord_source_map_table m,
799 $asset_call_number_table cn,
800 $asset_copy_table cp,
803 $metabib_record_descriptor rd,
806 AND m.source = f.source
807 AND cn.record = m.source
808 AND rd.record = m.source
809 AND cp.status = cs.id
810 AND cp.location = cl.id
816 GROUP BY 1 $visible_count_test
818 $limit_clause $offset_clause
822 SELECT m.metarecord $rank_calc, 0, 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 $metabib_record_descriptor rd
827 AND m.source = f.source
828 AND rd.record = m.source
833 $limit_clause $offset_clause
837 $log->debug("Field Search SQL :: [$select]",DEBUG);
839 my $SQLstring = join('%',$fts->words);
840 my $REstring = join('\\s+',$fts->words);
841 my $first_word = ($fts->words)[0].'%';
842 my $recs = ($self->api_name =~ /unordered/o) ?
843 $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
844 $class->db_Main->selectall_arrayref($select, {},
845 '%'.lc($SQLstring).'%', # phrase order match
846 lc($first_word), # first word match
847 '^\\s*'.lc($REstring).'\\s*/?\s*$', # full exact match
851 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
853 $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
857 for my $class ( qw/title author subject keyword series identifier/ ) {
858 __PACKAGE__->register_method(
859 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord",
860 method => 'search_class_fts',
863 cdbi => "metabib::${class}_field_entry",
866 __PACKAGE__->register_method(
867 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
868 method => 'search_class_fts',
871 cdbi => "metabib::${class}_field_entry",
874 __PACKAGE__->register_method(
875 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
876 method => 'search_class_fts',
879 cdbi => "metabib::${class}_field_entry",
882 __PACKAGE__->register_method(
883 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
884 method => 'search_class_fts',
887 cdbi => "metabib::${class}_field_entry",
892 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
893 sub search_class_fts_count {
898 my $term = $args{term};
899 my $ou = $args{org_unit};
900 my $ou_type = $args{depth};
901 my $limit = $args{limit} || 100;
902 my $offset = $args{offset} || 0;
904 my $descendants = defined($ou_type) ?
905 "actor.org_unit_descendants($ou, $ou_type)" :
906 "actor.org_unit_descendants($ou)";
909 my ($t_filter, $f_filter) = ('','');
912 my ($t, $f) = split '-', $args{format};
913 @types = split '', $t;
914 @forms = split '', $f;
916 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
920 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
925 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
927 my $class = $self->{cdbi};
928 my $search_table = $class->table;
930 my $metabib_record_descriptor = metabib::record_descriptor->table;
931 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
932 my $asset_call_number_table = asset::call_number->table;
933 my $asset_copy_table = asset::copy->table;
934 my $cs_table = config::copy_status->table;
935 my $cl_table = asset::copy_location->table;
937 my ($index_col) = $class->columns('FTS');
938 $index_col ||= 'value';
940 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
942 my $fts_where = $fts->sql_where_clause;
944 my $has_vols = 'AND cn.owning_lib = d.id';
945 my $has_copies = 'AND cp.call_number = cn.id';
946 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';
947 if ($self->api_name =~ /staff/o) {
948 $copies_visible = '';
949 $has_vols = '' if ($ou_type == 0);
950 $has_copies = '' if ($ou_type == 0);
953 # XXX test an "EXISTS version of descendant checking...
955 if ($copies_visible) {
957 SELECT count(distinct m.metarecord)
958 FROM $search_table f,
959 $metabib_metarecord_source_map_table m,
960 $metabib_metarecord_source_map_table mr,
961 $asset_call_number_table cn,
962 $asset_copy_table cp,
965 $metabib_record_descriptor rd,
968 AND mr.source = f.source
969 AND mr.metarecord = m.metarecord
970 AND cn.record = m.source
971 AND rd.record = m.source
972 AND cp.status = cs.id
973 AND cp.location = cl.id
982 SELECT count(distinct m.metarecord)
983 FROM $search_table f,
984 $metabib_metarecord_source_map_table m,
985 $metabib_metarecord_source_map_table mr,
986 $metabib_record_descriptor rd
988 AND mr.source = f.source
989 AND mr.metarecord = m.metarecord
990 AND rd.record = m.source
996 $log->debug("Field Search Count SQL :: [$select]",DEBUG);
998 my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1000 $log->debug("Count Search yielded $recs results.",DEBUG);
1005 for my $class ( qw/title author subject keyword series identifier/ ) {
1006 __PACKAGE__->register_method(
1007 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
1008 method => 'search_class_fts_count',
1011 cdbi => "metabib::${class}_field_entry",
1014 __PACKAGE__->register_method(
1015 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
1016 method => 'search_class_fts_count',
1019 cdbi => "metabib::${class}_field_entry",
1025 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1026 sub postfilter_search_class_fts {
1031 my $term = $args{term};
1032 my $sort = $args{'sort'};
1033 my $sort_dir = $args{sort_dir} || 'DESC';
1034 my $ou = $args{org_unit};
1035 my $ou_type = $args{depth};
1036 my $limit = $args{limit} || 10;
1037 my $visibility_limit = $args{visibility_limit} || 5000;
1038 my $offset = $args{offset} || 0;
1040 my $outer_limit = 1000;
1042 my $limit_clause = '';
1043 my $offset_clause = '';
1045 $limit_clause = "LIMIT $outer_limit";
1046 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1048 my (@types,@forms,@lang,@aud,@lit_form);
1049 my ($t_filter, $f_filter) = ('','');
1050 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1051 my ($ot_filter, $of_filter) = ('','');
1052 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1054 if (my $a = $args{audience}) {
1055 $a = [$a] if (!ref($a));
1058 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1059 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1062 if (my $l = $args{language}) {
1063 $l = [$l] if (!ref($l));
1066 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1067 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1070 if (my $f = $args{lit_form}) {
1071 $f = [$f] if (!ref($f));
1074 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1075 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1078 if ($args{format}) {
1079 my ($t, $f) = split '-', $args{format};
1080 @types = split '', $t;
1081 @forms = split '', $f;
1083 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1084 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1088 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1089 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1094 my $descendants = defined($ou_type) ?
1095 "actor.org_unit_descendants($ou, $ou_type)" :
1096 "actor.org_unit_descendants($ou)";
1098 my $class = $self->{cdbi};
1099 my $search_table = $class->table;
1101 my $metabib_full_rec = metabib::full_rec->table;
1102 my $metabib_record_descriptor = metabib::record_descriptor->table;
1103 my $metabib_metarecord = metabib::metarecord->table;
1104 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1105 my $asset_call_number_table = asset::call_number->table;
1106 my $asset_copy_table = asset::copy->table;
1107 my $cs_table = config::copy_status->table;
1108 my $cl_table = asset::copy_location->table;
1109 my $br_table = biblio::record_entry->table;
1111 my ($index_col) = $class->columns('FTS');
1112 $index_col ||= 'value';
1114 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1116 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1118 my $SQLstring = join('%',map { lc($_) } $fts->words);
1119 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1120 my $first_word = lc(($fts->words)[0]).'%';
1122 my $fts_where = $fts->sql_where_clause;
1123 my @fts_ranks = $fts->fts_rank;
1126 $bonus{'metabib::identifier_field_entry'} =
1127 $bonus{'metabib::keyword_field_entry'} = [
1128 { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring }
1131 $bonus{'metabib::title_field_entry'} =
1132 $bonus{'metabib::series_field_entry'} = [
1133 { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1134 { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1135 @{ $bonus{'metabib::keyword_field_entry'} }
1138 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1139 $bonus_list ||= '1';
1141 my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1143 my $relevance = join(' + ', @fts_ranks);
1144 $relevance = <<" RANK";
1145 (SUM( ( $relevance ) * ( $bonus_list ) )/COUNT(m.source))
1148 my $string_default_sort = 'zzzz';
1149 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1151 my $number_default_sort = '9999';
1152 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1154 my $rank = $relevance;
1155 if (lc($sort) eq 'pubdate') {
1158 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1159 FROM $metabib_full_rec frp
1160 WHERE frp.record = mr.master_record
1162 AND frp.subfield = 'c'
1166 } elsif (lc($sort) eq 'create_date') {
1168 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1170 } elsif (lc($sort) eq 'edit_date') {
1172 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1174 } elsif (lc($sort) eq 'title') {
1177 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1178 FROM $metabib_full_rec frt
1179 WHERE frt.record = mr.master_record
1181 AND frt.subfield = 'a'
1185 } elsif (lc($sort) eq 'author') {
1188 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1189 FROM $metabib_full_rec fra
1190 WHERE fra.record = mr.master_record
1191 AND fra.tag LIKE '1%'
1192 AND fra.subfield = 'a'
1193 ORDER BY fra.tag::text::int
1201 my $select = <<" SQL";
1202 SELECT m.metarecord,
1204 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1206 FROM $search_table f,
1207 $metabib_metarecord_source_map_table m,
1208 $metabib_metarecord_source_map_table smrs,
1209 $metabib_metarecord mr,
1210 $metabib_record_descriptor rd
1212 AND smrs.metarecord = mr.id
1213 AND m.source = f.source
1214 AND m.metarecord = mr.id
1215 AND rd.record = smrs.source
1221 GROUP BY m.metarecord
1222 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1223 LIMIT $visibility_limit
1230 FROM $asset_call_number_table cn,
1231 $metabib_metarecord_source_map_table mrs,
1232 $asset_copy_table cp,
1237 $metabib_record_descriptor ord,
1239 WHERE mrs.metarecord = s.metarecord
1240 AND br.id = mrs.source
1241 AND cn.record = mrs.source
1242 AND cp.status = cs.id
1243 AND cp.location = cl.id
1244 AND cn.owning_lib = d.id
1245 AND cp.call_number = cn.id
1246 AND cp.opac_visible IS TRUE
1247 AND cs.opac_visible IS TRUE
1248 AND cl.opac_visible IS TRUE
1249 AND d.opac_visible IS TRUE
1250 AND br.active IS TRUE
1251 AND br.deleted IS FALSE
1252 AND ord.record = mrs.source
1258 ORDER BY 4 $sort_dir
1260 } elsif ($self->api_name !~ /staff/o) {
1267 FROM $asset_call_number_table cn,
1268 $metabib_metarecord_source_map_table mrs,
1269 $asset_copy_table cp,
1274 $metabib_record_descriptor ord
1276 WHERE mrs.metarecord = s.metarecord
1277 AND br.id = mrs.source
1278 AND cn.record = mrs.source
1279 AND cp.status = cs.id
1280 AND cp.location = cl.id
1281 AND cp.circ_lib = d.id
1282 AND cp.call_number = cn.id
1283 AND cp.opac_visible IS TRUE
1284 AND cs.opac_visible IS TRUE
1285 AND cl.opac_visible IS TRUE
1286 AND d.opac_visible IS TRUE
1287 AND br.active IS TRUE
1288 AND br.deleted IS FALSE
1289 AND ord.record = mrs.source
1297 ORDER BY 4 $sort_dir
1306 FROM $asset_call_number_table cn,
1307 $asset_copy_table cp,
1308 $metabib_metarecord_source_map_table mrs,
1311 $metabib_record_descriptor ord
1313 WHERE mrs.metarecord = s.metarecord
1314 AND br.id = mrs.source
1315 AND cn.record = mrs.source
1316 AND cn.id = cp.call_number
1317 AND br.deleted IS FALSE
1318 AND cn.deleted IS FALSE
1319 AND ord.record = mrs.source
1320 AND ( cn.owning_lib = d.id
1321 OR ( cp.circ_lib = d.id
1322 AND cp.deleted IS FALSE
1334 FROM $asset_call_number_table cn,
1335 $metabib_metarecord_source_map_table mrs,
1336 $metabib_record_descriptor ord
1337 WHERE mrs.metarecord = s.metarecord
1338 AND cn.record = mrs.source
1339 AND ord.record = mrs.source
1347 ORDER BY 4 $sort_dir
1352 $log->debug("Field Search SQL :: [$select]",DEBUG);
1354 my $recs = $class->db_Main->selectall_arrayref(
1356 (@bonus_values > 0 ? @bonus_values : () ),
1357 ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1358 @types, @forms, @aud, @lang, @lit_form,
1359 @types, @forms, @aud, @lang, @lit_form,
1360 ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1362 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1365 $max = 1 if (!@$recs);
1367 $max = $$_[1] if ($$_[1] > $max);
1370 my $count = scalar(@$recs);
1371 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1372 my ($mrid,$rank,$skip) = @$rec;
1373 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1378 for my $class ( qw/title author subject keyword series identifier/ ) {
1379 __PACKAGE__->register_method(
1380 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1381 method => 'postfilter_search_class_fts',
1384 cdbi => "metabib::${class}_field_entry",
1387 __PACKAGE__->register_method(
1388 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1389 method => 'postfilter_search_class_fts',
1392 cdbi => "metabib::${class}_field_entry",
1399 my $_cdbi = { title => "metabib::title_field_entry",
1400 author => "metabib::author_field_entry",
1401 subject => "metabib::subject_field_entry",
1402 keyword => "metabib::keyword_field_entry",
1403 series => "metabib::series_field_entry",
1404 identifier => "metabib::identifier_field_entry",
1407 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1408 sub postfilter_search_multi_class_fts {
1413 my $sort = $args{'sort'};
1414 my $sort_dir = $args{sort_dir} || 'DESC';
1415 my $ou = $args{org_unit};
1416 my $ou_type = $args{depth};
1417 my $limit = $args{limit} || 10;
1418 my $offset = $args{offset} || 0;
1419 my $visibility_limit = $args{visibility_limit} || 5000;
1422 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1425 if (!defined($args{org_unit})) {
1426 die "No target organizational unit passed to ".$self->api_name;
1429 if (! scalar( keys %{$args{searches}} )) {
1430 die "No search arguments were passed to ".$self->api_name;
1433 my $outer_limit = 1000;
1435 my $limit_clause = '';
1436 my $offset_clause = '';
1438 $limit_clause = "LIMIT $outer_limit";
1439 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1441 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1442 my ($t_filter, $f_filter, $v_filter) = ('','','');
1443 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1444 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1445 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1447 if ($args{available}) {
1448 $avail_filter = ' AND cp.status IN (0,7,12)';
1451 if (my $a = $args{audience}) {
1452 $a = [$a] if (!ref($a));
1455 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1456 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1459 if (my $l = $args{language}) {
1460 $l = [$l] if (!ref($l));
1463 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1464 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1467 if (my $f = $args{lit_form}) {
1468 $f = [$f] if (!ref($f));
1471 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1472 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1475 if (my $f = $args{item_form}) {
1476 $f = [$f] if (!ref($f));
1479 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1480 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1483 if (my $t = $args{item_type}) {
1484 $t = [$t] if (!ref($t));
1487 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1488 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1491 if (my $v = $args{vr_format}) {
1492 $v = [$v] if (!ref($v));
1495 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1496 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1500 # XXX legacy format and item type support
1501 if ($args{format}) {
1502 my ($t, $f) = split '-', $args{format};
1503 @types = split '', $t;
1504 @forms = split '', $f;
1506 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1507 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1511 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1512 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1518 my $descendants = defined($ou_type) ?
1519 "actor.org_unit_descendants($ou, $ou_type)" :
1520 "actor.org_unit_descendants($ou)";
1522 my $search_table_list = '';
1524 my $join_table_list = '';
1527 my $field_table = config::metabib_field->table;
1531 my $prev_search_group;
1532 my $curr_search_group;
1536 for my $search_group (sort keys %{$args{searches}}) {
1537 (my $search_group_name = $search_group) =~ s/\|/_/gso;
1538 ($search_class,$search_field) = split /\|/, $search_group;
1539 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1541 if ($search_field) {
1542 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1543 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1548 $prev_search_group = $curr_search_group if ($curr_search_group);
1550 $curr_search_group = $search_group_name;
1552 my $class = $_cdbi->{$search_class};
1553 my $search_table = $class->table;
1555 my ($index_col) = $class->columns('FTS');
1556 $index_col ||= 'value';
1559 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1561 my $fts_where = $fts->sql_where_clause;
1562 my @fts_ranks = $fts->fts_rank;
1564 my $SQLstring = join('%',map { lc($_) } $fts->words);
1565 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1566 my $first_word = lc(($fts->words)[0]).'%';
1568 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1569 my $rank = join(' + ', @fts_ranks);
1572 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1573 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1575 $bonus{'series'} = [
1576 { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1577 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1580 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1582 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1583 $bonus_list ||= '1';
1585 push @bonus_lists, $bonus_list;
1586 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1589 #---------------------
1591 $search_table_list .= "$search_table $search_group_name, ";
1592 push @rank_list,$rank;
1593 $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1595 if ($metabib_field) {
1596 $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1597 $metabib_field = undef;
1600 if ($prev_search_group) {
1601 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1605 my $metabib_record_descriptor = metabib::record_descriptor->table;
1606 my $metabib_full_rec = metabib::full_rec->table;
1607 my $metabib_metarecord = metabib::metarecord->table;
1608 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1609 my $asset_call_number_table = asset::call_number->table;
1610 my $asset_copy_table = asset::copy->table;
1611 my $cs_table = config::copy_status->table;
1612 my $cl_table = asset::copy_location->table;
1613 my $br_table = biblio::record_entry->table;
1614 my $source_table = config::bib_source->table;
1616 my $bonuses = join (' * ', @bonus_lists);
1617 my $relevance = join (' + ', @rank_list);
1618 $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1620 my $string_default_sort = 'zzzz';
1621 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1623 my $number_default_sort = '9999';
1624 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1628 my $secondary_sort = <<" SORT";
1630 SELECT COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1631 FROM $metabib_full_rec sfrt,
1632 $metabib_metarecord mr
1633 WHERE sfrt.record = mr.master_record
1634 AND sfrt.tag = '245'
1635 AND sfrt.subfield = 'a'
1640 my $rank = $relevance;
1641 if (lc($sort) eq 'pubdate') {
1644 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1645 FROM $metabib_full_rec frp
1646 WHERE frp.record = mr.master_record
1648 AND frp.subfield = 'c'
1652 } elsif (lc($sort) eq 'create_date') {
1654 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1656 } elsif (lc($sort) eq 'edit_date') {
1658 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1660 } elsif (lc($sort) eq 'title') {
1663 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1664 FROM $metabib_full_rec frt
1665 WHERE frt.record = mr.master_record
1667 AND frt.subfield = 'a'
1671 $secondary_sort = <<" SORT";
1673 SELECT COALESCE(SUBSTRING(sfrp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1674 FROM $metabib_full_rec sfrp
1675 WHERE sfrp.record = mr.master_record
1676 AND sfrp.tag = '260'
1677 AND sfrp.subfield = 'c'
1681 } elsif (lc($sort) eq 'author') {
1684 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1685 FROM $metabib_full_rec fra
1686 WHERE fra.record = mr.master_record
1687 AND fra.tag LIKE '1%'
1688 AND fra.subfield = 'a'
1689 ORDER BY fra.tag::text::int
1694 push @bonus_values, @bonus_values;
1699 my $select = <<" SQL";
1700 SELECT m.metarecord,
1702 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1705 FROM $search_table_list
1706 $metabib_metarecord mr,
1707 $metabib_metarecord_source_map_table m,
1708 $metabib_metarecord_source_map_table smrs
1709 WHERE m.metarecord = smrs.metarecord
1710 AND mr.id = m.metarecord
1713 GROUP BY m.metarecord
1714 -- ORDER BY 4 $sort_dir
1715 LIMIT $visibility_limit
1718 if ($self->api_name !~ /staff/o) {
1725 FROM $asset_call_number_table cn,
1726 $metabib_metarecord_source_map_table mrs,
1727 $asset_copy_table cp,
1732 $metabib_record_descriptor ord
1733 WHERE mrs.metarecord = s.metarecord
1734 AND br.id = mrs.source
1735 AND cn.record = mrs.source
1736 AND cp.status = cs.id
1737 AND cp.location = cl.id
1738 AND cp.circ_lib = d.id
1739 AND cp.call_number = cn.id
1740 AND cp.opac_visible IS TRUE
1741 AND cs.opac_visible IS TRUE
1742 AND cl.opac_visible IS TRUE
1743 AND d.opac_visible IS TRUE
1744 AND br.active IS TRUE
1745 AND br.deleted IS FALSE
1746 AND cp.deleted IS FALSE
1747 AND cn.deleted IS FALSE
1748 AND ord.record = mrs.source
1761 $metabib_metarecord_source_map_table mrs,
1762 $metabib_record_descriptor ord,
1764 WHERE mrs.metarecord = s.metarecord
1765 AND ord.record = mrs.source
1766 AND br.id = mrs.source
1767 AND br.source = src.id
1768 AND src.transcendant IS TRUE
1776 ORDER BY 4 $sort_dir, 5
1783 $metabib_metarecord_source_map_table omrs,
1784 $metabib_record_descriptor ord
1785 WHERE omrs.metarecord = s.metarecord
1786 AND ord.record = omrs.source
1789 FROM $asset_call_number_table cn,
1790 $asset_copy_table cp,
1793 WHERE br.id = omrs.source
1794 AND cn.record = omrs.source
1795 AND br.deleted IS FALSE
1796 AND cn.deleted IS FALSE
1797 AND cp.call_number = cn.id
1798 AND ( cn.owning_lib = d.id
1799 OR ( cp.circ_lib = d.id
1800 AND cp.deleted IS FALSE
1808 FROM $asset_call_number_table cn
1809 WHERE cn.record = omrs.source
1810 AND cn.deleted IS FALSE
1816 $metabib_metarecord_source_map_table mrs,
1817 $metabib_record_descriptor ord,
1819 WHERE mrs.metarecord = s.metarecord
1820 AND br.id = mrs.source
1821 AND br.source = src.id
1822 AND src.transcendant IS TRUE
1838 ORDER BY 4 $sort_dir, 5
1843 $log->debug("Field Search SQL :: [$select]",DEBUG);
1845 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1848 @types, @forms, @vformats, @aud, @lang, @lit_form,
1849 @types, @forms, @vformats, @aud, @lang, @lit_form,
1850 # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1853 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1856 $max = 1 if (!@$recs);
1858 $max = $$_[1] if ($$_[1] > $max);
1861 my $count = scalar(@$recs);
1862 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1863 next unless ($$rec[0]);
1864 my ($mrid,$rank,$skip) = @$rec;
1865 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1870 __PACKAGE__->register_method(
1871 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1872 method => 'postfilter_search_multi_class_fts',
1877 __PACKAGE__->register_method(
1878 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1879 method => 'postfilter_search_multi_class_fts',
1885 __PACKAGE__->register_method(
1886 api_name => "open-ils.storage.metabib.multiclass.search_fts",
1887 method => 'postfilter_search_multi_class_fts',
1892 __PACKAGE__->register_method(
1893 api_name => "open-ils.storage.metabib.multiclass.search_fts.staff",
1894 method => 'postfilter_search_multi_class_fts',
1900 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1901 sub biblio_search_multi_class_fts {
1906 my $sort = $args{'sort'};
1907 my $sort_dir = $args{sort_dir} || 'DESC';
1908 my $ou = $args{org_unit};
1909 my $ou_type = $args{depth};
1910 my $limit = $args{limit} || 10;
1911 my $offset = $args{offset} || 0;
1912 my $pref_lang = $args{preferred_language} || 'eng';
1913 my $visibility_limit = $args{visibility_limit} || 5000;
1916 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1919 if (! scalar( keys %{$args{searches}} )) {
1920 die "No search arguments were passed to ".$self->api_name;
1923 my $outer_limit = 1000;
1925 my $limit_clause = '';
1926 my $offset_clause = '';
1928 $limit_clause = "LIMIT $outer_limit";
1929 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1931 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1932 my ($t_filter, $f_filter, $v_filter) = ('','','');
1933 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1934 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1935 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1937 if ($args{available}) {
1938 $avail_filter = ' AND cp.status IN (0,7,12)';
1941 if (my $a = $args{audience}) {
1942 $a = [$a] if (!ref($a));
1945 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1946 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1949 if (my $l = $args{language}) {
1950 $l = [$l] if (!ref($l));
1953 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1954 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1957 if (my $f = $args{lit_form}) {
1958 $f = [$f] if (!ref($f));
1961 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1962 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1965 if (my $f = $args{item_form}) {
1966 $f = [$f] if (!ref($f));
1969 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1970 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1973 if (my $t = $args{item_type}) {
1974 $t = [$t] if (!ref($t));
1977 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1978 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1981 if (my $v = $args{vr_format}) {
1982 $v = [$v] if (!ref($v));
1985 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1986 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1989 # XXX legacy format and item type support
1990 if ($args{format}) {
1991 my ($t, $f) = split '-', $args{format};
1992 @types = split '', $t;
1993 @forms = split '', $f;
1995 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1996 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2000 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2001 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2006 my $descendants = defined($ou_type) ?
2007 "actor.org_unit_descendants($ou, $ou_type)" :
2008 "actor.org_unit_descendants($ou)";
2010 my $search_table_list = '';
2012 my $join_table_list = '';
2015 my $field_table = config::metabib_field->table;
2019 my $prev_search_group;
2020 my $curr_search_group;
2024 for my $search_group (sort keys %{$args{searches}}) {
2025 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2026 ($search_class,$search_field) = split /\|/, $search_group;
2027 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2029 if ($search_field) {
2030 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2031 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2036 $prev_search_group = $curr_search_group if ($curr_search_group);
2038 $curr_search_group = $search_group_name;
2040 my $class = $_cdbi->{$search_class};
2041 my $search_table = $class->table;
2043 my ($index_col) = $class->columns('FTS');
2044 $index_col ||= 'value';
2047 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2049 my $fts_where = $fts->sql_where_clause;
2050 my @fts_ranks = $fts->fts_rank;
2052 my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2053 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2054 my $first_word = lc(($fts->words)[0]).'%';
2056 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2057 my $rank = join(' + ', @fts_ranks);
2060 $bonus{'subject'} = [];
2061 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2063 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2065 $bonus{'series'} = [
2066 { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2067 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2070 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2073 push @{ $bonus{'title'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2074 push @{ $bonus{'author'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2075 push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2076 push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2077 push @{ $bonus{'series'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2080 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2081 $bonus_list ||= '1';
2083 push @bonus_lists, $bonus_list;
2084 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2086 #---------------------
2088 $search_table_list .= "$search_table $search_group_name, ";
2089 push @rank_list,$rank;
2090 $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2092 if ($metabib_field) {
2093 $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2094 $metabib_field = undef;
2097 if ($prev_search_group) {
2098 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2102 my $metabib_record_descriptor = metabib::record_descriptor->table;
2103 my $metabib_full_rec = metabib::full_rec->table;
2104 my $metabib_metarecord = metabib::metarecord->table;
2105 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2106 my $asset_call_number_table = asset::call_number->table;
2107 my $asset_copy_table = asset::copy->table;
2108 my $cs_table = config::copy_status->table;
2109 my $cl_table = asset::copy_location->table;
2110 my $br_table = biblio::record_entry->table;
2111 my $source_table = config::bib_source->table;
2114 my $bonuses = join (' * ', @bonus_lists);
2115 my $relevance = join (' + ', @rank_list);
2116 $relevance = "AVG( ($relevance) * ($bonuses) )";
2118 my $string_default_sort = 'zzzz';
2119 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2121 my $number_default_sort = '9999';
2122 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2124 my $rank = $relevance;
2125 if (lc($sort) eq 'pubdate') {
2128 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d{4}'),'$number_default_sort')::INT
2129 FROM $metabib_full_rec frp
2130 WHERE frp.record = b.id
2132 AND frp.subfield = 'c'
2136 } elsif (lc($sort) eq 'create_date') {
2138 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2140 } elsif (lc($sort) eq 'edit_date') {
2142 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2144 } elsif (lc($sort) eq 'title') {
2147 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2148 FROM $metabib_full_rec frt
2149 WHERE frt.record = b.id
2151 AND frt.subfield = 'a'
2155 } elsif (lc($sort) eq 'author') {
2158 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
2159 FROM $metabib_full_rec fra
2160 WHERE fra.record = b.id
2161 AND fra.tag LIKE '1%'
2162 AND fra.subfield = 'a'
2163 ORDER BY fra.tag::text::int
2168 push @bonus_values, @bonus_values;
2173 my $select = <<" SQL";
2178 FROM $search_table_list
2179 $metabib_record_descriptor rd,
2182 WHERE rd.record = b.id
2183 AND b.active IS TRUE
2184 AND b.deleted IS FALSE
2193 GROUP BY b.id, b.source
2194 ORDER BY 3 $sort_dir
2195 LIMIT $visibility_limit
2198 if ($self->api_name !~ /staff/o) {
2203 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2206 FROM $asset_call_number_table cn,
2207 $asset_copy_table cp,
2211 WHERE cn.record = s.id
2212 AND cp.status = cs.id
2213 AND cp.location = cl.id
2214 AND cp.call_number = cn.id
2215 AND cp.opac_visible IS TRUE
2216 AND cs.opac_visible IS TRUE
2217 AND cl.opac_visible IS TRUE
2218 AND d.opac_visible IS TRUE
2219 AND cp.deleted IS FALSE
2220 AND cn.deleted IS FALSE
2221 AND cp.circ_lib = d.id
2225 OR src.transcendant IS TRUE
2226 ORDER BY 3 $sort_dir
2233 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2236 FROM $asset_call_number_table cn,
2237 $asset_copy_table cp,
2239 WHERE cn.record = s.id
2240 AND cp.call_number = cn.id
2241 AND cn.deleted IS FALSE
2242 AND cp.circ_lib = d.id
2243 AND cp.deleted IS FALSE
2249 FROM $asset_call_number_table cn
2250 WHERE cn.record = s.id
2253 OR src.transcendant IS TRUE
2254 ORDER BY 3 $sort_dir
2259 $log->debug("Field Search SQL :: [$select]",DEBUG);
2261 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2263 @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2266 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2268 my $count = scalar(@$recs);
2269 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2270 next unless ($$rec[0]);
2271 my ($mrid,$rank) = @$rec;
2272 $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2277 __PACKAGE__->register_method(
2278 api_name => "open-ils.storage.biblio.multiclass.search_fts.record",
2279 method => 'biblio_search_multi_class_fts',
2284 __PACKAGE__->register_method(
2285 api_name => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2286 method => 'biblio_search_multi_class_fts',
2291 __PACKAGE__->register_method(
2292 api_name => "open-ils.storage.biblio.multiclass.search_fts",
2293 method => 'biblio_search_multi_class_fts',
2298 __PACKAGE__->register_method(
2299 api_name => "open-ils.storage.biblio.multiclass.search_fts.staff",
2300 method => 'biblio_search_multi_class_fts',
2308 my $default_preferred_language;
2309 my $default_preferred_language_weight;
2311 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2317 if (!$locale_map{COMPLETE}) {
2319 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2320 for my $locale ( @locales ) {
2321 $locale_map{lc($locale->code)} = $locale->marc_code;
2323 $locale_map{COMPLETE} = 1;
2327 my $config = OpenSRF::Utils::SettingsClient->new();
2329 if (!$default_preferred_language) {
2331 $default_preferred_language = $config->config_value(
2332 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2333 ) || $config->config_value(
2334 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2339 if (!$default_preferred_language_weight) {
2341 $default_preferred_language_weight = $config->config_value(
2342 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2343 ) || $config->config_value(
2344 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2348 # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2349 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2351 my $ou = $args{org_unit};
2352 my $limit = $args{limit} || 10;
2353 my $offset = $args{offset} || 0;
2356 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2359 if (! scalar( keys %{$args{searches}} )) {
2360 die "No search arguments were passed to ".$self->api_name;
2363 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2365 if (!defined($args{preferred_language})) {
2366 my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2367 $args{preferred_language} =
2368 $locale_map{ lc($ses_locale) } || 'eng';
2371 if (!defined($args{preferred_language_weight})) {
2372 $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2375 if ($args{available}) {
2376 @statuses = (0,7,12);
2379 if (my $s = $args{locations}) {
2380 $s = [$s] if (!ref($s));
2384 if (my $b = $args{between}) {
2385 if (ref($b) && @$b == 2) {
2390 if (my $s = $args{statuses}) {
2391 $s = [$s] if (!ref($s));
2395 if (my $a = $args{audience}) {
2396 $a = [$a] if (!ref($a));
2400 if (my $l = $args{language}) {
2401 $l = [$l] if (!ref($l));
2405 if (my $f = $args{lit_form}) {
2406 $f = [$f] if (!ref($f));
2410 if (my $f = $args{item_form}) {
2411 $f = [$f] if (!ref($f));
2415 if (my $t = $args{item_type}) {
2416 $t = [$t] if (!ref($t));
2420 if (my $b = $args{bib_level}) {
2421 $b = [$b] if (!ref($b));
2425 if (my $v = $args{vr_format}) {
2426 $v = [$v] if (!ref($v));
2430 # XXX legacy format and item type support
2431 if ($args{format}) {
2432 my ($t, $f) = split '-', $args{format};
2433 @types = split '', $t;
2434 @forms = split '', $f;
2437 my %stored_proc_search_args;
2438 for my $search_group (sort keys %{$args{searches}}) {
2439 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2440 my ($search_class,$search_field) = split /\|/, $search_group;
2441 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2443 if ($search_field) {
2444 unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2445 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2450 my $class = $_cdbi->{$search_class};
2451 my $search_table = $class->table;
2453 my ($index_col) = $class->columns('FTS');
2454 $index_col ||= 'value';
2457 my $fts = OpenILS::Application::Storage::FTS->compile(
2458 $search_class => $args{searches}{$search_group}{term},
2459 $search_group_name.'.value',
2460 "$search_group_name.$index_col"
2462 $fts->sql_where_clause; # this builds the ranks for us
2464 my @fts_ranks = $fts->fts_rank;
2465 my @fts_queries = $fts->fts_query;
2466 my @phrases = map { lc($_) } $fts->phrases;
2467 my @words = map { lc($_) } $fts->words;
2469 $stored_proc_search_args{$search_group} = {
2470 fts_rank => \@fts_ranks,
2471 fts_query => \@fts_queries,
2472 phrase => \@phrases,
2478 my $param_search_ou = $ou;
2479 my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2480 my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2481 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2482 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2483 my $param_audience = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud ) . '}$$';
2484 my $param_language = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang ) . '}$$';
2485 my $param_lit_form = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2486 my $param_types = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types ) . '}$$';
2487 my $param_forms = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms ) . '}$$';
2488 my $param_vformats = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2489 my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2490 my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2491 my $param_after = $args{after} ; $param_after = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2492 my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2493 my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2494 my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2495 my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2496 my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2497 my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2498 my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2499 my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2500 my $param_rel_limit = $args{core_limit}; $param_rel_limit ||= 'NULL';
2501 my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2502 my $param_skip_chk = $args{skip_check}; $param_skip_chk ||= 'NULL';
2504 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2506 FROM search.staged_fts(
2507 $param_search_ou\:\:INT,
2508 $param_depth\:\:INT,
2509 $param_searches\:\:TEXT,
2510 $param_statuses\:\:INT[],
2511 $param_locations\:\:INT[],
2512 $param_audience\:\:TEXT[],
2513 $param_language\:\:TEXT[],
2514 $param_lit_form\:\:TEXT[],
2515 $param_types\:\:TEXT[],
2516 $param_forms\:\:TEXT[],
2517 $param_vformats\:\:TEXT[],
2518 $param_bib_level\:\:TEXT[],
2519 $param_before\:\:TEXT,
2520 $param_after\:\:TEXT,
2521 $param_during\:\:TEXT,
2522 $param_between\:\:TEXT[],
2523 $param_pref_lang\:\:TEXT,
2524 $param_pref_lang_multiplier\:\:REAL,
2525 $param_sort\:\:TEXT,
2526 $param_sort_desc\:\:BOOL,
2527 $metarecord\:\:BOOL,
2529 $param_rel_limit\:\:INT,
2530 $param_chk_limit\:\:INT,
2531 $param_skip_chk\:\:INT
2537 my $recs = $sth->fetchall_arrayref({});
2538 my $summary_row = pop @$recs;
2540 my $total = $$summary_row{total};
2541 my $checked = $$summary_row{checked};
2542 my $visible = $$summary_row{visible};
2543 my $deleted = $$summary_row{deleted};
2544 my $excluded = $$summary_row{excluded};
2546 my $estimate = $visible;
2547 if ( $total > $checked && $checked ) {
2549 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2550 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2554 delete $$summary_row{id};
2555 delete $$summary_row{rel};
2556 delete $$summary_row{record};
2558 $client->respond( $summary_row );
2560 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2562 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2563 delete $$rec{checked};
2564 delete $$rec{visible};
2565 delete $$rec{excluded};
2566 delete $$rec{deleted};
2567 delete $$rec{total};
2568 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2570 $client->respond( $rec );
2574 __PACKAGE__->register_method(
2575 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
2576 method => 'staged_fts',
2581 __PACKAGE__->register_method(
2582 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2583 method => 'staged_fts',
2588 __PACKAGE__->register_method(
2589 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
2590 method => 'staged_fts',
2595 __PACKAGE__->register_method(
2596 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2597 method => 'staged_fts',
2603 sub FTS_paging_estimate {
2607 my $checked = shift;
2608 my $visible = shift;
2609 my $excluded = shift;
2610 my $deleted = shift;
2613 my $deleted_ratio = $deleted / $checked;
2614 my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2616 my $exclusion_ratio = $excluded / $checked;
2617 my $delete_adjusted_exclusion_ratio = $checked - $deleted ? $excluded / ($checked - $deleted) : 1;
2619 my $inclusion_ratio = $visible / $checked;
2620 my $delete_adjusted_inclusion_ratio = $checked - $deleted ? $visible / ($checked - $deleted) : 0;
2623 exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2624 inclusion => int($delete_adjusted_total * $inclusion_ratio),
2625 delete_adjusted_exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2626 delete_adjusted_inclusion => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2629 __PACKAGE__->register_method(
2630 api_name => "open-ils.storage.fts_paging_estimate",
2631 method => 'FTS_paging_estimate',
2637 Hash of estimation values based on four variant estimation strategies:
2638 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2639 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2640 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2641 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2644 Helper method used to determin the approximate number of
2645 hits for a search that spans multiple superpages. For
2646 sparse superpages, the inclusion estimate will likely be the
2647 best estimate. The exclusion strategy is the original, but
2648 inclusion is the default.
2651 { name => 'checked',
2652 desc => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2655 { name => 'visible',
2656 desc => 'Number of records visible to the search location on the current superpage.',
2659 { name => 'excluded',
2660 desc => 'Number of records excluded from the search location on the current superpage.',
2663 { name => 'deleted',
2664 desc => 'Number of deleted records on the current superpage.',
2668 desc => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2681 my $term = $$args{term};
2682 my $limit = $$args{max} || 1;
2683 my $min = $$args{min} || 1;
2684 my @classes = @{$$args{class}};
2686 $limit = $min if ($min > $limit);
2689 @classes = ( qw/ title author subject series keyword / );
2693 my $bre_table = biblio::record_entry->table;
2694 my $cn_table = asset::call_number->table;
2695 my $cp_table = asset::copy->table;
2697 for my $search_class ( @classes ) {
2699 my $class = $_cdbi->{$search_class};
2700 my $search_table = $class->table;
2702 my ($index_col) = $class->columns('FTS');
2703 $index_col ||= 'value';
2706 my $where = OpenILS::Application::Storage::FTS
2707 ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2711 SELECT COUNT(DISTINCT X.source)
2712 FROM (SELECT $search_class.source
2713 FROM $search_table $search_class
2714 JOIN $bre_table b ON (b.id = $search_class.source)
2719 HAVING COUNT(DISTINCT X.source) >= $min;
2722 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2723 $matches{$search_class} = $res ? $res->[0] : 0;
2728 __PACKAGE__->register_method(
2729 api_name => "open-ils.storage.search.xref",
2730 method => 'xref_count',
2734 # Takes an abstract query object and recursively turns it back into a string
2736 sub abstract_query2str {
2737 my ($self, $conn, $query) = @_;
2739 return QueryParser::Canonicalize::abstract_query2str_impl($query, 0);
2742 __PACKAGE__->register_method(
2743 api_name => "open-ils.storage.query_parser.abstract_query.canonicalize",
2744 method => "abstract_query2str",
2749 Abstract query parser object, with complete config data. For example input,
2750 see the 'abstract_query' part of the output of an API call like
2751 open-ils.search.biblio.multiclass.query, when called with the return_abstract
2755 return => { type => "string", desc => "String representation of abstract query object" }
2759 sub str2abstract_query {
2760 my ($self, $conn, $query, $qp_opts, $with_config) = @_;
2762 my %use_opts = ( # reasonable defaults? should these even be hardcoded here?
2764 superpage_size => 1000,
2765 core_limit => 25000,
2767 (ref $opts eq 'HASH' ? %$opts : ())
2772 # grab the query parser and initialize it
2773 my $parser = $OpenILS::Application::Storage::QParser;
2776 _initialize_parser($parser) unless $parser->initialization_complete;
2778 my $query = $parser->new(%use_opts)->parse;
2780 return $query->parse_tree->to_abstract_query(with_config => $with_config);
2783 __PACKAGE__->register_method(
2784 api_name => "open-ils.storage.query_parser.abstract_query.from_string",
2785 method => "str2abstract_query",
2789 {desc => "Query", type => "string"},
2790 {desc => q/Arguments for initializing QueryParser (optional)/,
2792 {desc => q/Flag enabling inclusion of QP config in returned object (optional, default false)/,
2795 return => { type => "object", desc => "abstract representation of query parser query" }
2799 sub query_parser_fts {
2805 # grab the query parser and initialize it
2806 my $parser = $OpenILS::Application::Storage::QParser;
2809 _initialize_parser($parser) unless $parser->initialization_complete;
2811 # populate the locale/language map
2812 if (!$locale_map{COMPLETE}) {
2814 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2815 for my $locale ( @locales ) {
2816 $locale_map{lc($locale->code)} = $locale->marc_code;
2818 $locale_map{COMPLETE} = 1;
2822 # I hope we have a query!
2823 if (! $args{query} ) {
2824 die "No query was passed to ".$self->api_name;
2827 my $default_CD_modifiers = OpenSRF::Utils::SettingsClient->new->config_value(
2828 apps => 'open-ils.search' => app_settings => 'default_CD_modifiers'
2831 # Protect against empty / missing default_CD_modifiers setting
2832 if ($default_CD_modifiers and !ref($default_CD_modifiers)) {
2833 $args{query} = "$default_CD_modifiers $args{query}";
2836 my $simple_plan = $args{_simple_plan};
2837 # remove bad chunks of the %args hash
2838 for my $bad ( grep { /^_/ } keys(%args)) {
2839 delete($args{$bad});
2843 # parse the query and supply any query-level %arg-based defaults
2844 # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2845 my $query = $parser->new( %args )->parse;
2847 my $config = OpenSRF::Utils::SettingsClient->new();
2849 # set the locale-based default preferred location
2850 if (!$query->parse_tree->find_filter('preferred_language')) {
2851 $parser->default_preferred_language( $args{preferred_language} );
2853 if (!$parser->default_preferred_language) {
2854 my $ses_locale = $client->session ? $client->session->session_locale : '';
2855 $parser->default_preferred_language( $locale_map{ lc($ses_locale) } );
2858 if (!$parser->default_preferred_language) { # still nothing...
2859 my $tmp_dpl = $config->config_value(
2860 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2861 ) || $config->config_value(
2862 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2865 $parser->default_preferred_language( $tmp_dpl )
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')) {
2874 if ($tmp_dplw = $args{preferred_language_weight} || $args{preferred_language_multiplier} ) {
2875 $parser->default_preferred_language_multiplier($tmp_dplw);
2878 $tmp_dplw = $config->config_value(
2879 apps => 'open-ils.search' => app_settings => 'default_preferred_language_weight'
2880 ) || $config->config_value(
2881 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2884 $parser->default_preferred_language_multiplier( $tmp_dplw );
2888 # gather the site, if one is specified, defaulting to the in-query version
2889 my $ou = $args{org_unit};
2890 if (my ($filter) = $query->parse_tree->find_filter('site')) {
2891 $ou = $filter->args->[0] if (@{$filter->args});
2893 $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^(-)?\d+$/);
2895 # gather lasso, as with $ou
2896 my $lasso = $args{lasso};
2897 if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
2898 $lasso = $filter->args->[0] if (@{$filter->args});
2900 $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
2901 $lasso = -$lasso if ($lasso);
2904 # # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
2905 # # gather user lasso, as with $ou and lasso
2906 # my $mylasso = $args{my_lasso};
2907 # if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
2908 # $mylasso = $filter->args->[0] if (@{$filter->args});
2910 # $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
2913 # if we have a lasso, go with that, otherwise ... ou
2914 $ou = $lasso if ($lasso);
2916 # gather the preferred OU, if one is specified, as with $ou
2917 my $pref_ou = $args{pref_ou};
2918 $log->info("pref_ou = $pref_ou");
2919 if (my ($filter) = $query->parse_tree->find_filter('pref_ou')) {
2920 $pref_ou = $filter->args->[0] if (@{$filter->args});
2922 $pref_ou = actor::org_unit->search( { shortname => $pref_ou } )->next->id if ($pref_ou and $pref_ou !~ /^(-)?\d+$/);
2924 # get the default $ou if we have nothing
2925 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
2928 # 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
2929 # gather the depth, if one is specified, defaulting to the in-query version
2930 my $depth = $args{depth};
2931 if (my ($filter) = $query->parse_tree->find_filter('depth')) {
2932 $depth = $filter->args->[0] if (@{$filter->args});
2934 $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
2937 # gather the limit or default to 10
2938 my $limit = $args{check_limit} || 'NULL';
2939 if (my ($filter) = $query->parse_tree->find_filter('limit')) {
2940 $limit = $filter->args->[0] if (@{$filter->args});
2942 if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
2943 $limit = $filter->args->[0] if (@{$filter->args});
2947 # gather the offset or default to 0
2948 my $offset = $args{skip_check} || $args{offset} || 0;
2949 if (my ($filter) = $query->parse_tree->find_filter('offset')) {
2950 $offset = $filter->args->[0] if (@{$filter->args});
2952 if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
2953 $offset = $filter->args->[0] if (@{$filter->args});
2957 # gather the estimation strategy or default to inclusion
2958 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2959 if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
2960 $estimation_strategy = $filter->args->[0] if (@{$filter->args});
2964 # gather the estimation strategy or default to inclusion
2965 my $core_limit = $args{core_limit};
2966 if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
2967 $core_limit = $filter->args->[0] if (@{$filter->args});
2971 # gather statuses, and then forget those if we have an #available modifier
2973 if (my ($filter) = $query->parse_tree->find_filter('statuses')) {
2974 @statuses = @{$filter->args} if (@{$filter->args});
2976 @statuses = (0,7,12) if ($query->parse_tree->find_modifier('available'));
2981 if (my ($filter) = $query->parse_tree->find_filter('locations')) {
2982 @location = @{$filter->args} if (@{$filter->args});
2985 # gather location_groups
2986 if (my ($filter) = $query->parse_tree->find_filter('location_groups')) {
2987 my @loc_groups = @{$filter->args} if (@{$filter->args});
2989 # collect the mapped locations and add them to the locations() filter
2992 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
2993 my $maps = $cstore->request(
2994 'open-ils.cstore.direct.asset.copy_location_group_map.search.atomic',
2995 {lgroup => \@loc_groups})->gather(1);
2997 push(@location, $_->location) for @$maps;
3002 my $param_check = $limit || $query->superpage_size || 'NULL';
3003 my $param_offset = $offset || 'NULL';
3004 my $param_limit = $core_limit || 'NULL';
3006 my $sp = $query->superpage || 1;
3008 $param_offset = ($sp - 1) * $sp_size;
3011 my $param_search_ou = $ou;
3012 my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
3013 my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
3014 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
3015 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
3016 my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
3017 my $deleted_search = ($query->parse_tree->find_modifier('deleted')) ? "'t'" : "'f'";
3018 my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
3019 my $param_pref_ou = $pref_ou || 'NULL';
3021 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3022 SELECT * -- bib search: $args{query}
3023 FROM search.query_parser_fts(
3024 $param_search_ou\:\:INT,
3025 $param_depth\:\:INT,
3026 $param_core_query\:\:TEXT,
3027 $param_statuses\:\:INT[],
3028 $param_locations\:\:INT[],
3029 $param_offset\:\:INT,
3030 $param_check\:\:INT,
3031 $param_limit\:\:INT,
3032 $metarecord\:\:BOOL,
3034 $deleted_search\:\:BOOL,
3035 $param_pref_ou\:\:INT
3041 my $recs = $sth->fetchall_arrayref({});
3042 my $summary_row = pop @$recs;
3044 my $total = $$summary_row{total};
3045 my $checked = $$summary_row{checked};
3046 my $visible = $$summary_row{visible};
3047 my $deleted = $$summary_row{deleted};
3048 my $excluded = $$summary_row{excluded};
3050 my $estimate = $visible;
3051 if ( $total > $checked && $checked ) {
3053 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
3054 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
3058 delete $$summary_row{id};
3059 delete $$summary_row{rel};
3060 delete $$summary_row{record};
3062 if (defined($simple_plan)) {
3063 $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3065 $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3068 $client->respond( $summary_row );
3070 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
3072 for my $rec (@$recs) {
3073 delete $$rec{checked};
3074 delete $$rec{visible};
3075 delete $$rec{excluded};
3076 delete $$rec{deleted};
3077 delete $$rec{total};
3078 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3080 $client->respond( $rec );
3084 __PACKAGE__->register_method(
3085 api_name => "open-ils.storage.query_parser_search",
3086 method => 'query_parser_fts',
3092 sub query_parser_fts_wrapper {
3097 $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3098 # grab the query parser and initialize it
3099 my $parser = $OpenILS::Application::Storage::QParser;
3102 _initialize_parser($parser) unless $parser->initialization_complete;
3104 if (! scalar( keys %{$args{searches}} )) {
3105 die "No search arguments were passed to ".$self->api_name;
3108 $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3109 my $base_query = '';
3110 for my $sclass ( keys %{$args{searches}} ) {
3111 $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3112 $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3115 my $query = $base_query;
3116 $log->debug("Full base query: $base_query", DEBUG);
3118 $query = "$args{facets} $query" if ($args{facets});
3120 if (!$locale_map{COMPLETE}) {
3122 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3123 for my $locale ( @locales ) {
3124 $locale_map{lc($locale->code)} = $locale->marc_code;
3126 $locale_map{COMPLETE} = 1;
3130 my $base_plan = $parser->new( query => $base_query )->parse;
3132 $query = "preferred_language($args{preferred_language}) $query"
3133 if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3134 $query = "preferred_language_weight($args{preferred_language_weight}) $query"
3135 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'));
3138 $query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
3139 $query = "site($args{org_unit}) $query" if ($args{org_unit});
3140 $query = "depth($args{depth}) $query" if (defined($args{depth}));
3141 $query = "sort($args{sort}) $query" if ($args{sort});
3142 $query = "limit($args{limit}) $query" if ($args{limit});
3143 $query = "core_limit($args{core_limit}) $query" if ($args{core_limit});
3144 $query = "skip_check($args{skip_check}) $query" if ($args{skip_check});
3145 $query = "superpage($args{superpage}) $query" if ($args{superpage});
3146 $query = "offset($args{offset}) $query" if ($args{offset});
3147 $query = "#metarecord $query" if ($self->api_name =~ /metabib/);
3148 $query = "#available $query" if ($args{available});
3149 $query = "#descending $query" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3150 $query = "#staff $query" if ($self->api_name =~ /staff/);
3151 $query = "before($args{before}) $query" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3152 $query = "after($args{after}) $query" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3153 $query = "during($args{during}) $query" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3154 $query = "between($args{between}[0],$args{between}[1]) $query"
3155 if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3158 my (@between,@statuses,@locations,@location_groups,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3160 # XXX legacy format and item type support
3161 if ($args{format}) {
3162 my ($t, $f) = split '-', $args{format};
3163 $args{item_type} = [ split '', $t ];
3164 $args{item_form} = [ split '', $f ];
3167 for my $filter ( qw/locations location_groups statuses between audience language lit_form item_form item_type bib_level vr_format/ ) {
3168 if (my $s = $args{$filter}) {
3169 $s = [$s] if (!ref($s));
3171 my @filter_list = @$s;
3173 next if ($filter eq 'between' and scalar(@filter_list) != 2);
3174 next if (@filter_list == 0);
3176 my $filter_string = join ',', @filter_list;
3177 $query = "$query $filter($filter_string)";
3181 $log->debug("Full QueryParser query: $query", DEBUG);
3183 return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan );
3185 __PACKAGE__->register_method(
3186 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
3187 method => 'query_parser_fts_wrapper',
3192 __PACKAGE__->register_method(
3193 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3194 method => 'query_parser_fts_wrapper',
3199 __PACKAGE__->register_method(
3200 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
3201 method => 'query_parser_fts_wrapper',
3206 __PACKAGE__->register_method(
3207 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3208 method => 'query_parser_fts_wrapper',