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