QueryParser Driver: Much work
[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 OpenSRF::Utils::Cache;
9 use OpenSRF::Utils::JSON;
10 use Data::Dumper;
11 use Digest::MD5 qw/md5_hex/;
12
13 use OpenILS::Application::Storage::QueryParser;
14
15 my $log = 'OpenSRF::Utils::Logger';
16
17 $VERSION = 1;
18
19 sub _initialize_parser {
20     my ($parser) = @_;
21
22     my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
23     $parser->initialize(
24         config_record_attr_index_norm_map =>
25             $cstore->request(
26                 'open-ils.cstore.direct.config.record_attr_index_norm_map.search.atomic',
27                 { id => { "!=" => undef } },
28                 { flesh => 1, flesh_fields => { crainm => [qw/norm/] }, order_by => [{ class => "crainm", field => "pos" }] }
29             )->gather(1),
30         search_relevance_adjustment         =>
31             $cstore->request(
32                 'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
33                 { id => { "!=" => undef } }
34             )->gather(1),
35         config_metabib_field                =>
36             $cstore->request(
37                 'open-ils.cstore.direct.config.metabib_field.search.atomic',
38                 { id => { "!=" => undef } }
39             )->gather(1),
40         config_metabib_search_alias         =>
41             $cstore->request(
42                 'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
43                 { alias => { "!=" => undef } }
44             )->gather(1),
45         config_metabib_field_index_norm_map =>
46             $cstore->request(
47                 'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
48                 { id => { "!=" => undef } },
49                 { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
50             )->gather(1),
51         config_record_attr_definition       =>
52             $cstore->request(
53                 'open-ils.cstore.direct.config.record_attr_definition.search.atomic',
54                 { name => { "!=" => undef } }
55             )->gather(1),
56         config_metabib_class_ts_map         =>
57             $cstore->request(
58                 'open-ils.cstore.direct.config.metabib_class_ts_map.search.atomic',
59                 { active => "t" }
60             )->gather(1),
61         config_metabib_field_ts_map         =>
62             $cstore->request(
63                 'open-ils.cstore.direct.config.metabib_field_ts_map.search.atomic',
64                 { active => "t" }
65             )->gather(1),
66         config_metabib_class                =>
67             $cstore->request(
68                 'open-ils.cstore.direct.config.metabib_class.search.atomic',
69                 { name => { "!=" => undef } }
70             )->gather(1),
71     );
72
73     $cstore->disconnect;
74     die("Cannot initialize $parser!") unless ($parser->initialization_complete);
75 }
76
77 sub ordered_records_from_metarecord {
78         my $self = shift;
79         my $client = shift;
80         my $mr = shift;
81         my $formats = shift;
82         my $org = shift || 1;
83         my $depth = shift;
84
85         my (@types,@forms,@blvl);
86
87         if ($formats) {
88                 my ($t, $f, $b) = split '-', $formats;
89                 @types = split '', $t;
90                 @forms = split '', $f;
91                 @blvl = split '', $b;
92         }
93
94         my $descendants =
95                 defined($depth) ?
96                         "actor.org_unit_descendants($org, $depth)" :
97                         "actor.org_unit_descendants($org)" ;
98
99
100         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';
101         $copies_visible = '' if ($self->api_name =~ /staff/o);
102
103         my $sm_table = metabib::metarecord_source_map->table;
104         my $rd_table = metabib::record_descriptor->table;
105         my $fr_table = metabib::full_rec->table;
106         my $cn_table = asset::call_number->table;
107         my $cl_table = asset::copy_location->table;
108         my $cp_table = asset::copy->table;
109         my $cs_table = config::copy_status->table;
110         my $src_table = config::bib_source->table;
111         my $out_table = actor::org_unit_type->table;
112         my $br_table = biblio::record_entry->table;
113
114         my $sql = <<"   SQL";
115                 SELECT  record,
116                         item_type,
117                         item_form,
118                         quality,
119                         FIRST(COALESCE(LTRIM(SUBSTR( value, COALESCE(SUBSTRING(ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'zzzzzzzz')) AS title
120                 FROM    (
121                         SELECT  rd.record,
122                                 rd.item_type,
123                                 rd.item_form,
124                                 br.quality,
125                                 fr.tag,
126                                 fr.subfield,
127                                 fr.value,
128                                 fr.ind2
129         SQL
130
131         if ($copies_visible) {
132                 $sql .= <<"             SQL";
133                           FROM  $sm_table sm,
134                                 $br_table br,
135                                 $fr_table fr,
136                                 $rd_table rd
137                           WHERE rd.record = sm.source
138                                 AND fr.record = sm.source
139                                 AND br.id = sm.source
140                                 AND sm.metarecord = ?
141                                 AND (EXISTS ((SELECT    1
142                                                 FROM    $cp_table cp
143                                                         JOIN $cn_table cn ON (cp.call_number = cn.id)
144                                                         JOIN $cs_table cs ON (cp.status = cs.id)
145                                                         JOIN $cl_table cl ON (cp.location = cl.id)
146                                                         JOIN $descendants d ON (cp.circ_lib = d.id)
147                                                 WHERE   cn.record = sm.source
148                                                         $copies_visible
149                                                 LIMIT 1))
150                                         OR EXISTS ((
151                                             SELECT      1
152                                               FROM      $src_table src
153                                               WHERE     src.id = br.source
154                                                         AND src.transcendant IS TRUE))
155                                 )
156                                           
157                 SQL
158         } else {
159                 $sql .= <<"             SQL";
160                           FROM  $sm_table sm
161                                 JOIN $br_table br ON (sm.source = br.id)
162                                 JOIN $fr_table fr ON (fr.record = br.id)
163                                 JOIN $rd_table rd ON (rd.record = br.id)
164                           WHERE sm.metarecord = ?
165                                 AND ((  EXISTS (
166                                                 SELECT  1
167                                                   FROM  $cp_table cp,
168                                                         $cn_table cn,
169                                                         $descendants d
170                                                   WHERE cn.record = br.id
171                                                         AND cn.deleted = FALSE
172                                                         AND cp.deleted = FALSE
173                                                         AND cp.circ_lib = d.id
174                                                         AND cn.id = cp.call_number
175                                                   LIMIT 1
176                                         ) OR NOT EXISTS (
177                                                 SELECT  1
178                                                   FROM  $cp_table cp,
179                                                         $cn_table cn
180                                                   WHERE cn.record = br.id
181                                                         AND cn.deleted = FALSE
182                                                         AND cp.deleted = FALSE
183                                                         AND cn.id = cp.call_number
184                                                   LIMIT 1
185                                         ))
186                                         OR EXISTS ((
187                                             SELECT      1
188                                               FROM      $src_table src
189                                               WHERE     src.id = br.source
190                                                         AND src.transcendant IS TRUE))
191                                 )
192                 SQL
193         }
194
195         if (@types) {
196                 $sql .= '                               AND rd.item_type IN ('.join(',',map{'?'}@types).')';
197         }
198
199         if (@forms) {
200                 $sql .= '                               AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
201         }
202
203         if (@blvl) {
204                 $sql .= '                               AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
205         }
206
207
208
209         $sql .= <<"     SQL";
210                           OFFSET 0
211                         ) AS x
212           WHERE tag = '245'
213                 AND subfield = 'a'
214           GROUP BY record, item_type, item_form, quality
215           ORDER BY
216                 CASE
217                         WHEN item_type IS NULL -- default
218                                 THEN 0
219                         WHEN item_type = '' -- default
220                                 THEN 0
221                         WHEN item_type IN ('a','t') -- books
222                                 THEN 1
223                         WHEN item_type = 'g' -- movies
224                                 THEN 2
225                         WHEN item_type IN ('i','j') -- sound recordings
226                                 THEN 3
227                         WHEN item_type = 'm' -- software
228                                 THEN 4
229                         WHEN item_type = 'k' -- images
230                                 THEN 5
231                         WHEN item_type IN ('e','f') -- maps
232                                 THEN 6
233                         WHEN item_type IN ('o','p') -- mixed
234                                 THEN 7
235                         WHEN item_type IN ('c','d') -- music
236                                 THEN 8
237                         WHEN item_type = 'r' -- 3d
238                                 THEN 9
239                 END,
240                 title ASC,
241                 quality DESC
242         SQL
243
244         my $ids = metabib::metarecord_source_map->db_Main->selectcol_arrayref($sql, {}, "$mr", @types, @forms, @blvl);
245         return $ids if ($self->api_name =~ /atomic$/o);
246
247         $client->respond( $_ ) for ( @$ids );
248         return undef;
249
250 }
251 __PACKAGE__->register_method(
252         api_name        => 'open-ils.storage.ordered.metabib.metarecord.records',
253         method          => 'ordered_records_from_metarecord',
254         api_level       => 1,
255         cachable        => 1,
256 );
257 __PACKAGE__->register_method(
258         api_name        => 'open-ils.storage.ordered.metabib.metarecord.records.staff',
259         method          => 'ordered_records_from_metarecord',
260         api_level       => 1,
261         cachable        => 1,
262 );
263
264 __PACKAGE__->register_method(
265         api_name        => 'open-ils.storage.ordered.metabib.metarecord.records.atomic',
266         method          => 'ordered_records_from_metarecord',
267         api_level       => 1,
268         cachable        => 1,
269 );
270 __PACKAGE__->register_method(
271         api_name        => 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic',
272         method          => 'ordered_records_from_metarecord',
273         api_level       => 1,
274         cachable        => 1,
275 );
276
277 # XXX: this subroutine and its two registered methods are marked for 
278 # deprecation, as they do not work properly in 2.x (these tags are no longer
279 # normalized in mfr) and are not in known use
280 sub isxn_search {
281         my $self = shift;
282         my $client = shift;
283         my $isxn = lc(shift());
284
285         $isxn =~ s/^\s*//o;
286         $isxn =~ s/\s*$//o;
287         $isxn =~ s/-//o if ($self->api_name =~ /isbn/o);
288
289         my $tag = ($self->api_name =~ /isbn/o) ? "'020' OR f.tag = '024'" : "'022'";
290
291         my $fr_table = metabib::full_rec->table;
292         my $bib_table = biblio::record_entry->table;
293
294         my $sql = <<"   SQL";
295                 SELECT  DISTINCT f.record
296                   FROM  $fr_table f
297                         JOIN $bib_table b ON (b.id = f.record)
298                   WHERE (f.tag = $tag)
299                         AND f.value LIKE ?
300                         AND b.deleted IS FALSE
301         SQL
302
303         my $list = metabib::full_rec->db_Main->selectcol_arrayref($sql, {}, "$isxn%");
304         $client->respond($_) for (@$list);
305         return undef;
306 }
307 __PACKAGE__->register_method(
308         api_name        => 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
309         method          => 'isxn_search',
310         api_level       => 1,
311         stream          => 1,
312 );
313 __PACKAGE__->register_method(
314         api_name        => 'open-ils.storage.id_list.biblio.record_entry.search.issn',
315         method          => 'isxn_search',
316         api_level       => 1,
317         stream          => 1,
318 );
319
320 sub metarecord_copy_count {
321         my $self = shift;
322         my $client = shift;
323
324         my %args = @_;
325
326         my $sm_table = metabib::metarecord_source_map->table;
327         my $rd_table = metabib::record_descriptor->table;
328         my $cn_table = asset::call_number->table;
329         my $cp_table = asset::copy->table;
330         my $br_table = biblio::record_entry->table;
331         my $src_table = config::bib_source->table;
332         my $cl_table = asset::copy_location->table;
333         my $cs_table = config::copy_status->table;
334         my $out_table = actor::org_unit_type->table;
335
336         my $descendants = "actor.org_unit_descendants(u.id)";
337         my $ancestors = "actor.org_unit_ancestors(?) u JOIN $out_table t ON (u.ou_type = t.id)";
338
339     if ($args{org_unit} < 0) {
340         $args{org_unit} *= -1;
341         $ancestors = "(select org_unit as id from actor.org_lasso_map where lasso = ?) u CROSS JOIN (SELECT -1 AS depth) t";
342     }
343
344         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';
345         $copies_visible = '' if ($self->api_name =~ /staff/o);
346
347         my (@types,@forms,@blvl);
348         my ($t_filter, $f_filter, $b_filter) = ('','','');
349
350         if ($args{format}) {
351                 my ($t, $f, $b) = split '-', $args{format};
352                 @types = split '', $t;
353                 @forms = split '', $f;
354                 @blvl = split '', $b;
355
356                 if (@types) {
357                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
358                 }
359
360                 if (@forms) {
361                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
362                 }
363
364                 if (@blvl) {
365                         $b_filter .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
366                 }
367         }
368
369         my $sql = <<"   SQL";
370                 SELECT  t.depth,
371                         u.id AS org_unit,
372                         sum(
373                                 (SELECT count(cp.id)
374                                   FROM  $sm_table r
375                                         JOIN $cn_table cn ON (cn.record = r.source)
376                                         JOIN $rd_table rd ON (cn.record = rd.record)
377                                         JOIN $cp_table cp ON (cn.id = cp.call_number)
378                                         JOIN $cs_table cs ON (cp.status = cs.id)
379                                         JOIN $cl_table cl ON (cp.location = cl.id)
380                                         JOIN $descendants a ON (cp.circ_lib = a.id)
381                                   WHERE r.metarecord = ?
382                                         AND cn.deleted IS FALSE
383                                         AND cp.deleted IS FALSE
384                                         $copies_visible
385                                         $t_filter
386                                         $f_filter
387                                         $b_filter
388                                 )
389                         ) AS count,
390                         sum(
391                                 (SELECT count(cp.id)
392                                   FROM  $sm_table r
393                                         JOIN $cn_table cn ON (cn.record = r.source)
394                                         JOIN $rd_table rd ON (cn.record = rd.record)
395                                         JOIN $cp_table cp ON (cn.id = cp.call_number)
396                                         JOIN $cs_table cs ON (cp.status = cs.id)
397                                         JOIN $cl_table cl ON (cp.location = cl.id)
398                                         JOIN $descendants a ON (cp.circ_lib = a.id)
399                                   WHERE r.metarecord = ?
400                                         AND cp.status IN (0,7,12)
401                                         AND cn.deleted IS FALSE
402                                         AND cp.deleted IS FALSE
403                                         $copies_visible
404                                         $t_filter
405                                         $f_filter
406                                         $b_filter
407                                 )
408                         ) AS available,
409                         sum(
410                                 (SELECT count(cp.id)
411                                   FROM  $sm_table r
412                                         JOIN $cn_table cn ON (cn.record = r.source)
413                                         JOIN $rd_table rd ON (cn.record = rd.record)
414                                         JOIN $cp_table cp ON (cn.id = cp.call_number)
415                                         JOIN $cs_table cs ON (cp.status = cs.id)
416                                         JOIN $cl_table cl ON (cp.location = cl.id)
417                                   WHERE r.metarecord = ?
418                                         AND cn.deleted IS FALSE
419                                         AND cp.deleted IS FALSE
420                                         AND cp.opac_visible IS TRUE
421                                         AND cs.opac_visible IS TRUE
422                                         AND cl.opac_visible IS TRUE
423                                         $t_filter
424                                         $f_filter
425                                         $b_filter
426                                 )
427                         ) AS unshadow,
428                         sum(    
429                                 (SELECT sum(1)
430                                   FROM  $sm_table r
431                                         JOIN $br_table br ON (br.id = r.source)
432                                         JOIN $src_table src ON (src.id = br.source)
433                                   WHERE r.metarecord = ?
434                                         AND src.transcendant IS TRUE
435                                 )
436                         ) AS transcendant
437
438                   FROM  $ancestors
439                   GROUP BY 1,2
440         SQL
441
442         my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
443         $sth->execute(  ''.$args{metarecord},
444                         @types, 
445                         @forms,
446                         @blvl,
447                         ''.$args{metarecord},
448                         @types, 
449                         @forms,
450                         @blvl,
451                         ''.$args{metarecord},
452                         @types, 
453                         @forms,
454                         @blvl,
455                         ''.$args{metarecord},
456                         ''.$args{org_unit}, 
457         ); 
458
459         while ( my $row = $sth->fetchrow_hashref ) {
460                 $client->respond( $row );
461         }
462         return undef;
463 }
464 __PACKAGE__->register_method(
465         api_name        => 'open-ils.storage.metabib.metarecord.copy_count',
466         method          => 'metarecord_copy_count',
467         api_level       => 1,
468         stream          => 1,
469         cachable        => 1,
470 );
471 __PACKAGE__->register_method(
472         api_name        => 'open-ils.storage.metabib.metarecord.copy_count.staff',
473         method          => 'metarecord_copy_count',
474         api_level       => 1,
475         stream          => 1,
476         cachable        => 1,
477 );
478
479 sub biblio_multi_search_full_rec {
480     my $self   = shift;
481     my $client = shift;
482     my %args   = @_;
483
484     my $class_join = $args{class_join} || 'AND';
485     my $limit      = $args{limit}      || 100;
486     my $offset     = $args{offset}     || 0;
487     my $sort       = $args{'sort'};
488     my $sort_dir   = $args{sort_dir}   || 'DESC';
489
490         my @binds;
491         my @selects;
492
493         for my $arg (@{ $args{searches} }) {
494                 my $term     = $$arg{term};
495                 my $limiters = $$arg{restrict};
496
497                 my ($index_col)  = metabib::full_rec->columns('FTS');
498                 $index_col ||= 'value';
499                 my $search_table = metabib::full_rec->table;
500
501                 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
502
503                 my $fts_where = $fts->sql_where_clause();
504                 my @fts_ranks = $fts->fts_rank;
505
506                 my $rank = join(' + ', @fts_ranks);
507
508                 my @wheres;
509                 for my $limit (@$limiters) {
510                         if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
511                                 # MARC control field; mfr.subfield is NULL
512                                 push @wheres, "( tag = ? AND $fts_where )";
513                                 push @binds, $$limit{tag};
514                                 $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
515                         } else {
516                                 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
517                                 push @binds, $$limit{tag}, $$limit{subfield};
518                                 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
519                         }
520                 }
521                 my $where = join(' OR ', @wheres);
522
523                 push @selects, "SELECT record, AVG($rank) as sum FROM $search_table WHERE $where GROUP BY record";
524
525         }
526
527         my $descendants = defined($args{depth}) ?
528                                 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
529                                 "actor.org_unit_descendants($args{org_unit})" ;
530
531
532         my $metabib_record_descriptor = metabib::record_descriptor->table;
533         my $metabib_full_rec = metabib::full_rec->table;
534         my $asset_call_number_table = asset::call_number->table;
535         my $asset_copy_table = asset::copy->table;
536         my $cs_table = config::copy_status->table;
537         my $cl_table = asset::copy_location->table;
538         my $br_table = biblio::record_entry->table;
539
540         my $cj = 'HAVING COUNT(x.record) = ' . scalar(@selects) if ($class_join eq 'AND');
541         my $search_table =
542                 '(SELECT x.record, sum(x.sum) FROM (('.
543                         join(') UNION ALL (', @selects).
544                         ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
545
546         my $has_vols = 'AND cn.owning_lib = d.id';
547         my $has_copies = 'AND cp.call_number = cn.id';
548         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';
549
550         if ($self->api_name =~ /staff/o) {
551                 $copies_visible = '';
552                 $has_copies     = '' if ($ou_type == 0);
553                 $has_vols       = '' if ($ou_type == 0);
554         }
555
556         my ($t_filter, $f_filter) = ('','');
557         my ($a_filter, $l_filter, $lf_filter) = ('','','');
558
559         if (my $a = $args{audience}) {
560                 $a = [$a] if (!ref($a));
561                 my @aud = @$a;
562                         
563                 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
564                 push @binds, @aud;
565         }
566
567         if (my $l = $args{language}) {
568                 $l = [$l] if (!ref($l));
569                 my @lang = @$l;
570
571                 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
572                 push @binds, @lang;
573         }
574
575         if (my $f = $args{lit_form}) {
576                 $f = [$f] if (!ref($f));
577                 my @lit_form = @$f;
578
579                 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
580                 push @binds, @lit_form;
581         }
582
583         if (my $f = $args{item_form}) {
584                 $f = [$f] if (!ref($f));
585                 my @forms = @$f;
586
587                 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
588                 push @binds, @forms;
589         }
590
591         if (my $t = $args{item_type}) {
592                 $t = [$t] if (!ref($t));
593                 my @types = @$t;
594
595                 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
596                 push @binds, @types;
597         }
598
599
600         if ($args{format}) {
601                 my ($t, $f) = split '-', $args{format};
602                 my @types = split '', $t;
603                 my @forms = split '', $f;
604                 if (@types) {
605                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
606                 }
607
608                 if (@forms) {
609                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
610                 }
611                 push @binds, @types, @forms;
612         }
613
614         my $relevance = 'sum(f.sum)';
615         $relevance = 1 if (!$copies_visible);
616
617         my $rank = $relevance;
618         if (lc($sort) eq 'pubdate') {
619                 $rank = <<"             RANK";
620                         ( FIRST ((
621                                 SELECT  COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'9999')::INT
622                                   FROM  $metabib_full_rec frp
623                                   WHERE frp.record = f.record
624                                         AND frp.tag = '260'
625                                         AND frp.subfield = 'c'
626                                   LIMIT 1
627                         )) )
628                 RANK
629         } elsif (lc($sort) eq 'create_date') {
630                 $rank = <<"             RANK";
631                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
632                 RANK
633         } elsif (lc($sort) eq 'edit_date') {
634                 $rank = <<"             RANK";
635                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
636                 RANK
637         } elsif (lc($sort) eq 'title') {
638                 $rank = <<"             RANK";
639                         ( FIRST ((
640                                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'zzzzzzzz')
641                                   FROM  $metabib_full_rec frt
642                                   WHERE frt.record = f.record
643                                         AND frt.tag = '245'
644                                         AND frt.subfield = 'a'
645                                   LIMIT 1
646                         )) )
647                 RANK
648         } elsif (lc($sort) eq 'author') {
649                 $rank = <<"             RANK";
650                         ( FIRST((
651                                 SELECT  COALESCE(LTRIM(fra.value),'zzzzzzzz')
652                                   FROM  $metabib_full_rec fra
653                                   WHERE fra.record = f.record
654                                         AND fra.tag LIKE '1%'
655                                         AND fra.subfield = 'a'
656                                   ORDER BY fra.tag::text::int
657                                   LIMIT 1
658                         )) )
659                 RANK
660         } else {
661                 $sort = undef;
662         }
663
664
665         if ($copies_visible) {
666                 $select = <<"           SQL";
667                         SELECT  f.record, $relevance, count(DISTINCT cp.id), $rank
668                         FROM    $search_table f,
669                                 $asset_call_number_table cn,
670                                 $asset_copy_table cp,
671                                 $cs_table cs,
672                                 $cl_table cl,
673                                 $br_table br,
674                                 $metabib_record_descriptor rd,
675                                 $descendants d
676                         WHERE   br.id = f.record
677                                 AND cn.record = f.record
678                                 AND rd.record = f.record
679                                 AND cp.status = cs.id
680                                 AND cp.location = cl.id
681                                 AND br.deleted IS FALSE
682                                 AND cn.deleted IS FALSE
683                                 AND cp.deleted IS FALSE
684                                 $has_vols
685                                 $has_copies
686                                 $copies_visible
687                                 $t_filter
688                                 $f_filter
689                                 $a_filter
690                                 $l_filter
691                                 $lf_filter
692                         GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
693                         ORDER BY 4 $sort_dir,3 DESC
694                 SQL
695         } else {
696                 $select = <<"           SQL";
697                         SELECT  f.record, 1, 1, $rank
698                         FROM    $search_table f,
699                                 $br_table br,
700                                 $metabib_record_descriptor rd
701                         WHERE   br.id = f.record
702                                 AND rd.record = f.record
703                                 AND br.deleted IS FALSE
704                                 $t_filter
705                                 $f_filter
706                                 $a_filter
707                                 $l_filter
708                                 $lf_filter
709                         GROUP BY 1,2,3 
710                         ORDER BY 4 $sort_dir
711                 SQL
712         }
713
714
715         $log->debug("Search SQL :: [$select]",DEBUG);
716
717         my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
718         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
719
720         my $max = 0;
721         $max = 1 if (!@$recs);
722         for (@$recs) {
723                 $max = $$_[1] if ($$_[1] > $max);
724         }
725
726         my $count = @$recs;
727         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
728                 next unless ($$rec[0]);
729                 my ($rid,$rank,$junk,$skip) = @$rec;
730                 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
731         }
732         return undef;
733 }
734 __PACKAGE__->register_method(
735         api_name        => 'open-ils.storage.biblio.full_rec.multi_search',
736         method          => 'biblio_multi_search_full_rec',
737         api_level       => 1,
738         stream          => 1,
739         cachable        => 1,
740 );
741 __PACKAGE__->register_method(
742         api_name        => 'open-ils.storage.biblio.full_rec.multi_search.staff',
743         method          => 'biblio_multi_search_full_rec',
744         api_level       => 1,
745         stream          => 1,
746         cachable        => 1,
747 );
748
749 sub search_full_rec {
750         my $self = shift;
751         my $client = shift;
752
753         my %args = @_;
754         
755         my $term = $args{term};
756         my $limiters = $args{restrict};
757
758         my ($index_col) = metabib::full_rec->columns('FTS');
759         $index_col ||= 'value';
760         my $search_table = metabib::full_rec->table;
761
762         my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
763
764         my $fts_where = $fts->sql_where_clause();
765         my @fts_ranks = $fts->fts_rank;
766
767         my $rank = join(' + ', @fts_ranks);
768
769         my @binds;
770         my @wheres;
771         for my $limit (@$limiters) {
772                 if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
773                         # MARC control field; mfr.subfield is NULL
774                         push @wheres, "( tag = ? AND $fts_where )";
775                         push @binds, $$limit{tag};
776                         $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
777                 } else {
778                         push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
779                         push @binds, $$limit{tag}, $$limit{subfield};
780                         $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
781                 }
782         }
783         my $where = join(' OR ', @wheres);
784
785         my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
786
787         $log->debug("Search SQL :: [$select]",DEBUG);
788
789         my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
790         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
791
792         $client->respond($_) for (@$recs);
793         return undef;
794 }
795 __PACKAGE__->register_method(
796         api_name        => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
797         method          => 'search_full_rec',
798         api_level       => 1,
799         stream          => 1,
800         cachable        => 1,
801 );
802 __PACKAGE__->register_method(
803         api_name        => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
804         method          => 'search_full_rec',
805         api_level       => 1,
806         stream          => 1,
807         cachable        => 1,
808 );
809
810
811 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
812 sub search_class_fts {
813         my $self = shift;
814         my $client = shift;
815         my %args = @_;
816         
817         my $term = $args{term};
818         my $ou = $args{org_unit};
819         my $ou_type = $args{depth};
820         my $limit = $args{limit};
821         my $offset = $args{offset};
822
823         my $limit_clause = '';
824         my $offset_clause = '';
825
826         $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
827         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
828
829         my (@types,@forms);
830         my ($t_filter, $f_filter) = ('','');
831
832         if ($args{format}) {
833                 my ($t, $f) = split '-', $args{format};
834                 @types = split '', $t;
835                 @forms = split '', $f;
836                 if (@types) {
837                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
838                 }
839
840                 if (@forms) {
841                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
842                 }
843         }
844
845
846
847         my $descendants = defined($ou_type) ?
848                                 "actor.org_unit_descendants($ou, $ou_type)" :
849                                 "actor.org_unit_descendants($ou)";
850
851         my $class = $self->{cdbi};
852         my $search_table = $class->table;
853
854         my $metabib_record_descriptor = metabib::record_descriptor->table;
855         my $metabib_metarecord = metabib::metarecord->table;
856         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
857         my $asset_call_number_table = asset::call_number->table;
858         my $asset_copy_table = asset::copy->table;
859         my $cs_table = config::copy_status->table;
860         my $cl_table = asset::copy_location->table;
861
862         my ($index_col) = $class->columns('FTS');
863         $index_col ||= 'value';
864
865         (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
866         my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
867
868         my $fts_where = $fts->sql_where_clause;
869         my @fts_ranks = $fts->fts_rank;
870
871         my $rank = join(' + ', @fts_ranks);
872
873         my $has_vols = 'AND cn.owning_lib = d.id';
874         my $has_copies = 'AND cp.call_number = cn.id';
875         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';
876
877         my $visible_count = ', count(DISTINCT cp.id)';
878         my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
879
880         if ($self->api_name =~ /staff/o) {
881                 $copies_visible = '';
882                 $visible_count_test = '';
883                 $has_copies = '' if ($ou_type == 0);
884                 $has_vols = '' if ($ou_type == 0);
885         }
886
887         my $rank_calc = <<"     RANK";
888                 , (SUM( $rank
889                         * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
890                         * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
891                         * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
892                 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
893         RANK
894
895         $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
896
897         if ($copies_visible) {
898                 $select = <<"           SQL";
899                         SELECT  m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
900                         FROM    $search_table f,
901                                 $metabib_metarecord_source_map_table m,
902                                 $asset_call_number_table cn,
903                                 $asset_copy_table cp,
904                                 $cs_table cs,
905                                 $cl_table cl,
906                                 $metabib_record_descriptor rd,
907                                 $descendants d
908                         WHERE   $fts_where
909                                 AND m.source = f.source
910                                 AND cn.record = m.source
911                                 AND rd.record = m.source
912                                 AND cp.status = cs.id
913                                 AND cp.location = cl.id
914                                 $has_vols
915                                 $has_copies
916                                 $copies_visible
917                                 $t_filter
918                                 $f_filter
919                         GROUP BY 1 $visible_count_test
920                         ORDER BY 2 DESC,3
921                         $limit_clause $offset_clause
922                 SQL
923         } else {
924                 $select = <<"           SQL";
925                         SELECT  m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
926                         FROM    $search_table f,
927                                 $metabib_metarecord_source_map_table m,
928                                 $metabib_record_descriptor rd
929                         WHERE   $fts_where
930                                 AND m.source = f.source
931                                 AND rd.record = m.source
932                                 $t_filter
933                                 $f_filter
934                         GROUP BY 1, 4
935                         ORDER BY 2 DESC,3
936                         $limit_clause $offset_clause
937                 SQL
938         }
939
940         $log->debug("Field Search SQL :: [$select]",DEBUG);
941
942         my $SQLstring = join('%',$fts->words);
943         my $REstring = join('\\s+',$fts->words);
944         my $first_word = ($fts->words)[0].'%';
945         my $recs = ($self->api_name =~ /unordered/o) ? 
946                         $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
947                         $class->db_Main->selectall_arrayref($select, {},
948                                 '%'.lc($SQLstring).'%',                 # phrase order match
949                                 lc($first_word),                        # first word match
950                                 '^\\s*'.lc($REstring).'\\s*/?\s*$',     # full exact match
951                                 @types, @forms
952                         );
953         
954         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
955
956         $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
957         return undef;
958 }
959
960 for my $class ( qw/title author subject keyword series identifier/ ) {
961         __PACKAGE__->register_method(
962                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord",
963                 method          => 'search_class_fts',
964                 api_level       => 1,
965                 stream          => 1,
966                 cdbi            => "metabib::${class}_field_entry",
967                 cachable        => 1,
968         );
969         __PACKAGE__->register_method(
970                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
971                 method          => 'search_class_fts',
972                 api_level       => 1,
973                 stream          => 1,
974                 cdbi            => "metabib::${class}_field_entry",
975                 cachable        => 1,
976         );
977         __PACKAGE__->register_method(
978                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
979                 method          => 'search_class_fts',
980                 api_level       => 1,
981                 stream          => 1,
982                 cdbi            => "metabib::${class}_field_entry",
983                 cachable        => 1,
984         );
985         __PACKAGE__->register_method(
986                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
987                 method          => 'search_class_fts',
988                 api_level       => 1,
989                 stream          => 1,
990                 cdbi            => "metabib::${class}_field_entry",
991                 cachable        => 1,
992         );
993 }
994
995 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
996 sub search_class_fts_count {
997         my $self = shift;
998         my $client = shift;
999         my %args = @_;
1000         
1001         my $term = $args{term};
1002         my $ou = $args{org_unit};
1003         my $ou_type = $args{depth};
1004         my $limit = $args{limit} || 100;
1005         my $offset = $args{offset} || 0;
1006
1007         my $descendants = defined($ou_type) ?
1008                                 "actor.org_unit_descendants($ou, $ou_type)" :
1009                                 "actor.org_unit_descendants($ou)";
1010                 
1011         my (@types,@forms);
1012         my ($t_filter, $f_filter) = ('','');
1013
1014         if ($args{format}) {
1015                 my ($t, $f) = split '-', $args{format};
1016                 @types = split '', $t;
1017                 @forms = split '', $f;
1018                 if (@types) {
1019                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1020                 }
1021
1022                 if (@forms) {
1023                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1024                 }
1025         }
1026
1027
1028         (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
1029
1030         my $class = $self->{cdbi};
1031         my $search_table = $class->table;
1032
1033         my $metabib_record_descriptor = metabib::record_descriptor->table;
1034         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1035         my $asset_call_number_table = asset::call_number->table;
1036         my $asset_copy_table = asset::copy->table;
1037         my $cs_table = config::copy_status->table;
1038         my $cl_table = asset::copy_location->table;
1039
1040         my ($index_col) = $class->columns('FTS');
1041         $index_col ||= 'value';
1042
1043         my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
1044
1045         my $fts_where = $fts->sql_where_clause;
1046
1047         my $has_vols = 'AND cn.owning_lib = d.id';
1048         my $has_copies = 'AND cp.call_number = cn.id';
1049         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';
1050         if ($self->api_name =~ /staff/o) {
1051                 $copies_visible = '';
1052                 $has_vols = '' if ($ou_type == 0);
1053                 $has_copies = '' if ($ou_type == 0);
1054         }
1055
1056         # XXX test an "EXISTS version of descendant checking...
1057         my $select;
1058         if ($copies_visible) {
1059                 $select = <<"           SQL";
1060                 SELECT  count(distinct  m.metarecord)
1061                   FROM  $search_table f,
1062                         $metabib_metarecord_source_map_table m,
1063                         $metabib_metarecord_source_map_table mr,
1064                         $asset_call_number_table cn,
1065                         $asset_copy_table cp,
1066                         $cs_table cs,
1067                         $cl_table cl,
1068                         $metabib_record_descriptor rd,
1069                         $descendants d
1070                   WHERE $fts_where
1071                         AND mr.source = f.source
1072                         AND mr.metarecord = m.metarecord
1073                         AND cn.record = m.source
1074                         AND rd.record = m.source
1075                         AND cp.status = cs.id
1076                         AND cp.location = cl.id
1077                         $has_vols
1078                         $has_copies
1079                         $copies_visible
1080                         $t_filter
1081                         $f_filter
1082                 SQL
1083         } else {
1084                 $select = <<"           SQL";
1085                 SELECT  count(distinct  m.metarecord)
1086                   FROM  $search_table f,
1087                         $metabib_metarecord_source_map_table m,
1088                         $metabib_metarecord_source_map_table mr,
1089                         $metabib_record_descriptor rd
1090                   WHERE $fts_where
1091                         AND mr.source = f.source
1092                         AND mr.metarecord = m.metarecord
1093                         AND rd.record = m.source
1094                         $t_filter
1095                         $f_filter
1096                 SQL
1097         }
1098
1099         $log->debug("Field Search Count SQL :: [$select]",DEBUG);
1100
1101         my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1102         
1103         $log->debug("Count Search yielded $recs results.",DEBUG);
1104
1105         return $recs;
1106
1107 }
1108 for my $class ( qw/title author subject keyword series identifier/ ) {
1109         __PACKAGE__->register_method(
1110                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
1111                 method          => 'search_class_fts_count',
1112                 api_level       => 1,
1113                 stream          => 1,
1114                 cdbi            => "metabib::${class}_field_entry",
1115                 cachable        => 1,
1116         );
1117         __PACKAGE__->register_method(
1118                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
1119                 method          => 'search_class_fts_count',
1120                 api_level       => 1,
1121                 stream          => 1,
1122                 cdbi            => "metabib::${class}_field_entry",
1123                 cachable        => 1,
1124         );
1125 }
1126
1127
1128 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1129 sub postfilter_search_class_fts {
1130         my $self = shift;
1131         my $client = shift;
1132         my %args = @_;
1133         
1134         my $term = $args{term};
1135         my $sort = $args{'sort'};
1136         my $sort_dir = $args{sort_dir} || 'DESC';
1137         my $ou = $args{org_unit};
1138         my $ou_type = $args{depth};
1139         my $limit = $args{limit} || 10;
1140         my $visibility_limit = $args{visibility_limit} || 5000;
1141         my $offset = $args{offset} || 0;
1142
1143         my $outer_limit = 1000;
1144
1145         my $limit_clause = '';
1146         my $offset_clause = '';
1147
1148         $limit_clause = "LIMIT $outer_limit";
1149         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1150
1151         my (@types,@forms,@lang,@aud,@lit_form);
1152         my ($t_filter, $f_filter) = ('','');
1153         my ($a_filter, $l_filter, $lf_filter) = ('','','');
1154         my ($ot_filter, $of_filter) = ('','');
1155         my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1156
1157         if (my $a = $args{audience}) {
1158                 $a = [$a] if (!ref($a));
1159                 @aud = @$a;
1160                         
1161                 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1162                 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1163         }
1164
1165         if (my $l = $args{language}) {
1166                 $l = [$l] if (!ref($l));
1167                 @lang = @$l;
1168
1169                 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1170                 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1171         }
1172
1173         if (my $f = $args{lit_form}) {
1174                 $f = [$f] if (!ref($f));
1175                 @lit_form = @$f;
1176
1177                 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1178                 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1179         }
1180
1181         if ($args{format}) {
1182                 my ($t, $f) = split '-', $args{format};
1183                 @types = split '', $t;
1184                 @forms = split '', $f;
1185                 if (@types) {
1186                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1187                         $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1188                 }
1189
1190                 if (@forms) {
1191                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1192                         $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1193                 }
1194         }
1195
1196
1197         my $descendants = defined($ou_type) ?
1198                                 "actor.org_unit_descendants($ou, $ou_type)" :
1199                                 "actor.org_unit_descendants($ou)";
1200
1201         my $class = $self->{cdbi};
1202         my $search_table = $class->table;
1203
1204         my $metabib_full_rec = metabib::full_rec->table;
1205         my $metabib_record_descriptor = metabib::record_descriptor->table;
1206         my $metabib_metarecord = metabib::metarecord->table;
1207         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1208         my $asset_call_number_table = asset::call_number->table;
1209         my $asset_copy_table = asset::copy->table;
1210         my $cs_table = config::copy_status->table;
1211         my $cl_table = asset::copy_location->table;
1212         my $br_table = biblio::record_entry->table;
1213
1214         my ($index_col) = $class->columns('FTS');
1215         $index_col ||= 'value';
1216
1217         (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1218
1219         my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1220
1221         my $SQLstring = join('%',map { lc($_) } $fts->words);
1222         my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1223         my $first_word = lc(($fts->words)[0]).'%';
1224
1225         my $fts_where = $fts->sql_where_clause;
1226         my @fts_ranks = $fts->fts_rank;
1227
1228         my %bonus = ();
1229         $bonus{'metabib::identifier_field_entry'} =
1230         $bonus{'metabib::keyword_field_entry'} = [
1231             { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring }
1232         ];
1233
1234         $bonus{'metabib::title_field_entry'} =
1235                 $bonus{'metabib::series_field_entry'} = [
1236                         { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1237                         { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1238                         @{ $bonus{'metabib::keyword_field_entry'} }
1239                 ];
1240
1241         my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1242         $bonus_list ||= '1';
1243
1244         my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1245
1246         my $relevance = join(' + ', @fts_ranks);
1247         $relevance = <<"        RANK";
1248                         (SUM( ( $relevance )  * ( $bonus_list ) )/COUNT(m.source))
1249         RANK
1250
1251         my $string_default_sort = 'zzzz';
1252         $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1253
1254         my $number_default_sort = '9999';
1255         $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1256
1257         my $rank = $relevance;
1258         if (lc($sort) eq 'pubdate') {
1259                 $rank = <<"             RANK";
1260                         ( FIRST ((
1261                                 SELECT  COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1262                                   FROM  $metabib_full_rec frp
1263                                   WHERE frp.record = mr.master_record
1264                                         AND frp.tag = '260'
1265                                         AND frp.subfield = 'c'
1266                                   LIMIT 1
1267                         )) )
1268                 RANK
1269         } elsif (lc($sort) eq 'create_date') {
1270                 $rank = <<"             RANK";
1271                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1272                 RANK
1273         } elsif (lc($sort) eq 'edit_date') {
1274                 $rank = <<"             RANK";
1275                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1276                 RANK
1277         } elsif (lc($sort) eq 'title') {
1278                 $rank = <<"             RANK";
1279                         ( FIRST ((
1280                                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1281                                   FROM  $metabib_full_rec frt
1282                                   WHERE frt.record = mr.master_record
1283                                         AND frt.tag = '245'
1284                                         AND frt.subfield = 'a'
1285                                   LIMIT 1
1286                         )) )
1287                 RANK
1288         } elsif (lc($sort) eq 'author') {
1289                 $rank = <<"             RANK";
1290                         ( FIRST((
1291                                 SELECT  COALESCE(LTRIM(fra.value),'$string_default_sort')
1292                                   FROM  $metabib_full_rec fra
1293                                   WHERE fra.record = mr.master_record
1294                                         AND fra.tag LIKE '1%'
1295                                         AND fra.subfield = 'a'
1296                                   ORDER BY fra.tag::text::int
1297                                   LIMIT 1
1298                         )) )
1299                 RANK
1300         } else {
1301                 $sort = undef;
1302         }
1303
1304         my $select = <<"        SQL";
1305                 SELECT  m.metarecord,
1306                         $relevance,
1307                         CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1308                         $rank
1309                 FROM    $search_table f,
1310                         $metabib_metarecord_source_map_table m,
1311                         $metabib_metarecord_source_map_table smrs,
1312                         $metabib_metarecord mr,
1313                         $metabib_record_descriptor rd
1314                 WHERE   $fts_where
1315                         AND smrs.metarecord = mr.id
1316                         AND m.source = f.source
1317                         AND m.metarecord = mr.id
1318                         AND rd.record = smrs.source
1319                         $t_filter
1320                         $f_filter
1321                         $a_filter
1322                         $l_filter
1323                         $lf_filter
1324                 GROUP BY m.metarecord
1325                 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1326                 LIMIT $visibility_limit
1327         SQL
1328
1329         if (0) {
1330                 $select = <<"           SQL";
1331
1332                         SELECT  DISTINCT s.*
1333                           FROM  $asset_call_number_table cn,
1334                                 $metabib_metarecord_source_map_table mrs,
1335                                 $asset_copy_table cp,
1336                                 $cs_table cs,
1337                                 $cl_table cl,
1338                                 $br_table br,
1339                                 $descendants d,
1340                                 $metabib_record_descriptor ord,
1341                                 ($select) s
1342                           WHERE mrs.metarecord = s.metarecord
1343                                 AND br.id = mrs.source
1344                                 AND cn.record = mrs.source
1345                                 AND cp.status = cs.id
1346                                 AND cp.location = cl.id
1347                                 AND cn.owning_lib = d.id
1348                                 AND cp.call_number = cn.id
1349                                 AND cp.opac_visible IS TRUE
1350                                 AND cs.opac_visible IS TRUE
1351                                 AND cl.opac_visible IS TRUE
1352                                 AND d.opac_visible IS TRUE
1353                                 AND br.active IS TRUE
1354                                 AND br.deleted IS FALSE
1355                                 AND ord.record = mrs.source
1356                                 $ot_filter
1357                                 $of_filter
1358                                 $oa_filter
1359                                 $ol_filter
1360                                 $olf_filter
1361                           ORDER BY 4 $sort_dir
1362                 SQL
1363         } elsif ($self->api_name !~ /staff/o) {
1364                 $select = <<"           SQL";
1365
1366                         SELECT  DISTINCT s.*
1367                           FROM  ($select) s
1368                           WHERE EXISTS (
1369                                 SELECT  1
1370                                   FROM  $asset_call_number_table cn,
1371                                         $metabib_metarecord_source_map_table mrs,
1372                                         $asset_copy_table cp,
1373                                         $cs_table cs,
1374                                         $cl_table cl,
1375                                         $br_table br,
1376                                         $descendants d,
1377                                         $metabib_record_descriptor ord
1378                                 
1379                                   WHERE mrs.metarecord = s.metarecord
1380                                         AND br.id = mrs.source
1381                                         AND cn.record = mrs.source
1382                                         AND cp.status = cs.id
1383                                         AND cp.location = cl.id
1384                                         AND cp.circ_lib = d.id
1385                                         AND cp.call_number = cn.id
1386                                         AND cp.opac_visible IS TRUE
1387                                         AND cs.opac_visible IS TRUE
1388                                         AND cl.opac_visible IS TRUE
1389                                         AND d.opac_visible IS TRUE
1390                                         AND br.active IS TRUE
1391                                         AND br.deleted IS FALSE
1392                                         AND ord.record = mrs.source
1393                                         $ot_filter
1394                                         $of_filter
1395                                         $oa_filter
1396                                         $ol_filter
1397                                         $olf_filter
1398                                   LIMIT 1
1399                                 )
1400                           ORDER BY 4 $sort_dir
1401                 SQL
1402         } else {
1403                 $select = <<"           SQL";
1404
1405                         SELECT  DISTINCT s.*
1406                           FROM  ($select) s
1407                           WHERE EXISTS (
1408                                 SELECT  1
1409                                   FROM  $asset_call_number_table cn,
1410                                         $asset_copy_table cp,
1411                                         $metabib_metarecord_source_map_table mrs,
1412                                         $br_table br,
1413                                         $descendants d,
1414                                         $metabib_record_descriptor ord
1415                                 
1416                                   WHERE mrs.metarecord = s.metarecord
1417                                         AND br.id = mrs.source
1418                                         AND cn.record = mrs.source
1419                                         AND cn.id = cp.call_number
1420                                         AND br.deleted IS FALSE
1421                                         AND cn.deleted IS FALSE
1422                                         AND ord.record = mrs.source
1423                                         AND (   cn.owning_lib = d.id
1424                                                 OR (    cp.circ_lib = d.id
1425                                                         AND cp.deleted IS FALSE
1426                                                 )
1427                                         )
1428                                         $ot_filter
1429                                         $of_filter
1430                                         $oa_filter
1431                                         $ol_filter
1432                                         $olf_filter
1433                                   LIMIT 1
1434                                 )
1435                                 OR NOT EXISTS (
1436                                 SELECT  1
1437                                   FROM  $asset_call_number_table cn,
1438                                         $metabib_metarecord_source_map_table mrs,
1439                                         $metabib_record_descriptor ord
1440                                   WHERE mrs.metarecord = s.metarecord
1441                                         AND cn.record = mrs.source
1442                                         AND ord.record = mrs.source
1443                                         $ot_filter
1444                                         $of_filter
1445                                         $oa_filter
1446                                         $ol_filter
1447                                         $olf_filter
1448                                   LIMIT 1
1449                                 )
1450                           ORDER BY 4 $sort_dir
1451                 SQL
1452         }
1453
1454
1455         $log->debug("Field Search SQL :: [$select]",DEBUG);
1456
1457         my $recs = $class->db_Main->selectall_arrayref(
1458                         $select, {},
1459                         (@bonus_values > 0 ? @bonus_values : () ),
1460                         ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1461                         @types, @forms, @aud, @lang, @lit_form,
1462                         @types, @forms, @aud, @lang, @lit_form,
1463                         ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1464         
1465         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1466
1467         my $max = 0;
1468         $max = 1 if (!@$recs);
1469         for (@$recs) {
1470                 $max = $$_[1] if ($$_[1] > $max);
1471         }
1472
1473         my $count = scalar(@$recs);
1474         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1475                 my ($mrid,$rank,$skip) = @$rec;
1476                 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1477         }
1478         return undef;
1479 }
1480
1481 for my $class ( qw/title author subject keyword series identifier/ ) {
1482         __PACKAGE__->register_method(
1483                 api_name        => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1484                 method          => 'postfilter_search_class_fts',
1485                 api_level       => 1,
1486                 stream          => 1,
1487                 cdbi            => "metabib::${class}_field_entry",
1488                 cachable        => 1,
1489         );
1490         __PACKAGE__->register_method(
1491                 api_name        => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1492                 method          => 'postfilter_search_class_fts',
1493                 api_level       => 1,
1494                 stream          => 1,
1495                 cdbi            => "metabib::${class}_field_entry",
1496                 cachable        => 1,
1497         );
1498 }
1499
1500
1501
1502 my $_cdbi = {   title   => "metabib::title_field_entry",
1503                 author  => "metabib::author_field_entry",
1504                 subject => "metabib::subject_field_entry",
1505                 keyword => "metabib::keyword_field_entry",
1506                 series  => "metabib::series_field_entry",
1507                 identifier      => "metabib::identifier_field_entry",
1508 };
1509
1510 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1511 sub postfilter_search_multi_class_fts {
1512     my $self   = shift;
1513     my $client = shift;
1514     my %args   = @_;
1515         
1516     my $sort             = $args{'sort'};
1517     my $sort_dir         = $args{sort_dir} || 'DESC';
1518     my $ou               = $args{org_unit};
1519     my $ou_type          = $args{depth};
1520     my $limit            = $args{limit}  || 10;
1521     my $offset           = $args{offset} ||  0;
1522     my $visibility_limit = $args{visibility_limit} || 5000;
1523
1524         if (!$ou) {
1525                 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1526         }
1527
1528         if (!defined($args{org_unit})) {
1529                 die "No target organizational unit passed to ".$self->api_name;
1530         }
1531
1532         if (! scalar( keys %{$args{searches}} )) {
1533                 die "No search arguments were passed to ".$self->api_name;
1534         }
1535
1536         my $outer_limit = 1000;
1537
1538         my $limit_clause  = '';
1539         my $offset_clause = '';
1540
1541         $limit_clause  = "LIMIT $outer_limit";
1542         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1543
1544         my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1545         my ($t_filter,   $f_filter,   $v_filter) = ('','','');
1546         my ($a_filter,   $l_filter,  $lf_filter) = ('','','');
1547         my ($ot_filter, $of_filter,  $ov_filter) = ('','','');
1548         my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1549
1550         if ($args{available}) {
1551                 $avail_filter = ' AND cp.status IN (0,7,12)';
1552         }
1553
1554         if (my $a = $args{audience}) {
1555                 $a = [$a] if (!ref($a));
1556                 @aud = @$a;
1557                         
1558                 $a_filter  = ' AND  rd.audience IN ('.join(',',map{'?'}@aud).')';
1559                 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1560         }
1561
1562         if (my $l = $args{language}) {
1563                 $l = [$l] if (!ref($l));
1564                 @lang = @$l;
1565
1566                 $l_filter  = ' AND  rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1567                 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1568         }
1569
1570         if (my $f = $args{lit_form}) {
1571                 $f = [$f] if (!ref($f));
1572                 @lit_form = @$f;
1573
1574                 $lf_filter  = ' AND  rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1575                 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1576         }
1577
1578         if (my $f = $args{item_form}) {
1579                 $f = [$f] if (!ref($f));
1580                 @forms = @$f;
1581
1582                 $f_filter  = ' AND  rd.item_form IN ('.join(',',map{'?'}@forms).')';
1583                 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1584         }
1585
1586         if (my $t = $args{item_type}) {
1587                 $t = [$t] if (!ref($t));
1588                 @types = @$t;
1589
1590                 $t_filter  = ' AND  rd.item_type IN ('.join(',',map{'?'}@types).')';
1591                 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1592         }
1593
1594         if (my $v = $args{vr_format}) {
1595                 $v = [$v] if (!ref($v));
1596                 @vformats = @$v;
1597
1598                 $v_filter  = ' AND  rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1599                 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1600         }
1601
1602
1603         # XXX legacy format and item type support
1604         if ($args{format}) {
1605                 my ($t, $f) = split '-', $args{format};
1606                 @types = split '', $t;
1607                 @forms = split '', $f;
1608                 if (@types) {
1609                         $t_filter  = ' AND  rd.item_type IN ('.join(',',map{'?'}@types).')';
1610                         $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1611                 }
1612
1613                 if (@forms) {
1614                         $f_filter  .= ' AND  rd.item_form IN ('.join(',',map{'?'}@forms).')';
1615                         $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1616                 }
1617         }
1618
1619
1620
1621         my $descendants = defined($ou_type) ?
1622                                 "actor.org_unit_descendants($ou, $ou_type)" :
1623                                 "actor.org_unit_descendants($ou)";
1624
1625     my $search_table_list = '';
1626     my $fts_list          = '';
1627     my $join_table_list   = '';
1628     my @rank_list;
1629
1630         my $field_table = config::metabib_field->table;
1631
1632         my @bonus_lists;
1633         my @bonus_values;
1634         my $prev_search_group;
1635         my $curr_search_group;
1636         my $search_class;
1637         my $search_field;
1638         my $metabib_field;
1639         for my $search_group (sort keys %{$args{searches}}) {
1640                 (my $search_group_name = $search_group) =~ s/\|/_/gso;
1641                 ($search_class,$search_field) = split /\|/, $search_group;
1642                 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1643
1644                 if ($search_field) {
1645                         unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1646                                 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1647                                 return undef;
1648                         }
1649                 }
1650
1651                 $prev_search_group = $curr_search_group if ($curr_search_group);
1652
1653                 $curr_search_group = $search_group_name;
1654
1655                 my $class = $_cdbi->{$search_class};
1656                 my $search_table = $class->table;
1657
1658                 my ($index_col) = $class->columns('FTS');
1659                 $index_col ||= 'value';
1660
1661                 
1662                 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1663
1664                 my $fts_where = $fts->sql_where_clause;
1665                 my @fts_ranks = $fts->fts_rank;
1666
1667                 my $SQLstring = join('%',map { lc($_) } $fts->words);
1668                 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1669                 my $first_word = lc(($fts->words)[0]).'%';
1670
1671                 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1672                 my $rank = join(' + ', @fts_ranks);
1673
1674                 my %bonus = ();
1675                 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1676                 $bonus{'author'}  = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1677
1678                 $bonus{'series'} = [
1679                         { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1680                         { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1681                 ];
1682
1683                 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1684
1685                 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1686                 $bonus_list ||= '1';
1687
1688                 push @bonus_lists, $bonus_list;
1689                 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1690
1691
1692                 #---------------------
1693
1694                 $search_table_list .= "$search_table $search_group_name, ";
1695                 push @rank_list,$rank;
1696                 $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1697
1698                 if ($metabib_field) {
1699                         $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1700                         $metabib_field = undef;
1701                 }
1702
1703                 if ($prev_search_group) {
1704                         $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1705                 }
1706         }
1707
1708         my $metabib_record_descriptor = metabib::record_descriptor->table;
1709         my $metabib_full_rec = metabib::full_rec->table;
1710         my $metabib_metarecord = metabib::metarecord->table;
1711         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1712         my $asset_call_number_table = asset::call_number->table;
1713         my $asset_copy_table = asset::copy->table;
1714         my $cs_table = config::copy_status->table;
1715         my $cl_table = asset::copy_location->table;
1716         my $br_table = biblio::record_entry->table;
1717         my $source_table = config::bib_source->table;
1718
1719         my $bonuses = join (' * ', @bonus_lists);
1720         my $relevance = join (' + ', @rank_list);
1721         $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1722
1723         my $string_default_sort = 'zzzz';
1724         $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1725
1726         my $number_default_sort = '9999';
1727         $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1728
1729
1730
1731         my $secondary_sort = <<"        SORT";
1732                 ( FIRST ((
1733                         SELECT  COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1734                           FROM  $metabib_full_rec sfrt,
1735                                 $metabib_metarecord mr
1736                           WHERE sfrt.record = mr.master_record
1737                                 AND sfrt.tag = '245'
1738                                 AND sfrt.subfield = 'a'
1739                           LIMIT 1
1740                 )) )
1741         SORT
1742
1743         my $rank = $relevance;
1744         if (lc($sort) eq 'pubdate') {
1745                 $rank = <<"             RANK";
1746                         ( FIRST ((
1747                                 SELECT  COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1748                                   FROM  $metabib_full_rec frp
1749                                   WHERE frp.record = mr.master_record
1750                                         AND frp.tag = '260'
1751                                         AND frp.subfield = 'c'
1752                                   LIMIT 1
1753                         )) )
1754                 RANK
1755         } elsif (lc($sort) eq 'create_date') {
1756                 $rank = <<"             RANK";
1757                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1758                 RANK
1759         } elsif (lc($sort) eq 'edit_date') {
1760                 $rank = <<"             RANK";
1761                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1762                 RANK
1763         } elsif (lc($sort) eq 'title') {
1764                 $rank = <<"             RANK";
1765                         ( FIRST ((
1766                                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1767                                   FROM  $metabib_full_rec frt
1768                                   WHERE frt.record = mr.master_record
1769                                         AND frt.tag = '245'
1770                                         AND frt.subfield = 'a'
1771                                   LIMIT 1
1772                         )) )
1773                 RANK
1774                 $secondary_sort = <<"           SORT";
1775                         ( FIRST ((
1776                                 SELECT  COALESCE(SUBSTRING(sfrp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1777                                   FROM  $metabib_full_rec sfrp
1778                                   WHERE sfrp.record = mr.master_record
1779                                         AND sfrp.tag = '260'
1780                                         AND sfrp.subfield = 'c'
1781                                   LIMIT 1
1782                         )) )
1783                 SORT
1784         } elsif (lc($sort) eq 'author') {
1785                 $rank = <<"             RANK";
1786                         ( FIRST((
1787                                 SELECT  COALESCE(LTRIM(fra.value),'$string_default_sort')
1788                                   FROM  $metabib_full_rec fra
1789                                   WHERE fra.record = mr.master_record
1790                                         AND fra.tag LIKE '1%'
1791                                         AND fra.subfield = 'a'
1792                                   ORDER BY fra.tag::text::int
1793                                   LIMIT 1
1794                         )) )
1795                 RANK
1796         } else {
1797                 push @bonus_values, @bonus_values;
1798                 $sort = undef;
1799         }
1800
1801
1802         my $select = <<"        SQL";
1803                 SELECT  m.metarecord,
1804                         $relevance,
1805                         CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1806                         $rank,
1807                         $secondary_sort
1808                 FROM    $search_table_list
1809                         $metabib_metarecord mr,
1810                         $metabib_metarecord_source_map_table m,
1811                         $metabib_metarecord_source_map_table smrs
1812                 WHERE   m.metarecord = smrs.metarecord 
1813                         AND mr.id = m.metarecord
1814                         $fts_list
1815                         $join_table_list
1816                 GROUP BY m.metarecord
1817                 -- ORDER BY 4 $sort_dir
1818                 LIMIT $visibility_limit
1819         SQL
1820
1821         if ($self->api_name !~ /staff/o) {
1822                 $select = <<"           SQL";
1823
1824                         SELECT  s.*
1825                           FROM  ($select) s
1826                           WHERE EXISTS (
1827                                 SELECT  1
1828                                   FROM  $asset_call_number_table cn,
1829                                         $metabib_metarecord_source_map_table mrs,
1830                                         $asset_copy_table cp,
1831                                         $cs_table cs,
1832                                         $cl_table cl,
1833                                         $br_table br,
1834                                         $descendants d,
1835                                         $metabib_record_descriptor ord
1836                                   WHERE mrs.metarecord = s.metarecord
1837                                         AND br.id = mrs.source
1838                                         AND cn.record = mrs.source
1839                                         AND cp.status = cs.id
1840                                         AND cp.location = cl.id
1841                                         AND cp.circ_lib = d.id
1842                                         AND cp.call_number = cn.id
1843                                         AND cp.opac_visible IS TRUE
1844                                         AND cs.opac_visible IS TRUE
1845                                         AND cl.opac_visible IS TRUE
1846                                         AND d.opac_visible IS TRUE
1847                                         AND br.active IS TRUE
1848                                         AND br.deleted IS FALSE
1849                                         AND cp.deleted IS FALSE
1850                                         AND cn.deleted IS FALSE
1851                                         AND ord.record = mrs.source
1852                                         $ot_filter
1853                                         $of_filter
1854                                         $ov_filter
1855                                         $oa_filter
1856                                         $ol_filter
1857                                         $olf_filter
1858                                         $avail_filter
1859                                   LIMIT 1
1860                                 )
1861                                 OR EXISTS (
1862                                 SELECT  1
1863                                   FROM  $br_table br,
1864                                         $metabib_metarecord_source_map_table mrs,
1865                                         $metabib_record_descriptor ord,
1866                                         $source_table src
1867                                   WHERE mrs.metarecord = s.metarecord
1868                                         AND ord.record = mrs.source
1869                                         AND br.id = mrs.source
1870                                         AND br.source = src.id
1871                                         AND src.transcendant IS TRUE
1872                                         $ot_filter
1873                                         $of_filter
1874                                         $ov_filter
1875                                         $oa_filter
1876                                         $ol_filter
1877                                         $olf_filter
1878                                 )
1879                           ORDER BY 4 $sort_dir, 5
1880                 SQL
1881         } else {
1882                 $select = <<"           SQL";
1883
1884                         SELECT  DISTINCT s.*
1885                           FROM  ($select) s,
1886                                 $metabib_metarecord_source_map_table omrs,
1887                                 $metabib_record_descriptor ord
1888                           WHERE omrs.metarecord = s.metarecord
1889                                 AND ord.record = omrs.source
1890                                 AND (   EXISTS (
1891                                                 SELECT  1
1892                                                   FROM  $asset_call_number_table cn,
1893                                                         $asset_copy_table cp,
1894                                                         $descendants d,
1895                                                         $br_table br
1896                                                   WHERE br.id = omrs.source
1897                                                         AND cn.record = omrs.source
1898                                                         AND br.deleted IS FALSE
1899                                                         AND cn.deleted IS FALSE
1900                                                         AND cp.call_number = cn.id
1901                                                         AND (   cn.owning_lib = d.id
1902                                                                 OR (    cp.circ_lib = d.id
1903                                                                         AND cp.deleted IS FALSE
1904                                                                 )
1905                                                         )
1906                                                         $avail_filter
1907                                                   LIMIT 1
1908                                         )
1909                                         OR NOT EXISTS (
1910                                                 SELECT  1
1911                                                   FROM  $asset_call_number_table cn
1912                                                   WHERE cn.record = omrs.source
1913                                                         AND cn.deleted IS FALSE
1914                                                   LIMIT 1
1915                                         )
1916                                         OR EXISTS (
1917                                         SELECT  1
1918                                           FROM  $br_table br,
1919                                                 $metabib_metarecord_source_map_table mrs,
1920                                                 $metabib_record_descriptor ord,
1921                                                 $source_table src
1922                                           WHERE mrs.metarecord = s.metarecord
1923                                                 AND br.id = mrs.source
1924                                                 AND br.source = src.id
1925                                                 AND src.transcendant IS TRUE
1926                                                 $ot_filter
1927                                                 $of_filter
1928                                                 $ov_filter
1929                                                 $oa_filter
1930                                                 $ol_filter
1931                                                 $olf_filter
1932                                         )
1933                                 )
1934                                 $ot_filter
1935                                 $of_filter
1936                                 $ov_filter
1937                                 $oa_filter
1938                                 $ol_filter
1939                                 $olf_filter
1940
1941                           ORDER BY 4 $sort_dir, 5
1942                 SQL
1943         }
1944
1945
1946         $log->debug("Field Search SQL :: [$select]",DEBUG);
1947
1948         my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1949                         $select, {},
1950                         @bonus_values,
1951                         @types, @forms, @vformats, @aud, @lang, @lit_form,
1952                         @types, @forms, @vformats, @aud, @lang, @lit_form,
1953                         # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1954         );
1955         
1956         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1957
1958         my $max = 0;
1959         $max = 1 if (!@$recs);
1960         for (@$recs) {
1961                 $max = $$_[1] if ($$_[1] > $max);
1962         }
1963
1964         my $count = scalar(@$recs);
1965         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1966                 next unless ($$rec[0]);
1967                 my ($mrid,$rank,$skip) = @$rec;
1968                 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1969         }
1970         return undef;
1971 }
1972
1973 __PACKAGE__->register_method(
1974         api_name        => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1975         method          => 'postfilter_search_multi_class_fts',
1976         api_level       => 1,
1977         stream          => 1,
1978         cachable        => 1,
1979 );
1980 __PACKAGE__->register_method(
1981         api_name        => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1982         method          => 'postfilter_search_multi_class_fts',
1983         api_level       => 1,
1984         stream          => 1,
1985         cachable        => 1,
1986 );
1987
1988 __PACKAGE__->register_method(
1989         api_name        => "open-ils.storage.metabib.multiclass.search_fts",
1990         method          => 'postfilter_search_multi_class_fts',
1991         api_level       => 1,
1992         stream          => 1,
1993         cachable        => 1,
1994 );
1995 __PACKAGE__->register_method(
1996         api_name        => "open-ils.storage.metabib.multiclass.search_fts.staff",
1997         method          => 'postfilter_search_multi_class_fts',
1998         api_level       => 1,
1999         stream          => 1,
2000         cachable        => 1,
2001 );
2002
2003 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2004 sub biblio_search_multi_class_fts {
2005         my $self = shift;
2006         my $client = shift;
2007         my %args = @_;
2008         
2009     my $sort             = $args{'sort'};
2010     my $sort_dir         = $args{sort_dir} || 'DESC';
2011     my $ou               = $args{org_unit};
2012     my $ou_type          = $args{depth};
2013     my $limit            = $args{limit}  || 10;
2014     my $offset           = $args{offset} ||  0;
2015     my $pref_lang        = $args{preferred_language} || 'eng';
2016     my $visibility_limit = $args{visibility_limit}  || 5000;
2017
2018         if (!$ou) {
2019                 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2020         }
2021
2022         if (! scalar( keys %{$args{searches}} )) {
2023                 die "No search arguments were passed to ".$self->api_name;
2024         }
2025
2026         my $outer_limit = 1000;
2027
2028         my $limit_clause  = '';
2029         my $offset_clause = '';
2030
2031         $limit_clause  = "LIMIT $outer_limit";
2032         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
2033
2034         my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
2035         my ($t_filter,   $f_filter,   $v_filter) = ('','','');
2036         my ($a_filter,   $l_filter,  $lf_filter) = ('','','');
2037         my ($ot_filter, $of_filter,  $ov_filter) = ('','','');
2038         my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
2039
2040         if ($args{available}) {
2041                 $avail_filter = ' AND cp.status IN (0,7,12)';
2042         }
2043
2044         if (my $a = $args{audience}) {
2045                 $a = [$a] if (!ref($a));
2046                 @aud = @$a;
2047                         
2048                 $a_filter  = ' AND rd.audience  IN ('.join(',',map{'?'}@aud).')';
2049                 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
2050         }
2051
2052         if (my $l = $args{language}) {
2053                 $l = [$l] if (!ref($l));
2054                 @lang = @$l;
2055
2056                 $l_filter  = ' AND rd.item_lang  IN ('.join(',',map{'?'}@lang).')';
2057                 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
2058         }
2059
2060         if (my $f = $args{lit_form}) {
2061                 $f = [$f] if (!ref($f));
2062                 @lit_form = @$f;
2063
2064                 $lf_filter  = ' AND rd.lit_form  IN ('.join(',',map{'?'}@lit_form).')';
2065                 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
2066         }
2067
2068         if (my $f = $args{item_form}) {
2069                 $f = [$f] if (!ref($f));
2070                 @forms = @$f;
2071
2072                 $f_filter  = ' AND rd.item_form  IN ('.join(',',map{'?'}@forms).')';
2073                 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2074         }
2075
2076         if (my $t = $args{item_type}) {
2077                 $t = [$t] if (!ref($t));
2078                 @types = @$t;
2079
2080                 $t_filter  = ' AND rd.item_type  IN ('.join(',',map{'?'}@types).')';
2081                 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2082         }
2083
2084         if (my $v = $args{vr_format}) {
2085                 $v = [$v] if (!ref($v));
2086                 @vformats = @$v;
2087
2088                 $v_filter  = ' AND rd.vr_format  IN ('.join(',',map{'?'}@vformats).')';
2089                 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
2090         }
2091
2092         # XXX legacy format and item type support
2093         if ($args{format}) {
2094                 my ($t, $f) = split '-', $args{format};
2095                 @types = split '', $t;
2096                 @forms = split '', $f;
2097                 if (@types) {
2098                         $t_filter  = ' AND rd.item_type  IN ('.join(',',map{'?'}@types).')';
2099                         $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2100                 }
2101
2102                 if (@forms) {
2103                         $f_filter  .= ' AND rd.item_form  IN ('.join(',',map{'?'}@forms).')';
2104                         $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2105                 }
2106         }
2107
2108
2109         my $descendants = defined($ou_type) ?
2110                                 "actor.org_unit_descendants($ou, $ou_type)" :
2111                                 "actor.org_unit_descendants($ou)";
2112
2113         my $search_table_list = '';
2114         my $fts_list = '';
2115         my $join_table_list = '';
2116         my @rank_list;
2117
2118         my $field_table = config::metabib_field->table;
2119
2120         my @bonus_lists;
2121         my @bonus_values;
2122         my $prev_search_group;
2123         my $curr_search_group;
2124         my $search_class;
2125         my $search_field;
2126         my $metabib_field;
2127         for my $search_group (sort keys %{$args{searches}}) {
2128                 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2129                 ($search_class,$search_field) = split /\|/, $search_group;
2130                 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2131
2132                 if ($search_field) {
2133                         unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2134                                 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2135                                 return undef;
2136                         }
2137                 }
2138
2139                 $prev_search_group = $curr_search_group if ($curr_search_group);
2140
2141                 $curr_search_group = $search_group_name;
2142
2143                 my $class = $_cdbi->{$search_class};
2144                 my $search_table = $class->table;
2145
2146                 my ($index_col) = $class->columns('FTS');
2147                 $index_col ||= 'value';
2148
2149                 
2150                 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2151
2152                 my $fts_where = $fts->sql_where_clause;
2153                 my @fts_ranks = $fts->fts_rank;
2154
2155                 my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2156                 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2157                 my $first_word = lc(($fts->words)[0]).'%';
2158
2159                 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2160                 my $rank = join('  + ', @fts_ranks);
2161
2162                 my %bonus = ();
2163                 $bonus{'subject'} = [];
2164                 $bonus{'author'}  = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2165
2166                 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2167
2168                 $bonus{'series'} = [
2169                         { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2170                         { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2171                 ];
2172
2173                 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2174
2175                 if ($pref_lang) {
2176                         push @{ $bonus{'title'}   }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2177                         push @{ $bonus{'author'}  }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2178                         push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2179                         push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2180                         push @{ $bonus{'series'}  }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2181                 }
2182
2183                 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2184                 $bonus_list ||= '1';
2185
2186                 push @bonus_lists, $bonus_list;
2187                 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2188
2189                 #---------------------
2190
2191                 $search_table_list .= "$search_table $search_group_name, ";
2192                 push @rank_list,$rank;
2193                 $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2194
2195                 if ($metabib_field) {
2196                         $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2197                         $metabib_field = undef;
2198                 }
2199
2200                 if ($prev_search_group) {
2201                         $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2202                 }
2203         }
2204
2205         my $metabib_record_descriptor = metabib::record_descriptor->table;
2206         my $metabib_full_rec = metabib::full_rec->table;
2207         my $metabib_metarecord = metabib::metarecord->table;
2208         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2209         my $asset_call_number_table = asset::call_number->table;
2210         my $asset_copy_table = asset::copy->table;
2211         my $cs_table = config::copy_status->table;
2212         my $cl_table = asset::copy_location->table;
2213         my $br_table = biblio::record_entry->table;
2214         my $source_table = config::bib_source->table;
2215
2216
2217         my $bonuses = join (' * ', @bonus_lists);
2218         my $relevance = join (' + ', @rank_list);
2219         $relevance = "AVG( ($relevance) * ($bonuses) )";
2220
2221         my $string_default_sort = 'zzzz';
2222         $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2223
2224         my $number_default_sort = '9999';
2225         $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2226
2227         my $rank = $relevance;
2228         if (lc($sort) eq 'pubdate') {
2229                 $rank = <<"             RANK";
2230                         ( FIRST ((
2231                                 SELECT  COALESCE(SUBSTRING(frp.value FROM E'\\\\d{4}'),'$number_default_sort')::INT
2232                                   FROM  $metabib_full_rec frp
2233                                   WHERE frp.record = b.id
2234                                         AND frp.tag = '260'
2235                                         AND frp.subfield = 'c'
2236                                   LIMIT 1
2237                         )) )
2238                 RANK
2239         } elsif (lc($sort) eq 'create_date') {
2240                 $rank = <<"             RANK";
2241                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2242                 RANK
2243         } elsif (lc($sort) eq 'edit_date') {
2244                 $rank = <<"             RANK";
2245                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2246                 RANK
2247         } elsif (lc($sort) eq 'title') {
2248                 $rank = <<"             RANK";
2249                         ( FIRST ((
2250                                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2251                                   FROM  $metabib_full_rec frt
2252                                   WHERE frt.record = b.id
2253                                         AND frt.tag = '245'
2254                                         AND frt.subfield = 'a'
2255                                   LIMIT 1
2256                         )) )
2257                 RANK
2258         } elsif (lc($sort) eq 'author') {
2259                 $rank = <<"             RANK";
2260                         ( FIRST((
2261                                 SELECT  COALESCE(LTRIM(fra.value),'$string_default_sort')
2262                                   FROM  $metabib_full_rec fra
2263                                   WHERE fra.record = b.id
2264                                         AND fra.tag LIKE '1%'
2265                                         AND fra.subfield = 'a'
2266                                   ORDER BY fra.tag::text::int
2267                                   LIMIT 1
2268                         )) )
2269                 RANK
2270         } else {
2271                 push @bonus_values, @bonus_values;
2272                 $sort = undef;
2273         }
2274
2275
2276         my $select = <<"        SQL";
2277                 SELECT  b.id,
2278                         $relevance AS rel,
2279                         $rank AS rank,
2280                         b.source
2281                 FROM    $search_table_list
2282                         $metabib_record_descriptor rd,
2283                         $source_table src,
2284                         $br_table b
2285                 WHERE   rd.record = b.id
2286                         AND b.active IS TRUE
2287                         AND b.deleted IS FALSE
2288                         $fts_list
2289                         $join_table_list
2290                         $t_filter
2291                         $f_filter
2292                         $v_filter
2293                         $a_filter
2294                         $l_filter
2295                         $lf_filter
2296                 GROUP BY b.id, b.source
2297                 ORDER BY 3 $sort_dir
2298                 LIMIT $visibility_limit
2299         SQL
2300
2301         if ($self->api_name !~ /staff/o) {
2302                 $select = <<"           SQL";
2303
2304                         SELECT  s.*
2305                           FROM  ($select) s
2306                                 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2307                           WHERE EXISTS (
2308                                 SELECT  1
2309                                   FROM  $asset_call_number_table cn,
2310                                         $asset_copy_table cp,
2311                                         $cs_table cs,
2312                                         $cl_table cl,
2313                                         $descendants d
2314                                   WHERE cn.record = s.id
2315                                         AND cp.status = cs.id
2316                                         AND cp.location = cl.id
2317                                         AND cp.call_number = cn.id
2318                                         AND cp.opac_visible IS TRUE
2319                                         AND cs.opac_visible IS TRUE
2320                                         AND cl.opac_visible IS TRUE
2321                                         AND d.opac_visible IS TRUE
2322                                         AND cp.deleted IS FALSE
2323                                         AND cn.deleted IS FALSE
2324                                         AND cp.circ_lib = d.id
2325                                         $avail_filter
2326                                   LIMIT 1
2327                                 )
2328                                 OR src.transcendant IS TRUE
2329                           ORDER BY 3 $sort_dir
2330                 SQL
2331         } else {
2332                 $select = <<"           SQL";
2333
2334                         SELECT  s.*
2335                           FROM  ($select) s
2336                                 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2337                           WHERE EXISTS (
2338                                 SELECT  1
2339                                   FROM  $asset_call_number_table cn,
2340                                         $asset_copy_table cp,
2341                                         $descendants d
2342                                   WHERE cn.record = s.id
2343                                         AND cp.call_number = cn.id
2344                                         AND cn.deleted IS FALSE
2345                                         AND cp.circ_lib = d.id
2346                                         AND cp.deleted IS FALSE
2347                                         $avail_filter
2348                                   LIMIT 1
2349                                 )
2350                                 OR NOT EXISTS (
2351                                 SELECT  1
2352                                   FROM  $asset_call_number_table cn
2353                                   WHERE cn.record = s.id
2354                                   LIMIT 1
2355                                 )
2356                                 OR src.transcendant IS TRUE
2357                           ORDER BY 3 $sort_dir
2358                 SQL
2359         }
2360
2361
2362         $log->debug("Field Search SQL :: [$select]",DEBUG);
2363
2364         my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2365                         $select, {},
2366                         @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2367         );
2368         
2369         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2370
2371         my $count = scalar(@$recs);
2372         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2373                 next unless ($$rec[0]);
2374                 my ($mrid,$rank) = @$rec;
2375                 $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2376         }
2377         return undef;
2378 }
2379
2380 __PACKAGE__->register_method(
2381         api_name        => "open-ils.storage.biblio.multiclass.search_fts.record",
2382         method          => 'biblio_search_multi_class_fts',
2383         api_level       => 1,
2384         stream          => 1,
2385         cachable        => 1,
2386 );
2387 __PACKAGE__->register_method(
2388         api_name        => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2389         method          => 'biblio_search_multi_class_fts',
2390         api_level       => 1,
2391         stream          => 1,
2392         cachable        => 1,
2393 );
2394 __PACKAGE__->register_method(
2395         api_name        => "open-ils.storage.biblio.multiclass.search_fts",
2396         method          => 'biblio_search_multi_class_fts',
2397         api_level       => 1,
2398         stream          => 1,
2399         cachable        => 1,
2400 );
2401 __PACKAGE__->register_method(
2402         api_name        => "open-ils.storage.biblio.multiclass.search_fts.staff",
2403         method          => 'biblio_search_multi_class_fts',
2404         api_level       => 1,
2405         stream          => 1,
2406         cachable        => 1,
2407 );
2408
2409
2410 my %locale_map;
2411 my $default_preferred_language;
2412 my $default_preferred_language_weight;
2413
2414 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2415 sub staged_fts {
2416     my $self   = shift;
2417     my $client = shift;
2418     my %args   = @_;
2419
2420     if (!$locale_map{COMPLETE}) {
2421
2422         my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2423         for my $locale ( @locales ) {
2424             $locale_map{lc($locale->code)} = $locale->marc_code;
2425         }
2426         $locale_map{COMPLETE} = 1;
2427
2428     }
2429
2430     my $config = OpenSRF::Utils::SettingsClient->new();
2431
2432     if (!$default_preferred_language) {
2433
2434         $default_preferred_language = $config->config_value(
2435                 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2436         ) || $config->config_value(
2437                 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2438         );
2439
2440     }
2441
2442     if (!$default_preferred_language_weight) {
2443
2444         $default_preferred_language_weight = $config->config_value(
2445                 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2446         ) || $config->config_value(
2447                 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2448         );
2449     }
2450
2451     # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2452     my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2453
2454     my $ou     = $args{org_unit};
2455     my $limit  = $args{limit}  || 10;
2456     my $offset = $args{offset} ||  0;
2457
2458         if (!$ou) {
2459                 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2460         }
2461
2462         if (! scalar( keys %{$args{searches}} )) {
2463                 die "No search arguments were passed to ".$self->api_name;
2464         }
2465
2466         my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2467
2468     if (!defined($args{preferred_language})) {
2469                 my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2470         $args{preferred_language} =
2471             $locale_map{ lc($ses_locale) } || 'eng';
2472     }
2473
2474     if (!defined($args{preferred_language_weight})) {
2475         $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2476     }
2477
2478         if ($args{available}) {
2479                 @statuses = (0,7,12);
2480         }
2481
2482         if (my $s = $args{locations}) {
2483                 $s = [$s] if (!ref($s));
2484                 @locations = @$s;
2485         }
2486
2487         if (my $b = $args{between}) {
2488                 if (ref($b) && @$b == 2) {
2489                     @between = @$b;
2490         }
2491         }
2492
2493         if (my $s = $args{statuses}) {
2494                 $s = [$s] if (!ref($s));
2495                 @statuses = @$s;
2496         }
2497
2498         if (my $a = $args{audience}) {
2499                 $a = [$a] if (!ref($a));
2500                 @aud = @$a;
2501         }
2502
2503         if (my $l = $args{language}) {
2504                 $l = [$l] if (!ref($l));
2505                 @lang = @$l;
2506         }
2507
2508         if (my $f = $args{lit_form}) {
2509                 $f = [$f] if (!ref($f));