]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/metabib.pm
LP#1549505: add flag to tweak popularity-adjusted relevance
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Storage / Publisher / metabib.pm
1 package OpenILS::Application::Storage::Publisher::metabib;
2 use base qw/OpenILS::Application::Storage::Publisher/;
3 use vars qw/$VERSION/;
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 OpenILS::Application::AppUtils;
9 use OpenSRF::Utils::Cache;
10 use OpenSRF::Utils::JSON;
11 use Data::Dumper;
12 use Digest::MD5 qw/md5_hex/;
13
14 use OpenILS::Application::Storage::QueryParser;
15
16 my $U = 'OpenILS::Application::AppUtils';
17
18 my $log = 'OpenSRF::Utils::Logger';
19
20 $VERSION = 1;
21
22 sub _initialize_parser {
23     my ($parser) = @_;
24
25     my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
26     $parser->initialize(
27         config_record_attr_index_norm_map =>
28             $cstore->request(
29                 'open-ils.cstore.direct.config.record_attr_index_norm_map.search.atomic',
30                 { id => { "!=" => undef } },
31                 { flesh => 1, flesh_fields => { crainm => [qw/norm/] }, order_by => [{ class => "crainm", field => "pos" }] }
32             )->gather(1),
33         search_relevance_adjustment         =>
34             $cstore->request(
35                 'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
36                 { id => { "!=" => undef } }
37             )->gather(1),
38         config_metabib_field                =>
39             $cstore->request(
40                 'open-ils.cstore.direct.config.metabib_field.search.atomic',
41                 { id => { "!=" => undef } }
42             )->gather(1),
43         config_metabib_search_alias         =>
44             $cstore->request(
45                 'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
46                 { alias => { "!=" => undef } }
47             )->gather(1),
48         config_metabib_field_index_norm_map =>
49             $cstore->request(
50                 'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
51                 { id => { "!=" => undef } },
52                 { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
53             )->gather(1),
54         config_record_attr_definition       =>
55             $cstore->request(
56                 'open-ils.cstore.direct.config.record_attr_definition.search.atomic',
57                 { name => { "!=" => undef } }
58             )->gather(1),
59         config_metabib_class_ts_map         =>
60             $cstore->request(
61                 'open-ils.cstore.direct.config.metabib_class_ts_map.search.atomic',
62                 { active => "t" }
63             )->gather(1),
64         config_metabib_field_ts_map         =>
65             $cstore->request(
66                 'open-ils.cstore.direct.config.metabib_field_ts_map.search.atomic',
67                 { active => "t" }
68             )->gather(1),
69         config_metabib_class                =>
70             $cstore->request(
71                 'open-ils.cstore.direct.config.metabib_class.search.atomic',
72                 { name => { "!=" => undef } }
73             )->gather(1),
74     );
75
76     my $max_mult;
77     my $cgf = $cstore->request(
78         'open-ils.cstore.direct.config.global_flag.retrieve',
79         'search.max_popularity_importance_multiplier'
80     )->gather(1);
81     $max_mult = $cgf->value if $cgf && $U->is_true($cgf->enabled);
82     $max_mult //= 2.0;
83     $max_mult = 2.0 unless $max_mult =~ /^-?(?:\d+\.?|\.\d)\d*\z/; # just in case
84     $parser->max_popularity_importance_multiplier($max_mult);
85
86     $cstore->disconnect;
87     die("Cannot initialize $parser!") unless ($parser->initialization_complete);
88 }
89
90 sub ordered_records_from_metarecord { # XXX Replace with QP-based search-within-MR
91     my $self = shift;
92     my $client = shift;
93     my $mr = shift;
94     my $formats = shift; # dead
95     my $org = shift;
96     my $depth = shift;
97
98     my $copies_visible = 'LEFT JOIN asset.opac_visible_copies vc ON (br.id = vc.record)';
99     $copies_visible = '' if ($self->api_name =~ /staff/o);
100
101     my $copies_visible_count = ',COUNT(vc.id)';
102     $copies_visible_count = '' if ($self->api_name =~ /staff/o);
103
104     my $descendants = '';
105     if ($org) {
106         $descendants = defined($depth) ?
107             ",actor.org_unit_descendants($org, $depth) d" :
108             ",actor.org_unit_descendants($org) d" ;
109     }
110
111     my $sql = <<"    SQL";
112         SELECT  br.id,
113                 br.quality,
114                 s.value
115                 $copies_visible_count
116           FROM  metabib.metarecord_source_map sm
117                 JOIN biblio.record_entry br ON (sm.source = br.id AND NOT br.deleted)
118                 LEFT JOIN metabib.record_sorter s ON (s.source = br.id AND s.attr = 'titlesort')
119                 LEFT JOIN config.bib_source bs ON (br.source = bs.id)
120                 $copies_visible
121                 $descendants
122           WHERE sm.metarecord = ?
123     SQL
124
125     my $having = '';
126     if ($copies_visible) {
127         $sql .= 'AND (bs.transcendant OR ';
128         if ($descendants) {
129                 $sql .= 'vc.circ_lib = d.id)';
130         } else {
131             $sql .= 'vc.id IS NOT NULL)'
132         }
133         $having = 'HAVING COUNT(vc.id) > 0';
134     }
135
136     $sql .= <<"    SQL";
137       GROUP BY 1, 2, 3
138       $having
139       ORDER BY
140         br.quality DESC,
141         s.value ASC NULLS LAST
142     SQL
143
144     my $ids = metabib::metarecord_source_map->db_Main->selectcol_arrayref($sql, {}, "$mr");
145     return $ids if ($self->api_name =~ /atomic$/o);
146
147     $client->respond( $_ ) for ( @$ids );
148     return undef;
149
150 }
151 __PACKAGE__->register_method(
152     api_name    => 'open-ils.storage.ordered.metabib.metarecord.records',
153     method      => 'ordered_records_from_metarecord',
154     api_level   => 1,
155     cachable    => 1,
156 );
157 __PACKAGE__->register_method(
158     api_name    => 'open-ils.storage.ordered.metabib.metarecord.records.staff',
159     method      => 'ordered_records_from_metarecord',
160     api_level   => 1,
161     cachable    => 1,
162 );
163
164 __PACKAGE__->register_method(
165     api_name    => 'open-ils.storage.ordered.metabib.metarecord.records.atomic',
166     method      => 'ordered_records_from_metarecord',
167     api_level   => 1,
168     cachable    => 1,
169 );
170 __PACKAGE__->register_method(
171     api_name    => 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic',
172     method      => 'ordered_records_from_metarecord',
173     api_level   => 1,
174     cachable    => 1,
175 );
176
177 # XXX: this subroutine and its two registered methods are marked for 
178 # deprecation, as they do not work properly in 2.x (these tags are no longer
179 # normalized in mfr) and are not in known use
180 sub isxn_search {
181     my $self = shift;
182     my $client = shift;
183     my $isxn = lc(shift());
184
185     $isxn =~ s/^\s*//o;
186     $isxn =~ s/\s*$//o;
187     $isxn =~ s/-//o if ($self->api_name =~ /isbn/o);
188
189     my $tag = ($self->api_name =~ /isbn/o) ? "'020' OR f.tag = '024'" : "'022'";
190
191     my $fr_table = metabib::full_rec->table;
192     my $bib_table = biblio::record_entry->table;
193
194     my $sql = <<"    SQL";
195         SELECT  DISTINCT f.record
196           FROM  $fr_table f
197             JOIN $bib_table b ON (b.id = f.record)
198           WHERE (f.tag = $tag)
199             AND f.value LIKE ?
200             AND b.deleted IS FALSE
201     SQL
202
203     my $list = metabib::full_rec->db_Main->selectcol_arrayref($sql, {}, "$isxn%");
204     $client->respond($_) for (@$list);
205     return undef;
206 }
207 __PACKAGE__->register_method(
208     api_name    => 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
209     method      => 'isxn_search',
210     api_level   => 1,
211     stream      => 1,
212 );
213 __PACKAGE__->register_method(
214     api_name    => 'open-ils.storage.id_list.biblio.record_entry.search.issn',
215     method      => 'isxn_search',
216     api_level   => 1,
217     stream      => 1,
218 );
219
220 sub metarecord_copy_count {
221     my $self = shift;
222     my $client = shift;
223
224     my %args = @_;
225
226     my $sm_table = metabib::metarecord_source_map->table;
227     my $rd_table = metabib::record_descriptor->table;
228     my $cn_table = asset::call_number->table;
229     my $cp_table = asset::copy->table;
230     my $br_table = biblio::record_entry->table;
231     my $src_table = config::bib_source->table;
232     my $cl_table = asset::copy_location->table;
233     my $cs_table = config::copy_status->table;
234     my $out_table = actor::org_unit_type->table;
235
236     my $descendants = "actor.org_unit_descendants(u.id)";
237     my $ancestors = "actor.org_unit_ancestors(?) u JOIN $out_table t ON (u.ou_type = t.id)";
238
239     if ($args{org_unit} < 0) {
240         $args{org_unit} *= -1;
241         $ancestors = "(select org_unit as id from actor.org_lasso_map where lasso = ?) u CROSS JOIN (SELECT -1 AS depth) t";
242     }
243
244     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';
245     $copies_visible = '' if ($self->api_name =~ /staff/o);
246
247     my (@types,@forms,@blvl);
248     my ($t_filter, $f_filter, $b_filter) = ('','','');
249
250     if ($args{format}) {
251         my ($t, $f, $b) = split '-', $args{format};
252         @types = split '', $t;
253         @forms = split '', $f;
254         @blvl = split '', $b;
255
256         if (@types) {
257             $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
258         }
259
260         if (@forms) {
261             $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
262         }
263
264         if (@blvl) {
265             $b_filter .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
266         }
267     }
268
269     my $sql = <<"    SQL";
270         SELECT  t.depth,
271             u.id AS org_unit,
272             sum(
273                 (SELECT count(cp.id)
274                   FROM  $sm_table r
275                     JOIN $cn_table cn ON (cn.record = r.source)
276                     JOIN $rd_table rd ON (cn.record = rd.record)
277                     JOIN $cp_table cp ON (cn.id = cp.call_number)
278                         JOIN $cs_table cs ON (cp.status = cs.id)
279                         JOIN $cl_table cl ON (cp.location = cl.id)
280                     JOIN $descendants a ON (cp.circ_lib = a.id)
281                   WHERE r.metarecord = ?
282                     AND cn.deleted IS FALSE
283                     AND cp.deleted IS FALSE
284                     $copies_visible
285                     $t_filter
286                     $f_filter
287                     $b_filter
288                 )
289             ) AS count,
290             sum(
291                 (SELECT count(cp.id)
292                   FROM  $sm_table r
293                     JOIN $cn_table cn ON (cn.record = r.source)
294                     JOIN $rd_table rd ON (cn.record = rd.record)
295                     JOIN $cp_table cp ON (cn.id = cp.call_number)
296                         JOIN $cs_table cs ON (cp.status = cs.id)
297                         JOIN $cl_table cl ON (cp.location = cl.id)
298                     JOIN $descendants a ON (cp.circ_lib = a.id)
299                   WHERE r.metarecord = ?
300                     AND cp.status IN (0,7,12)
301                     AND cn.deleted IS FALSE
302                     AND cp.deleted IS FALSE
303                     $copies_visible
304                     $t_filter
305                     $f_filter
306                     $b_filter
307                 )
308             ) AS available,
309             sum(
310                 (SELECT count(cp.id)
311                   FROM  $sm_table r
312                     JOIN $cn_table cn ON (cn.record = r.source)
313                     JOIN $rd_table rd ON (cn.record = rd.record)
314                     JOIN $cp_table cp ON (cn.id = cp.call_number)
315                         JOIN $cs_table cs ON (cp.status = cs.id)
316                         JOIN $cl_table cl ON (cp.location = cl.id)
317                   WHERE r.metarecord = ?
318                     AND cn.deleted IS FALSE
319                     AND cp.deleted IS FALSE
320                     AND cp.opac_visible IS TRUE
321                     AND cs.opac_visible IS TRUE
322                     AND cl.opac_visible IS TRUE
323                     $t_filter
324                     $f_filter
325                     $b_filter
326                 )
327             ) AS unshadow,
328             sum(    
329                 (SELECT sum(1)
330                   FROM  $sm_table r
331                         JOIN $br_table br ON (br.id = r.source)
332                         JOIN $src_table src ON (src.id = br.source)
333                   WHERE r.metarecord = ?
334                     AND src.transcendant IS TRUE
335                 )
336             ) AS transcendant
337
338           FROM  $ancestors
339           GROUP BY 1,2
340     SQL
341
342     my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
343     $sth->execute(  ''.$args{metarecord},
344             @types, 
345             @forms,
346             @blvl,
347             ''.$args{metarecord},
348             @types, 
349             @forms,
350             @blvl,
351             ''.$args{metarecord},
352             @types, 
353             @forms,
354             @blvl,
355             ''.$args{metarecord},
356             ''.$args{org_unit}, 
357     ); 
358
359     while ( my $row = $sth->fetchrow_hashref ) {
360         $client->respond( $row );
361     }
362     return undef;
363 }
364 __PACKAGE__->register_method(
365     api_name    => 'open-ils.storage.metabib.metarecord.copy_count',
366     method      => 'metarecord_copy_count',
367     api_level   => 1,
368     stream      => 1,
369     cachable    => 1,
370 );
371 __PACKAGE__->register_method(
372     api_name    => 'open-ils.storage.metabib.metarecord.copy_count.staff',
373     method      => 'metarecord_copy_count',
374     api_level   => 1,
375     stream      => 1,
376     cachable    => 1,
377 );
378
379 sub biblio_multi_search_full_rec {
380     my $self   = shift;
381     my $client = shift;
382     my %args   = @_;
383
384     my $class_join = $args{class_join} || 'AND';
385     my $limit      = $args{limit}      || 100;
386     my $offset     = $args{offset}     || 0;
387     my $sort       = $args{'sort'};
388     my $sort_dir   = $args{sort_dir}   || 'DESC';
389
390     my @binds;
391     my @selects;
392
393     for my $arg (@{ $args{searches} }) {
394         my $term     = $$arg{term};
395         my $limiters = $$arg{restrict};
396
397         my ($index_col)  = metabib::full_rec->columns('FTS');
398         $index_col ||= 'value';
399         my $search_table = metabib::full_rec->table;
400
401         my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
402
403         my $fts_where = $fts->sql_where_clause();
404         my @fts_ranks = $fts->fts_rank;
405
406         my $rank = join(' + ', @fts_ranks);
407
408         my @wheres;
409         for my $limit (@$limiters) {
410             if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
411                 # MARC control field; mfr.subfield is NULL
412                 push @wheres, "( tag = ? AND $fts_where )";
413                 push @binds, $$limit{tag};
414                 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
415             } else {
416                 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
417                 push @binds, $$limit{tag}, $$limit{subfield};
418                 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
419             }
420         }
421         my $where = join(' OR ', @wheres);
422
423         push @selects, "SELECT record, AVG($rank) as sum FROM $search_table WHERE $where GROUP BY record";
424
425     }
426
427     my $descendants = defined($args{depth}) ?
428                 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
429                 "actor.org_unit_descendants($args{org_unit})" ;
430
431
432     my $metabib_record_descriptor = metabib::record_descriptor->table;
433     my $metabib_full_rec = metabib::full_rec->table;
434     my $asset_call_number_table = asset::call_number->table;
435     my $asset_copy_table = asset::copy->table;
436     my $cs_table = config::copy_status->table;
437     my $cl_table = asset::copy_location->table;
438     my $br_table = biblio::record_entry->table;
439
440     my $cj = 'HAVING COUNT(x.record) = ' . scalar(@selects) if ($class_join eq 'AND');
441     my $search_table =
442         '(SELECT x.record, sum(x.sum) FROM (('.
443             join(') UNION ALL (', @selects).
444             ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
445
446     my $has_vols = 'AND cn.owning_lib = d.id';
447     my $has_copies = 'AND cp.call_number = cn.id';
448     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';
449
450     if ($self->api_name =~ /staff/o) {
451         $copies_visible = '';
452         $has_copies     = '' if ($ou_type == 0);
453         $has_vols       = '' if ($ou_type == 0);
454     }
455
456     my ($t_filter, $f_filter) = ('','');
457     my ($a_filter, $l_filter, $lf_filter) = ('','','');
458
459     my $use_rd = 0;
460     if (my $a = $args{audience}) {
461         $a = [$a] if (!ref($a));
462         my @aud = @$a;
463             
464         $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
465         push @binds, @aud;
466         $use_rd = 1;
467     }
468
469     if (my $l = $args{language}) {
470         $l = [$l] if (!ref($l));
471         my @lang = @$l;
472
473         $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
474         push @binds, @lang;
475         $use_rd = 1;
476     }
477
478     if (my $f = $args{lit_form}) {
479         $f = [$f] if (!ref($f));
480         my @lit_form = @$f;
481
482         $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
483         push @binds, @lit_form;
484         $use_rd = 1;
485     }
486
487     if (my $f = $args{item_form}) {
488         $f = [$f] if (!ref($f));
489         my @forms = @$f;
490
491         $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
492         push @binds, @forms;
493         $use_rd = 1;
494     }
495
496     if (my $t = $args{item_type}) {
497         $t = [$t] if (!ref($t));
498         my @types = @$t;
499
500         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
501         push @binds, @types;
502         $use_rd = 1;
503     }
504
505
506     if ($args{format}) {
507         my ($t, $f) = split '-', $args{format};
508         my @types = split '', $t;
509         my @forms = split '', $f;
510         if (@types) {
511             $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
512             $use_rd = 1;
513         }
514
515         if (@forms) {
516             $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
517             $use_rd = 1;
518         }
519         push @binds, @types, @forms;
520     }
521
522     my $relevance = 'sum(f.sum)';
523     $relevance = 1 if (!$copies_visible);
524
525     my $string_default_sort = 'zzzz';
526     $string_default_sort = 'AAAA' if ($sort_dir =~ /^DESC$/i);
527
528     my $number_default_sort = '9999';
529     $number_default_sort = '0000' if ($sort_dir =~/^DESC$/i);
530
531     my $rank = $relevance;
532     if (lc($sort) eq 'pubdate') {
533         $rank = <<"        RANK";
534             ( FIRST ((
535                 SELECT  COALESCE(SUBSTRING(MAX(frp.value) FROM E'\\\\d{4}'), '$number_default_sort')::INT
536                   FROM  $metabib_full_rec frp
537                   WHERE frp.record = f.record
538                     AND frp.tag = '260'
539                     AND frp.subfield = 'c'
540                   LIMIT 1
541             )) )
542         RANK
543     } elsif (lc($sort) eq 'create_date') {
544         $rank = <<"        RANK";
545             ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
546         RANK
547     } elsif (lc($sort) eq 'edit_date') {
548         $rank = <<"        RANK";
549             ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
550         RANK
551     } elsif (lc($sort) =~ /^title/i) {
552         $rank = <<"        RANK";
553             ( FIRST ((
554                 SELECT  COALESCE(LTRIM(SUBSTR(MAX(frt.value), COALESCE(SUBSTRING(MAX(frt.ind2) FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
555                   FROM  $metabib_full_rec frt
556                   WHERE frt.record = f.record
557                     AND frt.tag = '245'
558                     AND frt.subfield = 'a'
559                   LIMIT 1
560             )) )
561         RANK
562     } elsif (lc($sort) =~ /^author/i) {
563         $rank = <<"        RANK";
564             ( FIRST((
565                 SELECT  COALESCE(LTRIM(MAX(query.value)), '$string_default_sort')
566                   FROM  (
567                             SELECT fra.value
568                             FROM $metabib_full_rec fra
569                             WHERE fra.record = f.record
570                                 AND fra.tag LIKE '1%'
571                                 AND fra.subfield = 'a'
572                             ORDER BY fra.tag::text::int
573                             LIMIT 1
574                         ) query
575             )) )
576         RANK
577     } else {
578         $sort = undef;
579     }
580
581     my $rd_join = $use_rd ? "$metabib_record_descriptor rd," : '';
582     my $rd_filter = $use_rd ? 'AND rd.record = f.record' : '';
583
584     if ($copies_visible) {
585         $select = <<"        SQL";
586             SELECT  f.record, $relevance, count(DISTINCT cp.id), $rank
587             FROM    $search_table f,
588                 $asset_call_number_table cn,
589                 $asset_copy_table cp,
590                 $cs_table cs,
591                 $cl_table cl,
592                 $br_table br,
593                 $rd_join
594                 $descendants d
595             WHERE   br.id = f.record
596                 AND cn.record = f.record
597                 AND cp.status = cs.id
598                 AND cp.location = cl.id
599                 AND br.deleted IS FALSE
600                 AND cn.deleted IS FALSE
601                 AND cp.deleted IS FALSE
602                 $rd_filter
603                 $has_vols
604                 $has_copies
605                 $copies_visible
606                 $t_filter
607                 $f_filter
608                 $a_filter
609                 $l_filter
610                 $lf_filter
611             GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
612             ORDER BY 4 $sort_dir,3 DESC
613         SQL
614     } else {
615         $select = <<"        SQL";
616             SELECT  f.record, 1, 1, $rank
617             FROM    $search_table f,
618                 $rd_join
619                 $br_table br
620             WHERE   br.id = f.record
621                 AND br.deleted IS FALSE
622                 $rd_filter
623                 $t_filter
624                 $f_filter
625                 $a_filter
626                 $l_filter
627                 $lf_filter
628             GROUP BY 1,2,3 
629             ORDER BY 4 $sort_dir
630         SQL
631     }
632
633
634     $log->debug("Search SQL :: [$select]",DEBUG);
635
636     my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
637     $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
638
639     my $max = 0;
640     $max = 1 if (!@$recs);
641     for (@$recs) {
642         $max = $$_[1] if ($$_[1] > $max);
643     }
644
645     my $count = @$recs;
646     for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
647         next unless ($$rec[0]);
648         my ($rid,$rank,$junk,$skip) = @$rec;
649         $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
650     }
651     return undef;
652 }
653 __PACKAGE__->register_method(
654     api_name    => 'open-ils.storage.biblio.full_rec.multi_search',
655     method      => 'biblio_multi_search_full_rec',
656     api_level   => 1,
657     stream      => 1,
658     cachable    => 1,
659 );
660 __PACKAGE__->register_method(
661     api_name    => 'open-ils.storage.biblio.full_rec.multi_search.staff',
662     method      => 'biblio_multi_search_full_rec',
663     api_level   => 1,
664     stream      => 1,
665     cachable    => 1,
666 );
667
668 sub search_full_rec {
669     my $self = shift;
670     my $client = shift;
671
672     my %args = @_;
673     
674     my $term = $args{term};
675     my $limiters = $args{restrict};
676
677     my ($index_col) = metabib::full_rec->columns('FTS');
678     $index_col ||= 'value';
679     my $search_table = metabib::full_rec->table;
680
681     my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
682
683     my $fts_where = $fts->sql_where_clause();
684     my @fts_ranks = $fts->fts_rank;
685
686     my $rank = join(' + ', @fts_ranks);
687
688     my @binds;
689     my @wheres;
690     for my $limit (@$limiters) {
691         if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
692             # MARC control field; mfr.subfield is NULL
693             push @wheres, "( tag = ? AND $fts_where )";
694             push @binds, $$limit{tag};
695             $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
696         } else {
697             push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
698             push @binds, $$limit{tag}, $$limit{subfield};
699             $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
700         }
701     }
702     my $where = join(' OR ', @wheres);
703
704     my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
705
706     $log->debug("Search SQL :: [$select]",DEBUG);
707
708     my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
709     $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
710
711     $client->respond($_) for (@$recs);
712     return undef;
713 }
714 __PACKAGE__->register_method(
715     api_name    => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
716     method      => 'search_full_rec',
717     api_level   => 1,
718     stream      => 1,
719     cachable    => 1,
720 );
721 __PACKAGE__->register_method(
722     api_name    => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
723     method      => 'search_full_rec',
724     api_level   => 1,
725     stream      => 1,
726     cachable    => 1,
727 );
728
729
730 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
731 sub search_class_fts {
732     my $self = shift;
733     my $client = shift;
734     my %args = @_;
735     
736     my $term = $args{term};
737     my $ou = $args{org_unit};
738     my $ou_type = $args{depth};
739     my $limit = $args{limit};
740     my $offset = $args{offset};
741
742     my $limit_clause = '';
743     my $offset_clause = '';
744
745     $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
746     $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
747
748     my (@types,@forms);
749     my ($t_filter, $f_filter) = ('','');
750
751     if ($args{format}) {
752         my ($t, $f) = split '-', $args{format};
753         @types = split '', $t;
754         @forms = split '', $f;
755         if (@types) {
756             $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
757         }
758
759         if (@forms) {
760             $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
761         }
762     }
763
764
765
766     my $descendants = defined($ou_type) ?
767                 "actor.org_unit_descendants($ou, $ou_type)" :
768                 "actor.org_unit_descendants($ou)";
769
770     my $class = $self->{cdbi};
771     my $search_table = $class->table;
772
773     my $metabib_record_descriptor = metabib::record_descriptor->table;
774     my $metabib_metarecord = metabib::metarecord->table;
775     my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
776     my $asset_call_number_table = asset::call_number->table;
777     my $asset_copy_table = asset::copy->table;
778     my $cs_table = config::copy_status->table;
779     my $cl_table = asset::copy_location->table;
780
781     my ($index_col) = $class->columns('FTS');
782     $index_col ||= 'value';
783
784     (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
785     my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
786
787     my $fts_where = $fts->sql_where_clause;
788     my @fts_ranks = $fts->fts_rank;
789
790     my $rank = join(' + ', @fts_ranks);
791
792     my $has_vols = 'AND cn.owning_lib = d.id';
793     my $has_copies = 'AND cp.call_number = cn.id';
794     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';
795
796     my $visible_count = ', count(DISTINCT cp.id)';
797     my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
798
799     if ($self->api_name =~ /staff/o) {
800         $copies_visible = '';
801         $visible_count_test = '';
802         $has_copies = '' if ($ou_type == 0);
803         $has_vols = '' if ($ou_type == 0);
804     }
805
806     my $rank_calc = <<"    RANK";
807         , (SUM( $rank
808             * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
809             * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
810             * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
811         )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
812     RANK
813
814     $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
815
816     if ($copies_visible) {
817         $select = <<"        SQL";
818             SELECT  m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
819             FROM    $search_table f,
820                 $metabib_metarecord_source_map_table m,
821                 $asset_call_number_table cn,
822                 $asset_copy_table cp,
823                 $cs_table cs,
824                 $cl_table cl,
825                 $metabib_record_descriptor rd,
826                 $descendants d
827             WHERE   $fts_where
828                 AND m.source = f.source
829                 AND cn.record = m.source
830                 AND rd.record = m.source
831                 AND cp.status = cs.id
832                 AND cp.location = cl.id
833                 $has_vols
834                 $has_copies
835                 $copies_visible
836                 $t_filter
837                 $f_filter
838             GROUP BY 1 $visible_count_test
839             ORDER BY 2 DESC,3
840             $limit_clause $offset_clause
841         SQL
842     } else {
843         $select = <<"        SQL";
844             SELECT  m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
845             FROM    $search_table f,
846                 $metabib_metarecord_source_map_table m,
847                 $metabib_record_descriptor rd
848             WHERE   $fts_where
849                 AND m.source = f.source
850                 AND rd.record = m.source
851                 $t_filter
852                 $f_filter
853             GROUP BY 1, 4
854             ORDER BY 2 DESC,3
855             $limit_clause $offset_clause
856         SQL
857     }
858
859     $log->debug("Field Search SQL :: [$select]",DEBUG);
860
861     my $SQLstring = join('%',$fts->words);
862     my $REstring = join('\\s+',$fts->words);
863     my $first_word = ($fts->words)[0].'%';
864     my $recs = ($self->api_name =~ /unordered/o) ? 
865             $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
866             $class->db_Main->selectall_arrayref($select, {},
867                 '%'.lc($SQLstring).'%',         # phrase order match
868                 lc($first_word),            # first word match
869                 '^\\s*'.lc($REstring).'\\s*/?\s*$', # full exact match
870                 @types, @forms
871             );
872     
873     $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
874
875     $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
876     return undef;
877 }
878
879 for my $class ( qw/title author subject keyword series identifier/ ) {
880     __PACKAGE__->register_method(
881         api_name    => "open-ils.storage.metabib.$class.search_fts.metarecord",
882         method      => 'search_class_fts',
883         api_level   => 1,
884         stream      => 1,
885         cdbi        => "metabib::${class}_field_entry",
886         cachable    => 1,
887     );
888     __PACKAGE__->register_method(
889         api_name    => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
890         method      => 'search_class_fts',
891         api_level   => 1,
892         stream      => 1,
893         cdbi        => "metabib::${class}_field_entry",
894         cachable    => 1,
895     );
896     __PACKAGE__->register_method(
897         api_name    => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
898         method      => 'search_class_fts',
899         api_level   => 1,
900         stream      => 1,
901         cdbi        => "metabib::${class}_field_entry",
902         cachable    => 1,
903     );
904     __PACKAGE__->register_method(
905         api_name    => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
906         method      => 'search_class_fts',
907         api_level   => 1,
908         stream      => 1,
909         cdbi        => "metabib::${class}_field_entry",
910         cachable    => 1,
911     );
912 }
913
914 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
915 sub search_class_fts_count {
916     my $self = shift;
917     my $client = shift;
918     my %args = @_;
919     
920     my $term = $args{term};
921     my $ou = $args{org_unit};
922     my $ou_type = $args{depth};
923     my $limit = $args{limit} || 100;
924     my $offset = $args{offset} || 0;
925
926     my $descendants = defined($ou_type) ?
927                 "actor.org_unit_descendants($ou, $ou_type)" :
928                 "actor.org_unit_descendants($ou)";
929         
930     my (@types,@forms);
931     my ($t_filter, $f_filter) = ('','');
932
933     if ($args{format}) {
934         my ($t, $f) = split '-', $args{format};
935         @types = split '', $t;
936         @forms = split '', $f;
937         if (@types) {
938             $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
939         }
940
941         if (@forms) {
942             $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
943         }
944     }
945
946
947     (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
948
949     my $class = $self->{cdbi};
950     my $search_table = $class->table;
951
952     my $metabib_record_descriptor = metabib::record_descriptor->table;
953     my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
954     my $asset_call_number_table = asset::call_number->table;
955     my $asset_copy_table = asset::copy->table;
956     my $cs_table = config::copy_status->table;
957     my $cl_table = asset::copy_location->table;
958
959     my ($index_col) = $class->columns('FTS');
960     $index_col ||= 'value';
961
962     my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
963
964     my $fts_where = $fts->sql_where_clause;
965
966     my $has_vols = 'AND cn.owning_lib = d.id';
967     my $has_copies = 'AND cp.call_number = cn.id';
968     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';
969     if ($self->api_name =~ /staff/o) {
970         $copies_visible = '';
971         $has_vols = '' if ($ou_type == 0);
972         $has_copies = '' if ($ou_type == 0);
973     }
974
975     # XXX test an "EXISTS version of descendant checking...
976     my $select;
977     if ($copies_visible) {
978         $select = <<"        SQL";
979         SELECT  count(distinct  m.metarecord)
980           FROM  $search_table f,
981             $metabib_metarecord_source_map_table m,
982             $metabib_metarecord_source_map_table mr,
983             $asset_call_number_table cn,
984             $asset_copy_table cp,
985             $cs_table cs,
986             $cl_table cl,
987             $metabib_record_descriptor rd,
988             $descendants d
989           WHERE $fts_where
990             AND mr.source = f.source
991             AND mr.metarecord = m.metarecord
992             AND cn.record = m.source
993             AND rd.record = m.source
994             AND cp.status = cs.id
995             AND cp.location = cl.id
996             $has_vols
997             $has_copies
998             $copies_visible
999             $t_filter
1000             $f_filter
1001         SQL
1002     } else {
1003         $select = <<"        SQL";
1004         SELECT  count(distinct  m.metarecord)
1005           FROM  $search_table f,
1006             $metabib_metarecord_source_map_table m,
1007             $metabib_metarecord_source_map_table mr,
1008             $metabib_record_descriptor rd
1009           WHERE $fts_where
1010             AND mr.source = f.source
1011             AND mr.metarecord = m.metarecord
1012             AND rd.record = m.source
1013             $t_filter
1014             $f_filter
1015         SQL
1016     }
1017
1018     $log->debug("Field Search Count SQL :: [$select]",DEBUG);
1019
1020     my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1021     
1022     $log->debug("Count Search yielded $recs results.",DEBUG);
1023
1024     return $recs;
1025
1026 }
1027 for my $class ( qw/title author subject keyword series identifier/ ) {
1028     __PACKAGE__->register_method(
1029         api_name    => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
1030         method      => 'search_class_fts_count',
1031         api_level   => 1,
1032         stream      => 1,
1033         cdbi        => "metabib::${class}_field_entry",
1034         cachable    => 1,
1035     );
1036     __PACKAGE__->register_method(
1037         api_name    => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
1038         method      => 'search_class_fts_count',
1039         api_level   => 1,
1040         stream      => 1,
1041         cdbi        => "metabib::${class}_field_entry",
1042         cachable    => 1,
1043     );
1044 }
1045
1046
1047 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1048 sub postfilter_search_class_fts {
1049     my $self = shift;
1050     my $client = shift;
1051     my %args = @_;
1052     
1053     my $term = $args{term};
1054     my $sort = $args{'sort'};
1055     my $sort_dir = $args{sort_dir} || 'DESC';
1056     my $ou = $args{org_unit};
1057     my $ou_type = $args{depth};
1058     my $limit = $args{limit} || 10;
1059     my $visibility_limit = $args{visibility_limit} || 5000;
1060     my $offset = $args{offset} || 0;
1061
1062     my $outer_limit = 1000;
1063
1064     my $limit_clause = '';
1065     my $offset_clause = '';
1066
1067     $limit_clause = "LIMIT $outer_limit";
1068     $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1069
1070     my (@types,@forms,@lang,@aud,@lit_form);
1071     my ($t_filter, $f_filter) = ('','');
1072     my ($a_filter, $l_filter, $lf_filter) = ('','','');
1073     my ($ot_filter, $of_filter) = ('','');
1074     my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1075
1076     if (my $a = $args{audience}) {
1077         $a = [$a] if (!ref($a));
1078         @aud = @$a;
1079             
1080         $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1081         $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1082     }
1083
1084     if (my $l = $args{language}) {
1085         $l = [$l] if (!ref($l));
1086         @lang = @$l;
1087
1088         $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1089         $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1090     }
1091
1092     if (my $f = $args{lit_form}) {
1093         $f = [$f] if (!ref($f));
1094         @lit_form = @$f;
1095
1096         $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1097         $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1098     }
1099
1100     if ($args{format}) {
1101         my ($t, $f) = split '-', $args{format};
1102         @types = split '', $t;
1103         @forms = split '', $f;
1104         if (@types) {
1105             $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1106             $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1107         }
1108
1109         if (@forms) {
1110             $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1111             $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1112         }
1113     }
1114
1115
1116     my $descendants = defined($ou_type) ?
1117                 "actor.org_unit_descendants($ou, $ou_type)" :
1118                 "actor.org_unit_descendants($ou)";
1119
1120     my $class = $self->{cdbi};
1121     my $search_table = $class->table;
1122
1123     my $metabib_full_rec = metabib::full_rec->table;
1124     my $metabib_record_descriptor = metabib::record_descriptor->table;
1125     my $metabib_metarecord = metabib::metarecord->table;
1126     my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1127     my $asset_call_number_table = asset::call_number->table;
1128     my $asset_copy_table = asset::copy->table;
1129     my $cs_table = config::copy_status->table;
1130     my $cl_table = asset::copy_location->table;
1131     my $br_table = biblio::record_entry->table;
1132
1133     my ($index_col) = $class->columns('FTS');
1134     $index_col ||= 'value';
1135
1136     (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1137
1138     my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1139
1140     my $SQLstring = join('%',map { lc($_) } $fts->words);
1141     my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1142     my $first_word = lc(($fts->words)[0]).'%';
1143
1144     my $fts_where = $fts->sql_where_clause;
1145     my @fts_ranks = $fts->fts_rank;
1146
1147     my %bonus = ();
1148     $bonus{'metabib::identifier_field_entry'} =
1149         $bonus{'metabib::keyword_field_entry'} = [
1150             { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring }
1151         ];
1152
1153     $bonus{'metabib::title_field_entry'} =
1154         $bonus{'metabib::series_field_entry'} = [
1155             { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1156             { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1157             @{ $bonus{'metabib::keyword_field_entry'} }
1158         ];
1159
1160     my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1161     $bonus_list ||= '1';
1162
1163     my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1164
1165     my $relevance = join(' + ', @fts_ranks);
1166     $relevance = <<"    RANK";
1167             (SUM( ( $relevance )  * ( $bonus_list ) )/COUNT(m.source))
1168     RANK
1169
1170     my $string_default_sort = 'zzzz';
1171     $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1172
1173     my $number_default_sort = '9999';
1174     $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1175
1176     my $rank = $relevance;
1177     if (lc($sort) eq 'pubdate') {
1178         $rank = <<"        RANK";
1179             ( FIRST ((
1180                 SELECT  COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1181                   FROM  $metabib_full_rec frp
1182                   WHERE frp.record = mr.master_record
1183                     AND frp.tag = '260'
1184                     AND frp.subfield = 'c'
1185                   LIMIT 1
1186             )) )
1187         RANK
1188     } elsif (lc($sort) eq 'create_date') {
1189         $rank = <<"        RANK";
1190             ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1191         RANK
1192     } elsif (lc($sort) eq 'edit_date') {
1193         $rank = <<"        RANK";
1194             ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1195         RANK
1196     } elsif (lc($sort) eq 'title') {
1197         $rank = <<"        RANK";
1198             ( FIRST ((
1199                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1200                   FROM  $metabib_full_rec frt
1201                   WHERE frt.record = mr.master_record
1202                     AND frt.tag = '245'
1203                     AND frt.subfield = 'a'
1204                   LIMIT 1
1205             )) )
1206         RANK
1207     } elsif (lc($sort) eq 'author') {
1208         $rank = <<"        RANK";
1209             ( FIRST((
1210                 SELECT  COALESCE(LTRIM(fra.value),'$string_default_sort')
1211                   FROM  $metabib_full_rec fra
1212                   WHERE fra.record = mr.master_record
1213                     AND fra.tag LIKE '1%'
1214                     AND fra.subfield = 'a'
1215                   ORDER BY fra.tag::text::int
1216                   LIMIT 1
1217             )) )
1218         RANK
1219     } else {
1220         $sort = undef;
1221     }
1222
1223     my $select = <<"    SQL";
1224         SELECT  m.metarecord,
1225             $relevance,
1226             CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1227             $rank
1228         FROM    $search_table f,
1229             $metabib_metarecord_source_map_table m,
1230             $metabib_metarecord_source_map_table smrs,
1231             $metabib_metarecord mr,
1232             $metabib_record_descriptor rd
1233         WHERE   $fts_where
1234             AND smrs.metarecord = mr.id
1235             AND m.source = f.source
1236             AND m.metarecord = mr.id
1237             AND rd.record = smrs.source
1238             $t_filter
1239             $f_filter
1240             $a_filter
1241             $l_filter
1242             $lf_filter
1243         GROUP BY m.metarecord
1244         ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1245         LIMIT $visibility_limit
1246     SQL
1247
1248     if (0) {
1249         $select = <<"        SQL";
1250
1251             SELECT  DISTINCT s.*
1252               FROM  $asset_call_number_table cn,
1253                 $metabib_metarecord_source_map_table mrs,
1254                 $asset_copy_table cp,
1255                 $cs_table cs,
1256                 $cl_table cl,
1257                 $br_table br,
1258                 $descendants d,
1259                 $metabib_record_descriptor ord,
1260                 ($select) s
1261               WHERE mrs.metarecord = s.metarecord
1262                 AND br.id = mrs.source
1263                 AND cn.record = mrs.source
1264                 AND cp.status = cs.id
1265                 AND cp.location = cl.id
1266                 AND cn.owning_lib = d.id
1267                 AND cp.call_number = cn.id
1268                 AND cp.opac_visible IS TRUE
1269                 AND cs.opac_visible IS TRUE
1270                 AND cl.opac_visible IS TRUE
1271                 AND d.opac_visible IS TRUE
1272                 AND br.active IS TRUE
1273                 AND br.deleted IS FALSE
1274                 AND ord.record = mrs.source
1275                 $ot_filter
1276                 $of_filter
1277                 $oa_filter
1278                 $ol_filter
1279                 $olf_filter
1280               ORDER BY 4 $sort_dir
1281         SQL
1282     } elsif ($self->api_name !~ /staff/o) {
1283         $select = <<"        SQL";
1284
1285             SELECT  DISTINCT s.*
1286               FROM  ($select) s
1287               WHERE EXISTS (
1288                 SELECT  1
1289                   FROM  $asset_call_number_table cn,
1290                     $metabib_metarecord_source_map_table mrs,
1291                     $asset_copy_table cp,
1292                     $cs_table cs,
1293                     $cl_table cl,
1294                     $br_table br,
1295                     $descendants d,
1296                     $metabib_record_descriptor ord
1297                 
1298                   WHERE mrs.metarecord = s.metarecord
1299                     AND br.id = mrs.source
1300                     AND cn.record = mrs.source
1301                     AND cp.status = cs.id
1302                     AND cp.location = cl.id
1303                     AND cp.circ_lib = d.id
1304                     AND cp.call_number = cn.id
1305                     AND cp.opac_visible IS TRUE
1306                     AND cs.opac_visible IS TRUE
1307                     AND cl.opac_visible IS TRUE
1308                     AND d.opac_visible IS TRUE
1309                     AND br.active IS TRUE
1310                     AND br.deleted IS FALSE
1311                     AND ord.record = mrs.source
1312                     $ot_filter
1313                     $of_filter
1314                     $oa_filter
1315                     $ol_filter
1316                     $olf_filter
1317                   LIMIT 1
1318                 )
1319               ORDER BY 4 $sort_dir
1320         SQL
1321     } else {
1322         $select = <<"        SQL";
1323
1324             SELECT  DISTINCT s.*
1325               FROM  ($select) s
1326               WHERE EXISTS (
1327                 SELECT  1
1328                   FROM  $asset_call_number_table cn,
1329                     $asset_copy_table cp,
1330                     $metabib_metarecord_source_map_table mrs,
1331                     $br_table br,
1332                     $descendants d,
1333                     $metabib_record_descriptor ord
1334                 
1335                   WHERE mrs.metarecord = s.metarecord
1336                     AND br.id = mrs.source
1337                     AND cn.record = mrs.source
1338                     AND cn.id = cp.call_number
1339                     AND br.deleted IS FALSE
1340                     AND cn.deleted IS FALSE
1341                     AND ord.record = mrs.source
1342                     AND (   cn.owning_lib = d.id
1343                         OR (    cp.circ_lib = d.id
1344                             AND cp.deleted IS FALSE
1345                         )
1346                     )
1347                     $ot_filter
1348                     $of_filter
1349                     $oa_filter
1350                     $ol_filter
1351                     $olf_filter
1352                   LIMIT 1
1353                 )
1354                 OR NOT EXISTS (
1355                 SELECT  1
1356                   FROM  $asset_call_number_table cn,
1357                     $metabib_metarecord_source_map_table mrs,
1358                     $metabib_record_descriptor ord
1359                   WHERE mrs.metarecord = s.metarecord
1360                     AND cn.record = mrs.source
1361                     AND ord.record = mrs.source
1362                     $ot_filter
1363                     $of_filter
1364                     $oa_filter
1365                     $ol_filter
1366                     $olf_filter
1367                   LIMIT 1
1368                 )
1369               ORDER BY 4 $sort_dir
1370         SQL
1371     }
1372
1373
1374     $log->debug("Field Search SQL :: [$select]",DEBUG);
1375
1376     my $recs = $class->db_Main->selectall_arrayref(
1377             $select, {},
1378             (@bonus_values > 0 ? @bonus_values : () ),
1379             ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1380             @types, @forms, @aud, @lang, @lit_form,
1381             @types, @forms, @aud, @lang, @lit_form,
1382             ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1383     
1384     $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1385
1386     my $max = 0;
1387     $max = 1 if (!@$recs);
1388     for (@$recs) {
1389         $max = $$_[1] if ($$_[1] > $max);
1390     }
1391
1392     my $count = scalar(@$recs);
1393     for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1394         my ($mrid,$rank,$skip) = @$rec;
1395         $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1396     }
1397     return undef;
1398 }
1399
1400 for my $class ( qw/title author subject keyword series identifier/ ) {
1401     __PACKAGE__->register_method(
1402         api_name    => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1403         method      => 'postfilter_search_class_fts',
1404         api_level   => 1,
1405         stream      => 1,
1406         cdbi        => "metabib::${class}_field_entry",
1407         cachable    => 1,
1408     );
1409     __PACKAGE__->register_method(
1410         api_name    => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1411         method      => 'postfilter_search_class_fts',
1412         api_level   => 1,
1413         stream      => 1,
1414         cdbi        => "metabib::${class}_field_entry",
1415         cachable    => 1,
1416     );
1417 }
1418
1419
1420
1421 my $_cdbi = {   title   => "metabib::title_field_entry",
1422         author  => "metabib::author_field_entry",
1423         subject => "metabib::subject_field_entry",
1424         keyword => "metabib::keyword_field_entry",
1425         series  => "metabib::series_field_entry",
1426         identifier  => "metabib::identifier_field_entry",
1427 };
1428
1429 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1430 sub postfilter_search_multi_class_fts {
1431     my $self   = shift;
1432     my $client = shift;
1433     my %args   = @_;
1434     
1435     my $sort             = $args{'sort'};
1436     my $sort_dir         = $args{sort_dir} || 'DESC';
1437     my $ou               = $args{org_unit};
1438     my $ou_type          = $args{depth};
1439     my $limit            = $args{limit}  || 10;
1440     my $offset           = $args{offset} ||  0;
1441     my $visibility_limit = $args{visibility_limit} || 5000;
1442
1443     if (!$ou) {
1444         $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1445     }
1446
1447     if (!defined($args{org_unit})) {
1448         die "No target organizational unit passed to ".$self->api_name;
1449     }
1450
1451     if (! scalar( keys %{$args{searches}} )) {
1452         die "No search arguments were passed to ".$self->api_name;
1453     }
1454
1455     my $outer_limit = 1000;
1456
1457     my $limit_clause  = '';
1458     my $offset_clause = '';
1459
1460     $limit_clause  = "LIMIT $outer_limit";
1461     $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1462
1463     my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1464     my ($t_filter,   $f_filter,   $v_filter) = ('','','');
1465     my ($a_filter,   $l_filter,  $lf_filter) = ('','','');
1466     my ($ot_filter, $of_filter,  $ov_filter) = ('','','');
1467     my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1468
1469     if ($args{available}) {
1470         $avail_filter = ' AND cp.status IN (0,7,12)';
1471     }
1472
1473     if (my $a = $args{audience}) {
1474         $a = [$a] if (!ref($a));
1475         @aud = @$a;
1476             
1477         $a_filter  = ' AND  rd.audience IN ('.join(',',map{'?'}@aud).')';
1478         $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1479     }
1480
1481     if (my $l = $args{language}) {
1482         $l = [$l] if (!ref($l));
1483         @lang = @$l;
1484
1485         $l_filter  = ' AND  rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1486         $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1487     }
1488
1489     if (my $f = $args{lit_form}) {
1490         $f = [$f] if (!ref($f));
1491         @lit_form = @$f;
1492
1493         $lf_filter  = ' AND  rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1494         $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1495     }
1496
1497     if (my $f = $args{item_form}) {
1498         $f = [$f] if (!ref($f));
1499         @forms = @$f;
1500
1501         $f_filter  = ' AND  rd.item_form IN ('.join(',',map{'?'}@forms).')';
1502         $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1503     }
1504
1505     if (my $t = $args{item_type}) {
1506         $t = [$t] if (!ref($t));
1507         @types = @$t;
1508
1509         $t_filter  = ' AND  rd.item_type IN ('.join(',',map{'?'}@types).')';
1510         $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1511     }
1512
1513     if (my $v = $args{vr_format}) {
1514         $v = [$v] if (!ref($v));
1515         @vformats = @$v;
1516
1517         $v_filter  = ' AND  rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1518         $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1519     }
1520
1521
1522     # XXX legacy format and item type support
1523     if ($args{format}) {
1524         my ($t, $f) = split '-', $args{format};
1525         @types = split '', $t;
1526         @forms = split '', $f;
1527         if (@types) {
1528             $t_filter  = ' AND  rd.item_type IN ('.join(',',map{'?'}@types).')';
1529             $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1530         }
1531
1532         if (@forms) {
1533             $f_filter  .= ' AND  rd.item_form IN ('.join(',',map{'?'}@forms).')';
1534             $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1535         }
1536     }
1537
1538
1539
1540     my $descendants = defined($ou_type) ?
1541                 "actor.org_unit_descendants($ou, $ou_type)" :
1542                 "actor.org_unit_descendants($ou)";
1543
1544     my $search_table_list = '';
1545     my $fts_list          = '';
1546     my $join_table_list   = '';
1547     my @rank_list;
1548
1549     my $field_table = config::metabib_field->table;
1550
1551     my @bonus_lists;
1552     my @bonus_values;
1553     my $prev_search_group;
1554     my $curr_search_group;
1555     my $search_class;
1556     my $search_field;
1557     my $metabib_field;
1558     for my $search_group (sort keys %{$args{searches}}) {
1559         (my $search_group_name = $search_group) =~ s/\|/_/gso;
1560         ($search_class,$search_field) = split /\|/, $search_group;
1561         $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1562
1563         if ($search_field) {
1564             unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1565                 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1566                 return undef;
1567             }
1568         }
1569
1570         $prev_search_group = $curr_search_group if ($curr_search_group);
1571
1572         $curr_search_group = $search_group_name;
1573
1574         my $class = $_cdbi->{$search_class};
1575         my $search_table = $class->table;
1576
1577         my ($index_col) = $class->columns('FTS');
1578         $index_col ||= 'value';
1579
1580         
1581         my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1582
1583         my $fts_where = $fts->sql_where_clause;
1584         my @fts_ranks = $fts->fts_rank;
1585
1586         my $SQLstring = join('%',map { lc($_) } $fts->words);
1587         my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1588         my $first_word = lc(($fts->words)[0]).'%';
1589
1590         $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1591         my $rank = join(' + ', @fts_ranks);
1592
1593         my %bonus = ();
1594         $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1595         $bonus{'author'}  = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1596
1597         $bonus{'series'} = [
1598             { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1599             { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1600         ];
1601
1602         $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1603
1604         my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1605         $bonus_list ||= '1';
1606
1607         push @bonus_lists, $bonus_list;
1608         push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1609
1610
1611         #---------------------
1612
1613         $search_table_list .= "$search_table $search_group_name, ";
1614         push @rank_list,$rank;
1615         $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1616
1617         if ($metabib_field) {
1618             $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1619             $metabib_field = undef;
1620         }
1621
1622         if ($prev_search_group) {
1623             $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1624         }
1625     }
1626
1627     my $metabib_record_descriptor = metabib::record_descriptor->table;
1628     my $metabib_full_rec = metabib::full_rec->table;
1629     my $metabib_metarecord = metabib::metarecord->table;
1630     my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1631     my $asset_call_number_table = asset::call_number->table;
1632     my $asset_copy_table = asset::copy->table;
1633     my $cs_table = config::copy_status->table;
1634     my $cl_table = asset::copy_location->table;
1635     my $br_table = biblio::record_entry->table;
1636     my $source_table = config::bib_source->table;
1637
1638     my $bonuses = join (' * ', @bonus_lists);
1639     my $relevance = join (' + ', @rank_list);
1640     $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1641
1642     my $string_default_sort = 'zzzz';
1643     $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1644
1645     my $number_default_sort = '9999';
1646     $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1647
1648
1649
1650     my $secondary_sort = <<"    SORT";
1651         ( FIRST ((
1652             SELECT  COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1653               FROM  $metabib_full_rec sfrt,
1654                 $metabib_metarecord mr
1655               WHERE sfrt.record = mr.master_record
1656                 AND sfrt.tag = '245'
1657                 AND sfrt.subfield = 'a'
1658               LIMIT 1
1659         )) )
1660     SORT
1661
1662     my $rank = $relevance;
1663     if (lc($sort) eq 'pubdate') {
1664         $rank = <<"        RANK";
1665             ( FIRST ((
1666                 SELECT  COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1667                   FROM  $metabib_full_rec frp
1668                   WHERE frp.record = mr.master_record
1669                     AND frp.tag = '260'
1670                     AND frp.subfield = 'c'
1671                   LIMIT 1
1672             )) )
1673         RANK
1674     } elsif (lc($sort) eq 'create_date') {
1675         $rank = <<"        RANK";
1676             ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1677         RANK
1678     } elsif (lc($sort) eq 'edit_date') {
1679         $rank = <<"        RANK";
1680             ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1681         RANK
1682     } elsif (lc($sort) eq 'title') {
1683         $rank = <<"        RANK";
1684             ( FIRST ((
1685                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1686                   FROM  $metabib_full_rec frt
1687                   WHERE frt.record = mr.master_record
1688                     AND frt.tag = '245'
1689                     AND frt.subfield = 'a'
1690                   LIMIT 1
1691             )) )
1692         RANK
1693         $secondary_sort = <<"        SORT";
1694             ( FIRST ((
1695                 SELECT  COALESCE(SUBSTRING(sfrp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1696                   FROM  $metabib_full_rec sfrp
1697                   WHERE sfrp.record = mr.master_record
1698                     AND sfrp.tag = '260'
1699                     AND sfrp.subfield = 'c'
1700                   LIMIT 1
1701             )) )
1702         SORT
1703     } elsif (lc($sort) eq 'author') {
1704         $rank = <<"        RANK";
1705             ( FIRST((
1706                 SELECT  COALESCE(LTRIM(fra.value),'$string_default_sort')
1707                   FROM  $metabib_full_rec fra
1708                   WHERE fra.record = mr.master_record
1709                     AND fra.tag LIKE '1%'
1710                     AND fra.subfield = 'a'
1711                   ORDER BY fra.tag::text::int
1712                   LIMIT 1
1713             )) )
1714         RANK
1715     } else {
1716         push @bonus_values, @bonus_values;
1717         $sort = undef;
1718     }
1719
1720
1721     my $select = <<"    SQL";
1722         SELECT  m.metarecord,
1723             $relevance,
1724             CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1725             $rank,
1726             $secondary_sort
1727         FROM    $search_table_list
1728             $metabib_metarecord mr,
1729             $metabib_metarecord_source_map_table m,
1730             $metabib_metarecord_source_map_table smrs
1731         WHERE   m.metarecord = smrs.metarecord 
1732             AND mr.id = m.metarecord
1733             $fts_list
1734             $join_table_list
1735         GROUP BY m.metarecord
1736         -- ORDER BY 4 $sort_dir
1737         LIMIT $visibility_limit
1738     SQL
1739
1740     if ($self->api_name !~ /staff/o) {
1741         $select = <<"        SQL";
1742
1743             SELECT  s.*
1744               FROM  ($select) s
1745               WHERE EXISTS (
1746                 SELECT  1
1747                   FROM  $asset_call_number_table cn,
1748                     $metabib_metarecord_source_map_table mrs,
1749                     $asset_copy_table cp,
1750                     $cs_table cs,
1751                     $cl_table cl,
1752                     $br_table br,
1753                     $descendants d,
1754                     $metabib_record_descriptor ord
1755                   WHERE mrs.metarecord = s.metarecord
1756                     AND br.id = mrs.source
1757                     AND cn.record = mrs.source
1758                     AND cp.status = cs.id
1759                     AND cp.location = cl.id
1760                     AND cp.circ_lib = d.id
1761                     AND cp.call_number = cn.id
1762                     AND cp.opac_visible IS TRUE
1763                     AND cs.opac_visible IS TRUE
1764                     AND cl.opac_visible IS TRUE
1765                     AND d.opac_visible IS TRUE
1766                     AND br.active IS TRUE
1767                     AND br.deleted IS FALSE
1768                     AND cp.deleted IS FALSE
1769                     AND cn.deleted IS FALSE
1770                     AND ord.record = mrs.source
1771                     $ot_filter
1772                     $of_filter
1773                     $ov_filter
1774                     $oa_filter
1775                     $ol_filter
1776                     $olf_filter
1777                     $avail_filter
1778                   LIMIT 1
1779                 )
1780                 OR EXISTS (
1781                 SELECT  1
1782                   FROM  $br_table br,
1783                     $metabib_metarecord_source_map_table mrs,
1784                     $metabib_record_descriptor ord,
1785                     $source_table src
1786                   WHERE mrs.metarecord = s.metarecord
1787                     AND ord.record = mrs.source
1788                     AND br.id = mrs.source
1789                     AND br.source = src.id
1790                     AND src.transcendant IS TRUE
1791                     $ot_filter
1792                     $of_filter
1793                     $ov_filter
1794                     $oa_filter
1795                     $ol_filter
1796                     $olf_filter
1797                 )
1798               ORDER BY 4 $sort_dir, 5
1799         SQL
1800     } else {
1801         $select = <<"        SQL";
1802
1803             SELECT  DISTINCT s.*
1804               FROM  ($select) s,
1805                 $metabib_metarecord_source_map_table omrs,
1806                 $metabib_record_descriptor ord
1807               WHERE omrs.metarecord = s.metarecord
1808                 AND ord.record = omrs.source
1809                 AND (   EXISTS (
1810                         SELECT  1
1811                           FROM  $asset_call_number_table cn,
1812                             $asset_copy_table cp,
1813                             $descendants d,
1814                             $br_table br
1815                           WHERE br.id = omrs.source
1816                             AND cn.record = omrs.source
1817                             AND br.deleted IS FALSE
1818                             AND cn.deleted IS FALSE
1819                             AND cp.call_number = cn.id
1820                             AND (   cn.owning_lib = d.id
1821                                 OR (    cp.circ_lib = d.id
1822                                     AND cp.deleted IS FALSE
1823                                 )
1824                             )
1825                             $avail_filter
1826                           LIMIT 1
1827                     )
1828                     OR NOT EXISTS (
1829                         SELECT  1
1830                           FROM  $asset_call_number_table cn
1831                           WHERE cn.record = omrs.source
1832                             AND cn.deleted IS FALSE
1833                           LIMIT 1
1834                     )
1835                     OR EXISTS (
1836                     SELECT  1
1837                       FROM  $br_table br,
1838                         $metabib_metarecord_source_map_table mrs,
1839                         $metabib_record_descriptor ord,
1840                         $source_table src
1841                       WHERE mrs.metarecord = s.metarecord
1842                         AND br.id = mrs.source
1843                         AND br.source = src.id
1844                         AND src.transcendant IS TRUE
1845                         $ot_filter
1846                         $of_filter
1847                         $ov_filter
1848                         $oa_filter
1849                         $ol_filter
1850                         $olf_filter
1851                     )
1852                 )
1853                 $ot_filter
1854                 $of_filter
1855                 $ov_filter
1856                 $oa_filter
1857                 $ol_filter
1858                 $olf_filter
1859
1860               ORDER BY 4 $sort_dir, 5
1861         SQL
1862     }
1863
1864
1865     $log->debug("Field Search SQL :: [$select]",DEBUG);
1866
1867     my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1868             $select, {},
1869             @bonus_values,
1870             @types, @forms, @vformats, @aud, @lang, @lit_form,
1871             @types, @forms, @vformats, @aud, @lang, @lit_form,
1872             # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1873     );
1874     
1875     $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1876
1877     my $max = 0;
1878     $max = 1 if (!@$recs);
1879     for (@$recs) {
1880         $max = $$_[1] if ($$_[1] > $max);
1881     }
1882
1883     my $count = scalar(@$recs);
1884     for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1885         next unless ($$rec[0]);
1886         my ($mrid,$rank,$skip) = @$rec;
1887         $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1888     }
1889     return undef;
1890 }
1891
1892 __PACKAGE__->register_method(
1893     api_name    => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1894     method      => 'postfilter_search_multi_class_fts',
1895     api_level   => 1,
1896     stream      => 1,
1897     cachable    => 1,
1898 );
1899 __PACKAGE__->register_method(
1900     api_name    => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1901     method      => 'postfilter_search_multi_class_fts',
1902     api_level   => 1,
1903     stream      => 1,
1904     cachable    => 1,
1905 );
1906
1907 __PACKAGE__->register_method(
1908     api_name    => "open-ils.storage.metabib.multiclass.search_fts",
1909     method      => 'postfilter_search_multi_class_fts',
1910     api_level   => 1,
1911     stream      => 1,
1912     cachable    => 1,
1913 );
1914 __PACKAGE__->register_method(
1915     api_name    => "open-ils.storage.metabib.multiclass.search_fts.staff",
1916     method      => 'postfilter_search_multi_class_fts',
1917     api_level   => 1,
1918     stream      => 1,
1919     cachable    => 1,
1920 );
1921
1922 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1923 sub biblio_search_multi_class_fts {
1924     my $self = shift;
1925     my $client = shift;
1926     my %args = @_;
1927     
1928     my $sort             = $args{'sort'};
1929     my $sort_dir         = $args{sort_dir} || 'DESC';
1930     my $ou               = $args{org_unit};
1931     my $ou_type          = $args{depth};
1932     my $limit            = $args{limit}  || 10;
1933     my $offset           = $args{offset} ||  0;
1934     my $pref_lang        = $args{preferred_language} || 'eng';
1935     my $visibility_limit = $args{visibility_limit}  || 5000;
1936
1937     if (!$ou) {
1938         $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1939     }
1940
1941     if (! scalar( keys %{$args{searches}} )) {
1942         die "No search arguments were passed to ".$self->api_name;
1943     }
1944
1945     my $outer_limit = 1000;
1946
1947     my $limit_clause  = '';
1948     my $offset_clause = '';
1949
1950     $limit_clause  = "LIMIT $outer_limit";
1951     $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1952
1953     my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1954     my ($t_filter,   $f_filter,   $v_filter) = ('','','');
1955     my ($a_filter,   $l_filter,  $lf_filter) = ('','','');
1956     my ($ot_filter, $of_filter,  $ov_filter) = ('','','');
1957     my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1958
1959     if ($args{available}) {
1960         $avail_filter = ' AND cp.status IN (0,7,12)';
1961     }
1962
1963     if (my $a = $args{audience}) {
1964         $a = [$a] if (!ref($a));
1965         @aud = @$a;
1966             
1967         $a_filter  = ' AND rd.audience  IN ('.join(',',map{'?'}@aud).')';
1968         $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1969     }
1970
1971     if (my $l = $args{language}) {
1972         $l = [$l] if (!ref($l));
1973         @lang = @$l;
1974
1975         $l_filter  = ' AND rd.item_lang  IN ('.join(',',map{'?'}@lang).')';
1976         $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1977     }
1978
1979     if (my $f = $args{lit_form}) {
1980         $f = [$f] if (!ref($f));
1981         @lit_form = @$f;
1982
1983         $lf_filter  = ' AND rd.lit_form  IN ('.join(',',map{'?'}@lit_form).')';
1984         $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1985     }
1986
1987     if (my $f = $args{item_form}) {
1988         $f = [$f] if (!ref($f));
1989         @forms = @$f;
1990
1991         $f_filter  = ' AND rd.item_form  IN ('.join(',',map{'?'}@forms).')';
1992         $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1993     }
1994
1995     if (my $t = $args{item_type}) {
1996         $t = [$t] if (!ref($t));
1997         @types = @$t;
1998
1999         $t_filter  = ' AND rd.item_type  IN ('.join(',',map{'?'}@types).')';
2000         $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2001     }
2002
2003     if (my $v = $args{vr_format}) {
2004         $v = [$v] if (!ref($v));
2005         @vformats = @$v;
2006
2007         $v_filter  = ' AND rd.vr_format  IN ('.join(',',map{'?'}@vformats).')';
2008         $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
2009     }
2010
2011     # XXX legacy format and item type support
2012     if ($args{format}) {
2013         my ($t, $f) = split '-', $args{format};
2014         @types = split '', $t;
2015         @forms = split '', $f;
2016         if (@types) {
2017             $t_filter  = ' AND rd.item_type  IN ('.join(',',map{'?'}@types).')';
2018             $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2019         }
2020
2021         if (@forms) {
2022             $f_filter  .= ' AND rd.item_form  IN ('.join(',',map{'?'}@forms).')';
2023             $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2024         }
2025     }
2026
2027
2028     my $descendants = defined($ou_type) ?
2029                 "actor.org_unit_descendants($ou, $ou_type)" :
2030                 "actor.org_unit_descendants($ou)";
2031
2032     my $search_table_list = '';
2033     my $fts_list = '';
2034     my $join_table_list = '';
2035     my @rank_list;
2036
2037     my $field_table = config::metabib_field->table;
2038
2039     my @bonus_lists;
2040     my @bonus_values;
2041     my $prev_search_group;
2042     my $curr_search_group;
2043     my $search_class;
2044     my $search_field;
2045     my $metabib_field;
2046     for my $search_group (sort keys %{$args{searches}}) {
2047         (my $search_group_name = $search_group) =~ s/\|/_/gso;
2048         ($search_class,$search_field) = split /\|/, $search_group;
2049         $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2050
2051         if ($search_field) {
2052             unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2053                 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2054                 return undef;
2055             }
2056         }
2057
2058         $prev_search_group = $curr_search_group if ($curr_search_group);
2059
2060         $curr_search_group = $search_group_name;
2061
2062         my $class = $_cdbi->{$search_class};
2063         my $search_table = $class->table;
2064
2065         my ($index_col) = $class->columns('FTS');
2066         $index_col ||= 'value';
2067
2068         
2069         my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2070
2071         my $fts_where = $fts->sql_where_clause;
2072         my @fts_ranks = $fts->fts_rank;
2073
2074         my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2075         my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2076         my $first_word = lc(($fts->words)[0]).'%';
2077
2078         $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2079         my $rank = join('  + ', @fts_ranks);
2080
2081         my %bonus = ();
2082         $bonus{'subject'} = [];
2083         $bonus{'author'}  = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2084
2085         $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2086
2087         $bonus{'series'} = [
2088             { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2089             { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2090         ];
2091
2092         $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2093
2094         if ($pref_lang) {
2095             push @{ $bonus{'title'}   }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2096             push @{ $bonus{'author'}  }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2097             push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2098             push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2099             push @{ $bonus{'series'}  }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2100         }
2101
2102         my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2103         $bonus_list ||= '1';
2104
2105         push @bonus_lists, $bonus_list;
2106         push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2107
2108         #---------------------
2109
2110         $search_table_list .= "$search_table $search_group_name, ";
2111         push @rank_list,$rank;
2112         $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2113
2114         if ($metabib_field) {
2115             $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2116             $metabib_field = undef;
2117         }
2118
2119         if ($prev_search_group) {
2120             $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2121         }
2122     }
2123
2124     my $metabib_record_descriptor = metabib::record_descriptor->table;
2125     my $metabib_full_rec = metabib::full_rec->table;
2126     my $metabib_metarecord = metabib::metarecord->table;
2127     my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2128     my $asset_call_number_table = asset::call_number->table;
2129     my $asset_copy_table = asset::copy->table;
2130     my $cs_table = config::copy_status->table;
2131     my $cl_table = asset::copy_location->table;
2132     my $br_table = biblio::record_entry->table;
2133     my $source_table = config::bib_source->table;
2134
2135
2136     my $bonuses = join (' * ', @bonus_lists);
2137     my $relevance = join (' + ', @rank_list);
2138     $relevance = "AVG( ($relevance) * ($bonuses) )";
2139
2140     my $string_default_sort = 'zzzz';
2141     $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2142
2143     my $number_default_sort = '9999';
2144     $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2145
2146     my $rank = $relevance;
2147     if (lc($sort) eq 'pubdate') {
2148         $rank = <<"        RANK";
2149             ( FIRST ((
2150                 SELECT  COALESCE(SUBSTRING(frp.value FROM E'\\\\d{4}'),'$number_default_sort')::INT
2151                   FROM  $metabib_full_rec frp
2152                   WHERE frp.record = b.id
2153                     AND frp.tag = '260'
2154                     AND frp.subfield = 'c'
2155                   LIMIT 1
2156             )) )
2157         RANK
2158     } elsif (lc($sort) eq 'create_date') {
2159         $rank = <<"        RANK";
2160             ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2161         RANK
2162     } elsif (lc($sort) eq 'edit_date') {
2163         $rank = <<"        RANK";
2164             ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2165         RANK
2166     } elsif (lc($sort) eq 'title') {
2167         $rank = <<"        RANK";
2168             ( FIRST ((
2169                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2170                   FROM  $metabib_full_rec frt
2171                   WHERE frt.record = b.id
2172                     AND frt.tag = '245'
2173                     AND frt.subfield = 'a'
2174                   LIMIT 1
2175             )) )
2176         RANK
2177     } elsif (lc($sort) eq 'author') {
2178         $rank = <<"        RANK";
2179             ( FIRST((
2180                 SELECT  COALESCE(LTRIM(fra.value),'$string_default_sort')
2181                   FROM  $metabib_full_rec fra
2182                   WHERE fra.record = b.id
2183                     AND fra.tag LIKE '1%'
2184                     AND fra.subfield = 'a'
2185                   ORDER BY fra.tag::text::int
2186                   LIMIT 1
2187             )) )
2188         RANK
2189     } else {
2190         push @bonus_values, @bonus_values;
2191         $sort = undef;
2192     }
2193
2194
2195     my $select = <<"    SQL";
2196         SELECT  b.id,
2197             $relevance AS rel,
2198             $rank AS rank,
2199             b.source
2200         FROM    $search_table_list
2201             $metabib_record_descriptor rd,
2202             $source_table src,
2203             $br_table b
2204         WHERE   rd.record = b.id
2205             AND b.active IS TRUE
2206             AND b.deleted IS FALSE
2207             $fts_list
2208             $join_table_list
2209             $t_filter
2210             $f_filter
2211             $v_filter
2212             $a_filter
2213             $l_filter
2214             $lf_filter
2215         GROUP BY b.id, b.source
2216         ORDER BY 3 $sort_dir
2217         LIMIT $visibility_limit
2218     SQL
2219
2220     if ($self->api_name !~ /staff/o) {
2221         $select = <<"        SQL";
2222
2223             SELECT  s.*
2224               FROM  ($select) s
2225                 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2226               WHERE EXISTS (
2227                 SELECT  1
2228                   FROM  $asset_call_number_table cn,
2229                     $asset_copy_table cp,
2230                     $cs_table cs,
2231                     $cl_table cl,
2232                     $descendants d
2233                   WHERE cn.record = s.id
2234                     AND cp.status = cs.id
2235                     AND cp.location = cl.id
2236                     AND cp.call_number = cn.id
2237                     AND cp.opac_visible IS TRUE
2238                     AND cs.opac_visible IS TRUE
2239                     AND cl.opac_visible IS TRUE
2240                     AND d.opac_visible IS TRUE
2241                     AND cp.deleted IS FALSE
2242                     AND cn.deleted IS FALSE
2243                     AND cp.circ_lib = d.id
2244                     $avail_filter
2245                   LIMIT 1
2246                 )
2247                 OR src.transcendant IS TRUE
2248               ORDER BY 3 $sort_dir
2249         SQL
2250     } else {
2251         $select = <<"        SQL";
2252
2253             SELECT  s.*
2254               FROM  ($select) s
2255                 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2256               WHERE EXISTS (
2257                 SELECT  1
2258                   FROM  $asset_call_number_table cn,
2259                     $asset_copy_table cp,
2260                     $descendants d
2261                   WHERE cn.record = s.id
2262                     AND cp.call_number = cn.id
2263                     AND cn.deleted IS FALSE
2264                     AND cp.circ_lib = d.id
2265                     AND cp.deleted IS FALSE
2266                     $avail_filter
2267                   LIMIT 1
2268                 )
2269                 OR NOT EXISTS (
2270                 SELECT  1
2271                   FROM  $asset_call_number_table cn
2272                   WHERE cn.record = s.id
2273                   LIMIT 1
2274                 )
2275                 OR src.transcendant IS TRUE
2276               ORDER BY 3 $sort_dir
2277         SQL
2278     }
2279
2280
2281     $log->debug("Field Search SQL :: [$select]",DEBUG);
2282
2283     my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2284             $select, {},
2285             @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2286     );
2287     
2288     $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2289
2290     my $count = scalar(@$recs);
2291     for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2292         next unless ($$rec[0]);
2293         my ($mrid,$rank) = @$rec;
2294         $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2295     }
2296     return undef;
2297 }
2298
2299 __PACKAGE__->register_method(
2300     api_name    => "open-ils.storage.biblio.multiclass.search_fts.record",
2301     method      => 'biblio_search_multi_class_fts',
2302     api_level   => 1,
2303     stream      => 1,
2304     cachable    => 1,
2305 );
2306 __PACKAGE__->register_method(
2307     api_name    => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2308     method      => 'biblio_search_multi_class_fts',
2309     api_level   => 1,
2310     stream      => 1,
2311     cachable    => 1,
2312 );
2313 __PACKAGE__->register_method(
2314     api_name    => "open-ils.storage.biblio.multiclass.search_fts",
2315     method      => 'biblio_search_multi_class_fts',
2316     api_level   => 1,
2317     stream      => 1,
2318     cachable    => 1,
2319 );
2320 __PACKAGE__->register_method(
2321     api_name    => "open-ils.storage.biblio.multiclass.search_fts.staff",
2322     method      => 'biblio_search_multi_class_fts',
2323     api_level   => 1,
2324     stream      => 1,
2325     cachable    => 1,
2326 );
2327
2328
2329 my %locale_map;
2330 my $default_preferred_language;
2331 my $default_preferred_language_weight;
2332
2333 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2334 sub staged_fts {
2335     my $self   = shift;
2336     my $client = shift;
2337     my %args   = @_;
2338
2339     if (!$locale_map{COMPLETE}) {
2340
2341         my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2342         for my $locale ( @locales ) {
2343             $locale_map{lc($locale->code)} = $locale->marc_code;
2344         }
2345         $locale_map{COMPLETE} = 1;
2346
2347     }
2348
2349     my $config = OpenSRF::Utils::SettingsClient->new();
2350
2351     if (!$default_preferred_language) {
2352
2353         $default_preferred_language = $config->config_value(
2354                 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2355         ) || $config->config_value(
2356                 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2357         );
2358
2359     }
2360
2361     if (!$default_preferred_language_weight) {
2362
2363         $default_preferred_language_weight = $config->config_value(
2364                 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2365         ) || $config->config_value(
2366                 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2367         );
2368     }
2369
2370     # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2371     my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2372
2373     my $ou     = $args{org_unit};
2374     my $limit  = $args{limit}  || 10;
2375     my $offset = $args{offset} ||  0;
2376
2377     if (!$ou) {
2378         $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2379     }
2380
2381     if (! scalar( keys %{$args{searches}} )) {
2382         die "No search arguments were passed to ".$self->api_name;
2383     }
2384
2385     my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2386
2387     if (!defined($args{preferred_language})) {
2388         my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2389         $args{preferred_language} =
2390             $locale_map{ lc($ses_locale) } || 'eng';
2391     }
2392
2393     if (!defined($args{preferred_language_weight})) {
2394         $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2395     }
2396
2397     if ($args{available}) {
2398         @statuses = (0,7,12);
2399     }
2400
2401     if (my $s = $args{locations}) {
2402         $s = [$s] if (!ref($s));
2403         @locations = @$s;
2404     }
2405
2406     if (my $b = $args{between}) {
2407         if (ref($b) && @$b == 2) {
2408             @between = @$b;
2409         }
2410     }
2411
2412     if (my $s = $args{statuses}) {
2413         $s = [$s] if (!ref($s));
2414         @statuses = @$s;
2415     }
2416
2417     if (my $a = $args{audience}) {
2418         $a = [$a] if (!ref($a));
2419         @aud = @$a;
2420     }
2421
2422     if (my $l = $args{language}) {
2423         $l = [$l] if (!ref($l));
2424         @lang = @$l;
2425     }
2426
2427     if (my $f = $args{lit_form}) {
2428         $f = [$f] if (!ref($f));
2429         @lit_form = @$f;
2430     }
2431
2432     if (my $f = $args{item_form}) {
2433         $f = [$f] if (!ref($f));
2434         @forms = @$f;
2435     }
2436
2437     if (my $t = $args{item_type}) {
2438         $t = [$t] if (!ref($t));
2439         @types = @$t;
2440     }
2441
2442     if (my $b = $args{bib_level}) {
2443         $b = [$b] if (!ref($b));
2444         @bib_level = @$b;
2445     }
2446
2447     if (my $v = $args{vr_format}) {
2448         $v = [$v] if (!ref($v));
2449         @vformats = @$v;
2450     }
2451
2452     # XXX legacy format and item type support
2453     if ($args{format}) {
2454         my ($t, $f) = split '-', $args{format};
2455         @types = split '', $t;
2456         @forms = split '', $f;
2457     }
2458
2459     my %stored_proc_search_args;
2460     for my $search_group (sort keys %{$args{searches}}) {
2461         (my $search_group_name = $search_group) =~ s/\|/_/gso;
2462         my ($search_class,$search_field) = split /\|/, $search_group;
2463         $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2464
2465         if ($search_field) {
2466             unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2467                 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2468                 return undef;
2469             }
2470         }
2471
2472         my $class = $_cdbi->{$search_class};
2473         my $search_table = $class->table;
2474
2475         my ($index_col) = $class->columns('FTS');
2476         $index_col ||= 'value';
2477
2478         
2479         my $fts = OpenILS::Application::Storage::FTS->compile(
2480             $search_class => $args{searches}{$search_group}{term},
2481             $search_group_name.'.value',
2482             "$search_group_name.$index_col"
2483         );
2484         $fts->sql_where_clause; # this builds the ranks for us
2485
2486         my @fts_ranks   = $fts->fts_rank;
2487         my @fts_queries = $fts->fts_query;
2488         my @phrases = map { lc($_) } $fts->phrases;
2489         my @words   = map { lc($_) } $fts->words;
2490
2491         $stored_proc_search_args{$search_group} = {
2492             fts_rank    => \@fts_ranks,
2493             fts_query   => \@fts_queries,
2494             phrase      => \@phrases,
2495             word        => \@words,
2496         };
2497
2498     }
2499
2500     my $param_search_ou = $ou;
2501     my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2502     my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2503     my $param_statuses  = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2504     my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2505     my $param_audience  = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud      ) . '}$$';
2506     my $param_language  = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang     ) . '}$$';
2507     my $param_lit_form  = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2508     my $param_types     = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types    ) . '}$$';
2509     my $param_forms     = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms    ) . '}$$';
2510     my $param_vformats  = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2511     my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2512     my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2513     my $param_after  = $args{after} ; $param_after  = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2514     my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2515     my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2516     my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2517     my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2518     my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2519     my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2520     my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2521     my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2522     my $param_rel_limit = $args{core_limit};  $param_rel_limit ||= 'NULL';
2523     my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2524     my $param_skip_chk  = $args{skip_check};  $param_skip_chk  ||= 'NULL';
2525
2526     my $sth = metabib::metarecord_source_map->db_Main->prepare(<<"    SQL");
2527         SELECT  *
2528           FROM  search.staged_fts(
2529                     $param_search_ou\:\:INT,
2530                     $param_depth\:\:INT,
2531                     $param_searches\:\:TEXT,
2532                     $param_statuses\:\:INT[],
2533                     $param_locations\:\:INT[],
2534                     $param_audience\:\:TEXT[],
2535                     $param_language\:\:TEXT[],
2536                     $param_lit_form\:\:TEXT[],
2537                     $param_types\:\:TEXT[],
2538                     $param_forms\:\:TEXT[],
2539                     $param_vformats\:\:TEXT[],
2540                     $param_bib_level\:\:TEXT[],
2541                     $param_before\:\:TEXT,
2542                     $param_after\:\:TEXT,
2543                     $param_during\:\:TEXT,
2544                     $param_between\:\:TEXT[],
2545                     $param_pref_lang\:\:TEXT,
2546                     $param_pref_lang_multiplier\:\:REAL,
2547                     $param_sort\:\:TEXT,
2548                     $param_sort_desc\:\:BOOL,
2549                     $metarecord\:\:BOOL,
2550                     $staff\:\:BOOL,
2551                     $param_rel_limit\:\:INT,
2552                     $param_chk_limit\:\:INT,
2553                     $param_skip_chk\:\:INT
2554                 );
2555     SQL
2556
2557     $sth->execute;
2558
2559     my $recs = $sth->fetchall_arrayref({});
2560     my $summary_row = pop @$recs;
2561
2562     my $total    = $$summary_row{total};
2563     my $checked  = $$summary_row{checked};
2564     my $visible  = $$summary_row{visible};
2565     my $deleted  = $$summary_row{deleted};
2566     my $excluded = $$summary_row{excluded};
2567
2568     my $estimate = $visible;
2569     if ( $total > $checked && $checked ) {
2570
2571         $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2572         $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2573
2574     }
2575
2576     delete $$summary_row{id};
2577     delete $$summary_row{rel};
2578     delete $$summary_row{record};
2579
2580     $client->respond( $summary_row );
2581
2582     $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2583
2584     for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2585         delete $$rec{checked};
2586         delete $$rec{visible};
2587         delete $$rec{excluded};
2588         delete $$rec{deleted};
2589         delete $$rec{total};
2590         $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2591
2592         $client->respond( $rec );
2593     }
2594     return undef;
2595 }
2596 __PACKAGE__->register_method(
2597     api_name    => "open-ils.storage.biblio.multiclass.staged.search_fts",
2598     method      => 'staged_fts',
2599     api_level   => 0,
2600     stream      => 1,
2601     cachable    => 1,
2602 );
2603 __PACKAGE__->register_method(
2604     api_name    => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2605     method      => 'staged_fts',
2606     api_level   => 0,
2607     stream      => 1,
2608     cachable    => 1,
2609 );
2610 __PACKAGE__->register_method(
2611     api_name    => "open-ils.storage.metabib.multiclass.staged.search_fts",
2612     method      => 'staged_fts',
2613     api_level   => 0,
2614     stream      => 1,
2615     cachable    => 1,
2616 );
2617 __PACKAGE__->register_method(
2618     api_name    => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2619     method      => 'staged_fts',
2620     api_level   => 0,
2621     stream      => 1,
2622     cachable    => 1,
2623 );
2624
2625 sub FTS_paging_estimate {
2626     my $self   = shift;
2627     my $client = shift;
2628
2629     my $checked  = shift;
2630     my $visible  = shift;
2631     my $excluded = shift;
2632     my $deleted  = shift;
2633     my $total    = shift;
2634
2635     my $deleted_ratio = $deleted / $checked;
2636     my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2637
2638     my $exclusion_ratio = $excluded / $checked;
2639     my $delete_adjusted_exclusion_ratio = $checked - $deleted ? $excluded / ($checked - $deleted) : 1;
2640
2641     my $inclusion_ratio = $visible / $checked;
2642     my $delete_adjusted_inclusion_ratio = $checked - $deleted ? $visible / ($checked - $deleted) : 0;
2643
2644     return {
2645         exclusion                   => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2646         inclusion                   => int($delete_adjusted_total * $inclusion_ratio),
2647         delete_adjusted_exclusion   => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2648         delete_adjusted_inclusion   => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2649     };
2650 }
2651 __PACKAGE__->register_method(
2652     api_name    => "open-ils.storage.fts_paging_estimate",
2653     method      => 'FTS_paging_estimate',
2654     argc        => 5,
2655     strict      => 1,
2656     api_level   => 1,
2657     signature   => {
2658         'return'=> q#
2659             Hash of estimation values based on four variant estimation strategies:
2660                 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2661                 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2662                 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2663                 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2664         #,
2665         desc    => q#
2666             Helper method used to determin the approximate number of
2667             hits for a search that spans multiple superpages.  For
2668             sparse superpages, the inclusion estimate will likely be the
2669             best estimate.  The exclusion strategy is the original, but
2670             inclusion is the default.
2671         #,
2672         params  => [
2673             {   name    => 'checked',
2674                 desc    => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2675                 type    => 'number'
2676             },
2677             {   name    => 'visible',
2678                 desc    => 'Number of records visible to the search location on the current superpage.',
2679                 type    => 'number'
2680             },
2681             {   name    => 'excluded',
2682                 desc    => 'Number of records excluded from the search location on the current superpage.',
2683                 type    => 'number'
2684             },
2685             {   name    => 'deleted',
2686                 desc    => 'Number of deleted records on the current superpage.',
2687                 type    => 'number'
2688             },
2689             {   name    => 'total',
2690                 desc    => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2691                 type    => 'number'
2692             }
2693         ]
2694     }
2695 );
2696
2697
2698 sub xref_count {
2699     my $self   = shift;
2700     my $client = shift;
2701     my $args   = shift;
2702
2703     my $term  = $$args{term};
2704     my $limit = $$args{max} || 1;
2705     my $min   = $$args{min} || 1;
2706     my @classes = @{$$args{class}};
2707
2708     $limit = $min if ($min > $limit);
2709
2710     if (!@classes) {
2711         @classes = ( qw/ title author subject series keyword / );
2712     }
2713
2714     my %matches;
2715     my $bre_table = biblio::record_entry->table;
2716     my $cn_table  = asset::call_number->table;
2717     my $cp_table  = asset::copy->table;
2718
2719     for my $search_class ( @classes ) {
2720
2721         my $class = $_cdbi->{$search_class};
2722         my $search_table = $class->table;
2723
2724         my ($index_col) = $class->columns('FTS');
2725         $index_col ||= 'value';
2726
2727         
2728         my $where = OpenILS::Application::Storage::FTS
2729             ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2730             ->sql_where_clause;
2731
2732         my $SQL = <<"        SQL";
2733             SELECT  COUNT(DISTINCT X.source)
2734               FROM  (SELECT $search_class.source
2735                   FROM  $search_table $search_class
2736                     JOIN $bre_table b ON (b.id = $search_class.source)
2737                   WHERE $where
2738                     AND NOT b.deleted
2739                     AND b.active
2740                   LIMIT $limit) X
2741               HAVING COUNT(DISTINCT X.source) >= $min;
2742         SQL
2743
2744         my $res = $class->db_Main->selectrow_arrayref( $SQL );
2745         $matches{$search_class} = $res ? $res->[0] : 0;
2746     }
2747
2748     return \%matches;
2749 }
2750 __PACKAGE__->register_method(
2751     api_name  => "open-ils.storage.search.xref",
2752     method    => 'xref_count',
2753     api_level => 1,
2754 );
2755
2756 # Takes an abstract query object and recursively turns it back into a string
2757 # for QueryParser.
2758 sub abstract_query2str {
2759     my ($self, $conn, $query) = @_;
2760
2761     return QueryParser::Canonicalize::abstract_query2str_impl($query, 0);
2762 }
2763
2764 __PACKAGE__->register_method(
2765     api_name    => "open-ils.storage.query_parser.abstract_query.canonicalize",
2766     method      => "abstract_query2str",
2767     api_level   => 1,
2768     signature   => {
2769         params  => [
2770             {desc => q/
2771 Abstract query parser object, with complete config data. For example input,
2772 see the 'abstract_query' part of the output of an API call like
2773 open-ils.search.biblio.multiclass.query, when called with the return_abstract
2774 flag set to true./,
2775                 type => "object"}
2776         ],
2777         return => { type => "string", desc => "String representation of abstract query object" }
2778     }
2779 );
2780
2781 sub str2abstract_query {
2782     my ($self, $conn, $query, $qp_opts, $with_config) = @_;
2783
2784     my %use_opts = ( # reasonable defaults? should these even be hardcoded here?
2785         superpage => 1,
2786         superpage_size => 1000,
2787         core_limit => 25000,
2788         query => $query,
2789         (ref $opts eq 'HASH' ? %$opts : ())
2790     );
2791
2792     $with_config ||= 0;
2793
2794     # grab the query parser and initialize it
2795     my $parser = $OpenILS::Application::Storage::QParser;
2796     $parser->use;
2797
2798     _initialize_parser($parser) unless $parser->initialization_complete;
2799
2800     my $query = $parser->new(%use_opts)->parse;
2801
2802     return $query->parse_tree->to_abstract_query(with_config => $with_config);
2803 }
2804
2805 __PACKAGE__->register_method(
2806     api_name    => "open-ils.storage.query_parser.abstract_query.from_string",
2807     method      => "str2abstract_query",
2808     api_level   => 1,
2809     signature   => {
2810         params  => [
2811             {desc => "Query", type => "string"},
2812             {desc => q/Arguments for initializing QueryParser (optional)/,
2813                 type => "object"},
2814             {desc => q/Flag enabling inclusion of QP config in returned object (optional, default false)/,
2815                 type => "bool"}
2816         ],
2817         return => { type => "object", desc => "abstract representation of query parser query" }
2818     }
2819 );
2820
2821 sub query_parser_fts {
2822     my $self = shift;
2823     my $client = shift;
2824     my %args = @_;
2825
2826
2827     # grab the query parser and initialize it
2828     my $parser = $OpenILS::Application::Storage::QParser;
2829     $parser->use;
2830
2831     _initialize_parser($parser) unless $parser->initialization_complete;
2832
2833     # populate the locale/language map
2834     if (!$locale_map{COMPLETE}) {
2835
2836         my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2837         for my $locale ( @locales ) {
2838             $locale_map{lc($locale->code)} = $locale->marc_code;
2839         }
2840         $locale_map{COMPLETE} = 1;
2841
2842     }
2843
2844     # I hope we have a query!
2845     if (! $args{query} ) {
2846         die "No query was passed to ".$self->api_name;
2847     }
2848
2849     my $default_CD_modifiers = OpenSRF::Utils::SettingsClient->new->config_value(
2850         apps => 'open-ils.search' => app_settings => 'default_CD_modifiers'
2851     );
2852
2853     # Protect against empty / missing default_CD_modifiers setting
2854     if ($default_CD_modifiers and !ref($default_CD_modifiers)) {
2855         $args{query} = "$default_CD_modifiers $args{query}";
2856     }
2857
2858     my $simple_plan = $args{_simple_plan};
2859     # remove bad chunks of the %args hash
2860     for my $bad ( grep { /^_/ } keys(%args)) {
2861         delete($args{$bad});
2862     }
2863
2864
2865     # parse the query and supply any query-level %arg-based defaults
2866     # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2867     my $query = $parser->new( %args )->parse;
2868
2869     my $config = OpenSRF::Utils::SettingsClient->new();
2870
2871     # set the locale-based default preferred location
2872     if (!$query->parse_tree->find_filter('preferred_language')) {
2873         $parser->default_preferred_language( $args{preferred_language} );
2874
2875         if (!$parser->default_preferred_language) {
2876             my $ses_locale = $client->session ? $client->session->session_locale : '';
2877             $parser->default_preferred_language( $locale_map{ lc($ses_locale) } );
2878         }
2879
2880         if (!$parser->default_preferred_language) { # still nothing...
2881             my $tmp_dpl = $config->config_value(
2882                 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2883             ) || $config->config_value(
2884                 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2885             );
2886
2887             $parser->default_preferred_language( $tmp_dpl )
2888         }
2889     }
2890
2891
2892     # set the global default language multiplier
2893     if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2894         my $tmp_dplw;
2895
2896         if ($tmp_dplw = $args{preferred_language_weight} || $args{preferred_language_multiplier} ) {
2897             $parser->default_preferred_language_multiplier($tmp_dplw);
2898
2899         } else {
2900             $tmp_dplw = $config->config_value(
2901                 apps => 'open-ils.search' => app_settings => 'default_preferred_language_weight'
2902             ) || $config->config_value(
2903                 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2904             );
2905
2906             $parser->default_preferred_language_multiplier( $tmp_dplw );
2907         }
2908     }
2909
2910     # gather the site, if one is specified, defaulting to the in-query version
2911     my $ou = $args{org_unit};
2912     if (my ($filter) = $query->parse_tree->find_filter('site')) {
2913             $ou = $filter->args->[0] if (@{$filter->args});
2914     }
2915     $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^(-)?\d+$/);
2916
2917     # gather lasso, as with $ou
2918     my $lasso = $args{lasso};
2919     if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
2920             $lasso = $filter->args->[0] if (@{$filter->args});
2921     }
2922     $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
2923     $lasso = -$lasso if ($lasso);
2924
2925
2926 #    # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
2927 #    # gather user lasso, as with $ou and lasso
2928 #    my $mylasso = $args{my_lasso};
2929 #    if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
2930 #            $mylasso = $filter->args->[0] if (@{$filter->args});
2931 #    }
2932 #    $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
2933
2934
2935     # if we have a lasso, go with that, otherwise ... ou
2936     $ou = $lasso if ($lasso);
2937
2938     # gather the preferred OU, if one is specified, as with $ou
2939     my $pref_ou = $args{pref_ou};
2940     $log->info("pref_ou = $pref_ou");
2941     if (my ($filter) = $query->parse_tree->find_filter('pref_ou')) {
2942             $pref_ou = $filter->args->[0] if (@{$filter->args});
2943     }
2944     $pref_ou = actor::org_unit->search( { shortname => $pref_ou } )->next->id if ($pref_ou and $pref_ou !~ /^(-)?\d+$/);
2945
2946     # get the default $ou if we have nothing
2947     $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
2948
2949
2950     # 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
2951     # gather the depth, if one is specified, defaulting to the in-query version
2952     my $depth = $args{depth};
2953     if (my ($filter) = $query->parse_tree->find_filter('depth')) {
2954             $depth = $filter->args->[0] if (@{$filter->args});
2955     }
2956     $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
2957
2958
2959     # gather the limit or default to 10
2960     my $limit = $args{check_limit} || 'NULL';
2961     if (my ($filter) = $query->parse_tree->find_filter('limit')) {
2962             $limit = $filter->args->[0] if (@{$filter->args});
2963     }
2964     if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
2965             $limit = $filter->args->[0] if (@{$filter->args});
2966     }
2967
2968
2969     # gather the offset or default to 0
2970     my $offset = $args{skip_check} || $args{offset} || 0;
2971     if (my ($filter) = $query->parse_tree->find_filter('offset')) {
2972             $offset = $filter->args->[0] if (@{$filter->args});
2973     }
2974     if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
2975             $offset = $filter->args->[0] if (@{$filter->args});
2976     }
2977
2978
2979     # gather the estimation strategy or default to inclusion
2980     my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2981     if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
2982             $estimation_strategy = $filter->args->[0] if (@{$filter->args});
2983     }
2984
2985
2986     # gather the estimation strategy or default to inclusion
2987     my $core_limit = $args{core_limit};
2988     if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
2989             $core_limit = $filter->args->[0] if (@{$filter->args});
2990     }
2991
2992
2993     # gather statuses, and then forget those if we have an #available modifier
2994     my @statuses;
2995     if (my ($filter) = $query->parse_tree->find_filter('statuses')) {
2996         @statuses = @{$filter->args} if (@{$filter->args});
2997     }
2998     @statuses = (0,7,12) if ($query->parse_tree->find_modifier('available'));
2999
3000
3001     # gather locations
3002     my @location;
3003     if (my ($filter) = $query->parse_tree->find_filter('locations')) {
3004         @location = @{$filter->args} if (@{$filter->args});
3005     }
3006
3007     # gather location_groups
3008     if (my ($filter) = $query->parse_tree->find_filter('location_groups')) {
3009         my @loc_groups = @{$filter->args} if (@{$filter->args});
3010         
3011         # collect the mapped locations and add them to the locations() filter
3012         if (@loc_groups) {
3013
3014             my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3015             my $maps = $cstore->request(
3016                 'open-ils.cstore.direct.asset.copy_location_group_map.search.atomic',
3017                 {lgroup => \@loc_groups})->gather(1);
3018
3019             push(@location, $_->location) for @$maps;
3020         }
3021     }
3022
3023
3024     my $param_check = $limit || $query->superpage_size || 'NULL';
3025     my $param_offset = $offset || 'NULL';
3026     my $param_limit = $core_limit || 'NULL';
3027
3028     my $sp = $query->superpage || 1;
3029     if ($sp > 1) {
3030         $param_offset = ($sp - 1) * $sp_size;
3031     }
3032
3033     my $param_search_ou = $ou;
3034     my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
3035     my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
3036     my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
3037     my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
3038     my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
3039     my $deleted_search = ($query->parse_tree->find_modifier('deleted')) ? "'t'" : "'f'";
3040     my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
3041     my $param_pref_ou = $pref_ou || 'NULL';
3042
3043     my $sth = metabib::metarecord_source_map->db_Main->prepare(<<"    SQL");
3044         SELECT  * -- bib search: $args{query}
3045           FROM  search.query_parser_fts(
3046                     $param_search_ou\:\:INT,
3047                     $param_depth\:\:INT,
3048                     $param_core_query\:\:TEXT,
3049                     $param_statuses\:\:INT[],
3050                     $param_locations\:\:INT[],
3051                     $param_offset\:\:INT,
3052                     $param_check\:\:INT,
3053                     $param_limit\:\:INT,
3054                     $metarecord\:\:BOOL,
3055                     $staff\:\:BOOL,
3056                     $deleted_search\:\:BOOL,
3057                     $param_pref_ou\:\:INT
3058                 );
3059     SQL
3060
3061     $sth->execute;
3062
3063     my $recs = $sth->fetchall_arrayref({});
3064     my $summary_row = pop @$recs;
3065
3066     my $total    = $$summary_row{total};
3067     my $checked  = $$summary_row{checked};
3068     my $visible  = $$summary_row{visible};
3069     my $deleted  = $$summary_row{deleted};
3070     my $excluded = $$summary_row{excluded};
3071
3072     my $estimate = $visible;
3073     if ( $total > $checked && $checked ) {
3074
3075         $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
3076         $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
3077
3078     }
3079
3080     delete $$summary_row{id};
3081     delete $$summary_row{rel};
3082     delete $$summary_row{record};
3083     delete $$summary_row{badges};
3084     delete $$summary_row{popularity};
3085
3086     if (defined($simple_plan)) {
3087         $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3088     } else {
3089         $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3090     }
3091
3092     $client->respond( $summary_row );
3093
3094     $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
3095
3096     for my $rec (@$recs) {
3097         delete $$rec{checked};
3098         delete $$rec{visible};
3099         delete $$rec{excluded};
3100         delete $$rec{deleted};
3101         delete $$rec{total};
3102         $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3103
3104         $client->respond( $rec );
3105     }
3106     return undef;
3107 }
3108 __PACKAGE__->register_method(
3109     api_name    => "open-ils.storage.query_parser_search",
3110     method      => 'query_parser_fts',
3111     api_level   => 1,
3112     stream      => 1,
3113     cachable    => 1,
3114 );
3115
3116 my $top_org;
3117
3118 sub query_parser_fts_wrapper {
3119     my $self = shift;
3120     my $client = shift;
3121     my %args = @_;
3122
3123     $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3124     # grab the query parser and initialize it
3125     my $parser = $OpenILS::Application::Storage::QParser;
3126     $parser->use;
3127
3128     _initialize_parser($parser) unless $parser->initialization_complete;
3129
3130     if (! scalar( keys %{$args{searches}} )) {
3131         die "No search arguments were passed to ".$self->api_name;
3132     }
3133
3134     $top_org ||= actor::org_unit->search( { parent_ou => undef } )->next;
3135
3136     $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3137     my $base_query = '';
3138     for my $sclass ( keys %{$args{searches}} ) {
3139         $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3140         $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3141     }
3142
3143     my $query = $base_query;
3144     $log->debug("Full base query: $base_query", DEBUG);
3145
3146     $query = "$args{facets} $query" if  ($args{facets});
3147
3148     if (!$locale_map{COMPLETE}) {
3149
3150         my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3151         for my $locale ( @locales ) {
3152             $locale_map{lc($locale->code)} = $locale->marc_code;
3153         }
3154         $locale_map{COMPLETE} = 1;
3155
3156     }
3157
3158     my $base_plan = $parser->new( query => $base_query )->parse;
3159
3160     $query = "preferred_language($args{preferred_language}) $query"
3161         if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3162     $query = "preferred_language_weight($args{preferred_language_weight}) $query"
3163         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'));
3164
3165
3166     my $borgs = undef;
3167     if (!$base_plan->parse_tree->find_filter('badge_orgs')) {
3168         # supply a suitable badge_orgs filter unless user has
3169         # explicitly supplied one
3170         my $site = undef;
3171
3172         my @lg_id_list = @{$args{location_groups}} if (ref $args{location_groups});
3173
3174         my ($lg_filter) = $base_plan->parse_tree->find_filter('location_groups');
3175         @lg_id_list = @{$lg_filter->args} if ($lg_filter && @{$lg_filter->args});
3176
3177         if (@lg_id_list) {
3178             my @borg_list;
3179             for my $lg ( grep { /^\d+$/ } @lg_id_list ) {
3180                 my $lg_obj = asset::copy_location_group->retrieve($lg);
3181                 next unless $lg_obj;
3182     
3183                 push(@borg_list, ''.$lg_obj->owner);
3184             }
3185             $borgs = join(',', @borg_list) if @borg_list;
3186         }
3187     
3188         if (!$borgs) {
3189             my ($site_filter) = $base_plan->parse_tree->find_filter('site');
3190             if ($site_filter && @{$site_filter->args}) {
3191                 $site = $top_org if ($site_filter->args->[0] eq '-');
3192                 $site = $top_org if ($site_filter->args->[0] eq $top_org->shortname);
3193                 $site = actor::org_unit->search( { shortname => $site_filter->args->[0] })->next unless ($site);
3194             } elsif ($args{org_unit}) {
3195                 $site = $top_org if ($args{org_unit} eq '-');
3196                 $site = $top_org if ($args{org_unit} eq $top_org->shortname);
3197                 $site = actor::org_unit->search( { shortname => $args{org_unit} })->next unless ($site);
3198             } else {
3199                 $site = $top_org;
3200             }
3201
3202             if ($site) {
3203                 $borgs = OpenSRF::AppSession->create( 'open-ils.cstore' )->request(
3204                     'open-ils.cstore.json_query.atomic',
3205                     { from => [ 'actor.org_unit_ancestors', $site->id ] }
3206                 )->gather(1);
3207
3208                 if (ref $borgs && @$borgs) {
3209                     $borgs = join(',', map { $_->{'id'} } @$borgs);
3210                 } else {
3211                     $borgs = undef;
3212                 }
3213             }
3214         }
3215     }
3216
3217     $query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
3218     $query = "badge_orgs($borgs) $query" if ($borgs);
3219     $query = "site($args{org_unit}) $query" if ($args{org_unit});
3220     $query = "depth($args{depth}) $query" if (defined($args{depth}));
3221     $query = "sort($args{sort}) $query" if ($args{sort});
3222     $query = "limit($args{limit}) $query" if ($args{limit});
3223     $query = "core_limit($args{core_limit}) $query" if ($args{core_limit});
3224     $query = "skip_check($args{skip_check}) $query" if ($args{skip_check});
3225     $query = "superpage($args{superpage}) $query" if ($args{superpage});
3226     $query = "offset($args{offset}) $query" if ($args{offset});
3227     $query = "#metarecord $query" if ($self->api_name =~ /metabib/);
3228     $query = "#available $query" if ($args{available});
3229     $query = "#descending $query" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3230     $query = "#staff $query" if ($self->api_name =~ /staff/);
3231     $query = "before($args{before}) $query" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3232     $query = "after($args{after}) $query" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3233     $query = "during($args{during}) $query" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3234     $query = "between($args{between}[0],$args{between}[1]) $query"
3235         if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3236
3237
3238     my (@between,@statuses,@locations,@location_groups,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3239
3240     # XXX legacy format and item type support
3241     if ($args{format}) {
3242         my ($t, $f) = split '-', $args{format};
3243         $args{item_type} = [ split '', $t ];
3244         $args{item_form} = [ split '', $f ];
3245     }
3246
3247     for my $filter ( qw/locations location_groups statuses between audience language lit_form item_form item_type bib_level vr_format badges/ ) {
3248         if (my $s = $args{$filter}) {
3249             $s = [$s] if (!ref($s));
3250
3251             my @filter_list = @$s;
3252
3253             next if ($filter eq 'between' and scalar(@filter_list) != 2);
3254             next if (@filter_list == 0);
3255
3256             my $filter_string = join ',', @filter_list;
3257             $query = "$query $filter($filter_string)";
3258         }
3259     }
3260
3261     $log->debug("Full QueryParser query: $query", DEBUG);
3262
3263     return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan );
3264 }
3265 __PACKAGE__->register_method(
3266     api_name    => "open-ils.storage.biblio.multiclass.staged.search_fts",
3267     method      => 'query_parser_fts_wrapper',
3268     api_level   => 1,
3269     stream      => 1,
3270     cachable    => 1,
3271 );
3272 __PACKAGE__->register_method(
3273     api_name    => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3274     method      => 'query_parser_fts_wrapper',
3275     api_level   => 1,
3276     stream      => 1,
3277     cachable    => 1,
3278 );
3279 __PACKAGE__->register_method(
3280     api_name    => "open-ils.storage.metabib.multiclass.staged.search_fts",
3281     method      => 'query_parser_fts_wrapper',
3282     api_level   => 1,
3283     stream      => 1,
3284     cachable    => 1,
3285 );
3286 __PACKAGE__->register_method(
3287     api_name    => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3288     method      => 'query_parser_fts_wrapper',
3289     api_level   => 1,
3290     stream      => 1,
3291     cachable    => 1,
3292 );
3293
3294
3295 1;
3296