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