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