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