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 $string_default_sort = 'zzzz';
513 $string_default_sort = 'AAAA' if ($sort_dir =~ /^DESC$/i);
515 my $number_default_sort = '9999';
516 $number_default_sort = '0000' if ($sort_dir =~/^DESC$/i);
518 my $rank = $relevance;
519 if (lc($sort) eq 'pubdate') {
522 SELECT COALESCE(SUBSTRING(MAX(frp.value) FROM E'\\\\d{4}'), '$number_default_sort')::INT
523 FROM $metabib_full_rec frp
524 WHERE frp.record = f.record
526 AND frp.subfield = 'c'
530 } elsif (lc($sort) eq 'create_date') {
532 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
534 } elsif (lc($sort) eq 'edit_date') {
536 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
538 } elsif (lc($sort) =~ /^title/i) {
541 SELECT COALESCE(LTRIM(SUBSTR(MAX(frt.value), COALESCE(SUBSTRING(MAX(frt.ind2) FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
542 FROM $metabib_full_rec frt
543 WHERE frt.record = f.record
545 AND frt.subfield = 'a'
549 } elsif (lc($sort) =~ /^author/i) {
552 SELECT COALESCE(LTRIM(MAX(query.value)), '$string_default_sort')
555 FROM $metabib_full_rec fra
556 WHERE fra.record = f.record
557 AND fra.tag LIKE '1%'
558 AND fra.subfield = 'a'
559 ORDER BY fra.tag::text::int
568 my $rd_join = $use_rd ? "$metabib_record_descriptor rd," : '';
569 my $rd_filter = $use_rd ? 'AND rd.record = f.record' : '';
571 if ($copies_visible) {
573 SELECT f.record, $relevance, count(DISTINCT cp.id), $rank
574 FROM $search_table f,
575 $asset_call_number_table cn,
576 $asset_copy_table cp,
582 WHERE br.id = f.record
583 AND cn.record = f.record
584 AND cp.status = cs.id
585 AND cp.location = cl.id
586 AND br.deleted IS FALSE
587 AND cn.deleted IS FALSE
588 AND cp.deleted IS FALSE
598 GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
599 ORDER BY 4 $sort_dir,3 DESC
603 SELECT f.record, 1, 1, $rank
604 FROM $search_table f,
607 WHERE br.id = f.record
608 AND br.deleted IS FALSE
621 $log->debug("Search SQL :: [$select]",DEBUG);
623 my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
624 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
627 $max = 1 if (!@$recs);
629 $max = $$_[1] if ($$_[1] > $max);
633 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
634 next unless ($$rec[0]);
635 my ($rid,$rank,$junk,$skip) = @$rec;
636 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
640 __PACKAGE__->register_method(
641 api_name => 'open-ils.storage.biblio.full_rec.multi_search',
642 method => 'biblio_multi_search_full_rec',
647 __PACKAGE__->register_method(
648 api_name => 'open-ils.storage.biblio.full_rec.multi_search.staff',
649 method => 'biblio_multi_search_full_rec',
655 sub search_full_rec {
661 my $term = $args{term};
662 my $limiters = $args{restrict};
664 my ($index_col) = metabib::full_rec->columns('FTS');
665 $index_col ||= 'value';
666 my $search_table = metabib::full_rec->table;
668 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
670 my $fts_where = $fts->sql_where_clause();
671 my @fts_ranks = $fts->fts_rank;
673 my $rank = join(' + ', @fts_ranks);
677 for my $limit (@$limiters) {
678 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
679 # MARC control field; mfr.subfield is NULL
680 push @wheres, "( tag = ? AND $fts_where )";
681 push @binds, $$limit{tag};
682 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
684 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
685 push @binds, $$limit{tag}, $$limit{subfield};
686 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
689 my $where = join(' OR ', @wheres);
691 my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
693 $log->debug("Search SQL :: [$select]",DEBUG);
695 my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
696 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
698 $client->respond($_) for (@$recs);
701 __PACKAGE__->register_method(
702 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
703 method => 'search_full_rec',
708 __PACKAGE__->register_method(
709 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
710 method => 'search_full_rec',
717 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
718 sub search_class_fts {
723 my $term = $args{term};
724 my $ou = $args{org_unit};
725 my $ou_type = $args{depth};
726 my $limit = $args{limit};
727 my $offset = $args{offset};
729 my $limit_clause = '';
730 my $offset_clause = '';
732 $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
733 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
736 my ($t_filter, $f_filter) = ('','');
739 my ($t, $f) = split '-', $args{format};
740 @types = split '', $t;
741 @forms = split '', $f;
743 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
747 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
753 my $descendants = defined($ou_type) ?
754 "actor.org_unit_descendants($ou, $ou_type)" :
755 "actor.org_unit_descendants($ou)";
757 my $class = $self->{cdbi};
758 my $search_table = $class->table;
760 my $metabib_record_descriptor = metabib::record_descriptor->table;
761 my $metabib_metarecord = metabib::metarecord->table;
762 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
763 my $asset_call_number_table = asset::call_number->table;
764 my $asset_copy_table = asset::copy->table;
765 my $cs_table = config::copy_status->table;
766 my $cl_table = asset::copy_location->table;
768 my ($index_col) = $class->columns('FTS');
769 $index_col ||= 'value';
771 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
772 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
774 my $fts_where = $fts->sql_where_clause;
775 my @fts_ranks = $fts->fts_rank;
777 my $rank = join(' + ', @fts_ranks);
779 my $has_vols = 'AND cn.owning_lib = d.id';
780 my $has_copies = 'AND cp.call_number = cn.id';
781 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';
783 my $visible_count = ', count(DISTINCT cp.id)';
784 my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
786 if ($self->api_name =~ /staff/o) {
787 $copies_visible = '';
788 $visible_count_test = '';
789 $has_copies = '' if ($ou_type == 0);
790 $has_vols = '' if ($ou_type == 0);
793 my $rank_calc = <<" RANK";
795 * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
796 * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
797 * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
798 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
801 $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
803 if ($copies_visible) {
805 SELECT m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
806 FROM $search_table f,
807 $metabib_metarecord_source_map_table m,
808 $asset_call_number_table cn,
809 $asset_copy_table cp,
812 $metabib_record_descriptor rd,
815 AND m.source = f.source
816 AND cn.record = m.source
817 AND rd.record = m.source
818 AND cp.status = cs.id
819 AND cp.location = cl.id
825 GROUP BY 1 $visible_count_test
827 $limit_clause $offset_clause
831 SELECT m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
832 FROM $search_table f,
833 $metabib_metarecord_source_map_table m,
834 $metabib_record_descriptor rd
836 AND m.source = f.source
837 AND rd.record = m.source
842 $limit_clause $offset_clause
846 $log->debug("Field Search SQL :: [$select]",DEBUG);
848 my $SQLstring = join('%',$fts->words);
849 my $REstring = join('\\s+',$fts->words);
850 my $first_word = ($fts->words)[0].'%';
851 my $recs = ($self->api_name =~ /unordered/o) ?
852 $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
853 $class->db_Main->selectall_arrayref($select, {},
854 '%'.lc($SQLstring).'%', # phrase order match
855 lc($first_word), # first word match
856 '^\\s*'.lc($REstring).'\\s*/?\s*$', # full exact match
860 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
862 $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
866 for my $class ( qw/title author subject keyword series identifier/ ) {
867 __PACKAGE__->register_method(
868 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord",
869 method => 'search_class_fts',
872 cdbi => "metabib::${class}_field_entry",
875 __PACKAGE__->register_method(
876 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
877 method => 'search_class_fts',
880 cdbi => "metabib::${class}_field_entry",
883 __PACKAGE__->register_method(
884 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
885 method => 'search_class_fts',
888 cdbi => "metabib::${class}_field_entry",
891 __PACKAGE__->register_method(
892 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
893 method => 'search_class_fts',
896 cdbi => "metabib::${class}_field_entry",
901 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
902 sub search_class_fts_count {
907 my $term = $args{term};
908 my $ou = $args{org_unit};
909 my $ou_type = $args{depth};
910 my $limit = $args{limit} || 100;
911 my $offset = $args{offset} || 0;
913 my $descendants = defined($ou_type) ?
914 "actor.org_unit_descendants($ou, $ou_type)" :
915 "actor.org_unit_descendants($ou)";
918 my ($t_filter, $f_filter) = ('','');
921 my ($t, $f) = split '-', $args{format};
922 @types = split '', $t;
923 @forms = split '', $f;
925 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
929 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
934 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
936 my $class = $self->{cdbi};
937 my $search_table = $class->table;
939 my $metabib_record_descriptor = metabib::record_descriptor->table;
940 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
941 my $asset_call_number_table = asset::call_number->table;
942 my $asset_copy_table = asset::copy->table;
943 my $cs_table = config::copy_status->table;
944 my $cl_table = asset::copy_location->table;
946 my ($index_col) = $class->columns('FTS');
947 $index_col ||= 'value';
949 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
951 my $fts_where = $fts->sql_where_clause;
953 my $has_vols = 'AND cn.owning_lib = d.id';
954 my $has_copies = 'AND cp.call_number = cn.id';
955 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';
956 if ($self->api_name =~ /staff/o) {
957 $copies_visible = '';
958 $has_vols = '' if ($ou_type == 0);
959 $has_copies = '' if ($ou_type == 0);
962 # XXX test an "EXISTS version of descendant checking...
964 if ($copies_visible) {
966 SELECT count(distinct m.metarecord)
967 FROM $search_table f,
968 $metabib_metarecord_source_map_table m,
969 $metabib_metarecord_source_map_table mr,
970 $asset_call_number_table cn,
971 $asset_copy_table cp,
974 $metabib_record_descriptor rd,
977 AND mr.source = f.source
978 AND mr.metarecord = m.metarecord
979 AND cn.record = m.source
980 AND rd.record = m.source
981 AND cp.status = cs.id
982 AND cp.location = cl.id
991 SELECT count(distinct m.metarecord)
992 FROM $search_table f,
993 $metabib_metarecord_source_map_table m,
994 $metabib_metarecord_source_map_table mr,
995 $metabib_record_descriptor rd
997 AND mr.source = f.source
998 AND mr.metarecord = m.metarecord
999 AND rd.record = m.source
1005 $log->debug("Field Search Count SQL :: [$select]",DEBUG);
1007 my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1009 $log->debug("Count Search yielded $recs results.",DEBUG);
1014 for my $class ( qw/title author subject keyword series identifier/ ) {
1015 __PACKAGE__->register_method(
1016 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
1017 method => 'search_class_fts_count',
1020 cdbi => "metabib::${class}_field_entry",
1023 __PACKAGE__->register_method(
1024 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
1025 method => 'search_class_fts_count',
1028 cdbi => "metabib::${class}_field_entry",
1034 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1035 sub postfilter_search_class_fts {
1040 my $term = $args{term};
1041 my $sort = $args{'sort'};
1042 my $sort_dir = $args{sort_dir} || 'DESC';
1043 my $ou = $args{org_unit};
1044 my $ou_type = $args{depth};
1045 my $limit = $args{limit} || 10;
1046 my $visibility_limit = $args{visibility_limit} || 5000;
1047 my $offset = $args{offset} || 0;
1049 my $outer_limit = 1000;
1051 my $limit_clause = '';
1052 my $offset_clause = '';
1054 $limit_clause = "LIMIT $outer_limit";
1055 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1057 my (@types,@forms,@lang,@aud,@lit_form);
1058 my ($t_filter, $f_filter) = ('','');
1059 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1060 my ($ot_filter, $of_filter) = ('','');
1061 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1063 if (my $a = $args{audience}) {
1064 $a = [$a] if (!ref($a));
1067 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1068 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1071 if (my $l = $args{language}) {
1072 $l = [$l] if (!ref($l));
1075 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1076 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1079 if (my $f = $args{lit_form}) {
1080 $f = [$f] if (!ref($f));
1083 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1084 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1087 if ($args{format}) {
1088 my ($t, $f) = split '-', $args{format};
1089 @types = split '', $t;
1090 @forms = split '', $f;
1092 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1093 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1097 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1098 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1103 my $descendants = defined($ou_type) ?
1104 "actor.org_unit_descendants($ou, $ou_type)" :
1105 "actor.org_unit_descendants($ou)";
1107 my $class = $self->{cdbi};
1108 my $search_table = $class->table;
1110 my $metabib_full_rec = metabib::full_rec->table;
1111 my $metabib_record_descriptor = metabib::record_descriptor->table;
1112 my $metabib_metarecord = metabib::metarecord->table;
1113 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1114 my $asset_call_number_table = asset::call_number->table;
1115 my $asset_copy_table = asset::copy->table;
1116 my $cs_table = config::copy_status->table;
1117 my $cl_table = asset::copy_location->table;
1118 my $br_table = biblio::record_entry->table;
1120 my ($index_col) = $class->columns('FTS');
1121 $index_col ||= 'value';
1123 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1125 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1127 my $SQLstring = join('%',map { lc($_) } $fts->words);
1128 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1129 my $first_word = lc(($fts->words)[0]).'%';
1131 my $fts_where = $fts->sql_where_clause;
1132 my @fts_ranks = $fts->fts_rank;
1135 $bonus{'metabib::identifier_field_entry'} =
1136 $bonus{'metabib::keyword_field_entry'} = [
1137 { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring }
1140 $bonus{'metabib::title_field_entry'} =
1141 $bonus{'metabib::series_field_entry'} = [
1142 { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1143 { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1144 @{ $bonus{'metabib::keyword_field_entry'} }
1147 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1148 $bonus_list ||= '1';
1150 my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1152 my $relevance = join(' + ', @fts_ranks);
1153 $relevance = <<" RANK";
1154 (SUM( ( $relevance ) * ( $bonus_list ) )/COUNT(m.source))
1157 my $string_default_sort = 'zzzz';
1158 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1160 my $number_default_sort = '9999';
1161 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1163 my $rank = $relevance;
1164 if (lc($sort) eq 'pubdate') {
1167 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1168 FROM $metabib_full_rec frp
1169 WHERE frp.record = mr.master_record
1171 AND frp.subfield = 'c'
1175 } elsif (lc($sort) eq 'create_date') {
1177 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1179 } elsif (lc($sort) eq 'edit_date') {
1181 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1183 } elsif (lc($sort) eq 'title') {
1186 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1187 FROM $metabib_full_rec frt
1188 WHERE frt.record = mr.master_record
1190 AND frt.subfield = 'a'
1194 } elsif (lc($sort) eq 'author') {
1197 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1198 FROM $metabib_full_rec fra
1199 WHERE fra.record = mr.master_record
1200 AND fra.tag LIKE '1%'
1201 AND fra.subfield = 'a'
1202 ORDER BY fra.tag::text::int
1210 my $select = <<" SQL";
1211 SELECT m.metarecord,
1213 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1215 FROM $search_table f,
1216 $metabib_metarecord_source_map_table m,
1217 $metabib_metarecord_source_map_table smrs,
1218 $metabib_metarecord mr,
1219 $metabib_record_descriptor rd
1221 AND smrs.metarecord = mr.id
1222 AND m.source = f.source
1223 AND m.metarecord = mr.id
1224 AND rd.record = smrs.source
1230 GROUP BY m.metarecord
1231 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1232 LIMIT $visibility_limit
1239 FROM $asset_call_number_table cn,
1240 $metabib_metarecord_source_map_table mrs,
1241 $asset_copy_table cp,
1246 $metabib_record_descriptor ord,
1248 WHERE mrs.metarecord = s.metarecord
1249 AND br.id = mrs.source
1250 AND cn.record = mrs.source
1251 AND cp.status = cs.id
1252 AND cp.location = cl.id
1253 AND cn.owning_lib = d.id
1254 AND cp.call_number = cn.id
1255 AND cp.opac_visible IS TRUE
1256 AND cs.opac_visible IS TRUE
1257 AND cl.opac_visible IS TRUE
1258 AND d.opac_visible IS TRUE
1259 AND br.active IS TRUE
1260 AND br.deleted IS FALSE
1261 AND ord.record = mrs.source
1267 ORDER BY 4 $sort_dir
1269 } elsif ($self->api_name !~ /staff/o) {
1276 FROM $asset_call_number_table cn,
1277 $metabib_metarecord_source_map_table mrs,
1278 $asset_copy_table cp,
1283 $metabib_record_descriptor ord
1285 WHERE mrs.metarecord = s.metarecord
1286 AND br.id = mrs.source
1287 AND cn.record = mrs.source
1288 AND cp.status = cs.id
1289 AND cp.location = cl.id
1290 AND cp.circ_lib = d.id
1291 AND cp.call_number = cn.id
1292 AND cp.opac_visible IS TRUE
1293 AND cs.opac_visible IS TRUE
1294 AND cl.opac_visible IS TRUE
1295 AND d.opac_visible IS TRUE
1296 AND br.active IS TRUE
1297 AND br.deleted IS FALSE
1298 AND ord.record = mrs.source
1306 ORDER BY 4 $sort_dir
1315 FROM $asset_call_number_table cn,
1316 $asset_copy_table cp,
1317 $metabib_metarecord_source_map_table mrs,
1320 $metabib_record_descriptor ord
1322 WHERE mrs.metarecord = s.metarecord
1323 AND br.id = mrs.source
1324 AND cn.record = mrs.source
1325 AND cn.id = cp.call_number
1326 AND br.deleted IS FALSE
1327 AND cn.deleted IS FALSE
1328 AND ord.record = mrs.source
1329 AND ( cn.owning_lib = d.id
1330 OR ( cp.circ_lib = d.id
1331 AND cp.deleted IS FALSE
1343 FROM $asset_call_number_table cn,
1344 $metabib_metarecord_source_map_table mrs,
1345 $metabib_record_descriptor ord
1346 WHERE mrs.metarecord = s.metarecord
1347 AND cn.record = mrs.source
1348 AND ord.record = mrs.source
1356 ORDER BY 4 $sort_dir
1361 $log->debug("Field Search SQL :: [$select]",DEBUG);
1363 my $recs = $class->db_Main->selectall_arrayref(
1365 (@bonus_values > 0 ? @bonus_values : () ),
1366 ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1367 @types, @forms, @aud, @lang, @lit_form,
1368 @types, @forms, @aud, @lang, @lit_form,
1369 ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1371 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1374 $max = 1 if (!@$recs);
1376 $max = $$_[1] if ($$_[1] > $max);
1379 my $count = scalar(@$recs);
1380 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1381 my ($mrid,$rank,$skip) = @$rec;
1382 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1387 for my $class ( qw/title author subject keyword series identifier/ ) {
1388 __PACKAGE__->register_method(
1389 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1390 method => 'postfilter_search_class_fts',
1393 cdbi => "metabib::${class}_field_entry",
1396 __PACKAGE__->register_method(
1397 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1398 method => 'postfilter_search_class_fts',
1401 cdbi => "metabib::${class}_field_entry",
1408 my $_cdbi = { title => "metabib::title_field_entry",
1409 author => "metabib::author_field_entry",
1410 subject => "metabib::subject_field_entry",
1411 keyword => "metabib::keyword_field_entry",
1412 series => "metabib::series_field_entry",
1413 identifier => "metabib::identifier_field_entry",
1416 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1417 sub postfilter_search_multi_class_fts {
1422 my $sort = $args{'sort'};
1423 my $sort_dir = $args{sort_dir} || 'DESC';
1424 my $ou = $args{org_unit};
1425 my $ou_type = $args{depth};
1426 my $limit = $args{limit} || 10;
1427 my $offset = $args{offset} || 0;
1428 my $visibility_limit = $args{visibility_limit} || 5000;
1431 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1434 if (!defined($args{org_unit})) {
1435 die "No target organizational unit passed to ".$self->api_name;
1438 if (! scalar( keys %{$args{searches}} )) {
1439 die "No search arguments were passed to ".$self->api_name;
1442 my $outer_limit = 1000;
1444 my $limit_clause = '';
1445 my $offset_clause = '';
1447 $limit_clause = "LIMIT $outer_limit";
1448 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1450 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1451 my ($t_filter, $f_filter, $v_filter) = ('','','');
1452 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1453 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1454 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1456 if ($args{available}) {
1457 $avail_filter = ' AND cp.status IN (0,7,12)';
1460 if (my $a = $args{audience}) {
1461 $a = [$a] if (!ref($a));
1464 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1465 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1468 if (my $l = $args{language}) {
1469 $l = [$l] if (!ref($l));
1472 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1473 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1476 if (my $f = $args{lit_form}) {
1477 $f = [$f] if (!ref($f));
1480 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1481 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1484 if (my $f = $args{item_form}) {
1485 $f = [$f] if (!ref($f));
1488 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1489 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1492 if (my $t = $args{item_type}) {
1493 $t = [$t] if (!ref($t));
1496 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1497 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1500 if (my $v = $args{vr_format}) {
1501 $v = [$v] if (!ref($v));
1504 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1505 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1509 # XXX legacy format and item type support
1510 if ($args{format}) {
1511 my ($t, $f) = split '-', $args{format};
1512 @types = split '', $t;
1513 @forms = split '', $f;
1515 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1516 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1520 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1521 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1527 my $descendants = defined($ou_type) ?
1528 "actor.org_unit_descendants($ou, $ou_type)" :
1529 "actor.org_unit_descendants($ou)";
1531 my $search_table_list = '';
1533 my $join_table_list = '';
1536 my $field_table = config::metabib_field->table;
1540 my $prev_search_group;
1541 my $curr_search_group;
1545 for my $search_group (sort keys %{$args{searches}}) {
1546 (my $search_group_name = $search_group) =~ s/\|/_/gso;
1547 ($search_class,$search_field) = split /\|/, $search_group;
1548 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1550 if ($search_field) {
1551 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1552 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1557 $prev_search_group = $curr_search_group if ($curr_search_group);
1559 $curr_search_group = $search_group_name;
1561 my $class = $_cdbi->{$search_class};
1562 my $search_table = $class->table;
1564 my ($index_col) = $class->columns('FTS');
1565 $index_col ||= 'value';
1568 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1570 my $fts_where = $fts->sql_where_clause;
1571 my @fts_ranks = $fts->fts_rank;
1573 my $SQLstring = join('%',map { lc($_) } $fts->words);
1574 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1575 my $first_word = lc(($fts->words)[0]).'%';
1577 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1578 my $rank = join(' + ', @fts_ranks);
1581 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1582 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1584 $bonus{'series'} = [
1585 { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1586 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1589 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1591 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1592 $bonus_list ||= '1';
1594 push @bonus_lists, $bonus_list;
1595 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1598 #---------------------
1600 $search_table_list .= "$search_table $search_group_name, ";
1601 push @rank_list,$rank;
1602 $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1604 if ($metabib_field) {
1605 $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1606 $metabib_field = undef;
1609 if ($prev_search_group) {
1610 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1614 my $metabib_record_descriptor = metabib::record_descriptor->table;
1615 my $metabib_full_rec = metabib::full_rec->table;
1616 my $metabib_metarecord = metabib::metarecord->table;
1617 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1618 my $asset_call_number_table = asset::call_number->table;
1619 my $asset_copy_table = asset::copy->table;
1620 my $cs_table = config::copy_status->table;
1621 my $cl_table = asset::copy_location->table;
1622 my $br_table = biblio::record_entry->table;
1623 my $source_table = config::bib_source->table;
1625 my $bonuses = join (' * ', @bonus_lists);
1626 my $relevance = join (' + ', @rank_list);
1627 $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1629 my $string_default_sort = 'zzzz';
1630 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1632 my $number_default_sort = '9999';
1633 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1637 my $secondary_sort = <<" SORT";
1639 SELECT COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1640 FROM $metabib_full_rec sfrt,
1641 $metabib_metarecord mr
1642 WHERE sfrt.record = mr.master_record
1643 AND sfrt.tag = '245'
1644 AND sfrt.subfield = 'a'
1649 my $rank = $relevance;
1650 if (lc($sort) eq 'pubdate') {
1653 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1654 FROM $metabib_full_rec frp
1655 WHERE frp.record = mr.master_record
1657 AND frp.subfield = 'c'
1661 } elsif (lc($sort) eq 'create_date') {
1663 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1665 } elsif (lc($sort) eq 'edit_date') {
1667 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1669 } elsif (lc($sort) eq 'title') {
1672 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1673 FROM $metabib_full_rec frt
1674 WHERE frt.record = mr.master_record
1676 AND frt.subfield = 'a'
1680 $secondary_sort = <<" SORT";
1682 SELECT COALESCE(SUBSTRING(sfrp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1683 FROM $metabib_full_rec sfrp
1684 WHERE sfrp.record = mr.master_record
1685 AND sfrp.tag = '260'
1686 AND sfrp.subfield = 'c'
1690 } elsif (lc($sort) eq 'author') {
1693 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1694 FROM $metabib_full_rec fra
1695 WHERE fra.record = mr.master_record
1696 AND fra.tag LIKE '1%'
1697 AND fra.subfield = 'a'
1698 ORDER BY fra.tag::text::int
1703 push @bonus_values, @bonus_values;
1708 my $select = <<" SQL";
1709 SELECT m.metarecord,
1711 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1714 FROM $search_table_list
1715 $metabib_metarecord mr,
1716 $metabib_metarecord_source_map_table m,
1717 $metabib_metarecord_source_map_table smrs
1718 WHERE m.metarecord = smrs.metarecord
1719 AND mr.id = m.metarecord
1722 GROUP BY m.metarecord
1723 -- ORDER BY 4 $sort_dir
1724 LIMIT $visibility_limit
1727 if ($self->api_name !~ /staff/o) {
1734 FROM $asset_call_number_table cn,
1735 $metabib_metarecord_source_map_table mrs,
1736 $asset_copy_table cp,
1741 $metabib_record_descriptor ord
1742 WHERE mrs.metarecord = s.metarecord
1743 AND br.id = mrs.source
1744 AND cn.record = mrs.source
1745 AND cp.status = cs.id
1746 AND cp.location = cl.id
1747 AND cp.circ_lib = d.id
1748 AND cp.call_number = cn.id
1749 AND cp.opac_visible IS TRUE
1750 AND cs.opac_visible IS TRUE
1751 AND cl.opac_visible IS TRUE
1752 AND d.opac_visible IS TRUE
1753 AND br.active IS TRUE
1754 AND br.deleted IS FALSE
1755 AND cp.deleted IS FALSE
1756 AND cn.deleted IS FALSE
1757 AND ord.record = mrs.source
1770 $metabib_metarecord_source_map_table mrs,
1771 $metabib_record_descriptor ord,
1773 WHERE mrs.metarecord = s.metarecord
1774 AND ord.record = mrs.source
1775 AND br.id = mrs.source
1776 AND br.source = src.id
1777 AND src.transcendant IS TRUE
1785 ORDER BY 4 $sort_dir, 5
1792 $metabib_metarecord_source_map_table omrs,
1793 $metabib_record_descriptor ord
1794 WHERE omrs.metarecord = s.metarecord
1795 AND ord.record = omrs.source
1798 FROM $asset_call_number_table cn,
1799 $asset_copy_table cp,
1802 WHERE br.id = omrs.source
1803 AND cn.record = omrs.source
1804 AND br.deleted IS FALSE
1805 AND cn.deleted IS FALSE
1806 AND cp.call_number = cn.id
1807 AND ( cn.owning_lib = d.id
1808 OR ( cp.circ_lib = d.id
1809 AND cp.deleted IS FALSE
1817 FROM $asset_call_number_table cn
1818 WHERE cn.record = omrs.source
1819 AND cn.deleted IS FALSE
1825 $metabib_metarecord_source_map_table mrs,
1826 $metabib_record_descriptor ord,
1828 WHERE mrs.metarecord = s.metarecord
1829 AND br.id = mrs.source
1830 AND br.source = src.id
1831 AND src.transcendant IS TRUE
1847 ORDER BY 4 $sort_dir, 5
1852 $log->debug("Field Search SQL :: [$select]",DEBUG);
1854 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1857 @types, @forms, @vformats, @aud, @lang, @lit_form,
1858 @types, @forms, @vformats, @aud, @lang, @lit_form,
1859 # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1862 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1865 $max = 1 if (!@$recs);
1867 $max = $$_[1] if ($$_[1] > $max);
1870 my $count = scalar(@$recs);
1871 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1872 next unless ($$rec[0]);
1873 my ($mrid,$rank,$skip) = @$rec;
1874 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1879 __PACKAGE__->register_method(
1880 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1881 method => 'postfilter_search_multi_class_fts',
1886 __PACKAGE__->register_method(
1887 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1888 method => 'postfilter_search_multi_class_fts',
1894 __PACKAGE__->register_method(
1895 api_name => "open-ils.storage.metabib.multiclass.search_fts",
1896 method => 'postfilter_search_multi_class_fts',
1901 __PACKAGE__->register_method(
1902 api_name => "open-ils.storage.metabib.multiclass.search_fts.staff",
1903 method => 'postfilter_search_multi_class_fts',
1909 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1910 sub biblio_search_multi_class_fts {
1915 my $sort = $args{'sort'};
1916 my $sort_dir = $args{sort_dir} || 'DESC';
1917 my $ou = $args{org_unit};
1918 my $ou_type = $args{depth};
1919 my $limit = $args{limit} || 10;
1920 my $offset = $args{offset} || 0;
1921 my $pref_lang = $args{preferred_language} || 'eng';
1922 my $visibility_limit = $args{visibility_limit} || 5000;
1925 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1928 if (! scalar( keys %{$args{searches}} )) {
1929 die "No search arguments were passed to ".$self->api_name;
1932 my $outer_limit = 1000;
1934 my $limit_clause = '';
1935 my $offset_clause = '';
1937 $limit_clause = "LIMIT $outer_limit";
1938 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1940 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1941 my ($t_filter, $f_filter, $v_filter) = ('','','');
1942 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1943 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1944 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1946 if ($args{available}) {
1947 $avail_filter = ' AND cp.status IN (0,7,12)';
1950 if (my $a = $args{audience}) {
1951 $a = [$a] if (!ref($a));
1954 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1955 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1958 if (my $l = $args{language}) {
1959 $l = [$l] if (!ref($l));
1962 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1963 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1966 if (my $f = $args{lit_form}) {
1967 $f = [$f] if (!ref($f));
1970 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1971 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1974 if (my $f = $args{item_form}) {
1975 $f = [$f] if (!ref($f));
1978 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1979 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1982 if (my $t = $args{item_type}) {
1983 $t = [$t] if (!ref($t));
1986 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1987 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1990 if (my $v = $args{vr_format}) {
1991 $v = [$v] if (!ref($v));
1994 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1995 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1998 # XXX legacy format and item type support
1999 if ($args{format}) {
2000 my ($t, $f) = split '-', $args{format};
2001 @types = split '', $t;
2002 @forms = split '', $f;
2004 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2005 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2009 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2010 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2015 my $descendants = defined($ou_type) ?
2016 "actor.org_unit_descendants($ou, $ou_type)" :
2017 "actor.org_unit_descendants($ou)";
2019 my $search_table_list = '';
2021 my $join_table_list = '';
2024 my $field_table = config::metabib_field->table;
2028 my $prev_search_group;
2029 my $curr_search_group;
2033 for my $search_group (sort keys %{$args{searches}}) {
2034 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2035 ($search_class,$search_field) = split /\|/, $search_group;
2036 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2038 if ($search_field) {
2039 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2040 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2045 $prev_search_group = $curr_search_group if ($curr_search_group);
2047 $curr_search_group = $search_group_name;
2049 my $class = $_cdbi->{$search_class};
2050 my $search_table = $class->table;
2052 my ($index_col) = $class->columns('FTS');
2053 $index_col ||= 'value';
2056 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2058 my $fts_where = $fts->sql_where_clause;
2059 my @fts_ranks = $fts->fts_rank;
2061 my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2062 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2063 my $first_word = lc(($fts->words)[0]).'%';
2065 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2066 my $rank = join(' + ', @fts_ranks);
2069 $bonus{'subject'} = [];
2070 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2072 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2074 $bonus{'series'} = [
2075 { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2076 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2079 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2082 push @{ $bonus{'title'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2083 push @{ $bonus{'author'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2084 push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2085 push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2086 push @{ $bonus{'series'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2089 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2090 $bonus_list ||= '1';
2092 push @bonus_lists, $bonus_list;
2093 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2095 #---------------------
2097 $search_table_list .= "$search_table $search_group_name, ";
2098 push @rank_list,$rank;
2099 $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2101 if ($metabib_field) {
2102 $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2103 $metabib_field = undef;
2106 if ($prev_search_group) {
2107 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2111 my $metabib_record_descriptor = metabib::record_descriptor->table;
2112 my $metabib_full_rec = metabib::full_rec->table;
2113 my $metabib_metarecord = metabib::metarecord->table;
2114 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2115 my $asset_call_number_table = asset::call_number->table;
2116 my $asset_copy_table = asset::copy->table;
2117 my $cs_table = config::copy_status->table;
2118 my $cl_table = asset::copy_location->table;
2119 my $br_table = biblio::record_entry->table;
2120 my $source_table = config::bib_source->table;
2123 my $bonuses = join (' * ', @bonus_lists);
2124 my $relevance = join (' + ', @rank_list);
2125 $relevance = "AVG( ($relevance) * ($bonuses) )";
2127 my $string_default_sort = 'zzzz';
2128 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2130 my $number_default_sort = '9999';
2131 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2133 my $rank = $relevance;
2134 if (lc($sort) eq 'pubdate') {
2137 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d{4}'),'$number_default_sort')::INT
2138 FROM $metabib_full_rec frp
2139 WHERE frp.record = b.id
2141 AND frp.subfield = 'c'
2145 } elsif (lc($sort) eq 'create_date') {
2147 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2149 } elsif (lc($sort) eq 'edit_date') {
2151 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2153 } elsif (lc($sort) eq 'title') {
2156 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2157 FROM $metabib_full_rec frt
2158 WHERE frt.record = b.id
2160 AND frt.subfield = 'a'
2164 } elsif (lc($sort) eq 'author') {
2167 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
2168 FROM $metabib_full_rec fra
2169 WHERE fra.record = b.id
2170 AND fra.tag LIKE '1%'
2171 AND fra.subfield = 'a'
2172 ORDER BY fra.tag::text::int
2177 push @bonus_values, @bonus_values;
2182 my $select = <<" SQL";
2187 FROM $search_table_list
2188 $metabib_record_descriptor rd,
2191 WHERE rd.record = b.id
2192 AND b.active IS TRUE
2193 AND b.deleted IS FALSE
2202 GROUP BY b.id, b.source
2203 ORDER BY 3 $sort_dir
2204 LIMIT $visibility_limit
2207 if ($self->api_name !~ /staff/o) {
2212 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2215 FROM $asset_call_number_table cn,
2216 $asset_copy_table cp,
2220 WHERE cn.record = s.id
2221 AND cp.status = cs.id
2222 AND cp.location = cl.id
2223 AND cp.call_number = cn.id
2224 AND cp.opac_visible IS TRUE
2225 AND cs.opac_visible IS TRUE
2226 AND cl.opac_visible IS TRUE
2227 AND d.opac_visible IS TRUE
2228 AND cp.deleted IS FALSE
2229 AND cn.deleted IS FALSE
2230 AND cp.circ_lib = d.id
2234 OR src.transcendant IS TRUE
2235 ORDER BY 3 $sort_dir
2242 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2245 FROM $asset_call_number_table cn,
2246 $asset_copy_table cp,
2248 WHERE cn.record = s.id
2249 AND cp.call_number = cn.id
2250 AND cn.deleted IS FALSE
2251 AND cp.circ_lib = d.id
2252 AND cp.deleted IS FALSE
2258 FROM $asset_call_number_table cn
2259 WHERE cn.record = s.id
2262 OR src.transcendant IS TRUE
2263 ORDER BY 3 $sort_dir
2268 $log->debug("Field Search SQL :: [$select]",DEBUG);
2270 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2272 @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2275 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2277 my $count = scalar(@$recs);
2278 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2279 next unless ($$rec[0]);
2280 my ($mrid,$rank) = @$rec;
2281 $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2286 __PACKAGE__->register_method(
2287 api_name => "open-ils.storage.biblio.multiclass.search_fts.record",
2288 method => 'biblio_search_multi_class_fts',
2293 __PACKAGE__->register_method(
2294 api_name => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2295 method => 'biblio_search_multi_class_fts',
2300 __PACKAGE__->register_method(
2301 api_name => "open-ils.storage.biblio.multiclass.search_fts",
2302 method => 'biblio_search_multi_class_fts',
2307 __PACKAGE__->register_method(
2308 api_name => "open-ils.storage.biblio.multiclass.search_fts.staff",
2309 method => 'biblio_search_multi_class_fts',
2317 my $default_preferred_language;
2318 my $default_preferred_language_weight;
2320 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2326 if (!$locale_map{COMPLETE}) {
2328 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2329 for my $locale ( @locales ) {
2330 $locale_map{lc($locale->code)} = $locale->marc_code;
2332 $locale_map{COMPLETE} = 1;
2336 my $config = OpenSRF::Utils::SettingsClient->new();
2338 if (!$default_preferred_language) {
2340 $default_preferred_language = $config->config_value(
2341 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2342 ) || $config->config_value(
2343 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2348 if (!$default_preferred_language_weight) {
2350 $default_preferred_language_weight = $config->config_value(
2351 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2352 ) || $config->config_value(
2353 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2357 # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2358 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2360 my $ou = $args{org_unit};
2361 my $limit = $args{limit} || 10;
2362 my $offset = $args{offset} || 0;
2365 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2368 if (! scalar( keys %{$args{searches}} )) {
2369 die "No search arguments were passed to ".$self->api_name;
2372 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2374 if (!defined($args{preferred_language})) {
2375 my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2376 $args{preferred_language} =
2377 $locale_map{ lc($ses_locale) } || 'eng';
2380 if (!defined($args{preferred_language_weight})) {
2381 $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2384 if ($args{available}) {
2385 @statuses = (0,7,12);
2388 if (my $s = $args{locations}) {
2389 $s = [$s] if (!ref($s));
2393 if (my $b = $args{between}) {
2394 if (ref($b) && @$b == 2) {
2399 if (my $s = $args{statuses}) {
2400 $s = [$s] if (!ref($s));
2404 if (my $a = $args{audience}) {
2405 $a = [$a] if (!ref($a));
2409 if (my $l = $args{language}) {
2410 $l = [$l] if (!ref($l));
2414 if (my $f = $args{lit_form}) {
2415 $f = [$f] if (!ref($f));
2419 if (my $f = $args{item_form}) {
2420 $f = [$f] if (!ref($f));
2424 if (my $t = $args{item_type}) {
2425 $t = [$t] if (!ref($t));
2429 if (my $b = $args{bib_level}) {
2430 $b = [$b] if (!ref($b));
2434 if (my $v = $args{vr_format}) {
2435 $v = [$v] if (!ref($v));
2439 # XXX legacy format and item type support
2440 if ($args{format}) {
2441 my ($t, $f) = split '-', $args{format};
2442 @types = split '', $t;
2443 @forms = split '', $f;
2446 my %stored_proc_search_args;
2447 for my $search_group (sort keys %{$args{searches}}) {
2448 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2449 my ($search_class,$search_field) = split /\|/, $search_group;
2450 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2452 if ($search_field) {
2453 unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2454 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2459 my $class = $_cdbi->{$search_class};
2460 my $search_table = $class->table;
2462 my ($index_col) = $class->columns('FTS');
2463 $index_col ||= 'value';
2466 my $fts = OpenILS::Application::Storage::FTS->compile(
2467 $search_class => $args{searches}{$search_group}{term},
2468 $search_group_name.'.value',
2469 "$search_group_name.$index_col"
2471 $fts->sql_where_clause; # this builds the ranks for us
2473 my @fts_ranks = $fts->fts_rank;
2474 my @fts_queries = $fts->fts_query;
2475 my @phrases = map { lc($_) } $fts->phrases;
2476 my @words = map { lc($_) } $fts->words;
2478 $stored_proc_search_args{$search_group} = {
2479 fts_rank => \@fts_ranks,
2480 fts_query => \@fts_queries,
2481 phrase => \@phrases,
2487 my $param_search_ou = $ou;
2488 my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2489 my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2490 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2491 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2492 my $param_audience = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud ) . '}$$';
2493 my $param_language = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang ) . '}$$';
2494 my $param_lit_form = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2495 my $param_types = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types ) . '}$$';
2496 my $param_forms = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms ) . '}$$';
2497 my $param_vformats = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2498 my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2499 my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2500 my $param_after = $args{after} ; $param_after = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2501 my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2502 my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2503 my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2504 my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2505 my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2506 my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2507 my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2508 my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2509 my $param_rel_limit = $args{core_limit}; $param_rel_limit ||= 'NULL';
2510 my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2511 my $param_skip_chk = $args{skip_check}; $param_skip_chk ||= 'NULL';
2513 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2515 FROM search.staged_fts(
2516 $param_search_ou\:\:INT,
2517 $param_depth\:\:INT,
2518 $param_searches\:\:TEXT,
2519 $param_statuses\:\:INT[],
2520 $param_locations\:\:INT[],
2521 $param_audience\:\:TEXT[],
2522 $param_language\:\:TEXT[],
2523 $param_lit_form\:\:TEXT[],
2524 $param_types\:\:TEXT[],
2525 $param_forms\:\:TEXT[],
2526 $param_vformats\:\:TEXT[],
2527 $param_bib_level\:\:TEXT[],
2528 $param_before\:\:TEXT,
2529 $param_after\:\:TEXT,
2530 $param_during\:\:TEXT,
2531 $param_between\:\:TEXT[],
2532 $param_pref_lang\:\:TEXT,
2533 $param_pref_lang_multiplier\:\:REAL,
2534 $param_sort\:\:TEXT,
2535 $param_sort_desc\:\:BOOL,
2536 $metarecord\:\:BOOL,
2538 $param_rel_limit\:\:INT,
2539 $param_chk_limit\:\:INT,
2540 $param_skip_chk\:\:INT
2546 my $recs = $sth->fetchall_arrayref({});
2547 my $summary_row = pop @$recs;
2549 my $total = $$summary_row{total};
2550 my $checked = $$summary_row{checked};
2551 my $visible = $$summary_row{visible};
2552 my $deleted = $$summary_row{deleted};
2553 my $excluded = $$summary_row{excluded};
2555 my $estimate = $visible;
2556 if ( $total > $checked && $checked ) {
2558 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2559 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2563 delete $$summary_row{id};
2564 delete $$summary_row{rel};
2565 delete $$summary_row{record};
2567 $client->respond( $summary_row );
2569 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2571 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2572 delete $$rec{checked};
2573 delete $$rec{visible};
2574 delete $$rec{excluded};
2575 delete $$rec{deleted};
2576 delete $$rec{total};
2577 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2579 $client->respond( $rec );
2583 __PACKAGE__->register_method(
2584 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
2585 method => 'staged_fts',
2590 __PACKAGE__->register_method(
2591 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2592 method => 'staged_fts',
2597 __PACKAGE__->register_method(
2598 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
2599 method => 'staged_fts',
2604 __PACKAGE__->register_method(
2605 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2606 method => 'staged_fts',
2612 sub FTS_paging_estimate {
2616 my $checked = shift;
2617 my $visible = shift;
2618 my $excluded = shift;
2619 my $deleted = shift;
2622 my $deleted_ratio = $deleted / $checked;
2623 my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2625 my $exclusion_ratio = $excluded / $checked;
2626 my $delete_adjusted_exclusion_ratio = $checked - $deleted ? $excluded / ($checked - $deleted) : 1;
2628 my $inclusion_ratio = $visible / $checked;
2629 my $delete_adjusted_inclusion_ratio = $checked - $deleted ? $visible / ($checked - $deleted) : 0;
2632 exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2633 inclusion => int($delete_adjusted_total * $inclusion_ratio),
2634 delete_adjusted_exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2635 delete_adjusted_inclusion => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2638 __PACKAGE__->register_method(
2639 api_name => "open-ils.storage.fts_paging_estimate",
2640 method => 'FTS_paging_estimate',
2646 Hash of estimation values based on four variant estimation strategies:
2647 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2648 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2649 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2650 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2653 Helper method used to determin the approximate number of
2654 hits for a search that spans multiple superpages. For
2655 sparse superpages, the inclusion estimate will likely be the
2656 best estimate. The exclusion strategy is the original, but
2657 inclusion is the default.
2660 { name => 'checked',
2661 desc => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2664 { name => 'visible',
2665 desc => 'Number of records visible to the search location on the current superpage.',
2668 { name => 'excluded',
2669 desc => 'Number of records excluded from the search location on the current superpage.',
2672 { name => 'deleted',
2673 desc => 'Number of deleted records on the current superpage.',
2677 desc => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2690 my $term = $$args{term};
2691 my $limit = $$args{max} || 1;
2692 my $min = $$args{min} || 1;
2693 my @classes = @{$$args{class}};
2695 $limit = $min if ($min > $limit);
2698 @classes = ( qw/ title author subject series keyword / );
2702 my $bre_table = biblio::record_entry->table;
2703 my $cn_table = asset::call_number->table;
2704 my $cp_table = asset::copy->table;
2706 for my $search_class ( @classes ) {
2708 my $class = $_cdbi->{$search_class};
2709 my $search_table = $class->table;
2711 my ($index_col) = $class->columns('FTS');
2712 $index_col ||= 'value';
2715 my $where = OpenILS::Application::Storage::FTS
2716 ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2720 SELECT COUNT(DISTINCT X.source)
2721 FROM (SELECT $search_class.source
2722 FROM $search_table $search_class
2723 JOIN $bre_table b ON (b.id = $search_class.source)
2728 HAVING COUNT(DISTINCT X.source) >= $min;
2731 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2732 $matches{$search_class} = $res ? $res->[0] : 0;
2737 __PACKAGE__->register_method(
2738 api_name => "open-ils.storage.search.xref",
2739 method => 'xref_count',
2743 # Takes an abstract query object and recursively turns it back into a string
2745 sub abstract_query2str {
2746 my ($self, $conn, $query) = @_;
2748 return QueryParser::Canonicalize::abstract_query2str_impl($query, 0);
2751 __PACKAGE__->register_method(
2752 api_name => "open-ils.storage.query_parser.abstract_query.canonicalize",
2753 method => "abstract_query2str",
2758 Abstract query parser object, with complete config data. For example input,
2759 see the 'abstract_query' part of the output of an API call like
2760 open-ils.search.biblio.multiclass.query, when called with the return_abstract
2764 return => { type => "string", desc => "String representation of abstract query object" }
2768 sub str2abstract_query {
2769 my ($self, $conn, $query, $qp_opts, $with_config) = @_;
2771 my %use_opts = ( # reasonable defaults? should these even be hardcoded here?
2773 superpage_size => 1000,
2774 core_limit => 25000,
2776 (ref $opts eq 'HASH' ? %$opts : ())
2781 # grab the query parser and initialize it
2782 my $parser = $OpenILS::Application::Storage::QParser;
2785 _initialize_parser($parser) unless $parser->initialization_complete;
2787 my $query = $parser->new(%use_opts)->parse;
2789 return $query->parse_tree->to_abstract_query(with_config => $with_config);
2792 __PACKAGE__->register_method(
2793 api_name => "open-ils.storage.query_parser.abstract_query.from_string",
2794 method => "str2abstract_query",
2798 {desc => "Query", type => "string"},
2799 {desc => q/Arguments for initializing QueryParser (optional)/,
2801 {desc => q/Flag enabling inclusion of QP config in returned object (optional, default false)/,
2804 return => { type => "object", desc => "abstract representation of query parser query" }
2808 sub query_parser_fts {
2814 # grab the query parser and initialize it
2815 my $parser = $OpenILS::Application::Storage::QParser;
2818 _initialize_parser($parser) unless $parser->initialization_complete;
2820 # populate the locale/language map
2821 if (!$locale_map{COMPLETE}) {
2823 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2824 for my $locale ( @locales ) {
2825 $locale_map{lc($locale->code)} = $locale->marc_code;
2827 $locale_map{COMPLETE} = 1;
2831 # I hope we have a query!
2832 if (! $args{query} ) {
2833 die "No query was passed to ".$self->api_name;
2836 my $default_CD_modifiers = OpenSRF::Utils::SettingsClient->new->config_value(
2837 apps => 'open-ils.search' => app_settings => 'default_CD_modifiers'
2840 # Protect against empty / missing default_CD_modifiers setting
2841 if ($default_CD_modifiers and !ref($default_CD_modifiers)) {
2842 $args{query} = "$default_CD_modifiers $args{query}";
2845 my $simple_plan = $args{_simple_plan};
2846 # remove bad chunks of the %args hash
2847 for my $bad ( grep { /^_/ } keys(%args)) {
2848 delete($args{$bad});
2852 # parse the query and supply any query-level %arg-based defaults
2853 # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2854 my $query = $parser->new( %args )->parse;
2856 my $config = OpenSRF::Utils::SettingsClient->new();
2858 # set the locale-based default preferred location
2859 if (!$query->parse_tree->find_filter('preferred_language')) {
2860 $parser->default_preferred_language( $args{preferred_language} );
2862 if (!$parser->default_preferred_language) {
2863 my $ses_locale = $client->session ? $client->session->session_locale : '';
2864 $parser->default_preferred_language( $locale_map{ lc($ses_locale) } );
2867 if (!$parser->default_preferred_language) { # still nothing...
2868 my $tmp_dpl = $config->config_value(
2869 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2870 ) || $config->config_value(
2871 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2874 $parser->default_preferred_language( $tmp_dpl )
2879 # set the global default language multiplier
2880 if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2883 if ($tmp_dplw = $args{preferred_language_weight} || $args{preferred_language_multiplier} ) {
2884 $parser->default_preferred_language_multiplier($tmp_dplw);
2887 $tmp_dplw = $config->config_value(
2888 apps => 'open-ils.search' => app_settings => 'default_preferred_language_weight'
2889 ) || $config->config_value(
2890 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2893 $parser->default_preferred_language_multiplier( $tmp_dplw );
2897 # gather the site, if one is specified, defaulting to the in-query version
2898 my $ou = $args{org_unit};
2899 if (my ($filter) = $query->parse_tree->find_filter('site')) {
2900 $ou = $filter->args->[0] if (@{$filter->args});
2902 $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^(-)?\d+$/);
2904 # gather lasso, as with $ou
2905 my $lasso = $args{lasso};
2906 if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
2907 $lasso = $filter->args->[0] if (@{$filter->args});
2909 $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
2910 $lasso = -$lasso if ($lasso);
2913 # # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
2914 # # gather user lasso, as with $ou and lasso
2915 # my $mylasso = $args{my_lasso};
2916 # if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
2917 # $mylasso = $filter->args->[0] if (@{$filter->args});
2919 # $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
2922 # if we have a lasso, go with that, otherwise ... ou
2923 $ou = $lasso if ($lasso);
2925 # gather the preferred OU, if one is specified, as with $ou
2926 my $pref_ou = $args{pref_ou};
2927 $log->info("pref_ou = $pref_ou");
2928 if (my ($filter) = $query->parse_tree->find_filter('pref_ou')) {
2929 $pref_ou = $filter->args->[0] if (@{$filter->args});
2931 $pref_ou = actor::org_unit->search( { shortname => $pref_ou } )->next->id if ($pref_ou and $pref_ou !~ /^(-)?\d+$/);
2933 # get the default $ou if we have nothing
2934 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
2937 # 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
2938 # gather the depth, if one is specified, defaulting to the in-query version
2939 my $depth = $args{depth};
2940 if (my ($filter) = $query->parse_tree->find_filter('depth')) {
2941 $depth = $filter->args->[0] if (@{$filter->args});
2943 $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
2946 # gather the limit or default to 10
2947 my $limit = $args{check_limit} || 'NULL';
2948 if (my ($filter) = $query->parse_tree->find_filter('limit')) {
2949 $limit = $filter->args->[0] if (@{$filter->args});
2951 if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
2952 $limit = $filter->args->[0] if (@{$filter->args});
2956 # gather the offset or default to 0
2957 my $offset = $args{skip_check} || $args{offset} || 0;
2958 if (my ($filter) = $query->parse_tree->find_filter('offset')) {
2959 $offset = $filter->args->[0] if (@{$filter->args});
2961 if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
2962 $offset = $filter->args->[0] if (@{$filter->args});
2966 # gather the estimation strategy or default to inclusion
2967 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2968 if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
2969 $estimation_strategy = $filter->args->[0] if (@{$filter->args});
2973 # gather the estimation strategy or default to inclusion
2974 my $core_limit = $args{core_limit};
2975 if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
2976 $core_limit = $filter->args->[0] if (@{$filter->args});
2980 # gather statuses, and then forget those if we have an #available modifier
2982 if (my ($filter) = $query->parse_tree->find_filter('statuses')) {
2983 @statuses = @{$filter->args} if (@{$filter->args});
2985 @statuses = (0,7,12) if ($query->parse_tree->find_modifier('available'));
2990 if (my ($filter) = $query->parse_tree->find_filter('locations')) {
2991 @location = @{$filter->args} if (@{$filter->args});
2994 # gather location_groups
2995 if (my ($filter) = $query->parse_tree->find_filter('location_groups')) {
2996 my @loc_groups = @{$filter->args} if (@{$filter->args});
2998 # collect the mapped locations and add them to the locations() filter
3001 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3002 my $maps = $cstore->request(
3003 'open-ils.cstore.direct.asset.copy_location_group_map.search.atomic',
3004 {lgroup => \@loc_groups})->gather(1);
3006 push(@location, $_->location) for @$maps;
3011 my $param_check = $limit || $query->superpage_size || 'NULL';
3012 my $param_offset = $offset || 'NULL';
3013 my $param_limit = $core_limit || 'NULL';
3015 my $sp = $query->superpage || 1;
3017 $param_offset = ($sp - 1) * $sp_size;
3020 my $param_search_ou = $ou;
3021 my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
3022 my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
3023 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
3024 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
3025 my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
3026 my $deleted_search = ($query->parse_tree->find_modifier('deleted')) ? "'t'" : "'f'";
3027 my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
3028 my $param_pref_ou = $pref_ou || 'NULL';
3030 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3031 SELECT * -- bib search: $args{query}
3032 FROM search.query_parser_fts(
3033 $param_search_ou\:\:INT,
3034 $param_depth\:\:INT,
3035 $param_core_query\:\:TEXT,
3036 $param_statuses\:\:INT[],
3037 $param_locations\:\:INT[],
3038 $param_offset\:\:INT,
3039 $param_check\:\:INT,
3040 $param_limit\:\:INT,
3041 $metarecord\:\:BOOL,
3043 $deleted_search\:\:BOOL,
3044 $param_pref_ou\:\:INT
3050 my $recs = $sth->fetchall_arrayref({});
3051 my $summary_row = pop @$recs;
3053 my $total = $$summary_row{total};
3054 my $checked = $$summary_row{checked};
3055 my $visible = $$summary_row{visible};
3056 my $deleted = $$summary_row{deleted};
3057 my $excluded = $$summary_row{excluded};
3059 my $estimate = $visible;
3060 if ( $total > $checked && $checked ) {
3062 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
3063 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
3067 delete $$summary_row{id};
3068 delete $$summary_row{rel};
3069 delete $$summary_row{record};
3071 if (defined($simple_plan)) {
3072 $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3074 $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3077 $client->respond( $summary_row );
3079 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
3081 for my $rec (@$recs) {
3082 delete $$rec{checked};
3083 delete $$rec{visible};
3084 delete $$rec{excluded};
3085 delete $$rec{deleted};
3086 delete $$rec{total};
3087 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3089 $client->respond( $rec );
3093 __PACKAGE__->register_method(
3094 api_name => "open-ils.storage.query_parser_search",
3095 method => 'query_parser_fts',
3101 sub query_parser_fts_wrapper {
3106 $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3107 # grab the query parser and initialize it
3108 my $parser = $OpenILS::Application::Storage::QParser;
3111 _initialize_parser($parser) unless $parser->initialization_complete;
3113 if (! scalar( keys %{$args{searches}} )) {
3114 die "No search arguments were passed to ".$self->api_name;
3117 $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3118 my $base_query = '';
3119 for my $sclass ( keys %{$args{searches}} ) {
3120 $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3121 $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3124 my $query = $base_query;
3125 $log->debug("Full base query: $base_query", DEBUG);
3127 $query = "$args{facets} $query" if ($args{facets});
3129 if (!$locale_map{COMPLETE}) {
3131 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3132 for my $locale ( @locales ) {
3133 $locale_map{lc($locale->code)} = $locale->marc_code;
3135 $locale_map{COMPLETE} = 1;
3139 my $base_plan = $parser->new( query => $base_query )->parse;
3141 $query = "preferred_language($args{preferred_language}) $query"
3142 if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3143 $query = "preferred_language_weight($args{preferred_language_weight}) $query"
3144 if ($args{preferred_language_weight} and !$base_plan->parse_tree->find_filter('preferred_language_weight') and !$base_plan->parse_tree->find_filter('preferred_language_multiplier'));
3147 $query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
3148 $query = "site($args{org_unit}) $query" if ($args{org_unit});
3149 $query = "depth($args{depth}) $query" if (defined($args{depth}));
3150 $query = "sort($args{sort}) $query" if ($args{sort});
3151 $query = "limit($args{limit}) $query" if ($args{limit});
3152 $query = "core_limit($args{core_limit}) $query" if ($args{core_limit});
3153 $query = "skip_check($args{skip_check}) $query" if ($args{skip_check});
3154 $query = "superpage($args{superpage}) $query" if ($args{superpage});
3155 $query = "offset($args{offset}) $query" if ($args{offset});
3156 $query = "#metarecord $query" if ($self->api_name =~ /metabib/);
3157 $query = "#available $query" if ($args{available});
3158 $query = "#descending $query" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3159 $query = "#staff $query" if ($self->api_name =~ /staff/);
3160 $query = "before($args{before}) $query" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3161 $query = "after($args{after}) $query" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3162 $query = "during($args{during}) $query" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3163 $query = "between($args{between}[0],$args{between}[1]) $query"
3164 if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3167 my (@between,@statuses,@locations,@location_groups,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3169 # XXX legacy format and item type support
3170 if ($args{format}) {
3171 my ($t, $f) = split '-', $args{format};
3172 $args{item_type} = [ split '', $t ];
3173 $args{item_form} = [ split '', $f ];
3176 for my $filter ( qw/locations location_groups statuses between audience language lit_form item_form item_type bib_level vr_format/ ) {
3177 if (my $s = $args{$filter}) {
3178 $s = [$s] if (!ref($s));
3180 my @filter_list = @$s;
3182 next if ($filter eq 'between' and scalar(@filter_list) != 2);
3183 next if (@filter_list == 0);
3185 my $filter_string = join ',', @filter_list;
3186 $query = "$query $filter($filter_string)";
3190 $log->debug("Full QueryParser query: $query", DEBUG);
3192 return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan );
3194 __PACKAGE__->register_method(
3195 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
3196 method => 'query_parser_fts_wrapper',
3201 __PACKAGE__->register_method(
3202 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3203 method => 'query_parser_fts_wrapper',
3208 __PACKAGE__->register_method(
3209 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
3210 method => 'query_parser_fts_wrapper',
3215 __PACKAGE__->register_method(
3216 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3217 method => 'query_parser_fts_wrapper',