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 {
85 my (@types,@forms,@blvl);
88 my ($t, $f, $b) = split '-', $formats;
89 @types = split '', $t;
90 @forms = split '', $f;
96 "actor.org_unit_descendants($org, $depth)" :
97 "actor.org_unit_descendants($org)" ;
100 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';
101 $copies_visible = '' if ($self->api_name =~ /staff/o);
103 my $sm_table = metabib::metarecord_source_map->table;
104 my $rd_table = metabib::record_descriptor->table;
105 my $fr_table = metabib::full_rec->table;
106 my $cn_table = asset::call_number->table;
107 my $cl_table = asset::copy_location->table;
108 my $cp_table = asset::copy->table;
109 my $cs_table = config::copy_status->table;
110 my $src_table = config::bib_source->table;
111 my $out_table = actor::org_unit_type->table;
112 my $br_table = biblio::record_entry->table;
119 FIRST(COALESCE(LTRIM(SUBSTR( value, COALESCE(SUBSTRING(ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'zzzzzzzz')) AS title
131 if ($copies_visible) {
137 WHERE rd.record = sm.source
138 AND fr.record = sm.source
139 AND br.id = sm.source
140 AND sm.metarecord = ?
141 AND (EXISTS ((SELECT 1
143 JOIN $cn_table cn ON (cp.call_number = cn.id)
144 JOIN $cs_table cs ON (cp.status = cs.id)
145 JOIN $cl_table cl ON (cp.location = cl.id)
146 JOIN $descendants d ON (cp.circ_lib = d.id)
147 WHERE cn.record = sm.source
153 WHERE src.id = br.source
154 AND src.transcendant IS TRUE))
161 JOIN $br_table br ON (sm.source = br.id)
162 JOIN $fr_table fr ON (fr.record = br.id)
163 JOIN $rd_table rd ON (rd.record = br.id)
164 WHERE sm.metarecord = ?
170 WHERE cn.record = br.id
171 AND cn.deleted = FALSE
172 AND cp.deleted = FALSE
173 AND cp.circ_lib = d.id
174 AND cn.id = cp.call_number
180 WHERE cn.record = br.id
181 AND cn.deleted = FALSE
182 AND cp.deleted = FALSE
183 AND cn.id = cp.call_number
189 WHERE src.id = br.source
190 AND src.transcendant IS TRUE))
196 $sql .= ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
200 $sql .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
204 $sql .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
214 GROUP BY record, item_type, item_form, quality
217 WHEN item_type IS NULL -- default
219 WHEN item_type = '' -- default
221 WHEN item_type IN ('a','t') -- books
223 WHEN item_type = 'g' -- movies
225 WHEN item_type IN ('i','j') -- sound recordings
227 WHEN item_type = 'm' -- software
229 WHEN item_type = 'k' -- images
231 WHEN item_type IN ('e','f') -- maps
233 WHEN item_type IN ('o','p') -- mixed
235 WHEN item_type IN ('c','d') -- music
237 WHEN item_type = 'r' -- 3d
244 my $ids = metabib::metarecord_source_map->db_Main->selectcol_arrayref($sql, {}, "$mr", @types, @forms, @blvl);
245 return $ids if ($self->api_name =~ /atomic$/o);
247 $client->respond( $_ ) for ( @$ids );
251 __PACKAGE__->register_method(
252 api_name => 'open-ils.storage.ordered.metabib.metarecord.records',
253 method => 'ordered_records_from_metarecord',
257 __PACKAGE__->register_method(
258 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff',
259 method => 'ordered_records_from_metarecord',
264 __PACKAGE__->register_method(
265 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.atomic',
266 method => 'ordered_records_from_metarecord',
270 __PACKAGE__->register_method(
271 api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic',
272 method => 'ordered_records_from_metarecord',
277 # XXX: this subroutine and its two registered methods are marked for
278 # deprecation, as they do not work properly in 2.x (these tags are no longer
279 # normalized in mfr) and are not in known use
283 my $isxn = lc(shift());
287 $isxn =~ s/-//o if ($self->api_name =~ /isbn/o);
289 my $tag = ($self->api_name =~ /isbn/o) ? "'020' OR f.tag = '024'" : "'022'";
291 my $fr_table = metabib::full_rec->table;
292 my $bib_table = biblio::record_entry->table;
295 SELECT DISTINCT f.record
297 JOIN $bib_table b ON (b.id = f.record)
300 AND b.deleted IS FALSE
303 my $list = metabib::full_rec->db_Main->selectcol_arrayref($sql, {}, "$isxn%");
304 $client->respond($_) for (@$list);
307 __PACKAGE__->register_method(
308 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
309 method => 'isxn_search',
313 __PACKAGE__->register_method(
314 api_name => 'open-ils.storage.id_list.biblio.record_entry.search.issn',
315 method => 'isxn_search',
320 sub metarecord_copy_count {
326 my $sm_table = metabib::metarecord_source_map->table;
327 my $rd_table = metabib::record_descriptor->table;
328 my $cn_table = asset::call_number->table;
329 my $cp_table = asset::copy->table;
330 my $br_table = biblio::record_entry->table;
331 my $src_table = config::bib_source->table;
332 my $cl_table = asset::copy_location->table;
333 my $cs_table = config::copy_status->table;
334 my $out_table = actor::org_unit_type->table;
336 my $descendants = "actor.org_unit_descendants(u.id)";
337 my $ancestors = "actor.org_unit_ancestors(?) u JOIN $out_table t ON (u.ou_type = t.id)";
339 if ($args{org_unit} < 0) {
340 $args{org_unit} *= -1;
341 $ancestors = "(select org_unit as id from actor.org_lasso_map where lasso = ?) u CROSS JOIN (SELECT -1 AS depth) t";
344 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';
345 $copies_visible = '' if ($self->api_name =~ /staff/o);
347 my (@types,@forms,@blvl);
348 my ($t_filter, $f_filter, $b_filter) = ('','','');
351 my ($t, $f, $b) = split '-', $args{format};
352 @types = split '', $t;
353 @forms = split '', $f;
354 @blvl = split '', $b;
357 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
361 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
365 $b_filter .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
375 JOIN $cn_table cn ON (cn.record = r.source)
376 JOIN $rd_table rd ON (cn.record = rd.record)
377 JOIN $cp_table cp ON (cn.id = cp.call_number)
378 JOIN $cs_table cs ON (cp.status = cs.id)
379 JOIN $cl_table cl ON (cp.location = cl.id)
380 JOIN $descendants a ON (cp.circ_lib = a.id)
381 WHERE r.metarecord = ?
382 AND cn.deleted IS FALSE
383 AND cp.deleted IS FALSE
393 JOIN $cn_table cn ON (cn.record = r.source)
394 JOIN $rd_table rd ON (cn.record = rd.record)
395 JOIN $cp_table cp ON (cn.id = cp.call_number)
396 JOIN $cs_table cs ON (cp.status = cs.id)
397 JOIN $cl_table cl ON (cp.location = cl.id)
398 JOIN $descendants a ON (cp.circ_lib = a.id)
399 WHERE r.metarecord = ?
400 AND cp.status IN (0,7,12)
401 AND cn.deleted IS FALSE
402 AND cp.deleted IS FALSE
412 JOIN $cn_table cn ON (cn.record = r.source)
413 JOIN $rd_table rd ON (cn.record = rd.record)
414 JOIN $cp_table cp ON (cn.id = cp.call_number)
415 JOIN $cs_table cs ON (cp.status = cs.id)
416 JOIN $cl_table cl ON (cp.location = cl.id)
417 WHERE r.metarecord = ?
418 AND cn.deleted IS FALSE
419 AND cp.deleted IS FALSE
420 AND cp.opac_visible IS TRUE
421 AND cs.opac_visible IS TRUE
422 AND cl.opac_visible IS TRUE
431 JOIN $br_table br ON (br.id = r.source)
432 JOIN $src_table src ON (src.id = br.source)
433 WHERE r.metarecord = ?
434 AND src.transcendant IS TRUE
442 my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
443 $sth->execute( ''.$args{metarecord},
447 ''.$args{metarecord},
451 ''.$args{metarecord},
455 ''.$args{metarecord},
459 while ( my $row = $sth->fetchrow_hashref ) {
460 $client->respond( $row );
464 __PACKAGE__->register_method(
465 api_name => 'open-ils.storage.metabib.metarecord.copy_count',
466 method => 'metarecord_copy_count',
471 __PACKAGE__->register_method(
472 api_name => 'open-ils.storage.metabib.metarecord.copy_count.staff',
473 method => 'metarecord_copy_count',
479 sub biblio_multi_search_full_rec {
484 my $class_join = $args{class_join} || 'AND';
485 my $limit = $args{limit} || 100;
486 my $offset = $args{offset} || 0;
487 my $sort = $args{'sort'};
488 my $sort_dir = $args{sort_dir} || 'DESC';
493 for my $arg (@{ $args{searches} }) {
494 my $term = $$arg{term};
495 my $limiters = $$arg{restrict};
497 my ($index_col) = metabib::full_rec->columns('FTS');
498 $index_col ||= 'value';
499 my $search_table = metabib::full_rec->table;
501 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
503 my $fts_where = $fts->sql_where_clause();
504 my @fts_ranks = $fts->fts_rank;
506 my $rank = join(' + ', @fts_ranks);
509 for my $limit (@$limiters) {
510 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
511 # MARC control field; mfr.subfield is NULL
512 push @wheres, "( tag = ? AND $fts_where )";
513 push @binds, $$limit{tag};
514 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
516 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
517 push @binds, $$limit{tag}, $$limit{subfield};
518 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
521 my $where = join(' OR ', @wheres);
523 push @selects, "SELECT record, AVG($rank) as sum FROM $search_table WHERE $where GROUP BY record";
527 my $descendants = defined($args{depth}) ?
528 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
529 "actor.org_unit_descendants($args{org_unit})" ;
532 my $metabib_record_descriptor = metabib::record_descriptor->table;
533 my $metabib_full_rec = metabib::full_rec->table;
534 my $asset_call_number_table = asset::call_number->table;
535 my $asset_copy_table = asset::copy->table;
536 my $cs_table = config::copy_status->table;
537 my $cl_table = asset::copy_location->table;
538 my $br_table = biblio::record_entry->table;
540 my $cj = 'HAVING COUNT(x.record) = ' . scalar(@selects) if ($class_join eq 'AND');
542 '(SELECT x.record, sum(x.sum) FROM (('.
543 join(') UNION ALL (', @selects).
544 ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
546 my $has_vols = 'AND cn.owning_lib = d.id';
547 my $has_copies = 'AND cp.call_number = cn.id';
548 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';
550 if ($self->api_name =~ /staff/o) {
551 $copies_visible = '';
552 $has_copies = '' if ($ou_type == 0);
553 $has_vols = '' if ($ou_type == 0);
556 my ($t_filter, $f_filter) = ('','');
557 my ($a_filter, $l_filter, $lf_filter) = ('','','');
560 if (my $a = $args{audience}) {
561 $a = [$a] if (!ref($a));
564 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
569 if (my $l = $args{language}) {
570 $l = [$l] if (!ref($l));
573 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
578 if (my $f = $args{lit_form}) {
579 $f = [$f] if (!ref($f));
582 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
583 push @binds, @lit_form;
587 if (my $f = $args{item_form}) {
588 $f = [$f] if (!ref($f));
591 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
596 if (my $t = $args{item_type}) {
597 $t = [$t] if (!ref($t));
600 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
607 my ($t, $f) = split '-', $args{format};
608 my @types = split '', $t;
609 my @forms = split '', $f;
611 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
616 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
619 push @binds, @types, @forms;
622 my $relevance = 'sum(f.sum)';
623 $relevance = 1 if (!$copies_visible);
625 my $rank = $relevance;
626 if (lc($sort) eq 'pubdate') {
629 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'9999')::INT
630 FROM $metabib_full_rec frp
631 WHERE frp.record = f.record
633 AND frp.subfield = 'c'
637 } elsif (lc($sort) eq 'create_date') {
639 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
641 } elsif (lc($sort) eq 'edit_date') {
643 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
645 } elsif (lc($sort) eq 'title') {
648 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'zzzzzzzz')
649 FROM $metabib_full_rec frt
650 WHERE frt.record = f.record
652 AND frt.subfield = 'a'
656 } elsif (lc($sort) eq 'author') {
659 SELECT COALESCE(LTRIM(fra.value),'zzzzzzzz')
660 FROM $metabib_full_rec fra
661 WHERE fra.record = f.record
662 AND fra.tag LIKE '1%'
663 AND fra.subfield = 'a'
664 ORDER BY fra.tag::text::int
672 my $rd_join = $use_rd ? "$metabib_record_descriptor rd," : '';
673 my $rd_filter = $use_rd ? 'AND rd.record = f.record' : '';
675 if ($copies_visible) {
677 SELECT f.record, $relevance, count(DISTINCT cp.id), $rank
678 FROM $search_table f,
679 $asset_call_number_table cn,
680 $asset_copy_table cp,
686 WHERE br.id = f.record
687 AND cn.record = f.record
688 AND cp.status = cs.id
689 AND cp.location = cl.id
690 AND br.deleted IS FALSE
691 AND cn.deleted IS FALSE
692 AND cp.deleted IS FALSE
702 GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
703 ORDER BY 4 $sort_dir,3 DESC
707 SELECT f.record, 1, 1, $rank
708 FROM $search_table f,
711 WHERE br.id = f.record
712 AND br.deleted IS FALSE
725 $log->debug("Search SQL :: [$select]",DEBUG);
727 my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
728 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
731 $max = 1 if (!@$recs);
733 $max = $$_[1] if ($$_[1] > $max);
737 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
738 next unless ($$rec[0]);
739 my ($rid,$rank,$junk,$skip) = @$rec;
740 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
744 __PACKAGE__->register_method(
745 api_name => 'open-ils.storage.biblio.full_rec.multi_search',
746 method => 'biblio_multi_search_full_rec',
751 __PACKAGE__->register_method(
752 api_name => 'open-ils.storage.biblio.full_rec.multi_search.staff',
753 method => 'biblio_multi_search_full_rec',
759 sub search_full_rec {
765 my $term = $args{term};
766 my $limiters = $args{restrict};
768 my ($index_col) = metabib::full_rec->columns('FTS');
769 $index_col ||= 'value';
770 my $search_table = metabib::full_rec->table;
772 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
774 my $fts_where = $fts->sql_where_clause();
775 my @fts_ranks = $fts->fts_rank;
777 my $rank = join(' + ', @fts_ranks);
781 for my $limit (@$limiters) {
782 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
783 # MARC control field; mfr.subfield is NULL
784 push @wheres, "( tag = ? AND $fts_where )";
785 push @binds, $$limit{tag};
786 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
788 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
789 push @binds, $$limit{tag}, $$limit{subfield};
790 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
793 my $where = join(' OR ', @wheres);
795 my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
797 $log->debug("Search SQL :: [$select]",DEBUG);
799 my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
800 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
802 $client->respond($_) for (@$recs);
805 __PACKAGE__->register_method(
806 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
807 method => 'search_full_rec',
812 __PACKAGE__->register_method(
813 api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
814 method => 'search_full_rec',
821 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
822 sub search_class_fts {
827 my $term = $args{term};
828 my $ou = $args{org_unit};
829 my $ou_type = $args{depth};
830 my $limit = $args{limit};
831 my $offset = $args{offset};
833 my $limit_clause = '';
834 my $offset_clause = '';
836 $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
837 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
840 my ($t_filter, $f_filter) = ('','');
843 my ($t, $f) = split '-', $args{format};
844 @types = split '', $t;
845 @forms = split '', $f;
847 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
851 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
857 my $descendants = defined($ou_type) ?
858 "actor.org_unit_descendants($ou, $ou_type)" :
859 "actor.org_unit_descendants($ou)";
861 my $class = $self->{cdbi};
862 my $search_table = $class->table;
864 my $metabib_record_descriptor = metabib::record_descriptor->table;
865 my $metabib_metarecord = metabib::metarecord->table;
866 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
867 my $asset_call_number_table = asset::call_number->table;
868 my $asset_copy_table = asset::copy->table;
869 my $cs_table = config::copy_status->table;
870 my $cl_table = asset::copy_location->table;
872 my ($index_col) = $class->columns('FTS');
873 $index_col ||= 'value';
875 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
876 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
878 my $fts_where = $fts->sql_where_clause;
879 my @fts_ranks = $fts->fts_rank;
881 my $rank = join(' + ', @fts_ranks);
883 my $has_vols = 'AND cn.owning_lib = d.id';
884 my $has_copies = 'AND cp.call_number = cn.id';
885 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';
887 my $visible_count = ', count(DISTINCT cp.id)';
888 my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
890 if ($self->api_name =~ /staff/o) {
891 $copies_visible = '';
892 $visible_count_test = '';
893 $has_copies = '' if ($ou_type == 0);
894 $has_vols = '' if ($ou_type == 0);
897 my $rank_calc = <<" RANK";
899 * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
900 * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
901 * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
902 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
905 $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
907 if ($copies_visible) {
909 SELECT m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
910 FROM $search_table f,
911 $metabib_metarecord_source_map_table m,
912 $asset_call_number_table cn,
913 $asset_copy_table cp,
916 $metabib_record_descriptor rd,
919 AND m.source = f.source
920 AND cn.record = m.source
921 AND rd.record = m.source
922 AND cp.status = cs.id
923 AND cp.location = cl.id
929 GROUP BY 1 $visible_count_test
931 $limit_clause $offset_clause
935 SELECT m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
936 FROM $search_table f,
937 $metabib_metarecord_source_map_table m,
938 $metabib_record_descriptor rd
940 AND m.source = f.source
941 AND rd.record = m.source
946 $limit_clause $offset_clause
950 $log->debug("Field Search SQL :: [$select]",DEBUG);
952 my $SQLstring = join('%',$fts->words);
953 my $REstring = join('\\s+',$fts->words);
954 my $first_word = ($fts->words)[0].'%';
955 my $recs = ($self->api_name =~ /unordered/o) ?
956 $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
957 $class->db_Main->selectall_arrayref($select, {},
958 '%'.lc($SQLstring).'%', # phrase order match
959 lc($first_word), # first word match
960 '^\\s*'.lc($REstring).'\\s*/?\s*$', # full exact match
964 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
966 $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
970 for my $class ( qw/title author subject keyword series identifier/ ) {
971 __PACKAGE__->register_method(
972 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord",
973 method => 'search_class_fts',
976 cdbi => "metabib::${class}_field_entry",
979 __PACKAGE__->register_method(
980 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
981 method => 'search_class_fts',
984 cdbi => "metabib::${class}_field_entry",
987 __PACKAGE__->register_method(
988 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
989 method => 'search_class_fts',
992 cdbi => "metabib::${class}_field_entry",
995 __PACKAGE__->register_method(
996 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
997 method => 'search_class_fts',
1000 cdbi => "metabib::${class}_field_entry",
1005 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1006 sub search_class_fts_count {
1011 my $term = $args{term};
1012 my $ou = $args{org_unit};
1013 my $ou_type = $args{depth};
1014 my $limit = $args{limit} || 100;
1015 my $offset = $args{offset} || 0;
1017 my $descendants = defined($ou_type) ?
1018 "actor.org_unit_descendants($ou, $ou_type)" :
1019 "actor.org_unit_descendants($ou)";
1022 my ($t_filter, $f_filter) = ('','');
1024 if ($args{format}) {
1025 my ($t, $f) = split '-', $args{format};
1026 @types = split '', $t;
1027 @forms = split '', $f;
1029 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1033 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1038 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
1040 my $class = $self->{cdbi};
1041 my $search_table = $class->table;
1043 my $metabib_record_descriptor = metabib::record_descriptor->table;
1044 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1045 my $asset_call_number_table = asset::call_number->table;
1046 my $asset_copy_table = asset::copy->table;
1047 my $cs_table = config::copy_status->table;
1048 my $cl_table = asset::copy_location->table;
1050 my ($index_col) = $class->columns('FTS');
1051 $index_col ||= 'value';
1053 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
1055 my $fts_where = $fts->sql_where_clause;
1057 my $has_vols = 'AND cn.owning_lib = d.id';
1058 my $has_copies = 'AND cp.call_number = cn.id';
1059 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';
1060 if ($self->api_name =~ /staff/o) {
1061 $copies_visible = '';
1062 $has_vols = '' if ($ou_type == 0);
1063 $has_copies = '' if ($ou_type == 0);
1066 # XXX test an "EXISTS version of descendant checking...
1068 if ($copies_visible) {
1070 SELECT count(distinct m.metarecord)
1071 FROM $search_table f,
1072 $metabib_metarecord_source_map_table m,
1073 $metabib_metarecord_source_map_table mr,
1074 $asset_call_number_table cn,
1075 $asset_copy_table cp,
1078 $metabib_record_descriptor rd,
1081 AND mr.source = f.source
1082 AND mr.metarecord = m.metarecord
1083 AND cn.record = m.source
1084 AND rd.record = m.source
1085 AND cp.status = cs.id
1086 AND cp.location = cl.id
1095 SELECT count(distinct m.metarecord)
1096 FROM $search_table f,
1097 $metabib_metarecord_source_map_table m,
1098 $metabib_metarecord_source_map_table mr,
1099 $metabib_record_descriptor rd
1101 AND mr.source = f.source
1102 AND mr.metarecord = m.metarecord
1103 AND rd.record = m.source
1109 $log->debug("Field Search Count SQL :: [$select]",DEBUG);
1111 my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1113 $log->debug("Count Search yielded $recs results.",DEBUG);
1118 for my $class ( qw/title author subject keyword series identifier/ ) {
1119 __PACKAGE__->register_method(
1120 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
1121 method => 'search_class_fts_count',
1124 cdbi => "metabib::${class}_field_entry",
1127 __PACKAGE__->register_method(
1128 api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
1129 method => 'search_class_fts_count',
1132 cdbi => "metabib::${class}_field_entry",
1138 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1139 sub postfilter_search_class_fts {
1144 my $term = $args{term};
1145 my $sort = $args{'sort'};
1146 my $sort_dir = $args{sort_dir} || 'DESC';
1147 my $ou = $args{org_unit};
1148 my $ou_type = $args{depth};
1149 my $limit = $args{limit} || 10;
1150 my $visibility_limit = $args{visibility_limit} || 5000;
1151 my $offset = $args{offset} || 0;
1153 my $outer_limit = 1000;
1155 my $limit_clause = '';
1156 my $offset_clause = '';
1158 $limit_clause = "LIMIT $outer_limit";
1159 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1161 my (@types,@forms,@lang,@aud,@lit_form);
1162 my ($t_filter, $f_filter) = ('','');
1163 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1164 my ($ot_filter, $of_filter) = ('','');
1165 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1167 if (my $a = $args{audience}) {
1168 $a = [$a] if (!ref($a));
1171 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1172 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1175 if (my $l = $args{language}) {
1176 $l = [$l] if (!ref($l));
1179 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1180 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1183 if (my $f = $args{lit_form}) {
1184 $f = [$f] if (!ref($f));
1187 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1188 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1191 if ($args{format}) {
1192 my ($t, $f) = split '-', $args{format};
1193 @types = split '', $t;
1194 @forms = split '', $f;
1196 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1197 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1201 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1202 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1207 my $descendants = defined($ou_type) ?
1208 "actor.org_unit_descendants($ou, $ou_type)" :
1209 "actor.org_unit_descendants($ou)";
1211 my $class = $self->{cdbi};
1212 my $search_table = $class->table;
1214 my $metabib_full_rec = metabib::full_rec->table;
1215 my $metabib_record_descriptor = metabib::record_descriptor->table;
1216 my $metabib_metarecord = metabib::metarecord->table;
1217 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1218 my $asset_call_number_table = asset::call_number->table;
1219 my $asset_copy_table = asset::copy->table;
1220 my $cs_table = config::copy_status->table;
1221 my $cl_table = asset::copy_location->table;
1222 my $br_table = biblio::record_entry->table;
1224 my ($index_col) = $class->columns('FTS');
1225 $index_col ||= 'value';
1227 (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1229 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1231 my $SQLstring = join('%',map { lc($_) } $fts->words);
1232 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1233 my $first_word = lc(($fts->words)[0]).'%';
1235 my $fts_where = $fts->sql_where_clause;
1236 my @fts_ranks = $fts->fts_rank;
1239 $bonus{'metabib::identifier_field_entry'} =
1240 $bonus{'metabib::keyword_field_entry'} = [
1241 { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring }
1244 $bonus{'metabib::title_field_entry'} =
1245 $bonus{'metabib::series_field_entry'} = [
1246 { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1247 { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1248 @{ $bonus{'metabib::keyword_field_entry'} }
1251 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1252 $bonus_list ||= '1';
1254 my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1256 my $relevance = join(' + ', @fts_ranks);
1257 $relevance = <<" RANK";
1258 (SUM( ( $relevance ) * ( $bonus_list ) )/COUNT(m.source))
1261 my $string_default_sort = 'zzzz';
1262 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1264 my $number_default_sort = '9999';
1265 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1267 my $rank = $relevance;
1268 if (lc($sort) eq 'pubdate') {
1271 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1272 FROM $metabib_full_rec frp
1273 WHERE frp.record = mr.master_record
1275 AND frp.subfield = 'c'
1279 } elsif (lc($sort) eq 'create_date') {
1281 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1283 } elsif (lc($sort) eq 'edit_date') {
1285 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1287 } elsif (lc($sort) eq 'title') {
1290 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1291 FROM $metabib_full_rec frt
1292 WHERE frt.record = mr.master_record
1294 AND frt.subfield = 'a'
1298 } elsif (lc($sort) eq 'author') {
1301 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1302 FROM $metabib_full_rec fra
1303 WHERE fra.record = mr.master_record
1304 AND fra.tag LIKE '1%'
1305 AND fra.subfield = 'a'
1306 ORDER BY fra.tag::text::int
1314 my $select = <<" SQL";
1315 SELECT m.metarecord,
1317 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1319 FROM $search_table f,
1320 $metabib_metarecord_source_map_table m,
1321 $metabib_metarecord_source_map_table smrs,
1322 $metabib_metarecord mr,
1323 $metabib_record_descriptor rd
1325 AND smrs.metarecord = mr.id
1326 AND m.source = f.source
1327 AND m.metarecord = mr.id
1328 AND rd.record = smrs.source
1334 GROUP BY m.metarecord
1335 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1336 LIMIT $visibility_limit
1343 FROM $asset_call_number_table cn,
1344 $metabib_metarecord_source_map_table mrs,
1345 $asset_copy_table cp,
1350 $metabib_record_descriptor ord,
1352 WHERE mrs.metarecord = s.metarecord
1353 AND br.id = mrs.source
1354 AND cn.record = mrs.source
1355 AND cp.status = cs.id
1356 AND cp.location = cl.id
1357 AND cn.owning_lib = d.id
1358 AND cp.call_number = cn.id
1359 AND cp.opac_visible IS TRUE
1360 AND cs.opac_visible IS TRUE
1361 AND cl.opac_visible IS TRUE
1362 AND d.opac_visible IS TRUE
1363 AND br.active IS TRUE
1364 AND br.deleted IS FALSE
1365 AND ord.record = mrs.source
1371 ORDER BY 4 $sort_dir
1373 } elsif ($self->api_name !~ /staff/o) {
1380 FROM $asset_call_number_table cn,
1381 $metabib_metarecord_source_map_table mrs,
1382 $asset_copy_table cp,
1387 $metabib_record_descriptor ord
1389 WHERE mrs.metarecord = s.metarecord
1390 AND br.id = mrs.source
1391 AND cn.record = mrs.source
1392 AND cp.status = cs.id
1393 AND cp.location = cl.id
1394 AND cp.circ_lib = d.id
1395 AND cp.call_number = cn.id
1396 AND cp.opac_visible IS TRUE
1397 AND cs.opac_visible IS TRUE
1398 AND cl.opac_visible IS TRUE
1399 AND d.opac_visible IS TRUE
1400 AND br.active IS TRUE
1401 AND br.deleted IS FALSE
1402 AND ord.record = mrs.source
1410 ORDER BY 4 $sort_dir
1419 FROM $asset_call_number_table cn,
1420 $asset_copy_table cp,
1421 $metabib_metarecord_source_map_table mrs,
1424 $metabib_record_descriptor ord
1426 WHERE mrs.metarecord = s.metarecord
1427 AND br.id = mrs.source
1428 AND cn.record = mrs.source
1429 AND cn.id = cp.call_number
1430 AND br.deleted IS FALSE
1431 AND cn.deleted IS FALSE
1432 AND ord.record = mrs.source
1433 AND ( cn.owning_lib = d.id
1434 OR ( cp.circ_lib = d.id
1435 AND cp.deleted IS FALSE
1447 FROM $asset_call_number_table cn,
1448 $metabib_metarecord_source_map_table mrs,
1449 $metabib_record_descriptor ord
1450 WHERE mrs.metarecord = s.metarecord
1451 AND cn.record = mrs.source
1452 AND ord.record = mrs.source
1460 ORDER BY 4 $sort_dir
1465 $log->debug("Field Search SQL :: [$select]",DEBUG);
1467 my $recs = $class->db_Main->selectall_arrayref(
1469 (@bonus_values > 0 ? @bonus_values : () ),
1470 ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1471 @types, @forms, @aud, @lang, @lit_form,
1472 @types, @forms, @aud, @lang, @lit_form,
1473 ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1475 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1478 $max = 1 if (!@$recs);
1480 $max = $$_[1] if ($$_[1] > $max);
1483 my $count = scalar(@$recs);
1484 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1485 my ($mrid,$rank,$skip) = @$rec;
1486 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1491 for my $class ( qw/title author subject keyword series identifier/ ) {
1492 __PACKAGE__->register_method(
1493 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1494 method => 'postfilter_search_class_fts',
1497 cdbi => "metabib::${class}_field_entry",
1500 __PACKAGE__->register_method(
1501 api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1502 method => 'postfilter_search_class_fts',
1505 cdbi => "metabib::${class}_field_entry",
1512 my $_cdbi = { title => "metabib::title_field_entry",
1513 author => "metabib::author_field_entry",
1514 subject => "metabib::subject_field_entry",
1515 keyword => "metabib::keyword_field_entry",
1516 series => "metabib::series_field_entry",
1517 identifier => "metabib::identifier_field_entry",
1520 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1521 sub postfilter_search_multi_class_fts {
1526 my $sort = $args{'sort'};
1527 my $sort_dir = $args{sort_dir} || 'DESC';
1528 my $ou = $args{org_unit};
1529 my $ou_type = $args{depth};
1530 my $limit = $args{limit} || 10;
1531 my $offset = $args{offset} || 0;
1532 my $visibility_limit = $args{visibility_limit} || 5000;
1535 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1538 if (!defined($args{org_unit})) {
1539 die "No target organizational unit passed to ".$self->api_name;
1542 if (! scalar( keys %{$args{searches}} )) {
1543 die "No search arguments were passed to ".$self->api_name;
1546 my $outer_limit = 1000;
1548 my $limit_clause = '';
1549 my $offset_clause = '';
1551 $limit_clause = "LIMIT $outer_limit";
1552 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1554 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1555 my ($t_filter, $f_filter, $v_filter) = ('','','');
1556 my ($a_filter, $l_filter, $lf_filter) = ('','','');
1557 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1558 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1560 if ($args{available}) {
1561 $avail_filter = ' AND cp.status IN (0,7,12)';
1564 if (my $a = $args{audience}) {
1565 $a = [$a] if (!ref($a));
1568 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1569 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1572 if (my $l = $args{language}) {
1573 $l = [$l] if (!ref($l));
1576 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1577 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1580 if (my $f = $args{lit_form}) {
1581 $f = [$f] if (!ref($f));
1584 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1585 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1588 if (my $f = $args{item_form}) {
1589 $f = [$f] if (!ref($f));
1592 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1593 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1596 if (my $t = $args{item_type}) {
1597 $t = [$t] if (!ref($t));
1600 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1601 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1604 if (my $v = $args{vr_format}) {
1605 $v = [$v] if (!ref($v));
1608 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1609 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1613 # XXX legacy format and item type support
1614 if ($args{format}) {
1615 my ($t, $f) = split '-', $args{format};
1616 @types = split '', $t;
1617 @forms = split '', $f;
1619 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1620 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1624 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1625 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1631 my $descendants = defined($ou_type) ?
1632 "actor.org_unit_descendants($ou, $ou_type)" :
1633 "actor.org_unit_descendants($ou)";
1635 my $search_table_list = '';
1637 my $join_table_list = '';
1640 my $field_table = config::metabib_field->table;
1644 my $prev_search_group;
1645 my $curr_search_group;
1649 for my $search_group (sort keys %{$args{searches}}) {
1650 (my $search_group_name = $search_group) =~ s/\|/_/gso;
1651 ($search_class,$search_field) = split /\|/, $search_group;
1652 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1654 if ($search_field) {
1655 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1656 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1661 $prev_search_group = $curr_search_group if ($curr_search_group);
1663 $curr_search_group = $search_group_name;
1665 my $class = $_cdbi->{$search_class};
1666 my $search_table = $class->table;
1668 my ($index_col) = $class->columns('FTS');
1669 $index_col ||= 'value';
1672 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1674 my $fts_where = $fts->sql_where_clause;
1675 my @fts_ranks = $fts->fts_rank;
1677 my $SQLstring = join('%',map { lc($_) } $fts->words);
1678 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1679 my $first_word = lc(($fts->words)[0]).'%';
1681 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1682 my $rank = join(' + ', @fts_ranks);
1685 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1686 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1688 $bonus{'series'} = [
1689 { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1690 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1693 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1695 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1696 $bonus_list ||= '1';
1698 push @bonus_lists, $bonus_list;
1699 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1702 #---------------------
1704 $search_table_list .= "$search_table $search_group_name, ";
1705 push @rank_list,$rank;
1706 $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1708 if ($metabib_field) {
1709 $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1710 $metabib_field = undef;
1713 if ($prev_search_group) {
1714 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1718 my $metabib_record_descriptor = metabib::record_descriptor->table;
1719 my $metabib_full_rec = metabib::full_rec->table;
1720 my $metabib_metarecord = metabib::metarecord->table;
1721 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1722 my $asset_call_number_table = asset::call_number->table;
1723 my $asset_copy_table = asset::copy->table;
1724 my $cs_table = config::copy_status->table;
1725 my $cl_table = asset::copy_location->table;
1726 my $br_table = biblio::record_entry->table;
1727 my $source_table = config::bib_source->table;
1729 my $bonuses = join (' * ', @bonus_lists);
1730 my $relevance = join (' + ', @rank_list);
1731 $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1733 my $string_default_sort = 'zzzz';
1734 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1736 my $number_default_sort = '9999';
1737 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1741 my $secondary_sort = <<" SORT";
1743 SELECT COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1744 FROM $metabib_full_rec sfrt,
1745 $metabib_metarecord mr
1746 WHERE sfrt.record = mr.master_record
1747 AND sfrt.tag = '245'
1748 AND sfrt.subfield = 'a'
1753 my $rank = $relevance;
1754 if (lc($sort) eq 'pubdate') {
1757 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1758 FROM $metabib_full_rec frp
1759 WHERE frp.record = mr.master_record
1761 AND frp.subfield = 'c'
1765 } elsif (lc($sort) eq 'create_date') {
1767 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1769 } elsif (lc($sort) eq 'edit_date') {
1771 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1773 } elsif (lc($sort) eq 'title') {
1776 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1777 FROM $metabib_full_rec frt
1778 WHERE frt.record = mr.master_record
1780 AND frt.subfield = 'a'
1784 $secondary_sort = <<" SORT";
1786 SELECT COALESCE(SUBSTRING(sfrp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1787 FROM $metabib_full_rec sfrp
1788 WHERE sfrp.record = mr.master_record
1789 AND sfrp.tag = '260'
1790 AND sfrp.subfield = 'c'
1794 } elsif (lc($sort) eq 'author') {
1797 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
1798 FROM $metabib_full_rec fra
1799 WHERE fra.record = mr.master_record
1800 AND fra.tag LIKE '1%'
1801 AND fra.subfield = 'a'
1802 ORDER BY fra.tag::text::int
1807 push @bonus_values, @bonus_values;
1812 my $select = <<" SQL";
1813 SELECT m.metarecord,
1815 CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1818 FROM $search_table_list
1819 $metabib_metarecord mr,
1820 $metabib_metarecord_source_map_table m,
1821 $metabib_metarecord_source_map_table smrs
1822 WHERE m.metarecord = smrs.metarecord
1823 AND mr.id = m.metarecord
1826 GROUP BY m.metarecord
1827 -- ORDER BY 4 $sort_dir
1828 LIMIT $visibility_limit
1831 if ($self->api_name !~ /staff/o) {
1838 FROM $asset_call_number_table cn,
1839 $metabib_metarecord_source_map_table mrs,
1840 $asset_copy_table cp,
1845 $metabib_record_descriptor ord
1846 WHERE mrs.metarecord = s.metarecord
1847 AND br.id = mrs.source
1848 AND cn.record = mrs.source
1849 AND cp.status = cs.id
1850 AND cp.location = cl.id
1851 AND cp.circ_lib = d.id
1852 AND cp.call_number = cn.id
1853 AND cp.opac_visible IS TRUE
1854 AND cs.opac_visible IS TRUE
1855 AND cl.opac_visible IS TRUE
1856 AND d.opac_visible IS TRUE
1857 AND br.active IS TRUE
1858 AND br.deleted IS FALSE
1859 AND cp.deleted IS FALSE
1860 AND cn.deleted IS FALSE
1861 AND ord.record = mrs.source
1874 $metabib_metarecord_source_map_table mrs,
1875 $metabib_record_descriptor ord,
1877 WHERE mrs.metarecord = s.metarecord
1878 AND ord.record = mrs.source
1879 AND br.id = mrs.source
1880 AND br.source = src.id
1881 AND src.transcendant IS TRUE
1889 ORDER BY 4 $sort_dir, 5
1896 $metabib_metarecord_source_map_table omrs,
1897 $metabib_record_descriptor ord
1898 WHERE omrs.metarecord = s.metarecord
1899 AND ord.record = omrs.source
1902 FROM $asset_call_number_table cn,
1903 $asset_copy_table cp,
1906 WHERE br.id = omrs.source
1907 AND cn.record = omrs.source
1908 AND br.deleted IS FALSE
1909 AND cn.deleted IS FALSE
1910 AND cp.call_number = cn.id
1911 AND ( cn.owning_lib = d.id
1912 OR ( cp.circ_lib = d.id
1913 AND cp.deleted IS FALSE
1921 FROM $asset_call_number_table cn
1922 WHERE cn.record = omrs.source
1923 AND cn.deleted IS FALSE
1929 $metabib_metarecord_source_map_table mrs,
1930 $metabib_record_descriptor ord,
1932 WHERE mrs.metarecord = s.metarecord
1933 AND br.id = mrs.source
1934 AND br.source = src.id
1935 AND src.transcendant IS TRUE
1951 ORDER BY 4 $sort_dir, 5
1956 $log->debug("Field Search SQL :: [$select]",DEBUG);
1958 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1961 @types, @forms, @vformats, @aud, @lang, @lit_form,
1962 @types, @forms, @vformats, @aud, @lang, @lit_form,
1963 # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1966 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1969 $max = 1 if (!@$recs);
1971 $max = $$_[1] if ($$_[1] > $max);
1974 my $count = scalar(@$recs);
1975 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1976 next unless ($$rec[0]);
1977 my ($mrid,$rank,$skip) = @$rec;
1978 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1983 __PACKAGE__->register_method(
1984 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1985 method => 'postfilter_search_multi_class_fts',
1990 __PACKAGE__->register_method(
1991 api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1992 method => 'postfilter_search_multi_class_fts',
1998 __PACKAGE__->register_method(
1999 api_name => "open-ils.storage.metabib.multiclass.search_fts",
2000 method => 'postfilter_search_multi_class_fts',
2005 __PACKAGE__->register_method(
2006 api_name => "open-ils.storage.metabib.multiclass.search_fts.staff",
2007 method => 'postfilter_search_multi_class_fts',
2013 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2014 sub biblio_search_multi_class_fts {
2019 my $sort = $args{'sort'};
2020 my $sort_dir = $args{sort_dir} || 'DESC';
2021 my $ou = $args{org_unit};
2022 my $ou_type = $args{depth};
2023 my $limit = $args{limit} || 10;
2024 my $offset = $args{offset} || 0;
2025 my $pref_lang = $args{preferred_language} || 'eng';
2026 my $visibility_limit = $args{visibility_limit} || 5000;
2029 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2032 if (! scalar( keys %{$args{searches}} )) {
2033 die "No search arguments were passed to ".$self->api_name;
2036 my $outer_limit = 1000;
2038 my $limit_clause = '';
2039 my $offset_clause = '';
2041 $limit_clause = "LIMIT $outer_limit";
2042 $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
2044 my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
2045 my ($t_filter, $f_filter, $v_filter) = ('','','');
2046 my ($a_filter, $l_filter, $lf_filter) = ('','','');
2047 my ($ot_filter, $of_filter, $ov_filter) = ('','','');
2048 my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
2050 if ($args{available}) {
2051 $avail_filter = ' AND cp.status IN (0,7,12)';
2054 if (my $a = $args{audience}) {
2055 $a = [$a] if (!ref($a));
2058 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
2059 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
2062 if (my $l = $args{language}) {
2063 $l = [$l] if (!ref($l));
2066 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
2067 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
2070 if (my $f = $args{lit_form}) {
2071 $f = [$f] if (!ref($f));
2074 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2075 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2078 if (my $f = $args{item_form}) {
2079 $f = [$f] if (!ref($f));
2082 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2083 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2086 if (my $t = $args{item_type}) {
2087 $t = [$t] if (!ref($t));
2090 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2091 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2094 if (my $v = $args{vr_format}) {
2095 $v = [$v] if (!ref($v));
2098 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
2099 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
2102 # XXX legacy format and item type support
2103 if ($args{format}) {
2104 my ($t, $f) = split '-', $args{format};
2105 @types = split '', $t;
2106 @forms = split '', $f;
2108 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2109 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2113 $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2114 $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2119 my $descendants = defined($ou_type) ?
2120 "actor.org_unit_descendants($ou, $ou_type)" :
2121 "actor.org_unit_descendants($ou)";
2123 my $search_table_list = '';
2125 my $join_table_list = '';
2128 my $field_table = config::metabib_field->table;
2132 my $prev_search_group;
2133 my $curr_search_group;
2137 for my $search_group (sort keys %{$args{searches}}) {
2138 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2139 ($search_class,$search_field) = split /\|/, $search_group;
2140 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2142 if ($search_field) {
2143 unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2144 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2149 $prev_search_group = $curr_search_group if ($curr_search_group);
2151 $curr_search_group = $search_group_name;
2153 my $class = $_cdbi->{$search_class};
2154 my $search_table = $class->table;
2156 my ($index_col) = $class->columns('FTS');
2157 $index_col ||= 'value';
2160 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2162 my $fts_where = $fts->sql_where_clause;
2163 my @fts_ranks = $fts->fts_rank;
2165 my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2166 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2167 my $first_word = lc(($fts->words)[0]).'%';
2169 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2170 my $rank = join(' + ', @fts_ranks);
2173 $bonus{'subject'} = [];
2174 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2176 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2178 $bonus{'series'} = [
2179 { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2180 { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2183 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2186 push @{ $bonus{'title'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2187 push @{ $bonus{'author'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2188 push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2189 push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2190 push @{ $bonus{'series'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2193 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2194 $bonus_list ||= '1';
2196 push @bonus_lists, $bonus_list;
2197 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2199 #---------------------
2201 $search_table_list .= "$search_table $search_group_name, ";
2202 push @rank_list,$rank;
2203 $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2205 if ($metabib_field) {
2206 $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2207 $metabib_field = undef;
2210 if ($prev_search_group) {
2211 $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2215 my $metabib_record_descriptor = metabib::record_descriptor->table;
2216 my $metabib_full_rec = metabib::full_rec->table;
2217 my $metabib_metarecord = metabib::metarecord->table;
2218 my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2219 my $asset_call_number_table = asset::call_number->table;
2220 my $asset_copy_table = asset::copy->table;
2221 my $cs_table = config::copy_status->table;
2222 my $cl_table = asset::copy_location->table;
2223 my $br_table = biblio::record_entry->table;
2224 my $source_table = config::bib_source->table;
2227 my $bonuses = join (' * ', @bonus_lists);
2228 my $relevance = join (' + ', @rank_list);
2229 $relevance = "AVG( ($relevance) * ($bonuses) )";
2231 my $string_default_sort = 'zzzz';
2232 $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2234 my $number_default_sort = '9999';
2235 $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2237 my $rank = $relevance;
2238 if (lc($sort) eq 'pubdate') {
2241 SELECT COALESCE(SUBSTRING(frp.value FROM E'\\\\d{4}'),'$number_default_sort')::INT
2242 FROM $metabib_full_rec frp
2243 WHERE frp.record = b.id
2245 AND frp.subfield = 'c'
2249 } elsif (lc($sort) eq 'create_date') {
2251 ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2253 } elsif (lc($sort) eq 'edit_date') {
2255 ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2257 } elsif (lc($sort) eq 'title') {
2260 SELECT COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2261 FROM $metabib_full_rec frt
2262 WHERE frt.record = b.id
2264 AND frt.subfield = 'a'
2268 } elsif (lc($sort) eq 'author') {
2271 SELECT COALESCE(LTRIM(fra.value),'$string_default_sort')
2272 FROM $metabib_full_rec fra
2273 WHERE fra.record = b.id
2274 AND fra.tag LIKE '1%'
2275 AND fra.subfield = 'a'
2276 ORDER BY fra.tag::text::int
2281 push @bonus_values, @bonus_values;
2286 my $select = <<" SQL";
2291 FROM $search_table_list
2292 $metabib_record_descriptor rd,
2295 WHERE rd.record = b.id
2296 AND b.active IS TRUE
2297 AND b.deleted IS FALSE
2306 GROUP BY b.id, b.source
2307 ORDER BY 3 $sort_dir
2308 LIMIT $visibility_limit
2311 if ($self->api_name !~ /staff/o) {
2316 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2319 FROM $asset_call_number_table cn,
2320 $asset_copy_table cp,
2324 WHERE cn.record = s.id
2325 AND cp.status = cs.id
2326 AND cp.location = cl.id
2327 AND cp.call_number = cn.id
2328 AND cp.opac_visible IS TRUE
2329 AND cs.opac_visible IS TRUE
2330 AND cl.opac_visible IS TRUE
2331 AND d.opac_visible IS TRUE
2332 AND cp.deleted IS FALSE
2333 AND cn.deleted IS FALSE
2334 AND cp.circ_lib = d.id
2338 OR src.transcendant IS TRUE
2339 ORDER BY 3 $sort_dir
2346 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2349 FROM $asset_call_number_table cn,
2350 $asset_copy_table cp,
2352 WHERE cn.record = s.id
2353 AND cp.call_number = cn.id
2354 AND cn.deleted IS FALSE
2355 AND cp.circ_lib = d.id
2356 AND cp.deleted IS FALSE
2362 FROM $asset_call_number_table cn
2363 WHERE cn.record = s.id
2366 OR src.transcendant IS TRUE
2367 ORDER BY 3 $sort_dir
2372 $log->debug("Field Search SQL :: [$select]",DEBUG);
2374 my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2376 @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2379 $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2381 my $count = scalar(@$recs);
2382 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2383 next unless ($$rec[0]);
2384 my ($mrid,$rank) = @$rec;
2385 $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2390 __PACKAGE__->register_method(
2391 api_name => "open-ils.storage.biblio.multiclass.search_fts.record",
2392 method => 'biblio_search_multi_class_fts',
2397 __PACKAGE__->register_method(
2398 api_name => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2399 method => 'biblio_search_multi_class_fts',
2404 __PACKAGE__->register_method(
2405 api_name => "open-ils.storage.biblio.multiclass.search_fts",
2406 method => 'biblio_search_multi_class_fts',
2411 __PACKAGE__->register_method(
2412 api_name => "open-ils.storage.biblio.multiclass.search_fts.staff",
2413 method => 'biblio_search_multi_class_fts',
2421 my $default_preferred_language;
2422 my $default_preferred_language_weight;
2424 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2430 if (!$locale_map{COMPLETE}) {
2432 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2433 for my $locale ( @locales ) {
2434 $locale_map{lc($locale->code)} = $locale->marc_code;
2436 $locale_map{COMPLETE} = 1;
2440 my $config = OpenSRF::Utils::SettingsClient->new();
2442 if (!$default_preferred_language) {
2444 $default_preferred_language = $config->config_value(
2445 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2446 ) || $config->config_value(
2447 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2452 if (!$default_preferred_language_weight) {
2454 $default_preferred_language_weight = $config->config_value(
2455 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2456 ) || $config->config_value(
2457 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2461 # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2462 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2464 my $ou = $args{org_unit};
2465 my $limit = $args{limit} || 10;
2466 my $offset = $args{offset} || 0;
2469 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2472 if (! scalar( keys %{$args{searches}} )) {
2473 die "No search arguments were passed to ".$self->api_name;
2476 my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2478 if (!defined($args{preferred_language})) {
2479 my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2480 $args{preferred_language} =
2481 $locale_map{ lc($ses_locale) } || 'eng';
2484 if (!defined($args{preferred_language_weight})) {
2485 $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2488 if ($args{available}) {
2489 @statuses = (0,7,12);
2492 if (my $s = $args{locations}) {
2493 $s = [$s] if (!ref($s));
2497 if (my $b = $args{between}) {
2498 if (ref($b) && @$b == 2) {
2503 if (my $s = $args{statuses}) {
2504 $s = [$s] if (!ref($s));
2508 if (my $a = $args{audience}) {
2509 $a = [$a] if (!ref($a));
2513 if (my $l = $args{language}) {
2514 $l = [$l] if (!ref($l));
2518 if (my $f = $args{lit_form}) {
2519 $f = [$f] if (!ref($f));
2523 if (my $f = $args{item_form}) {
2524 $f = [$f] if (!ref($f));
2528 if (my $t = $args{item_type}) {
2529 $t = [$t] if (!ref($t));
2533 if (my $b = $args{bib_level}) {
2534 $b = [$b] if (!ref($b));
2538 if (my $v = $args{vr_format}) {
2539 $v = [$v] if (!ref($v));
2543 # XXX legacy format and item type support
2544 if ($args{format}) {
2545 my ($t, $f) = split '-', $args{format};
2546 @types = split '', $t;
2547 @forms = split '', $f;
2550 my %stored_proc_search_args;
2551 for my $search_group (sort keys %{$args{searches}}) {
2552 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2553 my ($search_class,$search_field) = split /\|/, $search_group;
2554 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2556 if ($search_field) {
2557 unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2558 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2563 my $class = $_cdbi->{$search_class};
2564 my $search_table = $class->table;
2566 my ($index_col) = $class->columns('FTS');
2567 $index_col ||= 'value';
2570 my $fts = OpenILS::Application::Storage::FTS->compile(
2571 $search_class => $args{searches}{$search_group}{term},
2572 $search_group_name.'.value',
2573 "$search_group_name.$index_col"
2575 $fts->sql_where_clause; # this builds the ranks for us
2577 my @fts_ranks = $fts->fts_rank;
2578 my @fts_queries = $fts->fts_query;
2579 my @phrases = map { lc($_) } $fts->phrases;
2580 my @words = map { lc($_) } $fts->words;
2582 $stored_proc_search_args{$search_group} = {
2583 fts_rank => \@fts_ranks,
2584 fts_query => \@fts_queries,
2585 phrase => \@phrases,
2591 my $param_search_ou = $ou;
2592 my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2593 my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2594 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2595 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2596 my $param_audience = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud ) . '}$$';
2597 my $param_language = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang ) . '}$$';
2598 my $param_lit_form = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2599 my $param_types = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types ) . '}$$';
2600 my $param_forms = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms ) . '}$$';
2601 my $param_vformats = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2602 my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2603 my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2604 my $param_after = $args{after} ; $param_after = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2605 my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2606 my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2607 my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2608 my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2609 my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2610 my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2611 my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2612 my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2613 my $param_rel_limit = $args{core_limit}; $param_rel_limit ||= 'NULL';
2614 my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2615 my $param_skip_chk = $args{skip_check}; $param_skip_chk ||= 'NULL';
2617 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
2619 FROM search.staged_fts(
2620 $param_search_ou\:\:INT,
2621 $param_depth\:\:INT,
2622 $param_searches\:\:TEXT,
2623 $param_statuses\:\:INT[],
2624 $param_locations\:\:INT[],
2625 $param_audience\:\:TEXT[],
2626 $param_language\:\:TEXT[],
2627 $param_lit_form\:\:TEXT[],
2628 $param_types\:\:TEXT[],
2629 $param_forms\:\:TEXT[],
2630 $param_vformats\:\:TEXT[],
2631 $param_bib_level\:\:TEXT[],
2632 $param_before\:\:TEXT,
2633 $param_after\:\:TEXT,
2634 $param_during\:\:TEXT,
2635 $param_between\:\:TEXT[],
2636 $param_pref_lang\:\:TEXT,
2637 $param_pref_lang_multiplier\:\:REAL,
2638 $param_sort\:\:TEXT,
2639 $param_sort_desc\:\:BOOL,
2640 $metarecord\:\:BOOL,
2642 $param_rel_limit\:\:INT,
2643 $param_chk_limit\:\:INT,
2644 $param_skip_chk\:\:INT
2650 my $recs = $sth->fetchall_arrayref({});
2651 my $summary_row = pop @$recs;
2653 my $total = $$summary_row{total};
2654 my $checked = $$summary_row{checked};
2655 my $visible = $$summary_row{visible};
2656 my $deleted = $$summary_row{deleted};
2657 my $excluded = $$summary_row{excluded};
2659 my $estimate = $visible;
2660 if ( $total > $checked && $checked ) {
2662 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2663 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2667 delete $$summary_row{id};
2668 delete $$summary_row{rel};
2669 delete $$summary_row{record};
2671 $client->respond( $summary_row );
2673 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2675 for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2676 delete $$rec{checked};
2677 delete $$rec{visible};
2678 delete $$rec{excluded};
2679 delete $$rec{deleted};
2680 delete $$rec{total};
2681 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2683 $client->respond( $rec );
2687 __PACKAGE__->register_method(
2688 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
2689 method => 'staged_fts',
2694 __PACKAGE__->register_method(
2695 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2696 method => 'staged_fts',
2701 __PACKAGE__->register_method(
2702 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
2703 method => 'staged_fts',
2708 __PACKAGE__->register_method(
2709 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2710 method => 'staged_fts',
2716 sub FTS_paging_estimate {
2720 my $checked = shift;
2721 my $visible = shift;
2722 my $excluded = shift;
2723 my $deleted = shift;
2726 my $deleted_ratio = $deleted / $checked;
2727 my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2729 my $exclusion_ratio = $excluded / $checked;
2730 my $delete_adjusted_exclusion_ratio = $checked - $deleted ? $excluded / ($checked - $deleted) : 1;
2732 my $inclusion_ratio = $visible / $checked;
2733 my $delete_adjusted_inclusion_ratio = $checked - $deleted ? $visible / ($checked - $deleted) : 0;
2736 exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2737 inclusion => int($delete_adjusted_total * $inclusion_ratio),
2738 delete_adjusted_exclusion => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2739 delete_adjusted_inclusion => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2742 __PACKAGE__->register_method(
2743 api_name => "open-ils.storage.fts_paging_estimate",
2744 method => 'FTS_paging_estimate',
2750 Hash of estimation values based on four variant estimation strategies:
2751 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2752 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2753 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2754 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2757 Helper method used to determin the approximate number of
2758 hits for a search that spans multiple superpages. For
2759 sparse superpages, the inclusion estimate will likely be the
2760 best estimate. The exclusion strategy is the original, but
2761 inclusion is the default.
2764 { name => 'checked',
2765 desc => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2768 { name => 'visible',
2769 desc => 'Number of records visible to the search location on the current superpage.',
2772 { name => 'excluded',
2773 desc => 'Number of records excluded from the search location on the current superpage.',
2776 { name => 'deleted',
2777 desc => 'Number of deleted records on the current superpage.',
2781 desc => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2794 my $term = $$args{term};
2795 my $limit = $$args{max} || 1;
2796 my $min = $$args{min} || 1;
2797 my @classes = @{$$args{class}};
2799 $limit = $min if ($min > $limit);
2802 @classes = ( qw/ title author subject series keyword / );
2806 my $bre_table = biblio::record_entry->table;
2807 my $cn_table = asset::call_number->table;
2808 my $cp_table = asset::copy->table;
2810 for my $search_class ( @classes ) {
2812 my $class = $_cdbi->{$search_class};
2813 my $search_table = $class->table;
2815 my ($index_col) = $class->columns('FTS');
2816 $index_col ||= 'value';
2819 my $where = OpenILS::Application::Storage::FTS
2820 ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2824 SELECT COUNT(DISTINCT X.source)
2825 FROM (SELECT $search_class.source
2826 FROM $search_table $search_class
2827 JOIN $bre_table b ON (b.id = $search_class.source)
2832 HAVING COUNT(DISTINCT X.source) >= $min;
2835 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2836 $matches{$search_class} = $res ? $res->[0] : 0;
2841 __PACKAGE__->register_method(
2842 api_name => "open-ils.storage.search.xref",
2843 method => 'xref_count',
2847 # Takes an abstract query object and recursively turns it back into a string
2849 sub abstract_query2str {
2850 my ($self, $conn, $query) = @_;
2852 return QueryParser::Canonicalize::abstract_query2str_impl($query, 0);
2855 __PACKAGE__->register_method(
2856 api_name => "open-ils.storage.query_parser.abstract_query.canonicalize",
2857 method => "abstract_query2str",
2862 Abstract query parser object, with complete config data. For example input,
2863 see the 'abstract_query' part of the output of an API call like
2864 open-ils.search.biblio.multiclass.query, when called with the return_abstract
2868 return => { type => "string", desc => "String representation of abstract query object" }
2872 sub str2abstract_query {
2873 my ($self, $conn, $query, $qp_opts, $with_config) = @_;
2875 my %use_opts = ( # reasonable defaults? should these even be hardcoded here?
2877 superpage_size => 1000,
2878 core_limit => 25000,
2880 (ref $opts eq 'HASH' ? %$opts : ())
2885 # grab the query parser and initialize it
2886 my $parser = $OpenILS::Application::Storage::QParser;
2889 _initialize_parser($parser) unless $parser->initialization_complete;
2891 my $query = $parser->new(%use_opts)->parse;
2893 return $query->parse_tree->to_abstract_query(with_config => $with_config);
2896 __PACKAGE__->register_method(
2897 api_name => "open-ils.storage.query_parser.abstract_query.from_string",
2898 method => "str2abstract_query",
2902 {desc => "Query", type => "string"},
2903 {desc => q/Arguments for initializing QueryParser (optional)/,
2905 {desc => q/Flag enabling inclusion of QP config in returned object (optional, default false)/,
2908 return => { type => "object", desc => "abstract representation of query parser query" }
2912 sub query_parser_fts {
2918 # grab the query parser and initialize it
2919 my $parser = $OpenILS::Application::Storage::QParser;
2922 _initialize_parser($parser) unless $parser->initialization_complete;
2924 # populate the locale/language map
2925 if (!$locale_map{COMPLETE}) {
2927 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2928 for my $locale ( @locales ) {
2929 $locale_map{lc($locale->code)} = $locale->marc_code;
2931 $locale_map{COMPLETE} = 1;
2935 # I hope we have a query!
2936 if (! $args{query} ) {
2937 die "No query was passed to ".$self->api_name;
2940 my $default_CD_modifiers = OpenSRF::Utils::SettingsClient->new->config_value(
2941 apps => 'open-ils.search' => app_settings => 'default_CD_modifiers'
2944 # Protect against empty / missing default_CD_modifiers setting
2945 if ($default_CD_modifiers and !ref($default_CD_modifiers)) {
2946 $args{query} = "$default_CD_modifiers $args{query}";
2949 my $simple_plan = $args{_simple_plan};
2950 # remove bad chunks of the %args hash
2951 for my $bad ( grep { /^_/ } keys(%args)) {
2952 delete($args{$bad});
2956 # parse the query and supply any query-level %arg-based defaults
2957 # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2958 my $query = $parser->new( %args )->parse;
2960 my $config = OpenSRF::Utils::SettingsClient->new();
2962 # set the locale-based default preferred location
2963 if (!$query->parse_tree->find_filter('preferred_language')) {
2964 $parser->default_preferred_language( $args{preferred_language} );
2966 if (!$parser->default_preferred_language) {
2967 my $ses_locale = $client->session ? $client->session->session_locale : '';
2968 $parser->default_preferred_language( $locale_map{ lc($ses_locale) } );
2971 if (!$parser->default_preferred_language) { # still nothing...
2972 my $tmp_dpl = $config->config_value(
2973 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2974 ) || $config->config_value(
2975 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2978 $parser->default_preferred_language( $tmp_dpl )
2983 # set the global default language multiplier
2984 if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2987 if ($tmp_dplw = $args{preferred_language_weight} || $args{preferred_language_multiplier} ) {
2988 $parser->default_preferred_language_multiplier($tmp_dplw);
2991 $tmp_dplw = $config->config_value(
2992 apps => 'open-ils.search' => app_settings => 'default_preferred_language_weight'
2993 ) || $config->config_value(
2994 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2997 $parser->default_preferred_language_multiplier( $tmp_dplw );
3001 # gather the site, if one is specified, defaulting to the in-query version
3002 my $ou = $args{org_unit};
3003 if (my ($filter) = $query->parse_tree->find_filter('site')) {
3004 $ou = $filter->args->[0] if (@{$filter->args});
3006 $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^(-)?\d+$/);
3008 # gather lasso, as with $ou
3009 my $lasso = $args{lasso};
3010 if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
3011 $lasso = $filter->args->[0] if (@{$filter->args});
3013 $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
3014 $lasso = -$lasso if ($lasso);
3017 # # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
3018 # # gather user lasso, as with $ou and lasso
3019 # my $mylasso = $args{my_lasso};
3020 # if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
3021 # $mylasso = $filter->args->[0] if (@{$filter->args});
3023 # $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
3026 # if we have a lasso, go with that, otherwise ... ou
3027 $ou = $lasso if ($lasso);
3029 # gather the preferred OU, if one is specified, as with $ou
3030 my $pref_ou = $args{pref_ou};
3031 $log->info("pref_ou = $pref_ou");
3032 if (my ($filter) = $query->parse_tree->find_filter('pref_ou')) {
3033 $pref_ou = $filter->args->[0] if (@{$filter->args});
3035 $pref_ou = actor::org_unit->search( { shortname => $pref_ou } )->next->id if ($pref_ou and $pref_ou !~ /^(-)?\d+$/);
3037 # get the default $ou if we have nothing
3038 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
3041 # 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
3042 # gather the depth, if one is specified, defaulting to the in-query version
3043 my $depth = $args{depth};
3044 if (my ($filter) = $query->parse_tree->find_filter('depth')) {
3045 $depth = $filter->args->[0] if (@{$filter->args});
3047 $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
3050 # gather the limit or default to 10
3051 my $limit = $args{check_limit} || 'NULL';
3052 if (my ($filter) = $query->parse_tree->find_filter('limit')) {
3053 $limit = $filter->args->[0] if (@{$filter->args});
3055 if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
3056 $limit = $filter->args->[0] if (@{$filter->args});
3060 # gather the offset or default to 0
3061 my $offset = $args{skip_check} || $args{offset} || 0;
3062 if (my ($filter) = $query->parse_tree->find_filter('offset')) {
3063 $offset = $filter->args->[0] if (@{$filter->args});
3065 if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
3066 $offset = $filter->args->[0] if (@{$filter->args});
3070 # gather the estimation strategy or default to inclusion
3071 my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
3072 if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
3073 $estimation_strategy = $filter->args->[0] if (@{$filter->args});
3077 # gather the estimation strategy or default to inclusion
3078 my $core_limit = $args{core_limit};
3079 if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
3080 $core_limit = $filter->args->[0] if (@{$filter->args});
3084 # gather statuses, and then forget those if we have an #available modifier
3086 if (my ($filter) = $query->parse_tree->find_filter('statuses')) {
3087 @statuses = @{$filter->args} if (@{$filter->args});
3089 @statuses = (0,7,12) if ($query->parse_tree->find_modifier('available'));
3094 if (my ($filter) = $query->parse_tree->find_filter('locations')) {
3095 @location = @{$filter->args} if (@{$filter->args});
3098 # gather location_groups
3099 if (my ($filter) = $query->parse_tree->find_filter('location_groups')) {
3100 my @loc_groups = @{$filter->args} if (@{$filter->args});
3102 # collect the mapped locations and add them to the locations() filter
3105 my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3106 my $maps = $cstore->request(
3107 'open-ils.cstore.direct.asset.copy_location_group_map.search.atomic',
3108 {lgroup => \@loc_groups})->gather(1);
3110 push(@location, $_->location) for @$maps;
3115 my $param_check = $limit || $query->superpage_size || 'NULL';
3116 my $param_offset = $offset || 'NULL';
3117 my $param_limit = $core_limit || 'NULL';
3119 my $sp = $query->superpage || 1;
3121 $param_offset = ($sp - 1) * $sp_size;
3124 my $param_search_ou = $ou;
3125 my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
3126 my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
3127 my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
3128 my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
3129 my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
3130 my $deleted_search = ($query->parse_tree->find_modifier('deleted')) ? "'t'" : "'f'";
3131 my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
3132 my $param_pref_ou = $pref_ou || 'NULL';
3134 my $sth = metabib::metarecord_source_map->db_Main->prepare(<<" SQL");
3135 SELECT * -- bib search: $args{query}
3136 FROM search.query_parser_fts(
3137 $param_search_ou\:\:INT,
3138 $param_depth\:\:INT,
3139 $param_core_query\:\:TEXT,
3140 $param_statuses\:\:INT[],
3141 $param_locations\:\:INT[],
3142 $param_offset\:\:INT,
3143 $param_check\:\:INT,
3144 $param_limit\:\:INT,
3145 $metarecord\:\:BOOL,
3147 $deleted_search\:\:BOOL,
3148 $param_pref_ou\:\:INT
3154 my $recs = $sth->fetchall_arrayref({});
3155 my $summary_row = pop @$recs;
3157 my $total = $$summary_row{total};
3158 my $checked = $$summary_row{checked};
3159 my $visible = $$summary_row{visible};
3160 my $deleted = $$summary_row{deleted};
3161 my $excluded = $$summary_row{excluded};
3163 my $estimate = $visible;
3164 if ( $total > $checked && $checked ) {
3166 $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
3167 $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
3171 delete $$summary_row{id};
3172 delete $$summary_row{rel};
3173 delete $$summary_row{record};
3175 if (defined($simple_plan)) {
3176 $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3178 $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3181 $client->respond( $summary_row );
3183 $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
3185 for my $rec (@$recs) {
3186 delete $$rec{checked};
3187 delete $$rec{visible};
3188 delete $$rec{excluded};
3189 delete $$rec{deleted};
3190 delete $$rec{total};
3191 $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3193 $client->respond( $rec );
3197 __PACKAGE__->register_method(
3198 api_name => "open-ils.storage.query_parser_search",
3199 method => 'query_parser_fts',
3205 sub query_parser_fts_wrapper {
3210 $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3211 # grab the query parser and initialize it
3212 my $parser = $OpenILS::Application::Storage::QParser;
3215 _initialize_parser($parser) unless $parser->initialization_complete;
3217 if (! scalar( keys %{$args{searches}} )) {
3218 die "No search arguments were passed to ".$self->api_name;
3221 $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3222 my $base_query = '';
3223 for my $sclass ( keys %{$args{searches}} ) {
3224 $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3225 $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3228 my $query = $base_query;
3229 $log->debug("Full base query: $base_query", DEBUG);
3231 $query = "$args{facets} $query" if ($args{facets});
3233 if (!$locale_map{COMPLETE}) {
3235 my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3236 for my $locale ( @locales ) {
3237 $locale_map{lc($locale->code)} = $locale->marc_code;
3239 $locale_map{COMPLETE} = 1;
3243 my $base_plan = $parser->new( query => $base_query )->parse;
3245 $query = "$query preferred_language($args{preferred_language})"
3246 if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3247 $query = "$query preferred_language_weight($args{preferred_language_weight})"
3248 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'));
3251 # we add these to the end of the query (last-wins) because in wrapper mode we want to retain the behaviour
3252 # of separately specified options taking precidenc -- IOW, the user should not be able to cause a change in,
3253 # say, superpage size by adjusting the query string.
3254 $query = "$query estimation_strategy($args{estimation_strategy})" if ($args{estimation_strategy});
3255 $query = "$query site($args{org_unit})" if ($args{org_unit});
3256 $query = "$query depth($args{depth})" if (defined($args{depth}));
3257 $query = "$query sort($args{sort})" if ($args{sort});
3258 $query = "$query limit($args{limit})" if ($args{limit});
3259 $query = "$query core_limit($args{core_limit})" if ($args{core_limit});
3260 $query = "$query skip_check($args{skip_check})" if ($args{skip_check});
3261 $query = "$query superpage($args{superpage})" if ($args{superpage});
3262 $query = "$query offset($args{offset})" if ($args{offset});
3263 $query = "$query #metarecord" if ($self->api_name =~ /metabib/);
3264 $query = "$query #available" if ($args{available});
3265 $query = "$query #descending" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3266 $query = "$query #staff" if ($self->api_name =~ /staff/);
3267 $query = "$query before($args{before})" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3268 $query = "$query after($args{after})" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3269 $query = "$query during($args{during})" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3270 $query = "$query between($args{between}[0],$args{between}[1])"
3271 if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3274 my (@between,@statuses,@locations,@location_groups,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3276 # XXX legacy format and item type support
3277 if ($args{format}) {
3278 my ($t, $f) = split '-', $args{format};
3279 $args{item_type} = [ split '', $t ];
3280 $args{item_form} = [ split '', $f ];
3283 for my $filter ( qw/locations location_groups statuses between audience language lit_form item_form item_type bib_level vr_format/ ) {
3284 if (my $s = $args{$filter}) {
3285 $s = [$s] if (!ref($s));
3287 my @filter_list = @$s;
3289 next if ($filter eq 'between' and scalar(@filter_list) != 2);
3290 next if (@filter_list == 0);
3292 my $filter_string = join ',', @filter_list;
3293 $query = "$query $filter($filter_string)";
3297 $log->debug("Full QueryParser query: $query", DEBUG);
3299 return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan );
3301 __PACKAGE__->register_method(
3302 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
3303 method => 'query_parser_fts_wrapper',
3308 __PACKAGE__->register_method(
3309 api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3310 method => 'query_parser_fts_wrapper',
3315 __PACKAGE__->register_method(
3316 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
3317 method => 'query_parser_fts_wrapper',
3322 __PACKAGE__->register_method(
3323 api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3324 method => 'query_parser_fts_wrapper',