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