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