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