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