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