]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Storage/Publisher/metabib.pm
adding bib_level filter support
[Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Storage / Publisher / metabib.pm
1 package OpenILS::Application::Storage::Publisher::metabib;
2 use base qw/OpenILS::Application::Storage::Publisher/;
3 use vars qw/$VERSION/;
4 use OpenSRF::EX qw/:try/;
5 use OpenILS::Application::Storage::FTS;
6 use OpenILS::Utils::Fieldmapper;
7 use OpenSRF::Utils::Logger qw/:level/;
8 use OpenSRF::Utils::Cache;
9 use OpenSRF::Utils::JSON;
10 use Data::Dumper;
11 use Digest::MD5 qw/md5_hex/;
12
13
14 my $log = 'OpenSRF::Utils::Logger';
15
16 $VERSION = 1;
17
18 sub ordered_records_from_metarecord {
19         my $self = shift;
20         my $client = shift;
21         my $mr = shift;
22         my $formats = shift;
23         my $org = shift || 1;
24         my $depth = shift;
25
26         my (@types,@forms);
27
28         if ($formats) {
29                 my ($t, $f) = split '-', $formats;
30                 @types = split '', $t;
31                 @forms = split '', $f;
32         }
33
34         my $descendants =
35                 defined($depth) ?
36                         "actor.org_unit_descendants($org, $depth)" :
37                         "actor.org_unit_descendants($org)" ;
38
39
40         my $copies_visible = 'AND d.opac_visible IS TRUE AND cp.opac_visible IS TRUE AND cs.holdable IS TRUE AND cl.opac_visible IS TRUE';
41         $copies_visible = '' if ($self->api_name =~ /staff/o);
42
43         my $sm_table = metabib::metarecord_source_map->table;
44         my $rd_table = metabib::record_descriptor->table;
45         my $fr_table = metabib::full_rec->table;
46         my $cn_table = asset::call_number->table;
47         my $cl_table = asset::copy_location->table;
48         my $cp_table = asset::copy->table;
49         my $cs_table = config::copy_status->table;
50         my $src_table = config::bib_source->table;
51         my $out_table = actor::org_unit_type->table;
52         my $br_table = biblio::record_entry->table;
53
54         my $sql = <<"   SQL";
55                 SELECT  record,
56                         item_type,
57                         item_form,
58                         quality,
59                         FIRST(COALESCE(LTRIM(SUBSTR( value, COALESCE(SUBSTRING(ind2 FROM '\\\\d+'),'0')::INT + 1 )),'zzzzzzzz')) AS title
60                 FROM    (
61                         SELECT  rd.record,
62                                 rd.item_type,
63                                 rd.item_form,
64                                 br.quality,
65                                 fr.tag,
66                                 fr.subfield,
67                                 fr.value,
68                                 fr.ind2
69         SQL
70
71         if ($copies_visible) {
72                 $sql .= <<"             SQL";
73                           FROM  $sm_table sm,
74                                 $br_table br,
75                                 $fr_table fr,
76                                 $rd_table rd
77                           WHERE rd.record = sm.source
78                                 AND fr.record = sm.source
79                                 AND br.id = sm.source
80                                 AND sm.metarecord = ?
81                                 AND (EXISTS ((SELECT    1
82                                                 FROM    $cp_table cp
83                                                         JOIN $cn_table cn ON (cp.call_number = cn.id)
84                                                         JOIN $cs_table cs ON (cp.status = cs.id)
85                                                         JOIN $cl_table cl ON (cp.location = cl.id)
86                                                         JOIN $descendants d ON (cp.circ_lib = d.id)
87                                                 WHERE   cn.record = sm.source
88                                                         $copies_visible
89                                                 LIMIT 1))
90                                         OR EXISTS ((
91                                             SELECT      1
92                                               FROM      $src_table src
93                                               WHERE     src.id = br.source
94                                                         AND src.transcendant IS TRUE))
95                                 )
96                                           
97                 SQL
98         } else {
99                 $sql .= <<"             SQL";
100                           FROM  $sm_table sm
101                                 JOIN $br_table br ON (sm.source = br.id)
102                                 JOIN $fr_table fr ON (fr.record = br.id)
103                                 JOIN $rd_table rd ON (rd.record = br.id)
104                           WHERE sm.metarecord = ?
105                                 AND ((  EXISTS (
106                                                 SELECT  1
107                                                   FROM  $cp_table cp,
108                                                         $cn_table cn,
109                                                         $descendants d
110                                                   WHERE cn.record = br.id
111                                                         AND cn.deleted = FALSE
112                                                         AND cp.deleted = FALSE
113                                                         AND cp.circ_lib = d.id
114                                                         AND cn.id = cp.call_number
115                                                   LIMIT 1
116                                         ) OR NOT EXISTS (
117                                                 SELECT  1
118                                                   FROM  $cp_table cp,
119                                                         $cn_table cn
120                                                   WHERE cn.record = br.id
121                                                         AND cn.deleted = FALSE
122                                                         AND cp.deleted = FALSE
123                                                         AND cn.id = cp.call_number
124                                                   LIMIT 1
125                                         ))
126                                         OR EXISTS ((
127                                             SELECT      1
128                                               FROM      $src_table src
129                                               WHERE     src.id = br.source
130                                                         AND src.transcendant IS TRUE))
131                                 )
132                 SQL
133         }
134
135         if (@types) {
136                 $sql .= '                               AND rd.item_type IN ('.join(',',map{'?'}@types).')';
137         }
138
139         if (@forms) {
140                 $sql .= '                               AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
141         }
142
143
144
145         $sql .= <<"     SQL";
146                           OFFSET 0
147                         ) AS x
148           WHERE tag = '245'
149                 AND subfield = 'a'
150           GROUP BY record, item_type, item_form, quality
151           ORDER BY
152                 CASE
153                         WHEN item_type IS NULL -- default
154                                 THEN 0
155                         WHEN item_type = '' -- default
156                                 THEN 0
157                         WHEN item_type IN ('a','t') -- books
158                                 THEN 1
159                         WHEN item_type = 'g' -- movies
160                                 THEN 2
161                         WHEN item_type IN ('i','j') -- sound recordings
162                                 THEN 3
163                         WHEN item_type = 'm' -- software
164                                 THEN 4
165                         WHEN item_type = 'k' -- images
166                                 THEN 5
167                         WHEN item_type IN ('e','f') -- maps
168                                 THEN 6
169                         WHEN item_type IN ('o','p') -- mixed
170                                 THEN 7
171                         WHEN item_type IN ('c','d') -- music
172                                 THEN 8
173                         WHEN item_type = 'r' -- 3d
174                                 THEN 9
175                 END,
176                 title ASC,
177                 quality DESC
178         SQL
179
180         my $ids = metabib::metarecord_source_map->db_Main->selectcol_arrayref($sql, {}, "$mr", @types, @forms);
181         return $ids if ($self->api_name =~ /atomic$/o);
182
183         $client->respond( $_ ) for ( @$ids );
184         return undef;
185
186 }
187 __PACKAGE__->register_method(
188         api_name        => 'open-ils.storage.ordered.metabib.metarecord.records',
189         method          => 'ordered_records_from_metarecord',
190         api_level       => 1,
191         cachable        => 1,
192 );
193 __PACKAGE__->register_method(
194         api_name        => 'open-ils.storage.ordered.metabib.metarecord.records.staff',
195         method          => 'ordered_records_from_metarecord',
196         api_level       => 1,
197         cachable        => 1,
198 );
199
200 __PACKAGE__->register_method(
201         api_name        => 'open-ils.storage.ordered.metabib.metarecord.records.atomic',
202         method          => 'ordered_records_from_metarecord',
203         api_level       => 1,
204         cachable        => 1,
205 );
206 __PACKAGE__->register_method(
207         api_name        => 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic',
208         method          => 'ordered_records_from_metarecord',
209         api_level       => 1,
210         cachable        => 1,
211 );
212
213 sub isxn_search {
214         my $self = shift;
215         my $client = shift;
216         my $isxn = lc(shift());
217
218         $isxn =~ s/^\s*//o;
219         $isxn =~ s/\s*$//o;
220         $isxn =~ s/-//o if ($self->api_name =~ /isbn/o);
221
222         my $tag = ($self->api_name =~ /isbn/o) ? "'020' OR f.tag = '024'" : "'022'";
223
224         my $fr_table = metabib::full_rec->table;
225         my $bib_table = biblio::record_entry->table;
226
227         my $sql = <<"   SQL";
228                 SELECT  DISTINCT f.record
229                   FROM  $fr_table f
230                         JOIN $bib_table b ON (b.id = f.record)
231                   WHERE (f.tag = $tag)
232                         AND f.value LIKE ?
233                         AND b.deleted IS FALSE
234         SQL
235
236         my $list = metabib::full_rec->db_Main->selectcol_arrayref($sql, {}, "$isxn%");
237         $client->respond($_) for (@$list);
238         return undef;
239 }
240 __PACKAGE__->register_method(
241         api_name        => 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
242         method          => 'isxn_search',
243         api_level       => 1,
244         stream          => 1,
245 );
246 __PACKAGE__->register_method(
247         api_name        => 'open-ils.storage.id_list.biblio.record_entry.search.issn',
248         method          => 'isxn_search',
249         api_level       => 1,
250         stream          => 1,
251 );
252
253 sub metarecord_copy_count {
254         my $self = shift;
255         my $client = shift;
256
257         my %args = @_;
258
259         my $sm_table = metabib::metarecord_source_map->table;
260         my $rd_table = metabib::record_descriptor->table;
261         my $cn_table = asset::call_number->table;
262         my $cp_table = asset::copy->table;
263         my $br_table = biblio::record_entry->table;
264         my $src_table = config::bib_source->table;
265         my $cl_table = asset::copy_location->table;
266         my $cs_table = config::copy_status->table;
267         my $out_table = actor::org_unit_type->table;
268         my $descendants = "actor.org_unit_descendants(u.id)";
269         my $ancestors = "actor.org_unit_ancestors(?)";
270
271         my $copies_visible = 'AND a.opac_visible IS TRUE AND cp.opac_visible IS TRUE AND cs.holdable IS TRUE AND cl.opac_visible IS TRUE';
272         $copies_visible = '' if ($self->api_name =~ /staff/o);
273
274         my (@types,@forms);
275         my ($t_filter, $f_filter) = ('','');
276
277         if ($args{format}) {
278                 my ($t, $f) = split '-', $args{format};
279                 @types = split '', $t;
280                 @forms = split '', $f;
281                 if (@types) {
282                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
283                 }
284
285                 if (@forms) {
286                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
287                 }
288         }
289
290         my $sql = <<"   SQL";
291                 SELECT  t.depth,
292                         u.id AS org_unit,
293                         sum(
294                                 (SELECT count(cp.id)
295                                   FROM  $sm_table r
296                                         JOIN $cn_table cn ON (cn.record = r.source)
297                                         JOIN $rd_table rd ON (cn.record = rd.record)
298                                         JOIN $cp_table cp ON (cn.id = cp.call_number)
299                                         JOIN $cs_table cs ON (cp.status = cs.id)
300                                         JOIN $cl_table cl ON (cp.location = cl.id)
301                                         JOIN $descendants a ON (cp.circ_lib = a.id)
302                                   WHERE r.metarecord = ?
303                                         AND cn.deleted IS FALSE
304                                         AND cp.deleted IS FALSE
305                                         $copies_visible
306                                         $t_filter
307                                         $f_filter
308                                 )
309                         ) AS count,
310                         sum(
311                                 (SELECT count(cp.id)
312                                   FROM  $sm_table r
313                                         JOIN $cn_table cn ON (cn.record = r.source)
314                                         JOIN $rd_table rd ON (cn.record = rd.record)
315                                         JOIN $cp_table cp ON (cn.id = cp.call_number)
316                                         JOIN $cs_table cs ON (cp.status = cs.id)
317                                         JOIN $cl_table cl ON (cp.location = cl.id)
318                                         JOIN $descendants a ON (cp.circ_lib = a.id)
319                                   WHERE r.metarecord = ?
320                                         AND cp.status IN (0,7)
321                                         AND cn.deleted IS FALSE
322                                         AND cp.deleted IS FALSE
323                                         $copies_visible
324                                         $t_filter
325                                         $f_filter
326                                 )
327                         ) AS available,
328                         sum(
329                                 (SELECT count(cp.id)
330                                   FROM  $sm_table r
331                                         JOIN $cn_table cn ON (cn.record = r.source)
332                                         JOIN $rd_table rd ON (cn.record = rd.record)
333                                         JOIN $cp_table cp ON (cn.id = cp.call_number)
334                                         JOIN $cs_table cs ON (cp.status = cs.id)
335                                         JOIN $cl_table cl ON (cp.location = cl.id)
336                                   WHERE r.metarecord = ?
337                                         AND cn.deleted IS FALSE
338                                         AND cp.deleted IS FALSE
339                                         AND cp.opac_visible IS TRUE
340                                         AND cs.holdable IS TRUE
341                                         AND cl.opac_visible IS TRUE
342                                         $t_filter
343                                         $f_filter
344                                 )
345                         ) AS unshadow,
346                         sum(    
347                                 (SELECT sum(1)
348                                   FROM  $sm_table r
349                                         JOIN $br_table br ON (br.id = r.source)
350                                         JOIN $src_table src ON (src.id = br.source)
351                                   WHERE r.metarecord = ?
352                                         AND src.transcendant IS TRUE
353                                 )
354                         ) AS transcendant
355
356                   FROM  $ancestors u
357                         JOIN $out_table t ON (u.ou_type = t.id)
358                   GROUP BY 1,2
359         SQL
360
361         my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
362         $sth->execute(  ''.$args{metarecord},
363                         @types, 
364                         @forms,
365                         ''.$args{metarecord},
366                         @types, 
367                         @forms,
368                         ''.$args{metarecord},
369                         @types, 
370                         @forms,
371                         ''.$args{metarecord},
372                         ''.$args{org_unit}, 
373         ); 
374
375         while ( my $row = $sth->fetchrow_hashref ) {
376                 $client->respond( $row );
377         }
378         return undef;
379 }
380 __PACKAGE__->register_method(
381         api_name        => 'open-ils.storage.metabib.metarecord.copy_count',
382         method          => 'metarecord_copy_count',
383         api_level       => 1,
384         stream          => 1,
385         cachable        => 1,
386 );
387 __PACKAGE__->register_method(
388         api_name        => 'open-ils.storage.metabib.metarecord.copy_count.staff',
389         method          => 'metarecord_copy_count',
390         api_level       => 1,
391         stream          => 1,
392         cachable        => 1,
393 );
394
395 sub biblio_multi_search_full_rec {
396         my $self = shift;
397         my $client = shift;
398
399         my %args = @_;  
400         my $class_join = $args{class_join} || 'AND';
401         my $limit = $args{limit} || 100;
402         my $offset = $args{offset} || 0;
403         my $sort = $args{'sort'};
404         my $sort_dir = $args{sort_dir} || 'DESC';
405
406         my @binds;
407         my @selects;
408
409         for my $arg (@{ $args{searches} }) {
410                 my $term = $$arg{term};
411                 my $limiters = $$arg{restrict};
412
413                 my ($index_col) = metabib::full_rec->columns('FTS');
414                 $index_col ||= 'value';
415                 my $search_table = metabib::full_rec->table;
416
417                 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
418
419                 my $fts_where = $fts->sql_where_clause();
420                 my @fts_ranks = $fts->fts_rank;
421
422                 my $rank = join(' + ', @fts_ranks);
423
424                 my @wheres;
425                 for my $limit (@$limiters) {
426                         push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
427                         push @binds, $$limit{tag}, $$limit{subfield};
428                         $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
429                 }
430                 my $where = join(' OR ', @wheres);
431
432                 push @selects, "SELECT id, record, $rank as sum FROM $search_table WHERE $where";
433
434         }
435
436         my $descendants = defined($args{depth}) ?
437                                 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
438                                 "actor.org_unit_descendants($args{org_unit})" ;
439
440
441         my $metabib_record_descriptor = metabib::record_descriptor->table;
442         my $metabib_full_rec = metabib::full_rec->table;
443         my $asset_call_number_table = asset::call_number->table;
444         my $asset_copy_table = asset::copy->table;
445         my $cs_table = config::copy_status->table;
446         my $cl_table = asset::copy_location->table;
447         my $br_table = biblio::record_entry->table;
448
449         my $cj = 'HAVING COUNT(x.id) = ' . scalar(@selects) if ($class_join eq 'AND');
450         my $search_table =
451                 '(SELECT x.record, sum(x.sum) FROM (('.
452                         join(') UNION ALL (', @selects).
453                         ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
454
455         my $has_vols = 'AND cn.owning_lib = d.id';
456         my $has_copies = 'AND cp.call_number = cn.id';
457         my $copies_visible = 'AND d.opac_visible IS TRUE AND cp.opac_visible IS TRUE AND cs.holdable IS TRUE AND cl.opac_visible IS TRUE';
458
459         if ($self->api_name =~ /staff/o) {
460                 $copies_visible = '';
461                 $has_copies = '' if ($ou_type == 0);
462                 $has_vols = '' if ($ou_type == 0);
463         }
464
465         my ($t_filter, $f_filter) = ('','');
466         my ($a_filter, $l_filter, $lf_filter) = ('','','');
467
468         if (my $a = $args{audience}) {
469                 $a = [$a] if (!ref($a));
470                 my @aud = @$a;
471                         
472                 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
473                 push @binds, @aud;
474         }
475
476         if (my $l = $args{language}) {
477                 $l = [$l] if (!ref($l));
478                 my @lang = @$l;
479
480                 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
481                 push @binds, @lang;
482         }
483
484         if (my $f = $args{lit_form}) {
485                 $f = [$f] if (!ref($f));
486                 my @lit_form = @$f;
487
488                 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
489                 push @binds, @lit_form;
490         }
491
492         if (my $f = $args{item_form}) {
493                 $f = [$f] if (!ref($f));
494                 my @forms = @$f;
495
496                 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
497                 push @binds, @forms;
498         }
499
500         if (my $t = $args{item_type}) {
501                 $t = [$t] if (!ref($t));
502                 my @types = @$t;
503
504                 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
505                 push @binds, @types;
506         }
507
508
509         if ($args{format}) {
510                 my ($t, $f) = split '-', $args{format};
511                 my @types = split '', $t;
512                 my @forms = split '', $f;
513                 if (@types) {
514                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
515                 }
516
517                 if (@forms) {
518                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
519                 }
520                 push @binds, @types, @forms;
521         }
522
523         my $relevance = 'sum(f.sum)';
524         $relevance = 1 if (!$copies_visible);
525
526         my $rank = $relevance;
527         if (lc($sort) eq 'pubdate') {
528                 $rank = <<"             RANK";
529                         ( FIRST ((
530                                 SELECT  COALESCE(SUBSTRING(frp.value FROM '\\\\d+'),'9999')::INT
531                                   FROM  $metabib_full_rec frp
532                                   WHERE frp.record = f.record
533                                         AND frp.tag = '260'
534                                         AND frp.subfield = 'c'
535                                   LIMIT 1
536                         )) )
537                 RANK
538         } elsif (lc($sort) eq 'create_date') {
539                 $rank = <<"             RANK";
540                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
541                 RANK
542         } elsif (lc($sort) eq 'edit_date') {
543                 $rank = <<"             RANK";
544                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
545                 RANK
546         } elsif (lc($sort) eq 'title') {
547                 $rank = <<"             RANK";
548                         ( FIRST ((
549                                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM '\\\\d+'),'0')::INT + 1 )),'zzzzzzzz')
550                                   FROM  $metabib_full_rec frt
551                                   WHERE frt.record = f.record
552                                         AND frt.tag = '245'
553                                         AND frt.subfield = 'a'
554                                   LIMIT 1
555                         )) )
556                 RANK
557         } elsif (lc($sort) eq 'author') {
558                 $rank = <<"             RANK";
559                         ( FIRST((
560                                 SELECT  COALESCE(LTRIM(fra.value),'zzzzzzzz')
561                                   FROM  $metabib_full_rec fra
562                                   WHERE fra.record = f.record
563                                         AND fra.tag LIKE '1%'
564                                         AND fra.subfield = 'a'
565                                   ORDER BY fra.tag::text::int
566                                   LIMIT 1
567                         )) )
568                 RANK
569         } else {
570                 $sort = undef;
571         }
572
573
574         if ($copies_visible) {
575                 $select = <<"           SQL";
576                         SELECT  f.record, $relevance, count(DISTINCT cp.id), $rank
577                         FROM    $search_table f,
578                                 $asset_call_number_table cn,
579                                 $asset_copy_table cp,
580                                 $cs_table cs,
581                                 $cl_table cl,
582                                 $br_table br,
583                                 $metabib_record_descriptor rd,
584                                 $descendants d
585                         WHERE   br.id = f.record
586                                 AND cn.record = f.record
587                                 AND rd.record = f.record
588                                 AND cp.status = cs.id
589                                 AND cp.location = cl.id
590                                 AND br.deleted IS FALSE
591                                 AND cn.deleted IS FALSE
592                                 AND cp.deleted IS FALSE
593                                 $has_vols
594                                 $has_copies
595                                 $copies_visible
596                                 $t_filter
597                                 $f_filter
598                                 $a_filter
599                                 $l_filter
600                                 $lf_filter
601                         GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
602                         ORDER BY 4 $sort_dir,3 DESC
603                 SQL
604         } else {
605                 $select = <<"           SQL";
606                         SELECT  f.record, 1, 1, $rank
607                         FROM    $search_table f,
608                                 $br_table br,
609                                 $metabib_record_descriptor rd
610                         WHERE   br.id = f.record
611                                 AND rd.record = f.record
612                                 AND br.deleted IS FALSE
613                                 $t_filter
614                                 $f_filter
615                                 $a_filter
616                                 $l_filter
617                                 $lf_filter
618                         GROUP BY 1,2,3 
619                         ORDER BY 4 $sort_dir
620                 SQL
621         }
622
623
624         $log->debug("Search SQL :: [$select]",DEBUG);
625
626         my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
627         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
628
629         my $max = 0;
630         $max = 1 if (!@$recs);
631         for (@$recs) {
632                 $max = $$_[1] if ($$_[1] > $max);
633         }
634
635         my $count = @$recs;
636         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
637                 next unless ($$rec[0]);
638                 my ($rid,$rank,$junk,$skip) = @$rec;
639                 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
640         }
641         return undef;
642 }
643 __PACKAGE__->register_method(
644         api_name        => 'open-ils.storage.biblio.full_rec.multi_search',
645         method          => 'biblio_multi_search_full_rec',
646         api_level       => 1,
647         stream          => 1,
648         cachable        => 1,
649 );
650 __PACKAGE__->register_method(
651         api_name        => 'open-ils.storage.biblio.full_rec.multi_search.staff',
652         method          => 'biblio_multi_search_full_rec',
653         api_level       => 1,
654         stream          => 1,
655         cachable        => 1,
656 );
657
658 sub search_full_rec {
659         my $self = shift;
660         my $client = shift;
661
662         my %args = @_;
663         
664         my $term = $args{term};
665         my $limiters = $args{restrict};
666
667         my ($index_col) = metabib::full_rec->columns('FTS');
668         $index_col ||= 'value';
669         my $search_table = metabib::full_rec->table;
670
671         my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
672
673         my $fts_where = $fts->sql_where_clause();
674         my @fts_ranks = $fts->fts_rank;
675
676         my $rank = join(' + ', @fts_ranks);
677
678         my @binds;
679         my @wheres;
680         for my $limit (@$limiters) {
681                 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
682                 push @binds, $$limit{tag}, $$limit{subfield};
683                 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
684         }
685         my $where = join(' OR ', @wheres);
686
687         my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
688
689         $log->debug("Search SQL :: [$select]",DEBUG);
690
691         my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
692         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
693
694         $client->respond($_) for (@$recs);
695         return undef;
696 }
697 __PACKAGE__->register_method(
698         api_name        => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
699         method          => 'search_full_rec',
700         api_level       => 1,
701         stream          => 1,
702         cachable        => 1,
703 );
704 __PACKAGE__->register_method(
705         api_name        => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
706         method          => 'search_full_rec',
707         api_level       => 1,
708         stream          => 1,
709         cachable        => 1,
710 );
711
712
713 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
714 sub search_class_fts {
715         my $self = shift;
716         my $client = shift;
717         my %args = @_;
718         
719         my $term = $args{term};
720         my $ou = $args{org_unit};
721         my $ou_type = $args{depth};
722         my $limit = $args{limit};
723         my $offset = $args{offset};
724
725         my $limit_clause = '';
726         my $offset_clause = '';
727
728         $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
729         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
730
731         my (@types,@forms);
732         my ($t_filter, $f_filter) = ('','');
733
734         if ($args{format}) {
735                 my ($t, $f) = split '-', $args{format};
736                 @types = split '', $t;
737                 @forms = split '', $f;
738                 if (@types) {
739                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
740                 }
741
742                 if (@forms) {
743                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
744                 }
745         }
746
747
748
749         my $descendants = defined($ou_type) ?
750                                 "actor.org_unit_descendants($ou, $ou_type)" :
751                                 "actor.org_unit_descendants($ou)";
752
753         my $class = $self->{cdbi};
754         my $search_table = $class->table;
755
756         my $metabib_record_descriptor = metabib::record_descriptor->table;
757         my $metabib_metarecord = metabib::metarecord->table;
758         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
759         my $asset_call_number_table = asset::call_number->table;
760         my $asset_copy_table = asset::copy->table;
761         my $cs_table = config::copy_status->table;
762         my $cl_table = asset::copy_location->table;
763
764         my ($index_col) = $class->columns('FTS');
765         $index_col ||= 'value';
766
767         (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
768         my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
769
770         my $fts_where = $fts->sql_where_clause;
771         my @fts_ranks = $fts->fts_rank;
772
773         my $rank = join(' + ', @fts_ranks);
774
775         my $has_vols = 'AND cn.owning_lib = d.id';
776         my $has_copies = 'AND cp.call_number = cn.id';
777         my $copies_visible = 'AND d.opac_visible IS TRUE AND cp.opac_visible IS TRUE AND cs.holdable IS TRUE AND cl.opac_visible IS TRUE';
778
779         my $visible_count = ', count(DISTINCT cp.id)';
780         my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
781
782         if ($self->api_name =~ /staff/o) {
783                 $copies_visible = '';
784                 $visible_count_test = '';
785                 $has_copies = '' if ($ou_type == 0);
786                 $has_vols = '' if ($ou_type == 0);
787         }
788
789         my $rank_calc = <<"     RANK";
790                 , (SUM( $rank
791                         * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
792                         * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
793                         * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
794                 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
795         RANK
796
797         $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
798
799         if ($copies_visible) {
800                 $select = <<"           SQL";
801                         SELECT  m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
802                         FROM    $search_table f,
803                                 $metabib_metarecord_source_map_table m,
804                                 $asset_call_number_table cn,
805                                 $asset_copy_table cp,
806                                 $cs_table cs,
807                                 $cl_table cl,
808                                 $metabib_record_descriptor rd,
809                                 $descendants d
810                         WHERE   $fts_where
811                                 AND m.source = f.source
812                                 AND cn.record = m.source
813                                 AND rd.record = m.source
814                                 AND cp.status = cs.id
815                                 AND cp.location = cl.id
816                                 $has_vols
817                                 $has_copies
818                                 $copies_visible
819                                 $t_filter
820                                 $f_filter
821                         GROUP BY 1 $visible_count_test
822                         ORDER BY 2 DESC,3
823                         $limit_clause $offset_clause
824                 SQL
825         } else {
826                 $select = <<"           SQL";
827                         SELECT  m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
828                         FROM    $search_table f,
829                                 $metabib_metarecord_source_map_table m,
830                                 $metabib_record_descriptor rd
831                         WHERE   $fts_where
832                                 AND m.source = f.source
833                                 AND rd.record = m.source
834                                 $t_filter
835                                 $f_filter
836                         GROUP BY 1, 4
837                         ORDER BY 2 DESC,3
838                         $limit_clause $offset_clause
839                 SQL
840         }
841
842         $log->debug("Field Search SQL :: [$select]",DEBUG);
843
844         my $SQLstring = join('%',$fts->words);
845         my $REstring = join('\\s+',$fts->words);
846         my $first_word = ($fts->words)[0].'%';
847         my $recs = ($self->api_name =~ /unordered/o) ? 
848                         $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
849                         $class->db_Main->selectall_arrayref($select, {},
850                                 '%'.lc($SQLstring).'%',                 # phrase order match
851                                 lc($first_word),                        # first word match
852                                 '^\\s*'.lc($REstring).'\\s*/?\s*$',     # full exact match
853                                 @types, @forms
854                         );
855         
856         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
857
858         $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
859         return undef;
860 }
861
862 for my $class ( qw/title author subject keyword series/ ) {
863         __PACKAGE__->register_method(
864                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord",
865                 method          => 'search_class_fts',
866                 api_level       => 1,
867                 stream          => 1,
868                 cdbi            => "metabib::${class}_field_entry",
869                 cachable        => 1,
870         );
871         __PACKAGE__->register_method(
872                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
873                 method          => 'search_class_fts',
874                 api_level       => 1,
875                 stream          => 1,
876                 cdbi            => "metabib::${class}_field_entry",
877                 cachable        => 1,
878         );
879         __PACKAGE__->register_method(
880                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
881                 method          => 'search_class_fts',
882                 api_level       => 1,
883                 stream          => 1,
884                 cdbi            => "metabib::${class}_field_entry",
885                 cachable        => 1,
886         );
887         __PACKAGE__->register_method(
888                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
889                 method          => 'search_class_fts',
890                 api_level       => 1,
891                 stream          => 1,
892                 cdbi            => "metabib::${class}_field_entry",
893                 cachable        => 1,
894         );
895 }
896
897 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
898 sub search_class_fts_count {
899         my $self = shift;
900         my $client = shift;
901         my %args = @_;
902         
903         my $term = $args{term};
904         my $ou = $args{org_unit};
905         my $ou_type = $args{depth};
906         my $limit = $args{limit} || 100;
907         my $offset = $args{offset} || 0;
908
909         my $descendants = defined($ou_type) ?
910                                 "actor.org_unit_descendants($ou, $ou_type)" :
911                                 "actor.org_unit_descendants($ou)";
912                 
913         my (@types,@forms);
914         my ($t_filter, $f_filter) = ('','');
915
916         if ($args{format}) {
917                 my ($t, $f) = split '-', $args{format};
918                 @types = split '', $t;
919                 @forms = split '', $f;
920                 if (@types) {
921                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
922                 }
923
924                 if (@forms) {
925                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
926                 }
927         }
928
929
930         (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
931
932         my $class = $self->{cdbi};
933         my $search_table = $class->table;
934
935         my $metabib_record_descriptor = metabib::record_descriptor->table;
936         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
937         my $asset_call_number_table = asset::call_number->table;
938         my $asset_copy_table = asset::copy->table;
939         my $cs_table = config::copy_status->table;
940         my $cl_table = asset::copy_location->table;
941
942         my ($index_col) = $class->columns('FTS');
943         $index_col ||= 'value';
944
945         my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
946
947         my $fts_where = $fts->sql_where_clause;
948
949         my $has_vols = 'AND cn.owning_lib = d.id';
950         my $has_copies = 'AND cp.call_number = cn.id';
951         my $copies_visible = 'AND d.opac_visible IS TRUE AND cp.opac_visible IS TRUE AND cs.holdable IS TRUE AND cl.opac_visible IS TRUE';
952         if ($self->api_name =~ /staff/o) {
953                 $copies_visible = '';
954                 $has_vols = '' if ($ou_type == 0);
955                 $has_copies = '' if ($ou_type == 0);
956         }
957
958         # XXX test an "EXISTS version of descendant checking...
959         my $select;
960         if ($copies_visible) {
961                 $select = <<"           SQL";
962                 SELECT  count(distinct  m.metarecord)
963                   FROM  $search_table f,
964                         $metabib_metarecord_source_map_table m,
965                         $metabib_metarecord_source_map_table mr,
966                         $asset_call_number_table cn,
967                         $asset_copy_table cp,
968                         $cs_table cs,
969                         $cl_table cl,
970                         $metabib_record_descriptor rd,
971                         $descendants d
972                   WHERE $fts_where
973                         AND mr.source = f.source
974                         AND mr.metarecord = m.metarecord
975                         AND cn.record = m.source
976                         AND rd.record = m.source
977                         AND cp.status = cs.id
978                         AND cp.location = cl.id
979                         $has_vols
980                         $has_copies
981                         $copies_visible
982                         $t_filter
983                         $f_filter
984                 SQL
985         } else {
986                 $select = <<"           SQL";
987                 SELECT  count(distinct  m.metarecord)
988                   FROM  $search_table f,
989                         $metabib_metarecord_source_map_table m,
990                         $metabib_metarecord_source_map_table mr,
991                         $metabib_record_descriptor rd
992                   WHERE $fts_where
993                         AND mr.source = f.source
994                         AND mr.metarecord = m.metarecord
995                         AND rd.record = m.source
996                         $t_filter
997                         $f_filter
998                 SQL
999         }
1000
1001         $log->debug("Field Search Count SQL :: [$select]",DEBUG);
1002
1003         my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1004         
1005         $log->debug("Count Search yielded $recs results.",DEBUG);
1006
1007         return $recs;
1008
1009 }
1010 for my $class ( qw/title author subject keyword series/ ) {
1011         __PACKAGE__->register_method(
1012                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
1013                 method          => 'search_class_fts_count',
1014                 api_level       => 1,
1015                 stream          => 1,
1016                 cdbi            => "metabib::${class}_field_entry",
1017                 cachable        => 1,
1018         );
1019         __PACKAGE__->register_method(
1020                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
1021                 method          => 'search_class_fts_count',
1022                 api_level       => 1,
1023                 stream          => 1,
1024                 cdbi            => "metabib::${class}_field_entry",
1025                 cachable        => 1,
1026         );
1027 }
1028
1029
1030 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1031 sub postfilter_search_class_fts {
1032         my $self = shift;
1033         my $client = shift;
1034         my %args = @_;
1035         
1036         my $term = $args{term};
1037         my $sort = $args{'sort'};
1038         my $sort_dir = $args{sort_dir} || 'DESC';
1039         my $ou = $args{org_unit};
1040         my $ou_type = $args{depth};
1041         my $limit = $args{limit} || 10;
1042         my $visibility_limit = $args{visibility_limit} || 5000;
1043         my $offset = $args{offset} || 0;
1044
1045         my $outer_limit = 1000;
1046
1047         my $limit_clause = '';
1048         my $offset_clause = '';
1049
1050         $limit_clause = "LIMIT $outer_limit";
1051         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1052
1053         my (@types,@forms,@lang,@aud,@lit_form);
1054         my ($t_filter, $f_filter) = ('','');
1055         my ($a_filter, $l_filter, $lf_filter) = ('','','');
1056         my ($ot_filter, $of_filter) = ('','');
1057         my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1058
1059         if (my $a = $args{audience}) {
1060                 $a = [$a] if (!ref($a));
1061                 @aud = @$a;
1062                         
1063                 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1064                 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1065         }
1066
1067         if (my $l = $args{language}) {
1068                 $l = [$l] if (!ref($l));
1069                 @lang = @$l;
1070
1071                 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1072                 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1073         }
1074
1075         if (my $f = $args{lit_form}) {
1076                 $f = [$f] if (!ref($f));
1077                 @lit_form = @$f;
1078
1079                 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1080                 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1081         }
1082
1083         if ($args{format}) {
1084                 my ($t, $f) = split '-', $args{format};
1085                 @types = split '', $t;
1086                 @forms = split '', $f;
1087                 if (@types) {
1088                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1089                         $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1090                 }
1091
1092                 if (@forms) {
1093                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1094                         $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1095                 }
1096         }
1097
1098
1099         my $descendants = defined($ou_type) ?
1100                                 "actor.org_unit_descendants($ou, $ou_type)" :
1101                                 "actor.org_unit_descendants($ou)";
1102
1103         my $class = $self->{cdbi};
1104         my $search_table = $class->table;
1105
1106         my $metabib_full_rec = metabib::full_rec->table;
1107         my $metabib_record_descriptor = metabib::record_descriptor->table;
1108         my $metabib_metarecord = metabib::metarecord->table;
1109         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1110         my $asset_call_number_table = asset::call_number->table;
1111         my $asset_copy_table = asset::copy->table;
1112         my $cs_table = config::copy_status->table;
1113         my $cl_table = asset::copy_location->table;
1114         my $br_table = biblio::record_entry->table;
1115
1116         my ($index_col) = $class->columns('FTS');
1117         $index_col ||= 'value';
1118
1119         (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1120
1121         my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1122
1123         my $SQLstring = join('%',map { lc($_) } $fts->words);
1124         my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1125         my $first_word = lc(($fts->words)[0]).'%';
1126
1127         my $fts_where = $fts->sql_where_clause;
1128         my @fts_ranks = $fts->fts_rank;
1129
1130         my %bonus = ();
1131         $bonus{'metabib::keyword_field_entry'} = [ { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring } ];
1132         $bonus{'metabib::title_field_entry'} =
1133                 $bonus{'metabib::series_field_entry'} = [
1134                         { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1135                         { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1136                         @{ $bonus{'metabib::keyword_field_entry'} }
1137                 ];
1138
1139         my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1140         $bonus_list ||= '1';
1141
1142         my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1143
1144         my $relevance = join(' + ', @fts_ranks);
1145         $relevance = <<"        RANK";
1146                         (SUM( ( $relevance )  * ( $bonus_list ) )/COUNT(m.source))
1147         RANK
1148
1149         my $string_default_sort = 'zzzz';
1150         $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1151
1152         my $number_default_sort = '9999';
1153         $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1154
1155         my $rank = $relevance;
1156         if (lc($sort) eq 'pubdate') {
1157                 $rank = <<"             RANK";
1158                         ( FIRST ((
1159                                 SELECT  COALESCE(SUBSTRING(frp.value FROM '\\\\d+'),'$number_default_sort')::INT
1160                                   FROM  $metabib_full_rec frp
1161                                   WHERE frp.record = mr.master_record
1162                                         AND frp.tag = '260'
1163                                         AND frp.subfield = 'c'
1164                                   LIMIT 1
1165                         )) )
1166                 RANK
1167         } elsif (lc($sort) eq 'create_date') {
1168                 $rank = <<"             RANK";
1169                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1170                 RANK
1171         } elsif (lc($sort) eq 'edit_date') {
1172                 $rank = <<"             RANK";
1173                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1174                 RANK
1175         } elsif (lc($sort) eq 'title') {
1176                 $rank = <<"             RANK";
1177                         ( FIRST ((
1178                                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM '\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1179                                   FROM  $metabib_full_rec frt
1180                                   WHERE frt.record = mr.master_record
1181                                         AND frt.tag = '245'
1182                                         AND frt.subfield = 'a'
1183                                   LIMIT 1
1184                         )) )
1185                 RANK
1186         } elsif (lc($sort) eq 'author') {
1187                 $rank = <<"             RANK";
1188                         ( FIRST((
1189                                 SELECT  COALESCE(LTRIM(fra.value),'$string_default_sort')
1190                                   FROM  $metabib_full_rec fra
1191                                   WHERE fra.record = mr.master_record
1192                                         AND fra.tag LIKE '1%'
1193                                         AND fra.subfield = 'a'
1194                                   ORDER BY fra.tag::text::int
1195                                   LIMIT 1
1196                         )) )
1197                 RANK
1198         } else {
1199                 $sort = undef;
1200         }
1201
1202         my $select = <<"        SQL";
1203                 SELECT  m.metarecord,
1204                         $relevance,
1205                         CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1206                         $rank
1207                 FROM    $search_table f,
1208                         $metabib_metarecord_source_map_table m,
1209                         $metabib_metarecord_source_map_table smrs,
1210                         $metabib_metarecord mr,
1211                         $metabib_record_descriptor rd
1212                 WHERE   $fts_where
1213                         AND smrs.metarecord = mr.id
1214                         AND m.source = f.source
1215                         AND m.metarecord = mr.id
1216                         AND rd.record = smrs.source
1217                         $t_filter
1218                         $f_filter
1219                         $a_filter
1220                         $l_filter
1221                         $lf_filter
1222                 GROUP BY m.metarecord
1223                 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1224                 LIMIT $visibility_limit
1225         SQL
1226
1227         if (0) {
1228                 $select = <<"           SQL";
1229
1230                         SELECT  DISTINCT s.*
1231                           FROM  $asset_call_number_table cn,
1232                                 $metabib_metarecord_source_map_table mrs,
1233                                 $asset_copy_table cp,
1234                                 $cs_table cs,
1235                                 $cl_table cl,
1236                                 $br_table br,
1237                                 $descendants d,
1238                                 $metabib_record_descriptor ord,
1239                                 ($select) s
1240                           WHERE mrs.metarecord = s.metarecord
1241                                 AND br.id = mrs.source
1242                                 AND cn.record = mrs.source
1243                                 AND cp.status = cs.id
1244                                 AND cp.location = cl.id
1245                                 AND cn.owning_lib = d.id
1246                                 AND cp.call_number = cn.id
1247                                 AND cp.opac_visible IS TRUE
1248                                 AND cs.holdable IS TRUE
1249                                 AND cl.opac_visible IS TRUE
1250                                 AND d.opac_visible IS TRUE
1251                                 AND br.active IS TRUE
1252                                 AND br.deleted IS FALSE
1253                                 AND ord.record = mrs.source
1254                                 $ot_filter
1255                                 $of_filter
1256                                 $oa_filter
1257                                 $ol_filter
1258                                 $olf_filter
1259                           ORDER BY 4 $sort_dir
1260                 SQL
1261         } elsif ($self->api_name !~ /staff/o) {
1262                 $select = <<"           SQL";
1263
1264                         SELECT  DISTINCT s.*
1265                           FROM  ($select) s
1266                           WHERE EXISTS (
1267                                 SELECT  1
1268                                   FROM  $asset_call_number_table cn,
1269                                         $metabib_metarecord_source_map_table mrs,
1270                                         $asset_copy_table cp,
1271                                         $cs_table cs,
1272                                         $cl_table cl,
1273                                         $br_table br,
1274                                         $descendants d,
1275                                         $metabib_record_descriptor ord
1276                                 
1277                                   WHERE mrs.metarecord = s.metarecord
1278                                         AND br.id = mrs.source
1279                                         AND cn.record = mrs.source
1280                                         AND cp.status = cs.id
1281                                         AND cp.location = cl.id
1282                                         AND cp.circ_lib = d.id
1283                                         AND cp.call_number = cn.id
1284                                         AND cp.opac_visible IS TRUE
1285                                         AND cs.holdable IS TRUE
1286                                         AND cl.opac_visible IS TRUE
1287                                         AND d.opac_visible IS TRUE
1288                                         AND br.active IS TRUE
1289                                         AND br.deleted IS FALSE
1290                                         AND ord.record = mrs.source
1291                                         $ot_filter
1292                                         $of_filter
1293                                         $oa_filter
1294                                         $ol_filter
1295                                         $olf_filter
1296                                   LIMIT 1
1297                                 )
1298                           ORDER BY 4 $sort_dir
1299                 SQL
1300         } else {
1301                 $select = <<"           SQL";
1302
1303                         SELECT  DISTINCT s.*
1304                           FROM  ($select) s
1305                           WHERE EXISTS (
1306                                 SELECT  1
1307                                   FROM  $asset_call_number_table cn,
1308                                         $asset_copy_table cp,
1309                                         $metabib_metarecord_source_map_table mrs,
1310                                         $br_table br,
1311                                         $descendants d,
1312                                         $metabib_record_descriptor ord
1313                                 
1314                                   WHERE mrs.metarecord = s.metarecord
1315                                         AND br.id = mrs.source
1316                                         AND cn.record = mrs.source
1317                                         AND cn.id = cp.call_number
1318                                         AND br.deleted IS FALSE
1319                                         AND cn.deleted IS FALSE
1320                                         AND ord.record = mrs.source
1321                                         AND (   cn.owning_lib = d.id
1322                                                 OR (    cp.circ_lib = d.id
1323                                                         AND cp.deleted IS FALSE
1324                                                 )
1325                                         )
1326                                         $ot_filter
1327                                         $of_filter
1328                                         $oa_filter
1329                                         $ol_filter
1330                                         $olf_filter
1331                                   LIMIT 1
1332                                 )
1333                                 OR NOT EXISTS (
1334                                 SELECT  1
1335                                   FROM  $asset_call_number_table cn,
1336                                         $metabib_metarecord_source_map_table mrs,
1337                                         $metabib_record_descriptor ord
1338                                   WHERE mrs.metarecord = s.metarecord
1339                                         AND cn.record = mrs.source
1340                                         AND ord.record = mrs.source
1341                                         $ot_filter
1342                                         $of_filter
1343                                         $oa_filter
1344                                         $ol_filter
1345                                         $olf_filter
1346                                   LIMIT 1
1347                                 )
1348                           ORDER BY 4 $sort_dir
1349                 SQL
1350         }
1351
1352
1353         $log->debug("Field Search SQL :: [$select]",DEBUG);
1354
1355         my $recs = $class->db_Main->selectall_arrayref(
1356                         $select, {},
1357                         (@bonus_values > 0 ? @bonus_values : () ),
1358                         ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1359                         @types, @forms, @aud, @lang, @lit_form,
1360                         @types, @forms, @aud, @lang, @lit_form,
1361                         ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1362         
1363         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1364
1365         my $max = 0;
1366         $max = 1 if (!@$recs);
1367         for (@$recs) {
1368                 $max = $$_[1] if ($$_[1] > $max);
1369         }
1370
1371         my $count = scalar(@$recs);
1372         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1373                 my ($mrid,$rank,$skip) = @$rec;
1374                 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1375         }
1376         return undef;
1377 }
1378
1379 for my $class ( qw/title author subject keyword series/ ) {
1380         __PACKAGE__->register_method(
1381                 api_name        => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1382                 method          => 'postfilter_search_class_fts',
1383                 api_level       => 1,
1384                 stream          => 1,
1385                 cdbi            => "metabib::${class}_field_entry",
1386                 cachable        => 1,
1387         );
1388         __PACKAGE__->register_method(
1389                 api_name        => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1390                 method          => 'postfilter_search_class_fts',
1391                 api_level       => 1,
1392                 stream          => 1,
1393                 cdbi            => "metabib::${class}_field_entry",
1394                 cachable        => 1,
1395         );
1396 }
1397
1398
1399
1400 my $_cdbi = {   title   => "metabib::title_field_entry",
1401                 author  => "metabib::author_field_entry",
1402                 subject => "metabib::subject_field_entry",
1403                 keyword => "metabib::keyword_field_entry",
1404                 series  => "metabib::series_field_entry",
1405 };
1406
1407 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1408 sub postfilter_search_multi_class_fts {
1409         my $self = shift;
1410         my $client = shift;
1411         my %args = @_;
1412         
1413         my $sort = $args{'sort'};
1414         my $sort_dir = $args{sort_dir} || 'DESC';
1415         my $ou = $args{org_unit};
1416         my $ou_type = $args{depth};
1417         my $limit = $args{limit} || 10;;
1418         my $visibility_limit = $args{visibility_limit} || 5000;;
1419         my $offset = $args{offset} || 0;
1420
1421         if (!$ou) {
1422                 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1423         }
1424
1425         if (!defined($args{org_unit})) {
1426                 die "No target organizational unit passed to ".$self->api_name;
1427         }
1428
1429         if (! scalar( keys %{$args{searches}} )) {
1430                 die "No search arguments were passed to ".$self->api_name;
1431         }
1432
1433         my $outer_limit = 1000;
1434
1435         my $limit_clause = '';
1436         my $offset_clause = '';
1437
1438         $limit_clause = "LIMIT $outer_limit";
1439         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1440
1441         my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1442         my ($t_filter, $f_filter, $v_filter) = ('','','');
1443         my ($a_filter, $l_filter, $lf_filter) = ('','','');
1444         my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1445         my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1446
1447         if ($args{available}) {
1448                 $avail_filter = ' AND cp.status IN (0,7)';
1449         }
1450
1451         if (my $a = $args{audience}) {
1452                 $a = [$a] if (!ref($a));
1453                 @aud = @$a;
1454                         
1455                 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1456                 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1457         }
1458
1459         if (my $l = $args{language}) {
1460                 $l = [$l] if (!ref($l));
1461                 @lang = @$l;
1462
1463                 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1464                 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1465         }
1466
1467         if (my $f = $args{lit_form}) {
1468                 $f = [$f] if (!ref($f));
1469                 @lit_form = @$f;
1470
1471                 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1472                 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1473         }
1474
1475         if (my $f = $args{item_form}) {
1476                 $f = [$f] if (!ref($f));
1477                 @forms = @$f;
1478
1479                 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1480                 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1481         }
1482
1483         if (my $t = $args{item_type}) {
1484                 $t = [$t] if (!ref($t));
1485                 @types = @$t;
1486
1487                 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1488                 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1489         }
1490
1491         if (my $v = $args{vr_format}) {
1492                 $v = [$v] if (!ref($v));
1493                 @vformats = @$v;
1494
1495                 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1496                 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1497         }
1498
1499
1500         # XXX legacy format and item type support
1501         if ($args{format}) {
1502                 my ($t, $f) = split '-', $args{format};
1503                 @types = split '', $t;
1504                 @forms = split '', $f;
1505                 if (@types) {
1506                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1507                         $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1508                 }
1509
1510                 if (@forms) {
1511                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1512                         $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1513                 }
1514         }
1515
1516
1517
1518         my $descendants = defined($ou_type) ?
1519                                 "actor.org_unit_descendants($ou, $ou_type)" :
1520                                 "actor.org_unit_descendants($ou)";
1521
1522         my $search_table_list = '';
1523         my $fts_list = '';
1524         my $join_table_list = '';
1525         my @rank_list;
1526
1527         my $field_table = config::metabib_field->table;
1528
1529         my @bonus_lists;
1530         my @bonus_values;
1531         my $prev_search_group;
1532         my $curr_search_group;
1533         my $search_class;
1534         my $search_field;
1535         my $metabib_field;
1536         for my $search_group (sort keys %{$args{searches}}) {
1537                 (my $search_group_name = $search_group) =~ s/\|/_/gso;
1538                 ($search_class,$search_field) = split /\|/, $search_group;
1539                 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1540
1541                 if ($search_field) {
1542                         unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1543                                 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1544                                 return undef;
1545                         }
1546                 }
1547
1548                 $prev_search_group = $curr_search_group if ($curr_search_group);
1549
1550                 $curr_search_group = $search_group_name;
1551
1552                 my $class = $_cdbi->{$search_class};
1553                 my $search_table = $class->table;
1554
1555                 my ($index_col) = $class->columns('FTS');
1556                 $index_col ||= 'value';
1557
1558                 
1559                 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1560
1561                 my $fts_where = $fts->sql_where_clause;
1562                 my @fts_ranks = $fts->fts_rank;
1563
1564                 my $SQLstring = join('%',map { lc($_) } $fts->words);
1565                 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1566                 my $first_word = lc(($fts->words)[0]).'%';
1567
1568                 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1569                 my $rank = join(' + ', @fts_ranks);
1570
1571                 my %bonus = ();
1572                 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1573                 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1574
1575                 $bonus{'series'} = [
1576                         { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1577                         { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1578                 ];
1579
1580                 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1581
1582                 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1583                 $bonus_list ||= '1';
1584
1585                 push @bonus_lists, $bonus_list;
1586                 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1587
1588
1589                 #---------------------
1590
1591                 $search_table_list .= "$search_table $search_group_name, ";
1592                 push @rank_list,$rank;
1593                 $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1594
1595                 if ($metabib_field) {
1596                         $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1597                         $metabib_field = undef;
1598                 }
1599
1600                 if ($prev_search_group) {
1601                         $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1602                 }
1603         }
1604
1605         my $metabib_record_descriptor = metabib::record_descriptor->table;
1606         my $metabib_full_rec = metabib::full_rec->table;
1607         my $metabib_metarecord = metabib::metarecord->table;
1608         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1609         my $asset_call_number_table = asset::call_number->table;
1610         my $asset_copy_table = asset::copy->table;
1611         my $cs_table = config::copy_status->table;
1612         my $cl_table = asset::copy_location->table;
1613         my $br_table = biblio::record_entry->table;
1614         my $source_table = config::bib_source->table;
1615
1616         my $bonuses = join (' * ', @bonus_lists);
1617         my $relevance = join (' + ', @rank_list);
1618         $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1619
1620         my $string_default_sort = 'zzzz';
1621         $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1622
1623         my $number_default_sort = '9999';
1624         $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1625
1626
1627
1628         my $secondary_sort = <<"        SORT";
1629                 ( FIRST ((
1630                         SELECT  COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM '\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1631                           FROM  $metabib_full_rec sfrt,
1632                                 $metabib_metarecord mr
1633                           WHERE sfrt.record = mr.master_record
1634                                 AND sfrt.tag = '245'
1635                                 AND sfrt.subfield = 'a'
1636                           LIMIT 1
1637                 )) )
1638         SORT
1639
1640         my $rank = $relevance;
1641         if (lc($sort) eq 'pubdate') {
1642                 $rank = <<"             RANK";
1643                         ( FIRST ((
1644                                 SELECT  COALESCE(SUBSTRING(frp.value FROM '\\\\d+'),'$number_default_sort')::INT
1645                                   FROM  $metabib_full_rec frp
1646                                   WHERE frp.record = mr.master_record
1647                                         AND frp.tag = '260'
1648                                         AND frp.subfield = 'c'
1649                                   LIMIT 1
1650                         )) )
1651                 RANK
1652         } elsif (lc($sort) eq 'create_date') {
1653                 $rank = <<"             RANK";
1654                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1655                 RANK
1656         } elsif (lc($sort) eq 'edit_date') {
1657                 $rank = <<"             RANK";
1658                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1659                 RANK
1660         } elsif (lc($sort) eq 'title') {
1661                 $rank = <<"             RANK";
1662                         ( FIRST ((
1663                                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM '\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1664                                   FROM  $metabib_full_rec frt
1665                                   WHERE frt.record = mr.master_record
1666                                         AND frt.tag = '245'
1667                                         AND frt.subfield = 'a'
1668                                   LIMIT 1
1669                         )) )
1670                 RANK
1671                 $secondary_sort = <<"           SORT";
1672                         ( FIRST ((
1673                                 SELECT  COALESCE(SUBSTRING(sfrp.value FROM '\\\\d+'),'$number_default_sort')::INT
1674                                   FROM  $metabib_full_rec sfrp
1675                                   WHERE sfrp.record = mr.master_record
1676                                         AND sfrp.tag = '260'
1677                                         AND sfrp.subfield = 'c'
1678                                   LIMIT 1
1679                         )) )
1680                 SORT
1681         } elsif (lc($sort) eq 'author') {
1682                 $rank = <<"             RANK";
1683                         ( FIRST((
1684                                 SELECT  COALESCE(LTRIM(fra.value),'$string_default_sort')
1685                                   FROM  $metabib_full_rec fra
1686                                   WHERE fra.record = mr.master_record
1687                                         AND fra.tag LIKE '1%'
1688                                         AND fra.subfield = 'a'
1689                                   ORDER BY fra.tag::text::int
1690                                   LIMIT 1
1691                         )) )
1692                 RANK
1693         } else {
1694                 push @bonus_values, @bonus_values;
1695                 $sort = undef;
1696         }
1697
1698
1699         my $select = <<"        SQL";
1700                 SELECT  m.metarecord,
1701                         $relevance,
1702                         CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1703                         $rank,
1704                         $secondary_sort
1705                 FROM    $search_table_list
1706                         $metabib_metarecord mr,
1707                         $metabib_metarecord_source_map_table m,
1708                         $metabib_metarecord_source_map_table smrs
1709                 WHERE   m.metarecord = smrs.metarecord 
1710                         AND mr.id = m.metarecord
1711                         $fts_list
1712                         $join_table_list
1713                 GROUP BY m.metarecord
1714                 -- ORDER BY 4 $sort_dir
1715                 LIMIT $visibility_limit
1716         SQL
1717
1718         if ($self->api_name !~ /staff/o) {
1719                 $select = <<"           SQL";
1720
1721                         SELECT  s.*
1722                           FROM  ($select) s
1723                           WHERE EXISTS (
1724                                 SELECT  1
1725                                   FROM  $asset_call_number_table cn,
1726                                         $metabib_metarecord_source_map_table mrs,
1727                                         $asset_copy_table cp,
1728                                         $cs_table cs,
1729                                         $cl_table cl,
1730                                         $br_table br,
1731                                         $descendants d,
1732                                         $metabib_record_descriptor ord
1733                                   WHERE mrs.metarecord = s.metarecord
1734                                         AND br.id = mrs.source
1735                                         AND cn.record = mrs.source
1736                                         AND cp.status = cs.id
1737                                         AND cp.location = cl.id
1738                                         AND cp.circ_lib = d.id
1739                                         AND cp.call_number = cn.id
1740                                         AND cp.opac_visible IS TRUE
1741                                         AND cs.holdable IS TRUE
1742                                         AND cl.opac_visible IS TRUE
1743                                         AND d.opac_visible IS TRUE
1744                                         AND br.active IS TRUE
1745                                         AND br.deleted IS FALSE
1746                                         AND cp.deleted IS FALSE
1747                                         AND cn.deleted IS FALSE
1748                                         AND ord.record = mrs.source
1749                                         $ot_filter
1750                                         $of_filter
1751                                         $ov_filter
1752                                         $oa_filter
1753                                         $ol_filter
1754                                         $olf_filter
1755                                         $avail_filter
1756                                   LIMIT 1
1757                                 )
1758                                 OR EXISTS (
1759                                 SELECT  1
1760                                   FROM  $br_table br,
1761                                         $metabib_metarecord_source_map_table mrs,
1762                                         $metabib_record_descriptor ord,
1763                                         $source_table src
1764                                   WHERE mrs.metarecord = s.metarecord
1765                                         AND ord.record = mrs.source
1766                                         AND br.id = mrs.source
1767                                         AND br.source = src.id
1768                                         AND src.transcendant IS TRUE
1769                                         $ot_filter
1770                                         $of_filter
1771                                         $ov_filter
1772                                         $oa_filter
1773                                         $ol_filter
1774                                         $olf_filter
1775                                 )
1776                           ORDER BY 4 $sort_dir, 5
1777                 SQL
1778         } else {
1779                 $select = <<"           SQL";
1780
1781                         SELECT  DISTINCT s.*
1782                           FROM  ($select) s,
1783                                 $metabib_metarecord_source_map_table omrs,
1784                                 $metabib_record_descriptor ord
1785                           WHERE omrs.metarecord = s.metarecord
1786                                 AND ord.record = omrs.source
1787                                 AND (   EXISTS (
1788                                                 SELECT  1
1789                                                   FROM  $asset_call_number_table cn,
1790                                                         $asset_copy_table cp,
1791                                                         $descendants d,
1792                                                         $br_table br
1793                                                   WHERE br.id = omrs.source
1794                                                         AND cn.record = omrs.source
1795                                                         AND br.deleted IS FALSE
1796                                                         AND cn.deleted IS FALSE
1797                                                         AND cp.call_number = cn.id
1798                                                         AND (   cn.owning_lib = d.id
1799                                                                 OR (    cp.circ_lib = d.id
1800                                                                         AND cp.deleted IS FALSE
1801                                                                 )
1802                                                         )
1803                                                         $avail_filter
1804                                                   LIMIT 1
1805                                         )
1806                                         OR NOT EXISTS (
1807                                                 SELECT  1
1808                                                   FROM  $asset_call_number_table cn
1809                                                   WHERE cn.record = omrs.source
1810                                                         AND cn.deleted IS FALSE
1811                                                   LIMIT 1
1812                                         )
1813                                         OR EXISTS (
1814                                         SELECT  1
1815                                           FROM  $br_table br,
1816                                                 $metabib_metarecord_source_map_table mrs,
1817                                                 $metabib_record_descriptor ord,
1818                                                 $source_table src
1819                                           WHERE mrs.metarecord = s.metarecord
1820                                                 AND br.id = mrs.source
1821                                                 AND br.source = src.id
1822                                                 AND src.transcendant IS TRUE
1823                                                 $ot_filter
1824                                                 $of_filter
1825                                                 $ov_filter
1826                                                 $oa_filter
1827                                                 $ol_filter
1828                                                 $olf_filter
1829                                         )
1830                                 )
1831                                 $ot_filter
1832                                 $of_filter
1833                                 $ov_filter
1834                                 $oa_filter
1835                                 $ol_filter
1836                                 $olf_filter
1837
1838                           ORDER BY 4 $sort_dir, 5
1839                 SQL
1840         }
1841
1842
1843         $log->debug("Field Search SQL :: [$select]",DEBUG);
1844
1845         my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1846                         $select, {},
1847                         @bonus_values,
1848                         @types, @forms, @vformats, @aud, @lang, @lit_form,
1849                         @types, @forms, @vformats, @aud, @lang, @lit_form,
1850                         # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1851         );
1852         
1853         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1854
1855         my $max = 0;
1856         $max = 1 if (!@$recs);
1857         for (@$recs) {
1858                 $max = $$_[1] if ($$_[1] > $max);
1859         }
1860
1861         my $count = scalar(@$recs);
1862         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1863                 next unless ($$rec[0]);
1864                 my ($mrid,$rank,$skip) = @$rec;
1865                 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1866         }
1867         return undef;
1868 }
1869
1870 __PACKAGE__->register_method(
1871         api_name        => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1872         method          => 'postfilter_search_multi_class_fts',
1873         api_level       => 1,
1874         stream          => 1,
1875         cachable        => 1,
1876 );
1877 __PACKAGE__->register_method(
1878         api_name        => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1879         method          => 'postfilter_search_multi_class_fts',
1880         api_level       => 1,
1881         stream          => 1,
1882         cachable        => 1,
1883 );
1884
1885 __PACKAGE__->register_method(
1886         api_name        => "open-ils.storage.metabib.multiclass.search_fts",
1887         method          => 'postfilter_search_multi_class_fts',
1888         api_level       => 1,
1889         stream          => 1,
1890         cachable        => 1,
1891 );
1892 __PACKAGE__->register_method(
1893         api_name        => "open-ils.storage.metabib.multiclass.search_fts.staff",
1894         method          => 'postfilter_search_multi_class_fts',
1895         api_level       => 1,
1896         stream          => 1,
1897         cachable        => 1,
1898 );
1899
1900 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1901 sub biblio_search_multi_class_fts {
1902         my $self = shift;
1903         my $client = shift;
1904         my %args = @_;
1905         
1906         my $sort = $args{'sort'};
1907         my $sort_dir = $args{sort_dir} || 'DESC';
1908         my $ou = $args{org_unit};
1909         my $ou_type = $args{depth};
1910         my $limit = $args{limit} || 10;
1911         my $pref_lang = $args{prefered_language} || 'eng';
1912         my $visibility_limit = $args{visibility_limit} || 5000;
1913         my $offset = $args{offset} || 0;
1914
1915         if (!$ou) {
1916                 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1917         }
1918
1919         if (! scalar( keys %{$args{searches}} )) {
1920                 die "No search arguments were passed to ".$self->api_name;
1921         }
1922
1923         my $outer_limit = 1000;
1924
1925         my $limit_clause = '';
1926         my $offset_clause = '';
1927
1928         $limit_clause = "LIMIT $outer_limit";
1929         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1930
1931         my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1932         my ($t_filter, $f_filter, $v_filter) = ('','','');
1933         my ($a_filter, $l_filter, $lf_filter) = ('','','');
1934         my ($ot_filter, $of_filter, $ov_filter) = ('','','');
1935         my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1936
1937         if ($args{available}) {
1938                 $avail_filter = ' AND cp.status IN (0,7)';
1939         }
1940
1941         if (my $a = $args{audience}) {
1942                 $a = [$a] if (!ref($a));
1943                 @aud = @$a;
1944                         
1945                 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1946                 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1947         }
1948
1949         if (my $l = $args{language}) {
1950                 $l = [$l] if (!ref($l));
1951                 @lang = @$l;
1952
1953                 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1954                 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1955         }
1956
1957         if (my $f = $args{lit_form}) {
1958                 $f = [$f] if (!ref($f));
1959                 @lit_form = @$f;
1960
1961                 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1962                 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1963         }
1964
1965         if (my $f = $args{item_form}) {
1966                 $f = [$f] if (!ref($f));
1967                 @forms = @$f;
1968
1969                 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1970                 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1971         }
1972
1973         if (my $t = $args{item_type}) {
1974                 $t = [$t] if (!ref($t));
1975                 @types = @$t;
1976
1977                 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1978                 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1979         }
1980
1981         if (my $v = $args{vr_format}) {
1982                 $v = [$v] if (!ref($v));
1983                 @vformats = @$v;
1984
1985                 $v_filter = ' AND rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1986                 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1987         }
1988
1989         # XXX legacy format and item type support
1990         if ($args{format}) {
1991                 my ($t, $f) = split '-', $args{format};
1992                 @types = split '', $t;
1993                 @forms = split '', $f;
1994                 if (@types) {
1995                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1996                         $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1997                 }
1998
1999                 if (@forms) {
2000                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2001                         $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2002                 }
2003         }
2004
2005
2006         my $descendants = defined($ou_type) ?
2007                                 "actor.org_unit_descendants($ou, $ou_type)" :
2008                                 "actor.org_unit_descendants($ou)";
2009
2010         my $search_table_list = '';
2011         my $fts_list = '';
2012         my $join_table_list = '';
2013         my @rank_list;
2014
2015         my $field_table = config::metabib_field->table;
2016
2017         my @bonus_lists;
2018         my @bonus_values;
2019         my $prev_search_group;
2020         my $curr_search_group;
2021         my $search_class;
2022         my $search_field;
2023         my $metabib_field;
2024         for my $search_group (sort keys %{$args{searches}}) {
2025                 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2026                 ($search_class,$search_field) = split /\|/, $search_group;
2027                 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2028
2029                 if ($search_field) {
2030                         unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2031                                 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2032                                 return undef;
2033                         }
2034                 }
2035
2036                 $prev_search_group = $curr_search_group if ($curr_search_group);
2037
2038                 $curr_search_group = $search_group_name;
2039
2040                 my $class = $_cdbi->{$search_class};
2041                 my $search_table = $class->table;
2042
2043                 my ($index_col) = $class->columns('FTS');
2044                 $index_col ||= 'value';
2045
2046                 
2047                 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2048
2049                 my $fts_where = $fts->sql_where_clause;
2050                 my @fts_ranks = $fts->fts_rank;
2051
2052                 my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2053                 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2054                 my $first_word = lc(($fts->words)[0]).'%';
2055
2056                 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2057                 my $rank = join('  + ', @fts_ranks);
2058
2059                 my %bonus = ();
2060                 $bonus{'subject'} = [];
2061                 $bonus{'author'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2062
2063                 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2064
2065                 $bonus{'series'} = [
2066                         { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2067                         { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2068                 ];
2069
2070                 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2071
2072                 if ($pref_lang) {
2073                         push @{ $bonus{'title'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2074                         push @{ $bonus{'author'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2075                         push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2076                         push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2077                         push @{ $bonus{'series'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2078                 }
2079
2080                 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2081                 $bonus_list ||= '1';
2082
2083                 push @bonus_lists, $bonus_list;
2084                 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2085
2086                 #---------------------
2087
2088                 $search_table_list .= "$search_table $search_group_name, ";
2089                 push @rank_list,$rank;
2090                 $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2091
2092                 if ($metabib_field) {
2093                         $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2094                         $metabib_field = undef;
2095                 }
2096
2097                 if ($prev_search_group) {
2098                         $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2099                 }
2100         }
2101
2102         my $metabib_record_descriptor = metabib::record_descriptor->table;
2103         my $metabib_full_rec = metabib::full_rec->table;
2104         my $metabib_metarecord = metabib::metarecord->table;
2105         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2106         my $asset_call_number_table = asset::call_number->table;
2107         my $asset_copy_table = asset::copy->table;
2108         my $cs_table = config::copy_status->table;
2109         my $cl_table = asset::copy_location->table;
2110         my $br_table = biblio::record_entry->table;
2111         my $source_table = config::bib_source->table;
2112
2113
2114         my $bonuses = join (' * ', @bonus_lists);
2115         my $relevance = join (' + ', @rank_list);
2116         $relevance = "AVG( ($relevance) * ($bonuses) )";
2117
2118         my $string_default_sort = 'zzzz';
2119         $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2120
2121         my $number_default_sort = '9999';
2122         $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2123
2124         my $rank = $relevance;
2125         if (lc($sort) eq 'pubdate') {
2126                 $rank = <<"             RANK";
2127                         ( FIRST ((
2128                                 SELECT  COALESCE(SUBSTRING(frp.value FROM '\\\\d{4}'),'$number_default_sort')::INT
2129                                   FROM  $metabib_full_rec frp
2130                                   WHERE frp.record = b.id
2131                                         AND frp.tag = '260'
2132                                         AND frp.subfield = 'c'
2133                                   LIMIT 1
2134                         )) )
2135                 RANK
2136         } elsif (lc($sort) eq 'create_date') {
2137                 $rank = <<"             RANK";
2138                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2139                 RANK
2140         } elsif (lc($sort) eq 'edit_date') {
2141                 $rank = <<"             RANK";
2142                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2143                 RANK
2144         } elsif (lc($sort) eq 'title') {
2145                 $rank = <<"             RANK";
2146                         ( FIRST ((
2147                                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM '\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2148                                   FROM  $metabib_full_rec frt
2149                                   WHERE frt.record = b.id
2150                                         AND frt.tag = '245'
2151                                         AND frt.subfield = 'a'
2152                                   LIMIT 1
2153                         )) )
2154                 RANK
2155         } elsif (lc($sort) eq 'author') {
2156                 $rank = <<"             RANK";
2157                         ( FIRST((
2158                                 SELECT  COALESCE(LTRIM(fra.value),'$string_default_sort')
2159                                   FROM  $metabib_full_rec fra
2160                                   WHERE fra.record = b.id
2161                                         AND fra.tag LIKE '1%'
2162                                         AND fra.subfield = 'a'
2163                                   ORDER BY fra.tag::text::int
2164                                   LIMIT 1
2165                         )) )
2166                 RANK
2167         } else {
2168                 push @bonus_values, @bonus_values;
2169                 $sort = undef;
2170         }
2171
2172
2173         my $select = <<"        SQL";
2174                 SELECT  b.id,
2175                         $relevance AS rel,
2176                         $rank AS rank,
2177                         b.source
2178                 FROM    $search_table_list
2179                         $metabib_record_descriptor rd,
2180                         $source_table src,
2181                         $br_table b
2182                 WHERE   rd.record = b.id
2183                         AND b.active IS TRUE
2184                         AND b.deleted IS FALSE
2185                         $fts_list
2186                         $join_table_list
2187                         $t_filter
2188                         $f_filter
2189                         $v_filter
2190                         $a_filter
2191                         $l_filter
2192                         $lf_filter
2193                 GROUP BY b.id, b.source
2194                 ORDER BY 3 $sort_dir
2195                 LIMIT $visibility_limit
2196         SQL
2197
2198         if ($self->api_name !~ /staff/o) {
2199                 $select = <<"           SQL";
2200
2201                         SELECT  s.*
2202                           FROM  ($select) s
2203                                 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2204                           WHERE EXISTS (
2205                                 SELECT  1
2206                                   FROM  $asset_call_number_table cn,
2207                                         $asset_copy_table cp,
2208                                         $cs_table cs,
2209                                         $cl_table cl,
2210                                         $descendants d
2211                                   WHERE cn.record = s.id
2212                                         AND cp.status = cs.id
2213                                         AND cp.location = cl.id
2214                                         AND cp.call_number = cn.id
2215                                         AND cp.opac_visible IS TRUE
2216                                         AND cs.holdable IS TRUE
2217                                         AND cl.opac_visible IS TRUE
2218                                         AND d.opac_visible IS TRUE
2219                                         AND cp.deleted IS FALSE
2220                                         AND cn.deleted IS FALSE
2221                                         AND cp.circ_lib = d.id
2222                                         $avail_filter
2223                                   LIMIT 1
2224                                 )
2225                                 OR src.transcendant IS TRUE
2226                           ORDER BY 3 $sort_dir
2227                 SQL
2228         } else {
2229                 $select = <<"           SQL";
2230
2231                         SELECT  s.*
2232                           FROM  ($select) s
2233                                 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2234                           WHERE EXISTS (
2235                                 SELECT  1
2236                                   FROM  $asset_call_number_table cn,
2237                                         $asset_copy_table cp,
2238                                         $descendants d
2239                                   WHERE cn.record = s.id
2240                                         AND cp.call_number = cn.id
2241                                         AND cn.deleted IS FALSE
2242                                         AND cp.circ_lib = d.id
2243                                         AND cp.deleted IS FALSE
2244                                         $avail_filter
2245                                   LIMIT 1
2246                                 )
2247                                 OR NOT EXISTS (
2248                                 SELECT  1
2249                                   FROM  $asset_call_number_table cn
2250                                   WHERE cn.record = s.id
2251                                   LIMIT 1
2252                                 )
2253                                 OR src.transcendant IS TRUE
2254                           ORDER BY 3 $sort_dir
2255                 SQL
2256         }
2257
2258
2259         $log->debug("Field Search SQL :: [$select]",DEBUG);
2260
2261         my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2262                         $select, {},
2263                         @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2264         );
2265         
2266         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2267
2268         my $count = scalar(@$recs);
2269         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2270                 next unless ($$rec[0]);
2271                 my ($mrid,$rank) = @$rec;
2272                 $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2273         }
2274         return undef;
2275 }
2276
2277 __PACKAGE__->register_method(
2278         api_name        => "open-ils.storage.biblio.multiclass.search_fts.record",
2279         method          => 'biblio_search_multi_class_fts',
2280         api_level       => 1,
2281         stream          => 1,
2282         cachable        => 1,
2283 );
2284 __PACKAGE__->register_method(
2285         api_name        => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2286         method          => 'biblio_search_multi_class_fts',
2287         api_level       => 1,
2288         stream          => 1,
2289         cachable        => 1,
2290 );
2291
2292
2293
2294 __PACKAGE__->register_method(
2295         api_name        => "open-ils.storage.biblio.multiclass.search_fts",
2296         method          => 'biblio_search_multi_class_fts',
2297         api_level       => 1,
2298         stream          => 1,
2299         cachable        => 1,
2300 );
2301 __PACKAGE__->register_method(
2302         api_name        => "open-ils.storage.biblio.multiclass.search_fts.staff",
2303         method          => 'biblio_search_multi_class_fts',
2304         api_level       => 1,
2305         stream          => 1,
2306         cachable        => 1,
2307 );
2308
2309 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2310 sub staged_fts {
2311         my $self = shift;
2312         my $client = shift;
2313         my %args = @_;
2314         
2315         my $ou = $args{org_unit};
2316         my $limit = $args{limit} || 10;
2317         my $offset = $args{offset} || 0;
2318
2319         if (!$ou) {
2320                 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2321         }
2322
2323         if (! scalar( keys %{$args{searches}} )) {
2324                 die "No search arguments were passed to ".$self->api_name;
2325         }
2326
2327         my (@statuses,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2328
2329         if ($args{available}) {
2330                 @statuses = (0,7);
2331         }
2332
2333         if (my $s = $args{statuses}) {
2334                 $s = [$s] if (!ref($s));
2335                 @statuses = @$s;
2336         }
2337
2338         if (my $a = $args{audience}) {
2339                 $a = [$a] if (!ref($a));
2340                 @aud = @$a;
2341         }
2342
2343         if (my $l = $args{language}) {
2344                 $l = [$l] if (!ref($l));
2345                 @lang = @$l;
2346         }
2347
2348         if (my $f = $args{lit_form}) {
2349                 $f = [$f] if (!ref($f));
2350                 @lit_form = @$f;
2351         }
2352
2353         if (my $f = $args{item_form}) {
2354                 $f = [$f] if (!ref($f));
2355                 @forms = @$f;
2356         }
2357
2358         if (my $t = $args{item_type}) {
2359                 $t = [$t] if (!ref($t));
2360                 @types = @$t;
2361         }
2362
2363         if (my $b = $args{bib_level}) {
2364                 $b = [$b] if (!ref($b));
2365                 @bib_level = @$b;
2366         }
2367
2368         if (my $v = $args{vr_format}) {
2369                 $v = [$v] if (!ref($v));
2370                 @vformats = @$v;
2371         }
2372
2373         # XXX legacy format and item type support
2374         if ($args{format}) {
2375                 my ($t, $f) = split '-', $args{format};
2376                 @types = split '', $t;
2377                 @forms = split '', $f;
2378         }
2379
2380     my %stored_proc_search_args;
2381         for my $search_group (sort keys %{$args{searches}}) {
2382                 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2383                 my ($search_class,$search_field) = split /\|/, $search_group;
2384                 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2385
2386                 if ($search_field) {
2387                         unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2388                                 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2389                                 return undef;
2390                         }
2391                 }
2392
2393                 my $class = $_cdbi->{$search_class};
2394                 my $search_table = $class->table;
2395
2396                 my ($index_col) = $class->columns('FTS');
2397                 $index_col ||= 'value';
2398
2399                 
2400                 my $fts = OpenILS::Application::Storage::FTS->compile(
2401             $search_class => $args{searches}{$search_group}{term},
2402             $search_group_name.'.value',
2403             "$search_group_name.$index_col"
2404         );
2405                 $fts->sql_where_clause; # this builds the ranks for us
2406
2407                 my @fts_ranks = $fts->fts_rank;
2408                 my @fts_queries = $fts->fts_query;
2409                 my @phrases = map { lc($_) } $fts->phrases;
2410                 my @words = map { lc($_) } $fts->words;
2411
2412         $stored_proc_search_args{$search_group} = {
2413             fts_rank    => \@fts_ranks,
2414             fts_query   => \@fts_queries,
2415             phrase      => \@phrases,
2416             word        => \@words,
2417         };
2418
2419         }
2420
2421         my $param_search_ou = $ou;
2422         my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2423     my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2424     my $param_statuses = '$${' . join(',', map { s/\$//go; $_ } @statuses) . '}$$';
2425     my $param_audience = '$${' . join(',', map { s/\$//go; $_ } @aud) . '}$$';
2426     my $param_language = '$${' . join(',', map { s/\$//go; $_ } @lang) . '}$$';
2427     my $param_lit_form = '$${' . join(',', map { s/\$//go; $_ } @lit_form) . '}$$';
2428     my $param_types = '$${' . join(',', map { s/\$//go; $_ } @types) . '}$$';
2429     my $param_forms = '$${' . join(',', map { s/\$//go; $_ } @forms) . '}$$';
2430     my $param_vformats = '$${' . join(',', map { s/\$//go; $_ } @vformats) . '}$$';
2431     my $param_bib_level = '$${' . join(',', map { s/\$//go; $_ } @bib_level) . '}$$';
2432         my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2433         my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2434         my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2435         my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2436         my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2437         my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2438     my $param_rel_limit = $args{core_limit}; $param_rel_limit ||= 'NULL';
2439     my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2440     my $param_skip_chk = $args{skip_check}; $param_skip_chk ||= 'NULL';
2441
2442         my $sth = metabib::metarecord_source_map->db_Main->prepare(<<"    SQL");
2443         SELECT  *
2444           FROM  search.staged_fts(
2445                     $param_search_ou,
2446                     $param_depth,
2447                     $param_searches,
2448                     $param_statuses,
2449                     $param_audience,
2450                     $param_language,
2451                     $param_lit_form,
2452                     $param_types,
2453                     $param_forms,
2454                     $param_vformats,
2455                     $param_bib_level,
2456                     $param_pref_lang,
2457                     $param_pref_lang_multiplier,
2458                     $param_sort,
2459                     $param_sort_desc,
2460                     $metarecord,
2461                     $staff,
2462                     $param_rel_limit,
2463                     $param_chk_limit,
2464                     $param_skip_chk
2465                 );
2466     SQL
2467
2468     $sth->execute;
2469
2470     my $recs = $sth->fetchall_arrayref({});
2471     my $summary_row = pop @$recs;
2472
2473     my $total = $$summary_row{total};
2474     my $checked = $$summary_row{checked};
2475     my $visible = $$summary_row{visible};
2476     my $deleted = $$summary_row{deleted};
2477     my $excluded = $$summary_row{excluded};
2478
2479     my $estimate = $visible;
2480     if ( $total > $checked && $checked ) {
2481         my $deleted_ratio = $deleted / $checked;
2482         my $exclution_ratio = $excluded / $checked;
2483         my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2484
2485         $estimate = $$summary_row{estimated_hit_count} = int($delete_adjusted_total - ( $delete_adjusted_total * $exclution_ratio ));
2486     }
2487
2488     delete $$summary_row{id};
2489     delete $$summary_row{rel};
2490     delete $$summary_row{record};
2491
2492     $client->respond( $summary_row );
2493
2494         $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2495
2496         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2497         delete $$rec{checked};
2498         delete $$rec{visible};
2499         delete $$rec{excluded};
2500         delete $$rec{deleted};
2501         delete $$rec{total};
2502         $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2503
2504                 $client->respond( $rec );
2505         }
2506         return undef;
2507 }
2508 __PACKAGE__->register_method(
2509         api_name        => "open-ils.storage.biblio.multiclass.staged.search_fts",
2510         method          => 'staged_fts',
2511         api_level       => 1,
2512         stream          => 1,
2513         cachable        => 1,
2514 );
2515 __PACKAGE__->register_method(
2516         api_name        => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2517         method          => 'staged_fts',
2518         api_level       => 1,
2519         stream          => 1,
2520         cachable        => 1,
2521 );
2522 __PACKAGE__->register_method(
2523         api_name        => "open-ils.storage.metabib.multiclass.staged.search_fts",
2524         method          => 'staged_fts',
2525         api_level       => 1,
2526         stream          => 1,
2527         cachable        => 1,
2528 );
2529 __PACKAGE__->register_method(
2530         api_name        => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2531         method          => 'staged_fts',
2532         api_level       => 1,
2533         stream          => 1,
2534         cachable        => 1,
2535 );
2536
2537
2538 sub xref_count {
2539         my $self = shift;
2540         my $client = shift;
2541         my $args = shift;
2542
2543         my $term = $$args{term};
2544         my $limit = $$args{max} || 1;
2545         my $min = $$args{min} || 1;
2546         my @classes = @{$$args{class}};
2547
2548         $limit = $min if ($min > $limit);
2549
2550         if (!@classes) {
2551                 @classes = ( qw/ title author subject series keyword / );
2552         }
2553
2554         my %matches;
2555         my $bre_table = biblio::record_entry->table;
2556         my $cn_table = asset::call_number->table;
2557         my $cp_table = asset::copy->table;
2558
2559         for my $search_class ( @classes ) {
2560
2561                 my $class = $_cdbi->{$search_class};
2562                 my $search_table = $class->table;
2563
2564                 my ($index_col) = $class->columns('FTS');
2565                 $index_col ||= 'value';
2566
2567                 
2568                 my $where = OpenILS::Application::Storage::FTS
2569                         ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2570                         ->sql_where_clause;
2571
2572                 my $SQL = <<"           SQL";
2573                         SELECT  COUNT(DISTINCT X.source)
2574                           FROM  (SELECT $search_class.source
2575                                   FROM  $search_table $search_class
2576                                         JOIN $bre_table b ON (b.id = $search_class.source)
2577                                   WHERE $where
2578                                         AND NOT b.deleted
2579                                         AND b.active
2580                                   LIMIT $limit) X
2581                           HAVING COUNT(DISTINCT X.source) >= $min;
2582                 SQL
2583
2584                 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2585                 $matches{$search_class} = $res ? $res->[0] : 0;
2586         }
2587
2588         return \%matches;
2589 }
2590 __PACKAGE__->register_method(
2591         api_name        => "open-ils.storage.search.xref",
2592         method          => 'xref_count',
2593         api_level       => 1,
2594 );
2595
2596
2597
2598 1;
2599