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