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