]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/metabib.pm
aebd348284e843bf18bb0b7925bfe4532fdecf57
[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 $string_default_sort = 'zzzz';
513     $string_default_sort = 'AAAA' if ($sort_dir =~ /^DESC$/i);
514
515     my $number_default_sort = '9999';
516     $number_default_sort = '0000' if ($sort_dir =~/^DESC$/i);
517
518     my $rank = $relevance;
519     if (lc($sort) eq 'pubdate') {
520         $rank = <<"        RANK";
521             ( FIRST ((
522                 SELECT  COALESCE(SUBSTRING(MAX(frp.value) FROM E'\\\\d{4}'), '$number_default_sort')::INT
523                   FROM  $metabib_full_rec frp
524                   WHERE frp.record = f.record
525                     AND frp.tag = '260'
526                     AND frp.subfield = 'c'
527                   LIMIT 1
528             )) )
529         RANK
530     } elsif (lc($sort) eq 'create_date') {
531         $rank = <<"        RANK";
532             ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
533         RANK
534     } elsif (lc($sort) eq 'edit_date') {
535         $rank = <<"        RANK";
536             ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
537         RANK
538     } elsif (lc($sort) =~ /^title/i) {
539         $rank = <<"        RANK";
540             ( FIRST ((
541                 SELECT  COALESCE(LTRIM(SUBSTR(MAX(frt.value), COALESCE(SUBSTRING(MAX(frt.ind2) FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
542                   FROM  $metabib_full_rec frt
543                   WHERE frt.record = f.record
544                     AND frt.tag = '245'
545                     AND frt.subfield = 'a'
546                   LIMIT 1
547             )) )
548         RANK
549     } elsif (lc($sort) =~ /^author/i) {
550         $rank = <<"        RANK";
551             ( FIRST((
552                 SELECT  COALESCE(LTRIM(MAX(query.value)), '$string_default_sort')
553                   FROM  (
554                             SELECT fra.value
555                             FROM $metabib_full_rec fra
556                             WHERE fra.record = f.record
557                                 AND fra.tag LIKE '1%'
558                                 AND fra.subfield = 'a'
559                             ORDER BY fra.tag::text::int
560                             LIMIT 1
561                         ) query
562             )) )
563         RANK
564     } else {
565         $sort = undef;
566     }
567
568     my $rd_join = $use_rd ? "$metabib_record_descriptor rd," : '';
569     my $rd_filter = $use_rd ? 'AND rd.record = f.record' : '';
570
571     if ($copies_visible) {
572         $select = <<"        SQL";
573             SELECT  f.record, $relevance, count(DISTINCT cp.id), $rank
574             FROM    $search_table f,
575                 $asset_call_number_table cn,
576                 $asset_copy_table cp,
577                 $cs_table cs,
578                 $cl_table cl,
579                 $br_table br,
580                 $rd_join
581                 $descendants d
582             WHERE   br.id = f.record
583                 AND cn.record = f.record
584                 AND cp.status = cs.id
585                 AND cp.location = cl.id
586                 AND br.deleted IS FALSE
587                 AND cn.deleted IS FALSE
588                 AND cp.deleted IS FALSE
589                 $rd_filter
590                 $has_vols
591                 $has_copies
592                 $copies_visible
593                 $t_filter
594                 $f_filter
595                 $a_filter
596                 $l_filter
597                 $lf_filter
598             GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
599             ORDER BY 4 $sort_dir,3 DESC
600         SQL
601     } else {
602         $select = <<"        SQL";
603             SELECT  f.record, 1, 1, $rank
604             FROM    $search_table f,
605                 $rd_join
606                 $br_table br
607             WHERE   br.id = f.record
608                 AND br.deleted IS FALSE
609                 $rd_filter
610                 $t_filter
611                 $f_filter
612                 $a_filter
613                 $l_filter
614                 $lf_filter
615             GROUP BY 1,2,3 
616             ORDER BY 4 $sort_dir
617         SQL
618     }
619
620
621     $log->debug("Search SQL :: [$select]",DEBUG);
622
623     my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
624     $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
625
626     my $max = 0;
627     $max = 1 if (!@$recs);
628     for (@$recs) {
629         $max = $$_[1] if ($$_[1] > $max);
630     }
631
632     my $count = @$recs;
633     for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
634         next unless ($$rec[0]);
635         my ($rid,$rank,$junk,$skip) = @$rec;
636         $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
637     }
638     return undef;
639 }
640 __PACKAGE__->register_method(
641     api_name    => 'open-ils.storage.biblio.full_rec.multi_search',
642     method      => 'biblio_multi_search_full_rec',
643     api_level   => 1,
644     stream      => 1,
645     cachable    => 1,
646 );
647 __PACKAGE__->register_method(
648     api_name    => 'open-ils.storage.biblio.full_rec.multi_search.staff',
649     method      => 'biblio_multi_search_full_rec',
650     api_level   => 1,
651     stream      => 1,
652     cachable    => 1,
653 );
654
655 sub search_full_rec {
656     my $self = shift;
657     my $client = shift;
658
659     my %args = @_;
660     
661     my $term = $args{term};
662     my $limiters = $args{restrict};
663
664     my ($index_col) = metabib::full_rec->columns('FTS');
665     $index_col ||= 'value';
666     my $search_table = metabib::full_rec->table;
667
668     my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
669
670     my $fts_where = $fts->sql_where_clause();
671     my @fts_ranks = $fts->fts_rank;
672
673     my $rank = join(' + ', @fts_ranks);
674
675     my @binds;
676     my @wheres;
677     for my $limit (@$limiters) {
678         if ($$limit{tag} =~ /^\d+$/ and $$limit{tag} < 10) {
679             # MARC control field; mfr.subfield is NULL
680             push @wheres, "( tag = ? AND $fts_where )";
681             push @binds, $$limit{tag};
682             $log->debug("Limiting query using { tag => $$limit{tag} }", DEBUG);
683         } else {
684             push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
685             push @binds, $$limit{tag}, $$limit{subfield};
686             $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
687         }
688     }
689     my $where = join(' OR ', @wheres);
690
691     my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
692
693     $log->debug("Search SQL :: [$select]",DEBUG);
694
695     my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
696     $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
697
698     $client->respond($_) for (@$recs);
699     return undef;
700 }
701 __PACKAGE__->register_method(
702     api_name    => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
703     method      => 'search_full_rec',
704     api_level   => 1,
705     stream      => 1,
706     cachable    => 1,
707 );
708 __PACKAGE__->register_method(
709     api_name    => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
710     method      => 'search_full_rec',
711     api_level   => 1,
712     stream      => 1,
713     cachable    => 1,
714 );
715
716
717 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
718 sub search_class_fts {
719     my $self = shift;
720     my $client = shift;
721     my %args = @_;
722     
723     my $term = $args{term};
724     my $ou = $args{org_unit};
725     my $ou_type = $args{depth};
726     my $limit = $args{limit};
727     my $offset = $args{offset};
728
729     my $limit_clause = '';
730     my $offset_clause = '';
731
732     $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
733     $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
734
735     my (@types,@forms);
736     my ($t_filter, $f_filter) = ('','');
737
738     if ($args{format}) {
739         my ($t, $f) = split '-', $args{format};
740         @types = split '', $t;
741         @forms = split '', $f;
742         if (@types) {
743             $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
744         }
745
746         if (@forms) {
747             $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
748         }
749     }
750
751
752
753     my $descendants = defined($ou_type) ?
754                 "actor.org_unit_descendants($ou, $ou_type)" :
755                 "actor.org_unit_descendants($ou)";
756
757     my $class = $self->{cdbi};
758     my $search_table = $class->table;
759
760     my $metabib_record_descriptor = metabib::record_descriptor->table;
761     my $metabib_metarecord = metabib::metarecord->table;
762     my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
763     my $asset_call_number_table = asset::call_number->table;
764     my $asset_copy_table = asset::copy->table;
765     my $cs_table = config::copy_status->table;
766     my $cl_table = asset::copy_location->table;
767
768     my ($index_col) = $class->columns('FTS');
769     $index_col ||= 'value';
770
771     (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
772     my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
773
774     my $fts_where = $fts->sql_where_clause;
775     my @fts_ranks = $fts->fts_rank;
776
777     my $rank = join(' + ', @fts_ranks);
778
779     my $has_vols = 'AND cn.owning_lib = d.id';
780     my $has_copies = 'AND cp.call_number = cn.id';
781     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';
782
783     my $visible_count = ', count(DISTINCT cp.id)';
784     my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
785
786     if ($self->api_name =~ /staff/o) {
787         $copies_visible = '';
788         $visible_count_test = '';
789         $has_copies = '' if ($ou_type == 0);
790         $has_vols = '' if ($ou_type == 0);
791     }
792
793     my $rank_calc = <<"    RANK";
794         , (SUM( $rank
795             * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
796             * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
797             * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
798         )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
799     RANK
800
801     $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
802
803     if ($copies_visible) {
804         $select = <<"        SQL";
805             SELECT  m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
806             FROM    $search_table f,
807                 $metabib_metarecord_source_map_table m,
808                 $asset_call_number_table cn,
809                 $asset_copy_table cp,
810                 $cs_table cs,
811                 $cl_table cl,
812                 $metabib_record_descriptor rd,
813                 $descendants d
814             WHERE   $fts_where
815                 AND m.source = f.source
816                 AND cn.record = m.source
817                 AND rd.record = m.source
818                 AND cp.status = cs.id
819                 AND cp.location = cl.id
820                 $has_vols
821                 $has_copies
822                 $copies_visible
823                 $t_filter
824                 $f_filter
825             GROUP BY 1 $visible_count_test
826             ORDER BY 2 DESC,3
827             $limit_clause $offset_clause
828         SQL
829     } else {
830         $select = <<"        SQL";
831             SELECT  m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
832             FROM    $search_table f,
833                 $metabib_metarecord_source_map_table m,
834                 $metabib_record_descriptor rd
835             WHERE   $fts_where
836                 AND m.source = f.source
837                 AND rd.record = m.source
838                 $t_filter
839                 $f_filter
840             GROUP BY 1, 4
841             ORDER BY 2 DESC,3
842             $limit_clause $offset_clause
843         SQL
844     }
845
846     $log->debug("Field Search SQL :: [$select]",DEBUG);
847
848     my $SQLstring = join('%',$fts->words);
849     my $REstring = join('\\s+',$fts->words);
850     my $first_word = ($fts->words)[0].'%';
851     my $recs = ($self->api_name =~ /unordered/o) ? 
852             $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
853             $class->db_Main->selectall_arrayref($select, {},
854                 '%'.lc($SQLstring).'%',         # phrase order match
855                 lc($first_word),            # first word match
856                 '^\\s*'.lc($REstring).'\\s*/?\s*$', # full exact match
857                 @types, @forms
858             );
859     
860     $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
861
862     $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
863     return undef;
864 }
865
866 for my $class ( qw/title author subject keyword series identifier/ ) {
867     __PACKAGE__->register_method(
868         api_name    => "open-ils.storage.metabib.$class.search_fts.metarecord",
869         method      => 'search_class_fts',
870         api_level   => 1,
871         stream      => 1,
872         cdbi        => "metabib::${class}_field_entry",
873         cachable    => 1,
874     );
875     __PACKAGE__->register_method(
876         api_name    => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
877         method      => 'search_class_fts',
878         api_level   => 1,
879         stream      => 1,
880         cdbi        => "metabib::${class}_field_entry",
881         cachable    => 1,
882     );
883     __PACKAGE__->register_method(
884         api_name    => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
885         method      => 'search_class_fts',
886         api_level   => 1,
887         stream      => 1,
888         cdbi        => "metabib::${class}_field_entry",
889         cachable    => 1,
890     );
891     __PACKAGE__->register_method(
892         api_name    => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
893         method      => 'search_class_fts',
894         api_level   => 1,
895         stream      => 1,
896         cdbi        => "metabib::${class}_field_entry",
897         cachable    => 1,
898     );
899 }
900
901 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
902 sub search_class_fts_count {
903     my $self = shift;
904     my $client = shift;
905     my %args = @_;
906     
907     my $term = $args{term};
908     my $ou = $args{org_unit};
909     my $ou_type = $args{depth};
910     my $limit = $args{limit} || 100;
911     my $offset = $args{offset} || 0;
912
913     my $descendants = defined($ou_type) ?
914                 "actor.org_unit_descendants($ou, $ou_type)" :
915                 "actor.org_unit_descendants($ou)";
916         
917     my (@types,@forms);
918     my ($t_filter, $f_filter) = ('','');
919
920     if ($args{format}) {
921         my ($t, $f) = split '-', $args{format};
922         @types = split '', $t;
923         @forms = split '', $f;
924         if (@types) {
925             $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
926         }
927
928         if (@forms) {
929             $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
930         }
931     }
932
933
934     (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
935
936     my $class = $self->{cdbi};
937     my $search_table = $class->table;
938
939     my $metabib_record_descriptor = metabib::record_descriptor->table;
940     my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
941     my $asset_call_number_table = asset::call_number->table;
942     my $asset_copy_table = asset::copy->table;
943     my $cs_table = config::copy_status->table;
944     my $cl_table = asset::copy_location->table;
945
946     my ($index_col) = $class->columns('FTS');
947     $index_col ||= 'value';
948
949     my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
950
951     my $fts_where = $fts->sql_where_clause;
952
953     my $has_vols = 'AND cn.owning_lib = d.id';
954     my $has_copies = 'AND cp.call_number = cn.id';
955     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';
956     if ($self->api_name =~ /staff/o) {
957         $copies_visible = '';
958         $has_vols = '' if ($ou_type == 0);
959         $has_copies = '' if ($ou_type == 0);
960     }
961
962     # XXX test an "EXISTS version of descendant checking...
963     my $select;
964     if ($copies_visible) {
965         $select = <<"        SQL";
966         SELECT  count(distinct  m.metarecord)
967           FROM  $search_table f,
968             $metabib_metarecord_source_map_table m,
969             $metabib_metarecord_source_map_table mr,
970             $asset_call_number_table cn,
971             $asset_copy_table cp,
972             $cs_table cs,
973             $cl_table cl,
974             $metabib_record_descriptor rd,
975             $descendants d
976           WHERE $fts_where
977             AND mr.source = f.source
978             AND mr.metarecord = m.metarecord
979             AND cn.record = m.source
980             AND rd.record = m.source
981             AND cp.status = cs.id
982             AND cp.location = cl.id
983             $has_vols
984             $has_copies
985             $copies_visible
986             $t_filter
987             $f_filter
988         SQL
989     } else {
990         $select = <<"        SQL";
991         SELECT  count(distinct  m.metarecord)
992           FROM  $search_table f,
993             $metabib_metarecord_source_map_table m,
994             $metabib_metarecord_source_map_table mr,
995             $metabib_record_descriptor rd
996           WHERE $fts_where
997             AND mr.source = f.source
998             AND mr.metarecord = m.metarecord
999             AND rd.record = m.source
1000             $t_filter
1001             $f_filter
1002         SQL
1003     }
1004
1005     $log->debug("Field Search Count SQL :: [$select]",DEBUG);
1006
1007     my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1008     
1009     $log->debug("Count Search yielded $recs results.",DEBUG);
1010
1011     return $recs;
1012
1013 }
1014 for my $class ( qw/title author subject keyword series identifier/ ) {
1015     __PACKAGE__->register_method(
1016         api_name    => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
1017         method      => 'search_class_fts_count',
1018         api_level   => 1,
1019         stream      => 1,
1020         cdbi        => "metabib::${class}_field_entry",
1021         cachable    => 1,
1022     );
1023     __PACKAGE__->register_method(
1024         api_name    => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
1025         method      => 'search_class_fts_count',
1026         api_level   => 1,
1027         stream      => 1,
1028         cdbi        => "metabib::${class}_field_entry",
1029         cachable    => 1,
1030     );
1031 }
1032
1033
1034 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1035 sub postfilter_search_class_fts {
1036     my $self = shift;
1037     my $client = shift;
1038     my %args = @_;
1039     
1040     my $term = $args{term};
1041     my $sort = $args{'sort'};
1042     my $sort_dir = $args{sort_dir} || 'DESC';
1043     my $ou = $args{org_unit};
1044     my $ou_type = $args{depth};
1045     my $limit = $args{limit} || 10;
1046     my $visibility_limit = $args{visibility_limit} || 5000;
1047     my $offset = $args{offset} || 0;
1048
1049     my $outer_limit = 1000;
1050
1051     my $limit_clause = '';
1052     my $offset_clause = '';
1053
1054     $limit_clause = "LIMIT $outer_limit";
1055     $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1056
1057     my (@types,@forms,@lang,@aud,@lit_form);
1058     my ($t_filter, $f_filter) = ('','');
1059     my ($a_filter, $l_filter, $lf_filter) = ('','','');
1060     my ($ot_filter, $of_filter) = ('','');
1061     my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1062
1063     if (my $a = $args{audience}) {
1064         $a = [$a] if (!ref($a));
1065         @aud = @$a;
1066             
1067         $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1068         $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1069     }
1070
1071     if (my $l = $args{language}) {
1072         $l = [$l] if (!ref($l));
1073         @lang = @$l;
1074
1075         $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1076         $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1077     }
1078
1079     if (my $f = $args{lit_form}) {
1080         $f = [$f] if (!ref($f));
1081         @lit_form = @$f;
1082
1083         $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1084         $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1085     }
1086
1087     if ($args{format}) {
1088         my ($t, $f) = split '-', $args{format};
1089         @types = split '', $t;
1090         @forms = split '', $f;
1091         if (@types) {
1092             $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1093             $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1094         }
1095
1096         if (@forms) {
1097             $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1098             $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1099         }
1100     }
1101
1102
1103     my $descendants = defined($ou_type) ?
1104                 "actor.org_unit_descendants($ou, $ou_type)" :
1105                 "actor.org_unit_descendants($ou)";
1106
1107     my $class = $self->{cdbi};
1108     my $search_table = $class->table;
1109
1110     my $metabib_full_rec = metabib::full_rec->table;
1111     my $metabib_record_descriptor = metabib::record_descriptor->table;
1112     my $metabib_metarecord = metabib::metarecord->table;
1113     my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1114     my $asset_call_number_table = asset::call_number->table;
1115     my $asset_copy_table = asset::copy->table;
1116     my $cs_table = config::copy_status->table;
1117     my $cl_table = asset::copy_location->table;
1118     my $br_table = biblio::record_entry->table;
1119
1120     my ($index_col) = $class->columns('FTS');
1121     $index_col ||= 'value';
1122
1123     (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1124
1125     my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1126
1127     my $SQLstring = join('%',map { lc($_) } $fts->words);
1128     my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1129     my $first_word = lc(($fts->words)[0]).'%';
1130
1131     my $fts_where = $fts->sql_where_clause;
1132     my @fts_ranks = $fts->fts_rank;
1133
1134     my %bonus = ();
1135     $bonus{'metabib::identifier_field_entry'} =
1136         $bonus{'metabib::keyword_field_entry'} = [
1137             { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring }
1138         ];
1139
1140     $bonus{'metabib::title_field_entry'} =
1141         $bonus{'metabib::series_field_entry'} = [
1142             { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1143             { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1144             @{ $bonus{'metabib::keyword_field_entry'} }
1145         ];
1146
1147     my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1148     $bonus_list ||= '1';
1149
1150     my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1151
1152     my $relevance = join(' + ', @fts_ranks);
1153     $relevance = <<"    RANK";
1154             (SUM( ( $relevance )  * ( $bonus_list ) )/COUNT(m.source))
1155     RANK
1156
1157     my $string_default_sort = 'zzzz';
1158     $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1159
1160     my $number_default_sort = '9999';
1161     $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1162
1163     my $rank = $relevance;
1164     if (lc($sort) eq 'pubdate') {
1165         $rank = <<"        RANK";
1166             ( FIRST ((
1167                 SELECT  COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1168                   FROM  $metabib_full_rec frp
1169                   WHERE frp.record = mr.master_record
1170                     AND frp.tag = '260'
1171                     AND frp.subfield = 'c'
1172                   LIMIT 1
1173             )) )
1174         RANK
1175     } elsif (lc($sort) eq 'create_date') {
1176         $rank = <<"        RANK";
1177             ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1178         RANK
1179     } elsif (lc($sort) eq 'edit_date') {
1180         $rank = <<"        RANK";
1181             ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1182         RANK
1183     } elsif (lc($sort) eq 'title') {
1184         $rank = <<"        RANK";
1185             ( FIRST ((
1186                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1187                   FROM  $metabib_full_rec frt
1188                   WHERE frt.record = mr.master_record
1189                     AND frt.tag = '245'
1190                     AND frt.subfield = 'a'
1191                   LIMIT 1
1192             )) )
1193         RANK
1194     } elsif (lc($sort) eq 'author') {
1195         $rank = <<"        RANK";
1196             ( FIRST((
1197                 SELECT  COALESCE(LTRIM(fra.value),'$string_default_sort')
1198                   FROM  $metabib_full_rec fra
1199                   WHERE fra.record = mr.master_record
1200                     AND fra.tag LIKE '1%'
1201                     AND fra.subfield = 'a'
1202                   ORDER BY fra.tag::text::int
1203                   LIMIT 1
1204             )) )
1205         RANK
1206     } else {
1207         $sort = undef;
1208     }
1209
1210     my $select = <<"    SQL";
1211         SELECT  m.metarecord,
1212             $relevance,
1213             CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1214             $rank
1215         FROM    $search_table f,
1216             $metabib_metarecord_source_map_table m,
1217             $metabib_metarecord_source_map_table smrs,
1218             $metabib_metarecord mr,
1219             $metabib_record_descriptor rd
1220         WHERE   $fts_where
1221             AND smrs.metarecord = mr.id
1222             AND m.source = f.source
1223             AND m.metarecord = mr.id
1224             AND rd.record = smrs.source
1225             $t_filter
1226             $f_filter
1227             $a_filter
1228             $l_filter
1229             $lf_filter
1230         GROUP BY m.metarecord
1231         ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1232         LIMIT $visibility_limit
1233     SQL
1234
1235     if (0) {
1236         $select = <<"        SQL";
1237
1238             SELECT  DISTINCT s.*
1239               FROM  $asset_call_number_table cn,
1240                 $metabib_metarecord_source_map_table mrs,
1241                 $asset_copy_table cp,
1242                 $cs_table cs,
1243                 $cl_table cl,
1244                 $br_table br,
1245                 $descendants d,
1246                 $metabib_record_descriptor ord,
1247                 ($select) s
1248               WHERE mrs.metarecord = s.metarecord
1249                 AND br.id = mrs.source
1250                 AND cn.record = mrs.source
1251                 AND cp.status = cs.id
1252                 AND cp.location = cl.id
1253                 AND cn.owning_lib = d.id
1254                 AND cp.call_number = cn.id
1255                 AND cp.opac_visible IS TRUE
1256                 AND cs.opac_visible IS TRUE
1257                 AND cl.opac_visible IS TRUE
1258                 AND d.opac_visible IS TRUE
1259                 AND br.active IS TRUE
1260                 AND br.deleted IS FALSE
1261                 AND ord.record = mrs.source
1262                 $ot_filter
1263                 $of_filter
1264                 $oa_filter
1265                 $ol_filter
1266                 $olf_filter
1267               ORDER BY 4 $sort_dir
1268         SQL
1269     } elsif ($self->api_name !~ /staff/o) {
1270         $select = <<"        SQL";
1271
1272             SELECT  DISTINCT s.*
1273               FROM  ($select) s
1274               WHERE EXISTS (
1275                 SELECT  1
1276                   FROM  $asset_call_number_table cn,
1277                     $metabib_metarecord_source_map_table mrs,
1278                     $asset_copy_table cp,
1279                     $cs_table cs,
1280                     $cl_table cl,
1281                     $br_table br,
1282                     $descendants d,
1283                     $metabib_record_descriptor ord
1284                 
1285                   WHERE mrs.metarecord = s.metarecord
1286                     AND br.id = mrs.source
1287                     AND cn.record = mrs.source
1288                     AND cp.status = cs.id
1289                     AND cp.location = cl.id
1290                     AND cp.circ_lib = d.id
1291                     AND cp.call_number = cn.id
1292                     AND cp.opac_visible IS TRUE
1293                     AND cs.opac_visible IS TRUE
1294                     AND cl.opac_visible IS TRUE
1295                     AND d.opac_visible IS TRUE
1296                     AND br.active IS TRUE
1297                     AND br.deleted IS FALSE
1298                     AND ord.record = mrs.source
1299                     $ot_filter
1300                     $of_filter
1301                     $oa_filter
1302                     $ol_filter
1303                     $olf_filter
1304                   LIMIT 1
1305                 )
1306               ORDER BY 4 $sort_dir
1307         SQL
1308     } else {
1309         $select = <<"        SQL";
1310
1311             SELECT  DISTINCT s.*
1312               FROM  ($select) s
1313               WHERE EXISTS (
1314                 SELECT  1
1315                   FROM  $asset_call_number_table cn,
1316                     $asset_copy_table cp,
1317                     $metabib_metarecord_source_map_table mrs,
1318                     $br_table br,
1319                     $descendants d,
1320                     $metabib_record_descriptor ord
1321                 
1322                   WHERE mrs.metarecord = s.metarecord
1323                     AND br.id = mrs.source
1324                     AND cn.record = mrs.source
1325                     AND cn.id = cp.call_number
1326                     AND br.deleted IS FALSE
1327                     AND cn.deleted IS FALSE
1328                     AND ord.record = mrs.source
1329                     AND (   cn.owning_lib = d.id
1330                         OR (    cp.circ_lib = d.id
1331                             AND cp.deleted IS FALSE
1332                         )
1333                     )
1334                     $ot_filter
1335                     $of_filter
1336                     $oa_filter
1337                     $ol_filter
1338                     $olf_filter
1339                   LIMIT 1
1340                 )
1341                 OR NOT EXISTS (
1342                 SELECT  1
1343                   FROM  $asset_call_number_table cn,
1344                     $metabib_metarecord_source_map_table mrs,
1345                     $metabib_record_descriptor ord
1346                   WHERE mrs.metarecord = s.metarecord
1347                     AND cn.record = mrs.source
1348                     AND ord.record = mrs.source
1349                     $ot_filter
1350                     $of_filter
1351                     $oa_filter
1352                     $ol_filter
1353                     $olf_filter
1354                   LIMIT 1
1355                 )
1356               ORDER BY 4 $sort_dir
1357         SQL
1358     }
1359
1360
1361     $log->debug("Field Search SQL :: [$select]",DEBUG);
1362
1363     my $recs = $class->db_Main->selectall_arrayref(
1364             $select, {},
1365             (@bonus_values > 0 ? @bonus_values : () ),
1366             ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1367             @types, @forms, @aud, @lang, @lit_form,
1368             @types, @forms, @aud, @lang, @lit_form,
1369             ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1370     
1371     $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1372
1373     my $max = 0;
1374     $max = 1 if (!@$recs);
1375     for (@$recs) {
1376         $max = $$_[1] if ($$_[1] > $max);
1377     }
1378
1379     my $count = scalar(@$recs);
1380     for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1381         my ($mrid,$rank,$skip) = @$rec;
1382         $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1383     }
1384     return undef;
1385 }
1386
1387 for my $class ( qw/title author subject keyword series identifier/ ) {
1388     __PACKAGE__->register_method(
1389         api_name    => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1390         method      => 'postfilter_search_class_fts',
1391         api_level   => 1,
1392         stream      => 1,
1393         cdbi        => "metabib::${class}_field_entry",
1394         cachable    => 1,
1395     );
1396     __PACKAGE__->register_method(
1397         api_name    => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1398         method      => 'postfilter_search_class_fts',
1399         api_level   => 1,
1400         stream      => 1,
1401         cdbi        => "metabib::${class}_field_entry",
1402         cachable    => 1,
1403     );
1404 }
1405
1406
1407
1408 my $_cdbi = {   title   => "metabib::title_field_entry",
1409         author  => "metabib::author_field_entry",
1410         subject => "metabib::subject_field_entry",
1411         keyword => "metabib::keyword_field_entry",
1412         series  => "metabib::series_field_entry",
1413         identifier  => "metabib::identifier_field_entry",
1414 };
1415
1416 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1417 sub postfilter_search_multi_class_fts {
1418     my $self   = shift;
1419     my $client = shift;
1420     my %args   = @_;
1421     
1422     my $sort             = $args{'sort'};
1423     my $sort_dir         = $args{sort_dir} || 'DESC';
1424     my $ou               = $args{org_unit};
1425     my $ou_type          = $args{depth};
1426     my $limit            = $args{limit}  || 10;
1427     my $offset           = $args{offset} ||  0;
1428     my $visibility_limit = $args{visibility_limit} || 5000;
1429
1430     if (!$ou) {
1431         $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1432     }
1433
1434     if (!defined($args{org_unit})) {
1435         die "No target organizational unit passed to ".$self->api_name;
1436     }
1437
1438     if (! scalar( keys %{$args{searches}} )) {
1439         die "No search arguments were passed to ".$self->api_name;
1440     }
1441
1442     my $outer_limit = 1000;
1443
1444     my $limit_clause  = '';
1445     my $offset_clause = '';
1446
1447     $limit_clause  = "LIMIT $outer_limit";
1448     $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1449
1450     my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1451     my ($t_filter,   $f_filter,   $v_filter) = ('','','');
1452     my ($a_filter,   $l_filter,  $lf_filter) = ('','','');
1453     my ($ot_filter, $of_filter,  $ov_filter) = ('','','');
1454     my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1455
1456     if ($args{available}) {
1457         $avail_filter = ' AND cp.status IN (0,7,12)';
1458     }
1459
1460     if (my $a = $args{audience}) {
1461         $a = [$a] if (!ref($a));
1462         @aud = @$a;
1463             
1464         $a_filter  = ' AND  rd.audience IN ('.join(',',map{'?'}@aud).')';
1465         $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1466     }
1467
1468     if (my $l = $args{language}) {
1469         $l = [$l] if (!ref($l));
1470         @lang = @$l;
1471
1472         $l_filter  = ' AND  rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1473         $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1474     }
1475
1476     if (my $f = $args{lit_form}) {
1477         $f = [$f] if (!ref($f));
1478         @lit_form = @$f;
1479
1480         $lf_filter  = ' AND  rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1481         $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1482     }
1483
1484     if (my $f = $args{item_form}) {
1485         $f = [$f] if (!ref($f));
1486         @forms = @$f;
1487
1488         $f_filter  = ' AND  rd.item_form IN ('.join(',',map{'?'}@forms).')';
1489         $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1490     }
1491
1492     if (my $t = $args{item_type}) {
1493         $t = [$t] if (!ref($t));
1494         @types = @$t;
1495
1496         $t_filter  = ' AND  rd.item_type IN ('.join(',',map{'?'}@types).')';
1497         $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1498     }
1499
1500     if (my $v = $args{vr_format}) {
1501         $v = [$v] if (!ref($v));
1502         @vformats = @$v;
1503
1504         $v_filter  = ' AND  rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1505         $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1506     }
1507
1508
1509     # XXX legacy format and item type support
1510     if ($args{format}) {
1511         my ($t, $f) = split '-', $args{format};
1512         @types = split '', $t;
1513         @forms = split '', $f;
1514         if (@types) {
1515             $t_filter  = ' AND  rd.item_type IN ('.join(',',map{'?'}@types).')';
1516             $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1517         }
1518
1519         if (@forms) {
1520             $f_filter  .= ' AND  rd.item_form IN ('.join(',',map{'?'}@forms).')';
1521             $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1522         }
1523     }
1524
1525
1526
1527     my $descendants = defined($ou_type) ?
1528                 "actor.org_unit_descendants($ou, $ou_type)" :
1529                 "actor.org_unit_descendants($ou)";
1530
1531     my $search_table_list = '';
1532     my $fts_list          = '';
1533     my $join_table_list   = '';
1534     my @rank_list;
1535
1536     my $field_table = config::metabib_field->table;
1537
1538     my @bonus_lists;
1539     my @bonus_values;
1540     my $prev_search_group;
1541     my $curr_search_group;
1542     my $search_class;
1543     my $search_field;
1544     my $metabib_field;
1545     for my $search_group (sort keys %{$args{searches}}) {
1546         (my $search_group_name = $search_group) =~ s/\|/_/gso;
1547         ($search_class,$search_field) = split /\|/, $search_group;
1548         $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1549
1550         if ($search_field) {
1551             unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1552                 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1553                 return undef;
1554             }
1555         }
1556
1557         $prev_search_group = $curr_search_group if ($curr_search_group);
1558
1559         $curr_search_group = $search_group_name;
1560
1561         my $class = $_cdbi->{$search_class};
1562         my $search_table = $class->table;
1563
1564         my ($index_col) = $class->columns('FTS');
1565         $index_col ||= 'value';
1566
1567         
1568         my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1569
1570         my $fts_where = $fts->sql_where_clause;
1571         my @fts_ranks = $fts->fts_rank;
1572
1573         my $SQLstring = join('%',map { lc($_) } $fts->words);
1574         my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1575         my $first_word = lc(($fts->words)[0]).'%';
1576
1577         $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1578         my $rank = join(' + ', @fts_ranks);
1579
1580         my %bonus = ();
1581         $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1582         $bonus{'author'}  = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1583
1584         $bonus{'series'} = [
1585             { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1586             { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1587         ];
1588
1589         $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1590
1591         my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1592         $bonus_list ||= '1';
1593
1594         push @bonus_lists, $bonus_list;
1595         push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1596
1597
1598         #---------------------
1599
1600         $search_table_list .= "$search_table $search_group_name, ";
1601         push @rank_list,$rank;
1602         $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1603
1604         if ($metabib_field) {
1605             $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1606             $metabib_field = undef;
1607         }
1608
1609         if ($prev_search_group) {
1610             $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1611         }
1612     }
1613
1614     my $metabib_record_descriptor = metabib::record_descriptor->table;
1615     my $metabib_full_rec = metabib::full_rec->table;
1616     my $metabib_metarecord = metabib::metarecord->table;
1617     my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1618     my $asset_call_number_table = asset::call_number->table;
1619     my $asset_copy_table = asset::copy->table;
1620     my $cs_table = config::copy_status->table;
1621     my $cl_table = asset::copy_location->table;
1622     my $br_table = biblio::record_entry->table;
1623     my $source_table = config::bib_source->table;
1624
1625     my $bonuses = join (' * ', @bonus_lists);
1626     my $relevance = join (' + ', @rank_list);
1627     $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1628
1629     my $string_default_sort = 'zzzz';
1630     $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1631
1632     my $number_default_sort = '9999';
1633     $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1634
1635
1636
1637     my $secondary_sort = <<"    SORT";
1638         ( FIRST ((
1639             SELECT  COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1640               FROM  $metabib_full_rec sfrt,
1641                 $metabib_metarecord mr
1642               WHERE sfrt.record = mr.master_record
1643                 AND sfrt.tag = '245'
1644                 AND sfrt.subfield = 'a'
1645               LIMIT 1
1646         )) )
1647     SORT
1648
1649     my $rank = $relevance;
1650     if (lc($sort) eq 'pubdate') {
1651         $rank = <<"        RANK";
1652             ( FIRST ((
1653                 SELECT  COALESCE(SUBSTRING(frp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1654                   FROM  $metabib_full_rec frp
1655                   WHERE frp.record = mr.master_record
1656                     AND frp.tag = '260'
1657                     AND frp.subfield = 'c'
1658                   LIMIT 1
1659             )) )
1660         RANK
1661     } elsif (lc($sort) eq 'create_date') {
1662         $rank = <<"        RANK";
1663             ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1664         RANK
1665     } elsif (lc($sort) eq 'edit_date') {
1666         $rank = <<"        RANK";
1667             ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1668         RANK
1669     } elsif (lc($sort) eq 'title') {
1670         $rank = <<"        RANK";
1671             ( FIRST ((
1672                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1673                   FROM  $metabib_full_rec frt
1674                   WHERE frt.record = mr.master_record
1675                     AND frt.tag = '245'
1676                     AND frt.subfield = 'a'
1677                   LIMIT 1
1678             )) )
1679         RANK
1680         $secondary_sort = <<"        SORT";
1681             ( FIRST ((
1682                 SELECT  COALESCE(SUBSTRING(sfrp.value FROM E'\\\\d+'),'$number_default_sort')::INT
1683                   FROM  $metabib_full_rec sfrp
1684                   WHERE sfrp.record = mr.master_record
1685                     AND sfrp.tag = '260'
1686                     AND sfrp.subfield = 'c'
1687                   LIMIT 1
1688             )) )
1689         SORT
1690     } elsif (lc($sort) eq 'author') {
1691         $rank = <<"        RANK";
1692             ( FIRST((
1693                 SELECT  COALESCE(LTRIM(fra.value),'$string_default_sort')
1694                   FROM  $metabib_full_rec fra
1695                   WHERE fra.record = mr.master_record
1696                     AND fra.tag LIKE '1%'
1697                     AND fra.subfield = 'a'
1698                   ORDER BY fra.tag::text::int
1699                   LIMIT 1
1700             )) )
1701         RANK
1702     } else {
1703         push @bonus_values, @bonus_values;
1704         $sort = undef;
1705     }
1706
1707
1708     my $select = <<"    SQL";
1709         SELECT  m.metarecord,
1710             $relevance,
1711             CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1712             $rank,
1713             $secondary_sort
1714         FROM    $search_table_list
1715             $metabib_metarecord mr,
1716             $metabib_metarecord_source_map_table m,
1717             $metabib_metarecord_source_map_table smrs
1718         WHERE   m.metarecord = smrs.metarecord 
1719             AND mr.id = m.metarecord
1720             $fts_list
1721             $join_table_list
1722         GROUP BY m.metarecord
1723         -- ORDER BY 4 $sort_dir
1724         LIMIT $visibility_limit
1725     SQL
1726
1727     if ($self->api_name !~ /staff/o) {
1728         $select = <<"        SQL";
1729
1730             SELECT  s.*
1731               FROM  ($select) s
1732               WHERE EXISTS (
1733                 SELECT  1
1734                   FROM  $asset_call_number_table cn,
1735                     $metabib_metarecord_source_map_table mrs,
1736                     $asset_copy_table cp,
1737                     $cs_table cs,
1738                     $cl_table cl,
1739                     $br_table br,
1740                     $descendants d,
1741                     $metabib_record_descriptor ord
1742                   WHERE mrs.metarecord = s.metarecord
1743                     AND br.id = mrs.source
1744                     AND cn.record = mrs.source
1745                     AND cp.status = cs.id
1746                     AND cp.location = cl.id
1747                     AND cp.circ_lib = d.id
1748                     AND cp.call_number = cn.id
1749                     AND cp.opac_visible IS TRUE
1750                     AND cs.opac_visible IS TRUE
1751                     AND cl.opac_visible IS TRUE
1752                     AND d.opac_visible IS TRUE
1753                     AND br.active IS TRUE
1754                     AND br.deleted IS FALSE
1755                     AND cp.deleted IS FALSE
1756                     AND cn.deleted IS FALSE
1757                     AND ord.record = mrs.source
1758                     $ot_filter
1759                     $of_filter
1760                     $ov_filter
1761                     $oa_filter
1762                     $ol_filter
1763                     $olf_filter
1764                     $avail_filter
1765                   LIMIT 1
1766                 )
1767                 OR EXISTS (
1768                 SELECT  1
1769                   FROM  $br_table br,
1770                     $metabib_metarecord_source_map_table mrs,
1771                     $metabib_record_descriptor ord,
1772                     $source_table src
1773                   WHERE mrs.metarecord = s.metarecord
1774                     AND ord.record = mrs.source
1775                     AND br.id = mrs.source
1776                     AND br.source = src.id
1777                     AND src.transcendant IS TRUE
1778                     $ot_filter
1779                     $of_filter
1780                     $ov_filter
1781                     $oa_filter
1782                     $ol_filter
1783                     $olf_filter
1784                 )
1785               ORDER BY 4 $sort_dir, 5
1786         SQL
1787     } else {
1788         $select = <<"        SQL";
1789
1790             SELECT  DISTINCT s.*
1791               FROM  ($select) s,
1792                 $metabib_metarecord_source_map_table omrs,
1793                 $metabib_record_descriptor ord
1794               WHERE omrs.metarecord = s.metarecord
1795                 AND ord.record = omrs.source
1796                 AND (   EXISTS (
1797                         SELECT  1
1798                           FROM  $asset_call_number_table cn,
1799                             $asset_copy_table cp,
1800                             $descendants d,
1801                             $br_table br
1802                           WHERE br.id = omrs.source
1803                             AND cn.record = omrs.source
1804                             AND br.deleted IS FALSE
1805                             AND cn.deleted IS FALSE
1806                             AND cp.call_number = cn.id
1807                             AND (   cn.owning_lib = d.id
1808                                 OR (    cp.circ_lib = d.id
1809                                     AND cp.deleted IS FALSE
1810                                 )
1811                             )
1812                             $avail_filter
1813                           LIMIT 1
1814                     )
1815                     OR NOT EXISTS (
1816                         SELECT  1
1817                           FROM  $asset_call_number_table cn
1818                           WHERE cn.record = omrs.source
1819                             AND cn.deleted IS FALSE
1820                           LIMIT 1
1821                     )
1822                     OR EXISTS (
1823                     SELECT  1
1824                       FROM  $br_table br,
1825                         $metabib_metarecord_source_map_table mrs,
1826                         $metabib_record_descriptor ord,
1827                         $source_table src
1828                       WHERE mrs.metarecord = s.metarecord
1829                         AND br.id = mrs.source
1830                         AND br.source = src.id
1831                         AND src.transcendant IS TRUE
1832                         $ot_filter
1833                         $of_filter
1834                         $ov_filter
1835                         $oa_filter
1836                         $ol_filter
1837                         $olf_filter
1838                     )
1839                 )
1840                 $ot_filter
1841                 $of_filter
1842                 $ov_filter
1843                 $oa_filter
1844                 $ol_filter
1845                 $olf_filter
1846
1847               ORDER BY 4 $sort_dir, 5
1848         SQL
1849     }
1850
1851
1852     $log->debug("Field Search SQL :: [$select]",DEBUG);
1853
1854     my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1855             $select, {},
1856             @bonus_values,
1857             @types, @forms, @vformats, @aud, @lang, @lit_form,
1858             @types, @forms, @vformats, @aud, @lang, @lit_form,
1859             # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1860     );
1861     
1862     $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1863
1864     my $max = 0;
1865     $max = 1 if (!@$recs);
1866     for (@$recs) {
1867         $max = $$_[1] if ($$_[1] > $max);
1868     }
1869
1870     my $count = scalar(@$recs);
1871     for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1872         next unless ($$rec[0]);
1873         my ($mrid,$rank,$skip) = @$rec;
1874         $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1875     }
1876     return undef;
1877 }
1878
1879 __PACKAGE__->register_method(
1880     api_name    => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1881     method      => 'postfilter_search_multi_class_fts',
1882     api_level   => 1,
1883     stream      => 1,
1884     cachable    => 1,
1885 );
1886 __PACKAGE__->register_method(
1887     api_name    => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1888     method      => 'postfilter_search_multi_class_fts',
1889     api_level   => 1,
1890     stream      => 1,
1891     cachable    => 1,
1892 );
1893
1894 __PACKAGE__->register_method(
1895     api_name    => "open-ils.storage.metabib.multiclass.search_fts",
1896     method      => 'postfilter_search_multi_class_fts',
1897     api_level   => 1,
1898     stream      => 1,
1899     cachable    => 1,
1900 );
1901 __PACKAGE__->register_method(
1902     api_name    => "open-ils.storage.metabib.multiclass.search_fts.staff",
1903     method      => 'postfilter_search_multi_class_fts',
1904     api_level   => 1,
1905     stream      => 1,
1906     cachable    => 1,
1907 );
1908
1909 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1910 sub biblio_search_multi_class_fts {
1911     my $self = shift;
1912     my $client = shift;
1913     my %args = @_;
1914     
1915     my $sort             = $args{'sort'};
1916     my $sort_dir         = $args{sort_dir} || 'DESC';
1917     my $ou               = $args{org_unit};
1918     my $ou_type          = $args{depth};
1919     my $limit            = $args{limit}  || 10;
1920     my $offset           = $args{offset} ||  0;
1921     my $pref_lang        = $args{preferred_language} || 'eng';
1922     my $visibility_limit = $args{visibility_limit}  || 5000;
1923
1924     if (!$ou) {
1925         $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1926     }
1927
1928     if (! scalar( keys %{$args{searches}} )) {
1929         die "No search arguments were passed to ".$self->api_name;
1930     }
1931
1932     my $outer_limit = 1000;
1933
1934     my $limit_clause  = '';
1935     my $offset_clause = '';
1936
1937     $limit_clause  = "LIMIT $outer_limit";
1938     $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1939
1940     my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1941     my ($t_filter,   $f_filter,   $v_filter) = ('','','');
1942     my ($a_filter,   $l_filter,  $lf_filter) = ('','','');
1943     my ($ot_filter, $of_filter,  $ov_filter) = ('','','');
1944     my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1945
1946     if ($args{available}) {
1947         $avail_filter = ' AND cp.status IN (0,7,12)';
1948     }
1949
1950     if (my $a = $args{audience}) {
1951         $a = [$a] if (!ref($a));
1952         @aud = @$a;
1953             
1954         $a_filter  = ' AND rd.audience  IN ('.join(',',map{'?'}@aud).')';
1955         $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1956     }
1957
1958     if (my $l = $args{language}) {
1959         $l = [$l] if (!ref($l));
1960         @lang = @$l;
1961
1962         $l_filter  = ' AND rd.item_lang  IN ('.join(',',map{'?'}@lang).')';
1963         $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1964     }
1965
1966     if (my $f = $args{lit_form}) {
1967         $f = [$f] if (!ref($f));
1968         @lit_form = @$f;
1969
1970         $lf_filter  = ' AND rd.lit_form  IN ('.join(',',map{'?'}@lit_form).')';
1971         $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1972     }
1973
1974     if (my $f = $args{item_form}) {
1975         $f = [$f] if (!ref($f));
1976         @forms = @$f;
1977
1978         $f_filter  = ' AND rd.item_form  IN ('.join(',',map{'?'}@forms).')';
1979         $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1980     }
1981
1982     if (my $t = $args{item_type}) {
1983         $t = [$t] if (!ref($t));
1984         @types = @$t;
1985
1986         $t_filter  = ' AND rd.item_type  IN ('.join(',',map{'?'}@types).')';
1987         $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1988     }
1989
1990     if (my $v = $args{vr_format}) {
1991         $v = [$v] if (!ref($v));
1992         @vformats = @$v;
1993
1994         $v_filter  = ' AND rd.vr_format  IN ('.join(',',map{'?'}@vformats).')';
1995         $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1996     }
1997
1998     # XXX legacy format and item type support
1999     if ($args{format}) {
2000         my ($t, $f) = split '-', $args{format};
2001         @types = split '', $t;
2002         @forms = split '', $f;
2003         if (@types) {
2004             $t_filter  = ' AND rd.item_type  IN ('.join(',',map{'?'}@types).')';
2005             $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2006         }
2007
2008         if (@forms) {
2009             $f_filter  .= ' AND rd.item_form  IN ('.join(',',map{'?'}@forms).')';
2010             $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2011         }
2012     }
2013
2014
2015     my $descendants = defined($ou_type) ?
2016                 "actor.org_unit_descendants($ou, $ou_type)" :
2017                 "actor.org_unit_descendants($ou)";
2018
2019     my $search_table_list = '';
2020     my $fts_list = '';
2021     my $join_table_list = '';
2022     my @rank_list;
2023
2024     my $field_table = config::metabib_field->table;
2025
2026     my @bonus_lists;
2027     my @bonus_values;
2028     my $prev_search_group;
2029     my $curr_search_group;
2030     my $search_class;
2031     my $search_field;
2032     my $metabib_field;
2033     for my $search_group (sort keys %{$args{searches}}) {
2034         (my $search_group_name = $search_group) =~ s/\|/_/gso;
2035         ($search_class,$search_field) = split /\|/, $search_group;
2036         $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2037
2038         if ($search_field) {
2039             unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2040                 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2041                 return undef;
2042             }
2043         }
2044
2045         $prev_search_group = $curr_search_group if ($curr_search_group);
2046
2047         $curr_search_group = $search_group_name;
2048
2049         my $class = $_cdbi->{$search_class};
2050         my $search_table = $class->table;
2051
2052         my ($index_col) = $class->columns('FTS');
2053         $index_col ||= 'value';
2054
2055         
2056         my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2057
2058         my $fts_where = $fts->sql_where_clause;
2059         my @fts_ranks = $fts->fts_rank;
2060
2061         my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2062         my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2063         my $first_word = lc(($fts->words)[0]).'%';
2064
2065         $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2066         my $rank = join('  + ', @fts_ranks);
2067
2068         my %bonus = ();
2069         $bonus{'subject'} = [];
2070         $bonus{'author'}  = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2071
2072         $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2073
2074         $bonus{'series'} = [
2075             { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2076             { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2077         ];
2078
2079         $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2080
2081         if ($pref_lang) {
2082             push @{ $bonus{'title'}   }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2083             push @{ $bonus{'author'}  }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2084             push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2085             push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2086             push @{ $bonus{'series'}  }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2087         }
2088
2089         my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2090         $bonus_list ||= '1';
2091
2092         push @bonus_lists, $bonus_list;
2093         push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2094
2095         #---------------------
2096
2097         $search_table_list .= "$search_table $search_group_name, ";
2098         push @rank_list,$rank;
2099         $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2100
2101         if ($metabib_field) {
2102             $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2103             $metabib_field = undef;
2104         }
2105
2106         if ($prev_search_group) {
2107             $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2108         }
2109     }
2110
2111     my $metabib_record_descriptor = metabib::record_descriptor->table;
2112     my $metabib_full_rec = metabib::full_rec->table;
2113     my $metabib_metarecord = metabib::metarecord->table;
2114     my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2115     my $asset_call_number_table = asset::call_number->table;
2116     my $asset_copy_table = asset::copy->table;
2117     my $cs_table = config::copy_status->table;
2118     my $cl_table = asset::copy_location->table;
2119     my $br_table = biblio::record_entry->table;
2120     my $source_table = config::bib_source->table;
2121
2122
2123     my $bonuses = join (' * ', @bonus_lists);
2124     my $relevance = join (' + ', @rank_list);
2125     $relevance = "AVG( ($relevance) * ($bonuses) )";
2126
2127     my $string_default_sort = 'zzzz';
2128     $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2129
2130     my $number_default_sort = '9999';
2131     $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2132
2133     my $rank = $relevance;
2134     if (lc($sort) eq 'pubdate') {
2135         $rank = <<"        RANK";
2136             ( FIRST ((
2137                 SELECT  COALESCE(SUBSTRING(frp.value FROM E'\\\\d{4}'),'$number_default_sort')::INT
2138                   FROM  $metabib_full_rec frp
2139                   WHERE frp.record = b.id
2140                     AND frp.tag = '260'
2141                     AND frp.subfield = 'c'
2142                   LIMIT 1
2143             )) )
2144         RANK
2145     } elsif (lc($sort) eq 'create_date') {
2146         $rank = <<"        RANK";
2147             ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2148         RANK
2149     } elsif (lc($sort) eq 'edit_date') {
2150         $rank = <<"        RANK";
2151             ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2152         RANK
2153     } elsif (lc($sort) eq 'title') {
2154         $rank = <<"        RANK";
2155             ( FIRST ((
2156                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM E'\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2157                   FROM  $metabib_full_rec frt
2158                   WHERE frt.record = b.id
2159                     AND frt.tag = '245'
2160                     AND frt.subfield = 'a'
2161                   LIMIT 1
2162             )) )
2163         RANK
2164     } elsif (lc($sort) eq 'author') {
2165         $rank = <<"        RANK";
2166             ( FIRST((
2167                 SELECT  COALESCE(LTRIM(fra.value),'$string_default_sort')
2168                   FROM  $metabib_full_rec fra
2169                   WHERE fra.record = b.id
2170                     AND fra.tag LIKE '1%'
2171                     AND fra.subfield = 'a'
2172                   ORDER BY fra.tag::text::int
2173                   LIMIT 1
2174             )) )
2175         RANK
2176     } else {
2177         push @bonus_values, @bonus_values;
2178         $sort = undef;
2179     }
2180
2181
2182     my $select = <<"    SQL";
2183         SELECT  b.id,
2184             $relevance AS rel,
2185             $rank AS rank,
2186             b.source
2187         FROM    $search_table_list
2188             $metabib_record_descriptor rd,
2189             $source_table src,
2190             $br_table b
2191         WHERE   rd.record = b.id
2192             AND b.active IS TRUE
2193             AND b.deleted IS FALSE
2194             $fts_list
2195             $join_table_list
2196             $t_filter
2197             $f_filter
2198             $v_filter
2199             $a_filter
2200             $l_filter
2201             $lf_filter
2202         GROUP BY b.id, b.source
2203         ORDER BY 3 $sort_dir
2204         LIMIT $visibility_limit
2205     SQL
2206
2207     if ($self->api_name !~ /staff/o) {
2208         $select = <<"        SQL";
2209
2210             SELECT  s.*
2211               FROM  ($select) s
2212                 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2213               WHERE EXISTS (
2214                 SELECT  1
2215                   FROM  $asset_call_number_table cn,
2216                     $asset_copy_table cp,
2217                     $cs_table cs,
2218                     $cl_table cl,
2219                     $descendants d
2220                   WHERE cn.record = s.id
2221                     AND cp.status = cs.id
2222                     AND cp.location = cl.id
2223                     AND cp.call_number = cn.id
2224                     AND cp.opac_visible IS TRUE
2225                     AND cs.opac_visible IS TRUE
2226                     AND cl.opac_visible IS TRUE
2227                     AND d.opac_visible IS TRUE
2228                     AND cp.deleted IS FALSE
2229                     AND cn.deleted IS FALSE
2230                     AND cp.circ_lib = d.id
2231                     $avail_filter
2232                   LIMIT 1
2233                 )
2234                 OR src.transcendant IS TRUE
2235               ORDER BY 3 $sort_dir
2236         SQL
2237     } else {
2238         $select = <<"        SQL";
2239
2240             SELECT  s.*
2241               FROM  ($select) s
2242                 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2243               WHERE EXISTS (
2244                 SELECT  1
2245                   FROM  $asset_call_number_table cn,
2246                     $asset_copy_table cp,
2247                     $descendants d
2248                   WHERE cn.record = s.id
2249                     AND cp.call_number = cn.id
2250                     AND cn.deleted IS FALSE
2251                     AND cp.circ_lib = d.id
2252                     AND cp.deleted IS FALSE
2253                     $avail_filter
2254                   LIMIT 1
2255                 )
2256                 OR NOT EXISTS (
2257                 SELECT  1
2258                   FROM  $asset_call_number_table cn
2259                   WHERE cn.record = s.id
2260                   LIMIT 1
2261                 )
2262                 OR src.transcendant IS TRUE
2263               ORDER BY 3 $sort_dir
2264         SQL
2265     }
2266
2267
2268     $log->debug("Field Search SQL :: [$select]",DEBUG);
2269
2270     my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2271             $select, {},
2272             @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2273     );
2274     
2275     $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2276
2277     my $count = scalar(@$recs);
2278     for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2279         next unless ($$rec[0]);
2280         my ($mrid,$rank) = @$rec;
2281         $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2282     }
2283     return undef;
2284 }
2285
2286 __PACKAGE__->register_method(
2287     api_name    => "open-ils.storage.biblio.multiclass.search_fts.record",
2288     method      => 'biblio_search_multi_class_fts',
2289     api_level   => 1,
2290     stream      => 1,
2291     cachable    => 1,
2292 );
2293 __PACKAGE__->register_method(
2294     api_name    => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2295     method      => 'biblio_search_multi_class_fts',
2296     api_level   => 1,
2297     stream      => 1,
2298     cachable    => 1,
2299 );
2300 __PACKAGE__->register_method(
2301     api_name    => "open-ils.storage.biblio.multiclass.search_fts",
2302     method      => 'biblio_search_multi_class_fts',
2303     api_level   => 1,
2304     stream      => 1,
2305     cachable    => 1,
2306 );
2307 __PACKAGE__->register_method(
2308     api_name    => "open-ils.storage.biblio.multiclass.search_fts.staff",
2309     method      => 'biblio_search_multi_class_fts',
2310     api_level   => 1,
2311     stream      => 1,
2312     cachable    => 1,
2313 );
2314
2315
2316 my %locale_map;
2317 my $default_preferred_language;
2318 my $default_preferred_language_weight;
2319
2320 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2321 sub staged_fts {
2322     my $self   = shift;
2323     my $client = shift;
2324     my %args   = @_;
2325
2326     if (!$locale_map{COMPLETE}) {
2327
2328         my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2329         for my $locale ( @locales ) {
2330             $locale_map{lc($locale->code)} = $locale->marc_code;
2331         }
2332         $locale_map{COMPLETE} = 1;
2333
2334     }
2335
2336     my $config = OpenSRF::Utils::SettingsClient->new();
2337
2338     if (!$default_preferred_language) {
2339
2340         $default_preferred_language = $config->config_value(
2341                 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2342         ) || $config->config_value(
2343                 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2344         );
2345
2346     }
2347
2348     if (!$default_preferred_language_weight) {
2349
2350         $default_preferred_language_weight = $config->config_value(
2351                 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2352         ) || $config->config_value(
2353                 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2354         );
2355     }
2356
2357     # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2358     my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2359
2360     my $ou     = $args{org_unit};
2361     my $limit  = $args{limit}  || 10;
2362     my $offset = $args{offset} ||  0;
2363
2364     if (!$ou) {
2365         $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2366     }
2367
2368     if (! scalar( keys %{$args{searches}} )) {
2369         die "No search arguments were passed to ".$self->api_name;
2370     }
2371
2372     my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2373
2374     if (!defined($args{preferred_language})) {
2375         my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2376         $args{preferred_language} =
2377             $locale_map{ lc($ses_locale) } || 'eng';
2378     }
2379
2380     if (!defined($args{preferred_language_weight})) {
2381         $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2382     }
2383
2384     if ($args{available}) {
2385         @statuses = (0,7,12);
2386     }
2387
2388     if (my $s = $args{locations}) {
2389         $s = [$s] if (!ref($s));
2390         @locations = @$s;
2391     }
2392
2393     if (my $b = $args{between}) {
2394         if (ref($b) && @$b == 2) {
2395             @between = @$b;
2396         }
2397     }
2398
2399     if (my $s = $args{statuses}) {
2400         $s = [$s] if (!ref($s));
2401         @statuses = @$s;
2402     }
2403
2404     if (my $a = $args{audience}) {
2405         $a = [$a] if (!ref($a));
2406         @aud = @$a;
2407     }
2408
2409     if (my $l = $args{language}) {
2410         $l = [$l] if (!ref($l));
2411         @lang = @$l;
2412     }
2413
2414     if (my $f = $args{lit_form}) {
2415         $f = [$f] if (!ref($f));
2416         @lit_form = @$f;
2417     }
2418
2419     if (my $f = $args{item_form}) {
2420         $f = [$f] if (!ref($f));
2421         @forms = @$f;
2422     }
2423
2424     if (my $t = $args{item_type}) {
2425         $t = [$t] if (!ref($t));
2426         @types = @$t;
2427     }
2428
2429     if (my $b = $args{bib_level}) {
2430         $b = [$b] if (!ref($b));
2431         @bib_level = @$b;
2432     }
2433
2434     if (my $v = $args{vr_format}) {
2435         $v = [$v] if (!ref($v));
2436         @vformats = @$v;
2437     }
2438
2439     # XXX legacy format and item type support
2440     if ($args{format}) {
2441         my ($t, $f) = split '-', $args{format};
2442         @types = split '', $t;
2443         @forms = split '', $f;
2444     }
2445
2446     my %stored_proc_search_args;
2447     for my $search_group (sort keys %{$args{searches}}) {
2448         (my $search_group_name = $search_group) =~ s/\|/_/gso;
2449         my ($search_class,$search_field) = split /\|/, $search_group;
2450         $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2451
2452         if ($search_field) {
2453             unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2454                 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2455                 return undef;
2456             }
2457         }
2458
2459         my $class = $_cdbi->{$search_class};
2460         my $search_table = $class->table;
2461
2462         my ($index_col) = $class->columns('FTS');
2463         $index_col ||= 'value';
2464
2465         
2466         my $fts = OpenILS::Application::Storage::FTS->compile(
2467             $search_class => $args{searches}{$search_group}{term},
2468             $search_group_name.'.value',
2469             "$search_group_name.$index_col"
2470         );
2471         $fts->sql_where_clause; # this builds the ranks for us
2472
2473         my @fts_ranks   = $fts->fts_rank;
2474         my @fts_queries = $fts->fts_query;
2475         my @phrases = map { lc($_) } $fts->phrases;
2476         my @words   = map { lc($_) } $fts->words;
2477
2478         $stored_proc_search_args{$search_group} = {
2479             fts_rank    => \@fts_ranks,
2480             fts_query   => \@fts_queries,
2481             phrase      => \@phrases,
2482             word        => \@words,
2483         };
2484
2485     }
2486
2487     my $param_search_ou = $ou;
2488     my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2489     my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2490     my $param_statuses  = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2491     my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2492     my $param_audience  = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud      ) . '}$$';
2493     my $param_language  = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang     ) . '}$$';
2494     my $param_lit_form  = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2495     my $param_types     = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types    ) . '}$$';
2496     my $param_forms     = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms    ) . '}$$';
2497     my $param_vformats  = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2498     my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2499     my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2500     my $param_after  = $args{after} ; $param_after  = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2501     my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2502     my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2503     my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2504     my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2505     my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2506     my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2507     my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2508     my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2509     my $param_rel_limit = $args{core_limit};  $param_rel_limit ||= 'NULL';
2510     my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2511     my $param_skip_chk  = $args{skip_check};  $param_skip_chk  ||= 'NULL';
2512
2513     my $sth = metabib::metarecord_source_map->db_Main->prepare(<<"    SQL");
2514         SELECT  *
2515           FROM  search.staged_fts(
2516                     $param_search_ou\:\:INT,
2517                     $param_depth\:\:INT,
2518                     $param_searches\:\:TEXT,
2519                     $param_statuses\:\:INT[],
2520                     $param_locations\:\:INT[],
2521                     $param_audience\:\:TEXT[],
2522                     $param_language\:\:TEXT[],
2523                     $param_lit_form\:\:TEXT[],
2524                     $param_types\:\:TEXT[],
2525                     $param_forms\:\:TEXT[],
2526                     $param_vformats\:\:TEXT[],
2527                     $param_bib_level\:\:TEXT[],
2528                     $param_before\:\:TEXT,
2529                     $param_after\:\:TEXT,
2530                     $param_during\:\:TEXT,
2531                     $param_between\:\:TEXT[],
2532                     $param_pref_lang\:\:TEXT,
2533                     $param_pref_lang_multiplier\:\:REAL,
2534                     $param_sort\:\:TEXT,
2535                     $param_sort_desc\:\:BOOL,
2536                     $metarecord\:\:BOOL,
2537                     $staff\:\:BOOL,
2538                     $param_rel_limit\:\:INT,
2539                     $param_chk_limit\:\:INT,
2540                     $param_skip_chk\:\:INT
2541                 );
2542     SQL
2543
2544     $sth->execute;
2545
2546     my $recs = $sth->fetchall_arrayref({});
2547     my $summary_row = pop @$recs;
2548
2549     my $total    = $$summary_row{total};
2550     my $checked  = $$summary_row{checked};
2551     my $visible  = $$summary_row{visible};
2552     my $deleted  = $$summary_row{deleted};
2553     my $excluded = $$summary_row{excluded};
2554
2555     my $estimate = $visible;
2556     if ( $total > $checked && $checked ) {
2557
2558         $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2559         $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2560
2561     }
2562
2563     delete $$summary_row{id};
2564     delete $$summary_row{rel};
2565     delete $$summary_row{record};
2566
2567     $client->respond( $summary_row );
2568
2569     $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2570
2571     for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2572         delete $$rec{checked};
2573         delete $$rec{visible};
2574         delete $$rec{excluded};
2575         delete $$rec{deleted};
2576         delete $$rec{total};
2577         $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2578
2579         $client->respond( $rec );
2580     }
2581     return undef;
2582 }
2583 __PACKAGE__->register_method(
2584     api_name    => "open-ils.storage.biblio.multiclass.staged.search_fts",
2585     method      => 'staged_fts',
2586     api_level   => 0,
2587     stream      => 1,
2588     cachable    => 1,
2589 );
2590 __PACKAGE__->register_method(
2591     api_name    => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2592     method      => 'staged_fts',
2593     api_level   => 0,
2594     stream      => 1,
2595     cachable    => 1,
2596 );
2597 __PACKAGE__->register_method(
2598     api_name    => "open-ils.storage.metabib.multiclass.staged.search_fts",
2599     method      => 'staged_fts',
2600     api_level   => 0,
2601     stream      => 1,
2602     cachable    => 1,
2603 );
2604 __PACKAGE__->register_method(
2605     api_name    => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2606     method      => 'staged_fts',
2607     api_level   => 0,
2608     stream      => 1,
2609     cachable    => 1,
2610 );
2611
2612 sub FTS_paging_estimate {
2613     my $self   = shift;
2614     my $client = shift;
2615
2616     my $checked  = shift;
2617     my $visible  = shift;
2618     my $excluded = shift;
2619     my $deleted  = shift;
2620     my $total    = shift;
2621
2622     my $deleted_ratio = $deleted / $checked;
2623     my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2624
2625     my $exclusion_ratio = $excluded / $checked;
2626     my $delete_adjusted_exclusion_ratio = $checked - $deleted ? $excluded / ($checked - $deleted) : 1;
2627
2628     my $inclusion_ratio = $visible / $checked;
2629     my $delete_adjusted_inclusion_ratio = $checked - $deleted ? $visible / ($checked - $deleted) : 0;
2630
2631     return {
2632         exclusion                   => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2633         inclusion                   => int($delete_adjusted_total * $inclusion_ratio),
2634         delete_adjusted_exclusion   => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2635         delete_adjusted_inclusion   => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2636     };
2637 }
2638 __PACKAGE__->register_method(
2639     api_name    => "open-ils.storage.fts_paging_estimate",
2640     method      => 'FTS_paging_estimate',
2641     argc        => 5,
2642     strict      => 1,
2643     api_level   => 1,
2644     signature   => {
2645         'return'=> q#
2646             Hash of estimation values based on four variant estimation strategies:
2647                 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2648                 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2649                 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2650                 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2651         #,
2652         desc    => q#
2653             Helper method used to determin the approximate number of
2654             hits for a search that spans multiple superpages.  For
2655             sparse superpages, the inclusion estimate will likely be the
2656             best estimate.  The exclusion strategy is the original, but
2657             inclusion is the default.
2658         #,
2659         params  => [
2660             {   name    => 'checked',
2661                 desc    => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2662                 type    => 'number'
2663             },
2664             {   name    => 'visible',
2665                 desc    => 'Number of records visible to the search location on the current superpage.',
2666                 type    => 'number'
2667             },
2668             {   name    => 'excluded',
2669                 desc    => 'Number of records excluded from the search location on the current superpage.',
2670                 type    => 'number'
2671             },
2672             {   name    => 'deleted',
2673                 desc    => 'Number of deleted records on the current superpage.',
2674                 type    => 'number'
2675             },
2676             {   name    => 'total',
2677                 desc    => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2678                 type    => 'number'
2679             }
2680         ]
2681     }
2682 );
2683
2684
2685 sub xref_count {
2686     my $self   = shift;
2687     my $client = shift;
2688     my $args   = shift;
2689
2690     my $term  = $$args{term};
2691     my $limit = $$args{max} || 1;
2692     my $min   = $$args{min} || 1;
2693     my @classes = @{$$args{class}};
2694
2695     $limit = $min if ($min > $limit);
2696
2697     if (!@classes) {
2698         @classes = ( qw/ title author subject series keyword / );
2699     }
2700
2701     my %matches;
2702     my $bre_table = biblio::record_entry->table;
2703     my $cn_table  = asset::call_number->table;
2704     my $cp_table  = asset::copy->table;
2705
2706     for my $search_class ( @classes ) {
2707
2708         my $class = $_cdbi->{$search_class};
2709         my $search_table = $class->table;
2710
2711         my ($index_col) = $class->columns('FTS');
2712         $index_col ||= 'value';
2713
2714         
2715         my $where = OpenILS::Application::Storage::FTS
2716             ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2717             ->sql_where_clause;
2718
2719         my $SQL = <<"        SQL";
2720             SELECT  COUNT(DISTINCT X.source)
2721               FROM  (SELECT $search_class.source
2722                   FROM  $search_table $search_class
2723                     JOIN $bre_table b ON (b.id = $search_class.source)
2724                   WHERE $where
2725                     AND NOT b.deleted
2726                     AND b.active
2727                   LIMIT $limit) X
2728               HAVING COUNT(DISTINCT X.source) >= $min;
2729         SQL
2730
2731         my $res = $class->db_Main->selectrow_arrayref( $SQL );
2732         $matches{$search_class} = $res ? $res->[0] : 0;
2733     }
2734
2735     return \%matches;
2736 }
2737 __PACKAGE__->register_method(
2738     api_name  => "open-ils.storage.search.xref",
2739     method    => 'xref_count',
2740     api_level => 1,
2741 );
2742
2743 # Takes an abstract query object and recursively turns it back into a string
2744 # for QueryParser.
2745 sub abstract_query2str {
2746     my ($self, $conn, $query) = @_;
2747
2748     return QueryParser::Canonicalize::abstract_query2str_impl($query, 0);
2749 }
2750
2751 __PACKAGE__->register_method(
2752     api_name    => "open-ils.storage.query_parser.abstract_query.canonicalize",
2753     method      => "abstract_query2str",
2754     api_level   => 1,
2755     signature   => {
2756         params  => [
2757             {desc => q/
2758 Abstract query parser object, with complete config data. For example input,
2759 see the 'abstract_query' part of the output of an API call like
2760 open-ils.search.biblio.multiclass.query, when called with the return_abstract
2761 flag set to true./,
2762                 type => "object"}
2763         ],
2764         return => { type => "string", desc => "String representation of abstract query object" }
2765     }
2766 );
2767
2768 sub str2abstract_query {
2769     my ($self, $conn, $query, $qp_opts, $with_config) = @_;
2770
2771     my %use_opts = ( # reasonable defaults? should these even be hardcoded here?
2772         superpage => 1,
2773         superpage_size => 1000,
2774         core_limit => 25000,
2775         query => $query,
2776         (ref $opts eq 'HASH' ? %$opts : ())
2777     );
2778
2779     $with_config ||= 0;
2780
2781     # grab the query parser and initialize it
2782     my $parser = $OpenILS::Application::Storage::QParser;
2783     $parser->use;
2784
2785     _initialize_parser($parser) unless $parser->initialization_complete;
2786
2787     my $query = $parser->new(%use_opts)->parse;
2788
2789     return $query->parse_tree->to_abstract_query(with_config => $with_config);
2790 }
2791
2792 __PACKAGE__->register_method(
2793     api_name    => "open-ils.storage.query_parser.abstract_query.from_string",
2794     method      => "str2abstract_query",
2795     api_level   => 1,
2796     signature   => {
2797         params  => [
2798             {desc => "Query", type => "string"},
2799             {desc => q/Arguments for initializing QueryParser (optional)/,
2800                 type => "object"},
2801             {desc => q/Flag enabling inclusion of QP config in returned object (optional, default false)/,
2802                 type => "bool"}
2803         ],
2804         return => { type => "object", desc => "abstract representation of query parser query" }
2805     }
2806 );
2807
2808 sub query_parser_fts {
2809     my $self = shift;
2810     my $client = shift;
2811     my %args = @_;
2812
2813
2814     # grab the query parser and initialize it
2815     my $parser = $OpenILS::Application::Storage::QParser;
2816     $parser->use;
2817
2818     _initialize_parser($parser) unless $parser->initialization_complete;
2819
2820     # populate the locale/language map
2821     if (!$locale_map{COMPLETE}) {
2822
2823         my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2824         for my $locale ( @locales ) {
2825             $locale_map{lc($locale->code)} = $locale->marc_code;
2826         }
2827         $locale_map{COMPLETE} = 1;
2828
2829     }
2830
2831     # I hope we have a query!
2832     if (! $args{query} ) {
2833         die "No query was passed to ".$self->api_name;
2834     }
2835
2836     my $default_CD_modifiers = OpenSRF::Utils::SettingsClient->new->config_value(
2837         apps => 'open-ils.search' => app_settings => 'default_CD_modifiers'
2838     );
2839
2840     # Protect against empty / missing default_CD_modifiers setting
2841     if ($default_CD_modifiers and !ref($default_CD_modifiers)) {
2842         $args{query} = "$default_CD_modifiers $args{query}";
2843     }
2844
2845     my $simple_plan = $args{_simple_plan};
2846     # remove bad chunks of the %args hash
2847     for my $bad ( grep { /^_/ } keys(%args)) {
2848         delete($args{$bad});
2849     }
2850
2851
2852     # parse the query and supply any query-level %arg-based defaults
2853     # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2854     my $query = $parser->new( %args )->parse;
2855
2856     my $config = OpenSRF::Utils::SettingsClient->new();
2857
2858     # set the locale-based default preferred location
2859     if (!$query->parse_tree->find_filter('preferred_language')) {
2860         $parser->default_preferred_language( $args{preferred_language} );
2861
2862         if (!$parser->default_preferred_language) {
2863             my $ses_locale = $client->session ? $client->session->session_locale : '';
2864             $parser->default_preferred_language( $locale_map{ lc($ses_locale) } );
2865         }
2866
2867         if (!$parser->default_preferred_language) { # still nothing...
2868             my $tmp_dpl = $config->config_value(
2869                 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2870             ) || $config->config_value(
2871                 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2872             );
2873
2874             $parser->default_preferred_language( $tmp_dpl )
2875         }
2876     }
2877
2878
2879     # set the global default language multiplier
2880     if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2881         my $tmp_dplw;
2882
2883         if ($tmp_dplw = $args{preferred_language_weight} || $args{preferred_language_multiplier} ) {
2884             $parser->default_preferred_language_multiplier($tmp_dplw);
2885
2886         } else {
2887             $tmp_dplw = $config->config_value(
2888                 apps => 'open-ils.search' => app_settings => 'default_preferred_language_weight'
2889             ) || $config->config_value(
2890                 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2891             );
2892
2893             $parser->default_preferred_language_multiplier( $tmp_dplw );
2894         }
2895     }
2896
2897     # gather the site, if one is specified, defaulting to the in-query version
2898     my $ou = $args{org_unit};
2899     if (my ($filter) = $query->parse_tree->find_filter('site')) {
2900             $ou = $filter->args->[0] if (@{$filter->args});
2901     }
2902     $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^(-)?\d+$/);
2903
2904     # gather lasso, as with $ou
2905     my $lasso = $args{lasso};
2906     if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
2907             $lasso = $filter->args->[0] if (@{$filter->args});
2908     }
2909     $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
2910     $lasso = -$lasso if ($lasso);
2911
2912
2913 #    # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
2914 #    # gather user lasso, as with $ou and lasso
2915 #    my $mylasso = $args{my_lasso};
2916 #    if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
2917 #            $mylasso = $filter->args->[0] if (@{$filter->args});
2918 #    }
2919 #    $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
2920
2921
2922     # if we have a lasso, go with that, otherwise ... ou
2923     $ou = $lasso if ($lasso);
2924
2925     # gather the preferred OU, if one is specified, as with $ou
2926     my $pref_ou = $args{pref_ou};
2927     $log->info("pref_ou = $pref_ou");
2928     if (my ($filter) = $query->parse_tree->find_filter('pref_ou')) {
2929             $pref_ou = $filter->args->[0] if (@{$filter->args});
2930     }
2931     $pref_ou = actor::org_unit->search( { shortname => $pref_ou } )->next->id if ($pref_ou and $pref_ou !~ /^(-)?\d+$/);
2932
2933     # get the default $ou if we have nothing
2934     $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
2935
2936
2937     # XXX when user lassos are here, check to make sure we don't have one -- it'll be passed in the depth, with an ou of 0
2938     # gather the depth, if one is specified, defaulting to the in-query version
2939     my $depth = $args{depth};
2940     if (my ($filter) = $query->parse_tree->find_filter('depth')) {
2941             $depth = $filter->args->[0] if (@{$filter->args});
2942     }
2943     $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
2944
2945
2946     # gather the limit or default to 10
2947     my $limit = $args{check_limit} || 'NULL';
2948     if (my ($filter) = $query->parse_tree->find_filter('limit')) {
2949             $limit = $filter->args->[0] if (@{$filter->args});
2950     }
2951     if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
2952             $limit = $filter->args->[0] if (@{$filter->args});
2953     }
2954
2955
2956     # gather the offset or default to 0
2957     my $offset = $args{skip_check} || $args{offset} || 0;
2958     if (my ($filter) = $query->parse_tree->find_filter('offset')) {
2959             $offset = $filter->args->[0] if (@{$filter->args});
2960     }
2961     if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
2962             $offset = $filter->args->[0] if (@{$filter->args});
2963     }
2964
2965
2966     # gather the estimation strategy or default to inclusion
2967     my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2968     if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
2969             $estimation_strategy = $filter->args->[0] if (@{$filter->args});
2970     }
2971
2972
2973     # gather the estimation strategy or default to inclusion
2974     my $core_limit = $args{core_limit};
2975     if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
2976             $core_limit = $filter->args->[0] if (@{$filter->args});
2977     }
2978
2979
2980     # gather statuses, and then forget those if we have an #available modifier
2981     my @statuses;
2982     if (my ($filter) = $query->parse_tree->find_filter('statuses')) {
2983         @statuses = @{$filter->args} if (@{$filter->args});
2984     }
2985     @statuses = (0,7,12) if ($query->parse_tree->find_modifier('available'));
2986
2987
2988     # gather locations
2989     my @location;
2990     if (my ($filter) = $query->parse_tree->find_filter('locations')) {
2991         @location = @{$filter->args} if (@{$filter->args});
2992     }
2993
2994     # gather location_groups
2995     if (my ($filter) = $query->parse_tree->find_filter('location_groups')) {
2996         my @loc_groups = @{$filter->args} if (@{$filter->args});
2997         
2998         # collect the mapped locations and add them to the locations() filter
2999         if (@loc_groups) {
3000
3001             my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3002             my $maps = $cstore->request(
3003                 'open-ils.cstore.direct.asset.copy_location_group_map.search.atomic',
3004                 {lgroup => \@loc_groups})->gather(1);
3005
3006             push(@location, $_->location) for @$maps;
3007         }
3008     }
3009
3010
3011     my $param_check = $limit || $query->superpage_size || 'NULL';
3012     my $param_offset = $offset || 'NULL';
3013     my $param_limit = $core_limit || 'NULL';
3014
3015     my $sp = $query->superpage || 1;
3016     if ($sp > 1) {
3017         $param_offset = ($sp - 1) * $sp_size;
3018     }
3019
3020     my $param_search_ou = $ou;
3021     my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
3022     my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
3023     my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
3024     my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
3025     my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
3026     my $deleted_search = ($query->parse_tree->find_modifier('deleted')) ? "'t'" : "'f'";
3027     my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
3028     my $param_pref_ou = $pref_ou || 'NULL';
3029
3030     my $sth = metabib::metarecord_source_map->db_Main->prepare(<<"    SQL");
3031         SELECT  * -- bib search: $args{query}
3032           FROM  search.query_parser_fts(
3033                     $param_search_ou\:\:INT,
3034                     $param_depth\:\:INT,
3035                     $param_core_query\:\:TEXT,
3036                     $param_statuses\:\:INT[],
3037                     $param_locations\:\:INT[],
3038                     $param_offset\:\:INT,
3039                     $param_check\:\:INT,
3040                     $param_limit\:\:INT,
3041                     $metarecord\:\:BOOL,
3042                     $staff\:\:BOOL,
3043                     $deleted_search\:\:BOOL,
3044                     $param_pref_ou\:\:INT
3045                 );
3046     SQL
3047
3048     $sth->execute;
3049
3050     my $recs = $sth->fetchall_arrayref({});
3051     my $summary_row = pop @$recs;
3052
3053     my $total    = $$summary_row{total};
3054     my $checked  = $$summary_row{checked};
3055     my $visible  = $$summary_row{visible};
3056     my $deleted  = $$summary_row{deleted};
3057     my $excluded = $$summary_row{excluded};
3058
3059     my $estimate = $visible;
3060     if ( $total > $checked && $checked ) {
3061
3062         $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
3063         $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
3064
3065     }
3066
3067     delete $$summary_row{id};
3068     delete $$summary_row{rel};
3069     delete $$summary_row{record};
3070     delete $$summary_row{badges};
3071     delete $$summary_row{popularity};
3072
3073     if (defined($simple_plan)) {
3074         $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3075     } else {
3076         $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3077     }
3078
3079     $client->respond( $summary_row );
3080
3081     $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
3082
3083     for my $rec (@$recs) {
3084         delete $$rec{checked};
3085         delete $$rec{visible};
3086         delete $$rec{excluded};
3087         delete $$rec{deleted};
3088         delete $$rec{total};
3089         $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3090
3091         $client->respond( $rec );
3092     }
3093     return undef;
3094 }
3095 __PACKAGE__->register_method(
3096     api_name    => "open-ils.storage.query_parser_search",
3097     method      => 'query_parser_fts',
3098     api_level   => 1,
3099     stream      => 1,
3100     cachable    => 1,
3101 );
3102
3103 my $top_org;
3104
3105 sub query_parser_fts_wrapper {
3106     my $self = shift;
3107     my $client = shift;
3108     my %args = @_;
3109
3110     $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3111     # grab the query parser and initialize it
3112     my $parser = $OpenILS::Application::Storage::QParser;
3113     $parser->use;
3114
3115     _initialize_parser($parser) unless $parser->initialization_complete;
3116
3117     if (! scalar( keys %{$args{searches}} )) {
3118         die "No search arguments were passed to ".$self->api_name;
3119     }
3120
3121     $top_org ||= actor::org_unit->search( { parent_ou => undef } )->next;
3122
3123     $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3124     my $base_query = '';
3125     for my $sclass ( keys %{$args{searches}} ) {
3126         $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3127         $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3128     }
3129
3130     my $query = $base_query;
3131     $log->debug("Full base query: $base_query", DEBUG);
3132
3133     $query = "$args{facets} $query" if  ($args{facets});
3134
3135     if (!$locale_map{COMPLETE}) {
3136
3137         my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3138         for my $locale ( @locales ) {
3139             $locale_map{lc($locale->code)} = $locale->marc_code;
3140         }
3141         $locale_map{COMPLETE} = 1;
3142
3143     }
3144
3145     my $base_plan = $parser->new( query => $base_query )->parse;
3146
3147     $query = "preferred_language($args{preferred_language}) $query"
3148         if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3149     $query = "preferred_language_weight($args{preferred_language_weight}) $query"
3150         if ($args{preferred_language_weight} and !$base_plan->parse_tree->find_filter('preferred_language_weight') and !$base_plan->parse_tree->find_filter('preferred_language_multiplier'));
3151
3152
3153     my $borgs = undef;
3154     if (!$base_plan->parse_tree->find_filter('badge_orgs')) {
3155         # supply a suitable badge_orgs filter unless user has
3156         # explicitly supplied one
3157         my $site = undef;
3158
3159         my @lg_id_list = @{$args{location_groups}} if (ref $args{location_groups});
3160
3161         my ($lg_filter) = $base_plan->parse_tree->find_filter('location_groups');
3162         @lg_id_list = @{$lg_filter->args} if ($lg_filter && @{$lg_filter->args});
3163
3164         if (@lg_id_list) {
3165             my @borg_list;
3166             for my $lg ( grep { /^\d+$/ } @lg_id_list ) {
3167                 my $lg_obj = asset::copy_location_group->retrieve($lg);
3168                 next unless $lg_obj;
3169     
3170                 push(@borg_list, ''.$lg_obj->owner);
3171             }
3172             $borgs = join(',', @borg_list) if @borg_list;
3173         }
3174     
3175         if (!$borgs) {
3176             my ($site_filter) = $base_plan->parse_tree->find_filter('site');
3177             if ($site_filter && @{$site_filter->args}) {
3178                 $site = $top_org if ($site_filter->args->[0] eq '-');
3179                 $site = $top_org if ($site_filter->args->[0] eq $top_org->shortname);
3180                 $site = actor::org_unit->search( { shortname => $site_filter->args->[0] })->next unless ($site);
3181             } elsif ($args{org_unit}) {
3182                 $site = $top_org if ($args{org_unit} eq '-');
3183                 $site = $top_org if ($args{org_unit} eq $top_org->shortname);
3184                 $site = actor::org_unit->search( { shortname => $args{org_unit} })->next unless ($site);
3185             } else {
3186                 $site = $top_org;
3187             }
3188
3189             if ($site) {
3190                 $borgs = OpenSRF::AppSession->create( 'open-ils.cstore' )->request(
3191                     'open-ils.cstore.json_query.atomic',
3192                     { from => [ 'actor.org_unit_ancestors', $site->id ] }
3193                 )->gather(1);
3194
3195                 if (ref $borgs && @$borgs) {
3196                     $borgs = join(',', map { $_->{'id'} } @$borgs);
3197                 } else {
3198                     $borgs = undef;
3199                 }
3200             }
3201         }
3202     }
3203
3204     $query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
3205     $query = "badge_orgs($borgs) $query" if ($borgs);
3206     $query = "site($args{org_unit}) $query" if ($args{org_unit});
3207     $query = "depth($args{depth}) $query" if (defined($args{depth}));
3208     $query = "sort($args{sort}) $query" if ($args{sort});
3209     $query = "limit($args{limit}) $query" if ($args{limit});
3210     $query = "core_limit($args{core_limit}) $query" if ($args{core_limit});
3211     $query = "skip_check($args{skip_check}) $query" if ($args{skip_check});
3212     $query = "superpage($args{superpage}) $query" if ($args{superpage});
3213     $query = "offset($args{offset}) $query" if ($args{offset});
3214     $query = "#metarecord $query" if ($self->api_name =~ /metabib/);
3215     $query = "#available $query" if ($args{available});
3216     $query = "#descending $query" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3217     $query = "#staff $query" if ($self->api_name =~ /staff/);
3218     $query = "before($args{before}) $query" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3219     $query = "after($args{after}) $query" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3220     $query = "during($args{during}) $query" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3221     $query = "between($args{between}[0],$args{between}[1]) $query"
3222         if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3223
3224
3225     my (@between,@statuses,@locations,@location_groups,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3226
3227     # XXX legacy format and item type support
3228     if ($args{format}) {
3229         my ($t, $f) = split '-', $args{format};
3230         $args{item_type} = [ split '', $t ];
3231         $args{item_form} = [ split '', $f ];
3232     }
3233
3234     for my $filter ( qw/locations location_groups statuses between audience language lit_form item_form item_type bib_level vr_format badges/ ) {
3235         if (my $s = $args{$filter}) {
3236             $s = [$s] if (!ref($s));
3237
3238             my @filter_list = @$s;
3239
3240             next if ($filter eq 'between' and scalar(@filter_list) != 2);
3241             next if (@filter_list == 0);
3242
3243             my $filter_string = join ',', @filter_list;
3244             $query = "$query $filter($filter_string)";
3245         }
3246     }
3247
3248     $log->debug("Full QueryParser query: $query", DEBUG);
3249
3250     return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan );
3251 }
3252 __PACKAGE__->register_method(
3253     api_name    => "open-ils.storage.biblio.multiclass.staged.search_fts",
3254     method      => 'query_parser_fts_wrapper',
3255     api_level   => 1,
3256     stream      => 1,
3257     cachable    => 1,
3258 );
3259 __PACKAGE__->register_method(
3260     api_name    => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3261     method      => 'query_parser_fts_wrapper',
3262     api_level   => 1,
3263     stream      => 1,
3264     cachable    => 1,
3265 );
3266 __PACKAGE__->register_method(
3267     api_name    => "open-ils.storage.metabib.multiclass.staged.search_fts",
3268     method      => 'query_parser_fts_wrapper',
3269     api_level   => 1,
3270     stream      => 1,
3271     cachable    => 1,
3272 );
3273 __PACKAGE__->register_method(
3274     api_name    => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3275     method      => 'query_parser_fts_wrapper',
3276     api_level   => 1,
3277     stream      => 1,
3278     cachable    => 1,
3279 );
3280
3281
3282 1;
3283