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