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