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