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