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