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