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