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