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