]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Storage/Publisher/metabib.pm
ignore case on MR search; cleaning house
[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         my $org = shift || 1;
27         my $depth = shift;
28
29         my (@types,@forms);
30
31         if ($formats) {
32                 my ($t, $f) = split '-', $formats;
33                 @types = split '', $t;
34                 @forms = split '', $f;
35         }
36
37         my $descendants =
38                 defined($depth) ?
39                         "actor.org_unit_descendants($org, $depth)" :
40                         "actor.org_unit_descendants($org)" ;
41
42
43         my $copies_visible = 'AND cp.opac_visible IS TRUE AND cs.holdable IS TRUE AND cl.opac_visible IS TRUE';
44         $copies_visible = '' if ($self->api_name =~ /staff/o);
45
46         my $sm_table = metabib::metarecord_source_map->table;
47         my $rd_table = metabib::record_descriptor->table;
48         my $fr_table = metabib::full_rec->table;
49         my $cn_table = asset::call_number->table;
50         my $cl_table = asset::copy_location->table;
51         my $cp_table = asset::copy->table;
52         my $cs_table = config::copy_status->table;
53         my $out_table = actor::org_unit_type->table;
54         my $br_table = biblio::record_entry->table;
55
56         my $sql = <<"   SQL";
57          SELECT record, item_type, item_form, count, title
58            FROM (
59                 SELECT  rd.record,
60                         rd.item_type,
61                         rd.item_form,
62                         br.quality,
63                         FIRST(COALESCE(LTRIM(SUBSTR( fr.value, fr.ind2::text::int )),'zzzzzzzz')) AS title,
64         SQL
65
66         if ($copies_visible) {
67                 $sql .= <<"             SQL"; 
68                         sum((SELECT     count(cp.id)
69                                FROM     $cp_table cp
70                                         JOIN $cs_table cs ON (cp.status = cs.id)
71                                         JOIN $cl_table cl ON (cp.location = cl.id)
72                                         JOIN $descendants d ON (cp.circ_lib = d.id)
73                                WHERE    cn.id = cp.call_number
74                                         $copies_visible
75                           )) AS count
76                 SQL
77         } else {
78                 $sql .= '0 AS count';
79         }
80
81         if ($copies_visible) {
82                 $sql .= <<"             SQL";
83                   FROM  $cn_table cn,
84                         $sm_table sm,
85                         $br_table br,
86                         $fr_table fr,
87                         $rd_table rd
88                   WHERE rd.record = sm.source
89                         AND fr.record = br.id
90                         AND fr.tag = '245'
91                         AND fr.subfield = 'a'
92                         AND br.id = rd.record
93                         AND cn.record = rd.record
94                         AND sm.metarecord = ?
95                 SQL
96         } else {
97                 $sql .= <<"             SQL";
98                   FROM  $sm_table sm,
99                         $br_table br,
100                         $fr_table fr,
101                         $rd_table rd
102                   WHERE rd.record = sm.source
103                         AND fr.record = br.id
104                         AND fr.tag = '245'
105                         AND fr.subfield = 'a'
106                         AND br.id = rd.record
107                         AND sm.metarecord = ?
108                 SQL
109         }
110
111         $sql .= <<"     SQL";
112                   GROUP BY rd.record, rd.item_type, rd.item_form, br.quality
113                   ORDER BY
114                         CASE
115                                 WHEN rd.item_type IS NULL -- default
116                                         THEN 0
117                                 WHEN rd.item_type = '' -- default
118                                         THEN 0
119                                 WHEN rd.item_type IN ('a','t') -- books
120                                         THEN 1
121                                 WHEN rd.item_type = 'g' -- movies
122                                         THEN 2
123                                 WHEN rd.item_type IN ('i','j') -- sound recordings
124                                         THEN 3
125                                 WHEN rd.item_type = 'm' -- software
126                                         THEN 4
127                                 WHEN rd.item_type = 'k' -- images
128                                         THEN 5
129                                 WHEN rd.item_type IN ('e','f') -- maps
130                                         THEN 6
131                                 WHEN rd.item_type IN ('o','p') -- mixed
132                                         THEN 7
133                                 WHEN rd.item_type IN ('c','d') -- music
134                                         THEN 8
135                                 WHEN rd.item_type = 'r' -- 3d
136                                         THEN 9
137                         END,
138                         title ASC,
139                         count DESC,
140                         br.quality DESC
141                 ) x
142         SQL
143
144         if ($copies_visible) {
145                 $sql .= ' WHERE x.count > 0'
146         }
147
148         if (@types) {
149                 $sql .= ' AND x.item_type IN ('.join(',',map{'?'}@types).')';
150         }
151
152         if (@forms) {
153                 $sql .= ' AND x.item_form IN ('.join(',',map{'?'}@forms).')';
154         }
155
156         my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
157         $sth->execute("$mr", @types, @forms);
158         while ( my $row = $sth->fetchrow_arrayref ) {
159                 $client->respond( $$row[0] );
160         }
161         return undef;
162
163 }
164 __PACKAGE__->register_method(
165         api_name        => 'open-ils.storage.ordered.metabib.metarecord.records',
166         method          => 'ordered_records_from_metarecord',
167         api_level       => 1,
168         stream          => 1,
169         cachable        => 1,
170 );
171 __PACKAGE__->register_method(
172         api_name        => 'open-ils.storage.ordered.metabib.metarecord.records.staff',
173         method          => 'ordered_records_from_metarecord',
174         api_level       => 1,
175         stream          => 1,
176         cachable        => 1,
177 );
178
179
180 sub metarecord_copy_count {
181         my $self = shift;
182         my $client = shift;
183
184         my %args = @_;
185
186         my $sm_table = metabib::metarecord_source_map->table;
187         my $rd_table = metabib::record_descriptor->table;
188         my $cn_table = asset::call_number->table;
189         my $cp_table = asset::copy->table;
190         my $cl_table = asset::copy_location->table;
191         my $cs_table = config::copy_status->table;
192         my $out_table = actor::org_unit_type->table;
193         my $descendants = "actor.org_unit_descendants(u.id)";
194         my $ancestors = "actor.org_unit_ancestors(?)";
195
196         my $copies_visible = 'AND cp.opac_visible IS TRUE AND cs.holdable IS TRUE AND cl.opac_visible IS TRUE';
197         $copies_visible = '' if ($self->api_name =~ /staff/o);
198
199         my (@types,@forms);
200         my ($t_filter, $f_filter) = ('','');
201
202         if ($args{format}) {
203                 my ($t, $f) = split '-', $args{format};
204                 @types = split '', $t;
205                 @forms = split '', $f;
206                 if (@types) {
207                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
208                 }
209
210                 if (@forms) {
211                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
212                 }
213         }
214
215         my $sql = <<"   SQL";
216                 SELECT  t.depth,
217                         u.id AS org_unit,
218                         sum(
219                                 (SELECT count(cp.id)
220                                   FROM  $sm_table r
221                                         JOIN $cn_table cn ON (cn.record = r.source)
222                                         JOIN $rd_table rd ON (cn.record = rd.record)
223                                         JOIN $cp_table cp ON (cn.id = cp.call_number)
224                                         JOIN $cs_table cs ON (cp.status = cs.id)
225                                         JOIN $cl_table cl ON (cp.location = cl.id)
226                                         JOIN $descendants a ON (cp.circ_lib = a.id)
227                                   WHERE r.metarecord = ?
228                                         $copies_visible
229                                         $t_filter
230                                         $f_filter
231                                 )
232                         ) AS count,
233                         sum(
234                                 (SELECT count(cp.id)
235                                   FROM  $sm_table r
236                                         JOIN $cn_table cn ON (cn.record = r.source)
237                                         JOIN $rd_table rd ON (cn.record = rd.record)
238                                         JOIN $cp_table cp ON (cn.id = cp.call_number)
239                                         JOIN $cs_table cs ON (cp.status = cs.id)
240                                         JOIN $cl_table cl ON (cp.location = cl.id)
241                                         JOIN $descendants a ON (cp.circ_lib = a.id)
242                                   WHERE r.metarecord = ?
243                                         AND cp.status = 0
244                                         $copies_visible
245                                         $t_filter
246                                         $f_filter
247                                 )
248                         ) AS available
249
250                   FROM  $ancestors u
251                         JOIN $out_table t ON (u.ou_type = t.id)
252                   GROUP BY 1,2
253         SQL
254
255         my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
256         $sth->execute(  ''.$args{metarecord},
257                         @types, 
258                         @forms,
259                         ''.$args{metarecord},
260                         @types, 
261                         @forms,
262                         ''.$args{org_unit}, 
263         ); 
264
265         while ( my $row = $sth->fetchrow_hashref ) {
266                 $client->respond( $row );
267         }
268         return undef;
269 }
270 __PACKAGE__->register_method(
271         api_name        => 'open-ils.storage.metabib.metarecord.copy_count',
272         method          => 'metarecord_copy_count',
273         api_level       => 1,
274         stream          => 1,
275         cachable        => 1,
276 );
277 __PACKAGE__->register_method(
278         api_name        => 'open-ils.storage.metabib.metarecord.copy_count.staff',
279         method          => 'metarecord_copy_count',
280         api_level       => 1,
281         stream          => 1,
282         cachable        => 1,
283 );
284
285 sub biblio_multi_search_full_rec {
286         my $self = shift;
287         my $client = shift;
288
289         my %args = @_;  
290         my $class_join = $args{class_join} || 'AND';
291         my $limit = $args{limit} || 100;
292         my $offset = $args{offset} || 0;
293         my $sort = $args{'sort'};
294         my $sort_dir = $args{sort_dir} || 'DESC';
295
296         my @binds;
297         my @selects;
298
299         for my $arg (@{ $args{searches} }) {
300                 my $term = $$arg{term};
301                 my $limiters = $$arg{restrict};
302
303                 my ($index_col) = metabib::full_rec->columns('FTS');
304                 $index_col ||= 'value';
305                 my $search_table = metabib::full_rec->table;
306
307                 my $fts = OpenILS::Application::Storage::FTS->compile($term, 'value',"$index_col");
308
309                 my $fts_where = $fts->sql_where_clause();
310                 my @fts_ranks = $fts->fts_rank;
311
312                 my $rank = join(' + ', @fts_ranks);
313
314                 my @wheres;
315                 for my $limit (@$limiters) {
316                         push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
317                         push @binds, $$limit{tag}, $$limit{subfield};
318                         $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
319                 }
320                 my $where = join(' OR ', @wheres);
321
322                 push @selects, "SELECT id, record, $rank as sum FROM $search_table WHERE $where";
323
324         }
325
326         my $descendants = defined($args{depth}) ?
327                                 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
328                                 "actor.org_unit_descendants($args{org_unit})" ;
329
330
331         my $metabib_record_descriptor = metabib::record_descriptor->table;
332         my $metabib_full_rec = metabib::full_rec->table;
333         my $asset_call_number_table = asset::call_number->table;
334         my $asset_copy_table = asset::copy->table;
335         my $cs_table = config::copy_status->table;
336         my $cl_table = asset::copy_location->table;
337         my $br_table = biblio::record_entry->table;
338
339         my $cj = 'HAVING COUNT(x.id) = ' . scalar(@selects) if ($class_join eq 'AND');
340         my $search_table =
341                 '(SELECT x.record, sum(x.sum) FROM (('.
342                         join(') UNION ALL (', @selects).
343                         ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
344
345         my $has_vols = 'AND cn.owning_lib = d.id';
346         my $has_copies = 'AND cp.call_number = cn.id';
347         my $copies_visible = 'AND cp.opac_visible IS TRUE AND cs.holdable IS TRUE AND cl.opac_visible IS TRUE';
348
349         if ($self->api_name =~ /staff/o) {
350                 $copies_visible = '';
351                 $has_copies = '' if ($ou_type == 0);
352                 $has_vols = '' if ($ou_type == 0);
353         }
354
355         my ($t_filter, $f_filter) = ('','');
356         my ($a_filter, $l_filter, $lf_filter) = ('','','');
357
358         if (my $a = $args{audience}) {
359                 $a = [$a] if (!ref($a));
360                 my @aud = @$a;
361                         
362                 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
363                 push @binds, @aud;
364         }
365
366         if (my $l = $args{language}) {
367                 $l = [$l] if (!ref($l));
368                 my @lang = @$l;
369
370                 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
371                 push @binds, @lang;
372         }
373
374         if (my $f = $args{lit_form}) {
375                 $f = [$f] if (!ref($f));
376                 my @lit_form = @$f;
377
378                 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
379                 push @binds, @lit_form;
380         }
381
382         if (my $f = $args{item_form}) {
383                 $f = [$f] if (!ref($f));
384                 my @forms = @$f;
385
386                 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
387                 push @binds, @forms;
388         }
389
390         if (my $t = $args{item_type}) {
391                 $t = [$t] if (!ref($t));
392                 my @types = @$t;
393
394                 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
395                 push @binds, @types;
396         }
397
398
399         if ($args{format}) {
400                 my ($t, $f) = split '-', $args{format};
401                 my @types = split '', $t;
402                 my @forms = split '', $f;
403                 if (@types) {
404                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
405                 }
406
407                 if (@forms) {
408                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
409                 }
410                 push @binds, @types, @forms;
411         }
412
413         my $relevance = 'sum(f.sum)';
414         $relevance = 1 if (!$copies_visible);
415
416         my $rank = $relevance;
417         if (lc($sort) eq 'pubdate') {
418                 $rank = <<"             RANK";
419                         ( FIRST ((
420                                 SELECT  COALESCE(SUBSTRING(frp.value FROM '\\\\d+'),'9999')::INT
421                                   FROM  $metabib_full_rec frp
422                                   WHERE frp.record = f.record
423                                         AND frp.tag = '260'
424                                         AND frp.subfield = 'c'
425                                   LIMIT 1
426                         )) )
427                 RANK
428         } elsif (lc($sort) eq 'create_date') {
429                 $rank = <<"             RANK";
430                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
431                 RANK
432         } elsif (lc($sort) eq 'edit_date') {
433                 $rank = <<"             RANK";
434                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
435                 RANK
436         } elsif (lc($sort) eq 'title') {
437                 $rank = <<"             RANK";
438                         ( FIRST ((
439                                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, frt.ind2::text::int )),'zzzzzzzz')
440                                   FROM  $metabib_full_rec frt
441                                   WHERE frt.record = f.record
442                                         AND frt.tag = '245'
443                                         AND frt.subfield = 'a'
444                                   LIMIT 1
445                         )) )
446                 RANK
447         } elsif (lc($sort) eq 'author') {
448                 $rank = <<"             RANK";
449                         ( FIRST((
450                                 SELECT  COALESCE(LTRIM(fra.value),'zzzzzzzz')
451                                   FROM  $metabib_full_rec fra
452                                   WHERE fra.record = f.record
453                                         AND fra.tag LIKE '1%'
454                                         AND fra.subfield = 'a'
455                                   ORDER BY fra.tag::text::int
456                                   LIMIT 1
457                         )) )
458                 RANK
459         } else {
460                 $sort = undef;
461         }
462
463
464         if ($copies_visible) {
465                 $select = <<"           SQL";
466                         SELECT  f.record, $relevance, count(DISTINCT cp.id), $rank
467                         FROM    $search_table f,
468                                 $asset_call_number_table cn,
469                                 $asset_copy_table cp,
470                                 $cs_table cs,
471                                 $cl_table cl,
472                                 $br_table br,
473                                 $metabib_record_descriptor rd,
474                                 $descendants d
475                         WHERE   br.id = f.record
476                                 AND cn.record = f.record
477                                 AND rd.record = f.record
478                                 AND cp.status = cs.id
479                                 AND cp.location = cl.id
480                                 AND br.deleted IS FALSE
481                                 AND cn.deleted IS FALSE
482                                 AND cp.deleted IS FALSE
483                                 $has_vols
484                                 $has_copies
485                                 $copies_visible
486                                 $t_filter
487                                 $f_filter
488                                 $a_filter
489                                 $l_filter
490                                 $lf_filter
491                         GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
492                         ORDER BY 4 $sort_dir,3 DESC
493                 SQL
494         } else {
495                 $select = <<"           SQL";
496                         SELECT  f.record, 1, 1, $rank
497                         FROM    $search_table f,
498                                 $br_table br,
499                                 $metabib_record_descriptor rd
500                         WHERE   br.id = f.record
501                                 AND rd.record = f.record
502                                 AND br.deleted IS FALSE
503                                 $t_filter
504                                 $f_filter
505                                 $a_filter
506                                 $l_filter
507                                 $lf_filter
508                         GROUP BY 1,2,3 
509                         ORDER BY 4 $sort_dir
510                 SQL
511         }
512
513
514         $log->debug("Search SQL :: [$select]",DEBUG);
515
516         my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
517         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
518
519         my $max = 0;
520         $max = 1 if (!@$recs);
521         for (@$recs) {
522                 $max = $$_[1] if ($$_[1] > $max);
523         }
524
525         my $count = @$recs;
526         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
527                 next unless ($$rec[0]);
528                 my ($rid,$rank,$junk,$skip) = @$rec;
529                 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
530         }
531         return undef;
532 }
533 __PACKAGE__->register_method(
534         api_name        => 'open-ils.storage.biblio.full_rec.multi_search',
535         method          => 'biblio_multi_search_full_rec',
536         api_level       => 1,
537         stream          => 1,
538         cachable        => 1,
539 );
540 __PACKAGE__->register_method(
541         api_name        => 'open-ils.storage.biblio.full_rec.multi_search.staff',
542         method          => 'biblio_multi_search_full_rec',
543         api_level       => 1,
544         stream          => 1,
545         cachable        => 1,
546 );
547
548 sub search_full_rec {
549         my $self = shift;
550         my $client = shift;
551
552         my %args = @_;
553         
554         my $term = $args{term};
555         my $limiters = $args{restrict};
556
557         my ($index_col) = metabib::full_rec->columns('FTS');
558         $index_col ||= 'value';
559         my $search_table = metabib::full_rec->table;
560
561         my $fts = OpenILS::Application::Storage::FTS->compile($term, 'value',"$index_col");
562
563         my $fts_where = $fts->sql_where_clause();
564         my @fts_ranks = $fts->fts_rank;
565
566         my $rank = join(' + ', @fts_ranks);
567
568         my @binds;
569         my @wheres;
570         for my $limit (@$limiters) {
571                 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
572                 push @binds, $$limit{tag}, $$limit{subfield};
573                 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
574         }
575         my $where = join(' OR ', @wheres);
576
577         my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
578
579         $log->debug("Search SQL :: [$select]",DEBUG);
580
581         my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
582         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
583
584         $client->respond($_) for (@$recs);
585         return undef;
586 }
587 __PACKAGE__->register_method(
588         api_name        => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
589         method          => 'search_full_rec',
590         api_level       => 1,
591         stream          => 1,
592         cachable        => 1,
593 );
594 __PACKAGE__->register_method(
595         api_name        => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
596         method          => 'search_full_rec',
597         api_level       => 1,
598         stream          => 1,
599         cachable        => 1,
600 );
601
602
603 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
604 sub search_class_fts {
605         my $self = shift;
606         my $client = shift;
607         my %args = @_;
608         
609         my $term = $args{term};
610         my $ou = $args{org_unit};
611         my $ou_type = $args{depth};
612         my $limit = $args{limit};
613         my $offset = $args{offset};
614
615         my $limit_clause = '';
616         my $offset_clause = '';
617
618         $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
619         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
620
621         my (@types,@forms);
622         my ($t_filter, $f_filter) = ('','');
623
624         if ($args{format}) {
625                 my ($t, $f) = split '-', $args{format};
626                 @types = split '', $t;
627                 @forms = split '', $f;
628                 if (@types) {
629                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
630                 }
631
632                 if (@forms) {
633                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
634                 }
635         }
636
637
638
639         my $descendants = defined($ou_type) ?
640                                 "actor.org_unit_descendants($ou, $ou_type)" :
641                                 "actor.org_unit_descendants($ou)";
642
643         my $class = $self->{cdbi};
644         my $search_table = $class->table;
645
646         my $metabib_record_descriptor = metabib::record_descriptor->table;
647         my $metabib_metarecord = metabib::metarecord->table;
648         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
649         my $asset_call_number_table = asset::call_number->table;
650         my $asset_copy_table = asset::copy->table;
651         my $cs_table = config::copy_status->table;
652         my $cl_table = asset::copy_location->table;
653
654         my ($index_col) = $class->columns('FTS');
655         $index_col ||= 'value';
656
657         my $fts = OpenILS::Application::Storage::FTS->compile($term, 'f.value', "f.$index_col");
658
659         my $fts_where = $fts->sql_where_clause;
660         my @fts_ranks = $fts->fts_rank;
661
662         my $rank = join(' + ', @fts_ranks);
663
664         my $has_vols = 'AND cn.owning_lib = d.id';
665         my $has_copies = 'AND cp.call_number = cn.id';
666         my $copies_visible = 'AND cp.opac_visible IS TRUE AND cs.holdable IS TRUE AND cl.opac_visible IS TRUE';
667
668         my $visible_count = ', count(DISTINCT cp.id)';
669         my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
670
671         if ($self->api_name =~ /staff/o) {
672                 $copies_visible = '';
673                 $visible_count_test = '';
674                 $has_copies = '' if ($ou_type == 0);
675                 $has_vols = '' if ($ou_type == 0);
676         }
677
678         my $rank_calc = <<"     RANK";
679                 , (SUM( $rank
680                         * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
681                         * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
682                         * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
683                 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
684         RANK
685
686         $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
687
688         if ($copies_visible) {
689                 $select = <<"           SQL";
690                         SELECT  m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
691                         FROM    $search_table f,
692                                 $metabib_metarecord_source_map_table m,
693                                 $asset_call_number_table cn,
694                                 $asset_copy_table cp,
695                                 $cs_table cs,
696                                 $cl_table cl,
697                                 $metabib_record_descriptor rd,
698                                 $descendants d
699                         WHERE   $fts_where
700                                 AND m.source = f.source
701                                 AND cn.record = m.source
702                                 AND rd.record = m.source
703                                 AND cp.status = cs.id
704                                 AND cp.location = cl.id
705                                 $has_vols
706                                 $has_copies
707                                 $copies_visible
708                                 $t_filter
709                                 $f_filter
710                         GROUP BY 1 $visible_count_test
711                         ORDER BY 2 DESC,3
712                         $limit_clause $offset_clause
713                 SQL
714         } else {
715                 $select = <<"           SQL";
716                         SELECT  m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
717                         FROM    $search_table f,
718                                 $metabib_metarecord_source_map_table m,
719                                 $metabib_record_descriptor rd
720                         WHERE   $fts_where
721                                 AND m.source = f.source
722                                 AND rd.record = m.source
723                                 $t_filter
724                                 $f_filter
725                         GROUP BY 1, 4
726                         ORDER BY 2 DESC,3
727                         $limit_clause $offset_clause
728                 SQL
729         }
730
731         $log->debug("Field Search SQL :: [$select]",DEBUG);
732
733         my $SQLstring = join('%',$fts->words);
734         my $REstring = join('\\s+',$fts->words);
735         my $first_word = ($fts->words)[0].'%';
736         my $recs = ($self->api_name =~ /unordered/o) ? 
737                         $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
738                         $class->db_Main->selectall_arrayref($select, {},
739                                 '%'.lc($SQLstring).'%',                 # phrase order match
740                                 lc($first_word),                        # first word match
741                                 '^\\s*'.lc($REstring).'\\s*/?\s*$',     # full exact match
742                                 @types, @forms
743                         );
744         
745         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
746
747         $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
748         return undef;
749 }
750
751 for my $class ( qw/title author subject keyword series/ ) {
752         __PACKAGE__->register_method(
753                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord",
754                 method          => 'search_class_fts',
755                 api_level       => 1,
756                 stream          => 1,
757                 cdbi            => "metabib::${class}_field_entry",
758                 cachable        => 1,
759         );
760         __PACKAGE__->register_method(
761                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
762                 method          => 'search_class_fts',
763                 api_level       => 1,
764                 stream          => 1,
765                 cdbi            => "metabib::${class}_field_entry",
766                 cachable        => 1,
767         );
768         __PACKAGE__->register_method(
769                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
770                 method          => 'search_class_fts',
771                 api_level       => 1,
772                 stream          => 1,
773                 cdbi            => "metabib::${class}_field_entry",
774                 cachable        => 1,
775         );
776         __PACKAGE__->register_method(
777                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
778                 method          => 'search_class_fts',
779                 api_level       => 1,
780                 stream          => 1,
781                 cdbi            => "metabib::${class}_field_entry",
782                 cachable        => 1,
783         );
784 }
785
786 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
787 sub search_class_fts_count {
788         my $self = shift;
789         my $client = shift;
790         my %args = @_;
791         
792         my $term = $args{term};
793         my $ou = $args{org_unit};
794         my $ou_type = $args{depth};
795         my $limit = $args{limit} || 100;
796         my $offset = $args{offset} || 0;
797
798         my $descendants = defined($ou_type) ?
799                                 "actor.org_unit_descendants($ou, $ou_type)" :
800                                 "actor.org_unit_descendants($ou)";
801                 
802         my (@types,@forms);
803         my ($t_filter, $f_filter) = ('','');
804
805         if ($args{format}) {
806                 my ($t, $f) = split '-', $args{format};
807                 @types = split '', $t;
808                 @forms = split '', $f;
809                 if (@types) {
810                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
811                 }
812
813                 if (@forms) {
814                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
815                 }
816         }
817
818
819         (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
820
821         my $class = $self->{cdbi};
822         my $search_table = $class->table;
823
824         my $metabib_record_descriptor = metabib::record_descriptor->table;
825         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
826         my $asset_call_number_table = asset::call_number->table;
827         my $asset_copy_table = asset::copy->table;
828         my $cs_table = config::copy_status->table;
829         my $cl_table = asset::copy_location->table;
830
831         my ($index_col) = $class->columns('FTS');
832         $index_col ||= 'value';
833
834         my $fts = OpenILS::Application::Storage::FTS->compile($term, 'value',"$index_col");
835
836         my $fts_where = $fts->sql_where_clause;
837
838         my $has_vols = 'AND cn.owning_lib = d.id';
839         my $has_copies = 'AND cp.call_number = cn.id';
840         my $copies_visible = 'AND cp.opac_visible IS TRUE AND cs.holdable IS TRUE AND cl.opac_visible IS TRUE';
841         if ($self->api_name =~ /staff/o) {
842                 $copies_visible = '';
843                 $has_vols = '' if ($ou_type == 0);
844                 $has_copies = '' if ($ou_type == 0);
845         }
846
847         # XXX test an "EXISTS version of descendant checking...
848         my $select;
849         if ($copies_visible) {
850                 $select = <<"           SQL";
851                 SELECT  count(distinct  m.metarecord)
852                   FROM  $search_table f,
853                         $metabib_metarecord_source_map_table m,
854                         $metabib_metarecord_source_map_table mr,
855                         $asset_call_number_table cn,
856                         $asset_copy_table cp,
857                         $cs_table cs,
858                         $cl_table cl,
859                         $metabib_record_descriptor rd,
860                         $descendants d
861                   WHERE $fts_where
862                         AND mr.source = f.source
863                         AND mr.metarecord = m.metarecord
864                         AND cn.record = m.source
865                         AND rd.record = m.source
866                         AND cp.status = cs.id
867                         AND cp.location = cl.id
868                         $has_vols
869                         $has_copies
870                         $copies_visible
871                         $t_filter
872                         $f_filter
873                 SQL
874         } else {
875                 $select = <<"           SQL";
876                 SELECT  count(distinct  m.metarecord)
877                   FROM  $search_table f,
878                         $metabib_metarecord_source_map_table m,
879                         $metabib_metarecord_source_map_table mr,
880                         $metabib_record_descriptor rd
881                   WHERE $fts_where
882                         AND mr.source = f.source
883                         AND mr.metarecord = m.metarecord
884                         AND rd.record = m.source
885                         $t_filter
886                         $f_filter
887                 SQL
888         }
889
890         $log->debug("Field Search Count SQL :: [$select]",DEBUG);
891
892         my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
893         
894         $log->debug("Count Search yielded $recs results.",DEBUG);
895
896         return $recs;
897
898 }
899 for my $class ( qw/title author subject keyword series/ ) {
900         __PACKAGE__->register_method(
901                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
902                 method          => 'search_class_fts_count',
903                 api_level       => 1,
904                 stream          => 1,
905                 cdbi            => "metabib::${class}_field_entry",
906                 cachable        => 1,
907         );
908         __PACKAGE__->register_method(
909                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
910                 method          => 'search_class_fts_count',
911                 api_level       => 1,
912                 stream          => 1,
913                 cdbi            => "metabib::${class}_field_entry",
914                 cachable        => 1,
915         );
916 }
917
918
919 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
920 sub postfilter_search_class_fts {
921         my $self = shift;
922         my $client = shift;
923         my %args = @_;
924         
925         my $term = $args{term};
926         my $sort = $args{'sort'};
927         my $sort_dir = $args{sort_dir} || 'DESC';
928         my $ou = $args{org_unit};
929         my $ou_type = $args{depth};
930         my $limit = $args{limit} || 10;
931         my $offset = $args{offset} || 0;
932
933         my $outer_limit = 1000;
934
935         my $limit_clause = '';
936         my $offset_clause = '';
937
938         $limit_clause = "LIMIT $outer_limit";
939         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
940
941         my (@types,@forms,@lang,@aud,@lit_form);
942         my ($t_filter, $f_filter) = ('','');
943         my ($a_filter, $l_filter, $lf_filter) = ('','','');
944         my ($ot_filter, $of_filter) = ('','');
945         my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
946
947         if (my $a = $args{audience}) {
948                 $a = [$a] if (!ref($a));
949                 @aud = @$a;
950                         
951                 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
952                 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
953         }
954
955         if (my $l = $args{language}) {
956                 $l = [$l] if (!ref($l));
957                 @lang = @$l;
958
959                 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
960                 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
961         }
962
963         if (my $f = $args{lit_form}) {
964                 $f = [$f] if (!ref($f));
965                 @lit_form = @$f;
966
967                 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
968                 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
969         }
970
971         if ($args{format}) {
972                 my ($t, $f) = split '-', $args{format};
973                 @types = split '', $t;
974                 @forms = split '', $f;
975                 if (@types) {
976                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
977                         $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
978                 }
979
980                 if (@forms) {
981                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
982                         $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
983                 }
984         }
985
986
987         my $descendants = defined($ou_type) ?
988                                 "actor.org_unit_descendants($ou, $ou_type)" :
989                                 "actor.org_unit_descendants($ou)";
990
991         my $class = $self->{cdbi};
992         my $search_table = $class->table;
993
994         my $metabib_full_rec = metabib::full_rec->table;
995         my $metabib_record_descriptor = metabib::record_descriptor->table;
996         my $metabib_metarecord = metabib::metarecord->table;
997         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
998         my $asset_call_number_table = asset::call_number->table;
999         my $asset_copy_table = asset::copy->table;
1000         my $cs_table = config::copy_status->table;
1001         my $cl_table = asset::copy_location->table;
1002         my $br_table = biblio::record_entry->table;
1003
1004         my ($index_col) = $class->columns('FTS');
1005         $index_col ||= 'value';
1006
1007         my $fts = OpenILS::Application::Storage::FTS->compile($term, 'f.value', "f.$index_col");
1008
1009         my $SQLstring = join('%',map { lc($_) } $fts->words);
1010         my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1011         my $first_word = lc(($fts->words)[0]).'%';
1012
1013         my $fts_where = $fts->sql_where_clause;
1014         my @fts_ranks = $fts->fts_rank;
1015
1016         my %bonus = ();
1017         $bonus{'metabib::keyword_field_entry'} = [ { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring } ];
1018         $bonus{'metabib::title_field_entry'} =
1019                 $bonus{'metabib::series_field_entry'} = [
1020                         { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1021                         { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1022                         @{ $bonus{'metabib::keyword_field_entry'} }
1023                 ];
1024
1025         my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1026         $bonus_list ||= '1';
1027
1028         my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1029
1030         my $relevance = join(' + ', @fts_ranks);
1031         $relevance = <<"        RANK";
1032                         (SUM( ( $relevance )  * ( $bonus_list ) )/COUNT(m.source))
1033         RANK
1034
1035         my $rank = $relevance;
1036         if (lc($sort) eq 'pubdate') {
1037                 $rank = <<"             RANK";
1038                         ( FIRST ((
1039                                 SELECT  COALESCE(SUBSTRING(frp.value FROM '\\\\d+'),'9999')::INT
1040                                   FROM  $metabib_full_rec frp
1041                                   WHERE frp.record = mr.master_record
1042                                         AND frp.tag = '260'
1043                                         AND frp.subfield = 'c'
1044                                   LIMIT 1
1045                         )) )
1046                 RANK
1047         } elsif (lc($sort) eq 'create_date') {
1048                 $rank = <<"             RANK";
1049                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1050                 RANK
1051         } elsif (lc($sort) eq 'edit_date') {
1052                 $rank = <<"             RANK";
1053                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1054                 RANK
1055         } elsif (lc($sort) eq 'title') {
1056                 $rank = <<"             RANK";
1057                         ( FIRST ((
1058                                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, frt.ind2::text::int )),'zzzzzzzz')
1059                                   FROM  $metabib_full_rec frt
1060                                   WHERE frt.record = mr.master_record
1061                                         AND frt.tag = '245'
1062                                         AND frt.subfield = 'a'
1063                                   LIMIT 1
1064                         )) )
1065                 RANK
1066         } elsif (lc($sort) eq 'author') {
1067                 $rank = <<"             RANK";
1068                         ( FIRST((
1069                                 SELECT  COALESCE(LTRIM(fra.value),'zzzzzzzz')
1070                                   FROM  $metabib_full_rec fra
1071                                   WHERE fra.record = mr.master_record
1072                                         AND fra.tag LIKE '1%'
1073                                         AND fra.subfield = 'a'
1074                                   ORDER BY fra.tag::text::int
1075                                   LIMIT 1
1076                         )) )
1077                 RANK
1078         } else {
1079                 $sort = undef;
1080         }
1081
1082         my $select = <<"        SQL";
1083                 SELECT  m.metarecord,
1084                         $relevance,
1085                         CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1086                         $rank
1087                 FROM    $search_table f,
1088                         $metabib_metarecord_source_map_table m,
1089                         $metabib_metarecord_source_map_table smrs,
1090                         $metabib_metarecord mr,
1091                         $metabib_record_descriptor rd
1092                 WHERE   $fts_where
1093                         AND smrs.metarecord = mr.id
1094                         AND m.source = f.source
1095                         AND m.metarecord = mr.id
1096                         AND rd.record = smrs.source
1097                         $t_filter
1098                         $f_filter
1099                         $a_filter
1100                         $l_filter
1101                         $lf_filter
1102                 GROUP BY m.metarecord
1103                 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1104                 LIMIT 10000
1105         SQL
1106
1107         if (0) {
1108                 $select = <<"           SQL";
1109
1110                         SELECT  DISTINCT s.*
1111                           FROM  $asset_call_number_table cn,
1112                                 $metabib_metarecord_source_map_table mrs,
1113                                 $asset_copy_table cp,
1114                                 $cs_table cs,
1115                                 $cl_table cl,
1116                                 $br_table br,
1117                                 $descendants d,
1118                                 $metabib_record_descriptor ord,
1119                                 ($select) s
1120                           WHERE mrs.metarecord = s.metarecord
1121                                 AND br.id = mrs.source
1122                                 AND cn.record = mrs.source
1123                                 AND cp.status = cs.id
1124                                 AND cp.location = cl.id
1125                                 AND cn.owning_lib = d.id
1126                                 AND cp.call_number = cn.id
1127                                 AND cp.opac_visible IS TRUE
1128                                 AND cs.holdable IS TRUE
1129                                 AND cl.opac_visible IS TRUE
1130                                 AND br.active IS TRUE
1131                                 AND br.deleted IS FALSE
1132                                 AND ord.record = mrs.source
1133                                 $ot_filter
1134                                 $of_filter
1135                                 $oa_filter
1136                                 $ol_filter
1137                                 $olf_filter
1138                           ORDER BY 4 $sort_dir
1139                 SQL
1140         } elsif ($self->api_name !~ /staff/o) {
1141                 $select = <<"           SQL";
1142
1143                         SELECT  DISTINCT s.*
1144                           FROM  ($select) s
1145                           WHERE EXISTS (
1146                                 SELECT  1
1147                                   FROM  $asset_call_number_table cn,
1148                                         $metabib_metarecord_source_map_table mrs,
1149                                         $asset_copy_table cp,
1150                                         $cs_table cs,
1151                                         $cl_table cl,
1152                                         $br_table br,
1153                                         $descendants d,
1154                                         $metabib_record_descriptor ord
1155                                 
1156                                   WHERE mrs.metarecord = s.metarecord
1157                                         AND br.id = mrs.source
1158                                         AND cn.record = mrs.source
1159                                         AND cp.status = cs.id
1160                                         AND cp.location = cl.id
1161                                         AND cn.owning_lib = d.id
1162                                         AND cp.call_number = cn.id
1163                                         AND cp.opac_visible IS TRUE
1164                                         AND cs.holdable IS TRUE
1165                                         AND cl.opac_visible IS TRUE
1166                                         AND br.active IS TRUE
1167                                         AND br.deleted IS FALSE
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         } else {
1179                 $select = <<"           SQL";
1180
1181                         SELECT  DISTINCT s.*
1182                           FROM  ($select) s
1183                           WHERE EXISTS (
1184                                 SELECT  1
1185                                   FROM  $asset_call_number_table cn,
1186                                         $metabib_metarecord_source_map_table mrs,
1187                                         $br_table br,
1188                                         $descendants d,
1189                                         $metabib_record_descriptor ord
1190                                 
1191                                   WHERE mrs.metarecord = s.metarecord
1192                                         AND br.id = mrs.source
1193                                         AND cn.record = mrs.source
1194                                         AND cn.owning_lib = d.id
1195                                         AND br.deleted IS FALSE
1196                                         AND ord.record = mrs.source
1197                                         $ot_filter
1198                                         $of_filter
1199                                         $oa_filter
1200                                         $ol_filter
1201                                         $olf_filter
1202                                   LIMIT 1
1203                                 )
1204                                 OR NOT EXISTS (
1205                                 SELECT  1
1206                                   FROM  $asset_call_number_table cn,
1207                                         $metabib_metarecord_source_map_table mrs,
1208                                         $metabib_record_descriptor ord
1209                                   WHERE mrs.metarecord = s.metarecord
1210                                         AND cn.record = mrs.source
1211                                         AND ord.record = mrs.source
1212                                         $ot_filter
1213                                         $of_filter
1214                                         $oa_filter
1215                                         $ol_filter
1216                                         $olf_filter
1217                                   LIMIT 1
1218                                 )
1219                           ORDER BY 4 $sort_dir
1220                 SQL
1221         }
1222
1223
1224         $log->debug("Field Search SQL :: [$select]",DEBUG);
1225
1226         my $recs = $class->db_Main->selectall_arrayref(
1227                         $select, {},
1228                         (@bonus_values > 0 ? @bonus_values : () ),
1229                         ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1230                         @types, @forms, @aud, @lang, @lit_form,
1231                         @types, @forms, @aud, @lang, @lit_form,
1232                         ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1233         
1234         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1235
1236         my $max = 0;
1237         $max = 1 if (!@$recs);
1238         for (@$recs) {
1239                 $max = $$_[1] if ($$_[1] > $max);
1240         }
1241
1242         my $count = scalar(@$recs);
1243         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1244                 my ($mrid,$rank,$skip) = @$rec;
1245                 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1246         }
1247         return undef;
1248 }
1249
1250 for my $class ( qw/title author subject keyword series/ ) {
1251         __PACKAGE__->register_method(
1252                 api_name        => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1253                 method          => 'postfilter_search_class_fts',
1254                 api_level       => 1,
1255                 stream          => 1,
1256                 cdbi            => "metabib::${class}_field_entry",
1257                 cachable        => 1,
1258         );
1259         __PACKAGE__->register_method(
1260                 api_name        => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1261                 method          => 'postfilter_search_class_fts',
1262                 api_level       => 1,
1263                 stream          => 1,
1264                 cdbi            => "metabib::${class}_field_entry",
1265                 cachable        => 1,
1266         );
1267 }
1268
1269
1270
1271 my $_cdbi = {   title   => "metabib::title_field_entry",
1272                 author  => "metabib::author_field_entry",
1273                 subject => "metabib::subject_field_entry",
1274                 keyword => "metabib::keyword_field_entry",
1275                 series  => "metabib::series_field_entry",
1276 };
1277
1278 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1279 sub postfilter_search_multi_class_fts {
1280         my $self = shift;
1281         my $client = shift;
1282         my %args = @_;
1283         
1284         my $sort = $args{'sort'};
1285         my $sort_dir = $args{sort_dir} || 'DESC';
1286         my $ou = $args{org_unit};
1287         my $ou_type = $args{depth};
1288         my $limit = $args{limit} || 10;;
1289         my $offset = $args{offset} || 0;
1290
1291         if (!$ou) {
1292                 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1293         }
1294
1295         if (!defined($args{org_unit})) {
1296                 die "No target organizational unit passed to ".$self->api_name;
1297         }
1298
1299         if (! scalar( keys %{$args{searches}} )) {
1300                 die "No search arguments were passed to ".$self->api_name;
1301         }
1302
1303         my $outer_limit = 1000;
1304
1305         my $limit_clause = '';
1306         my $offset_clause = '';
1307
1308         $limit_clause = "LIMIT $outer_limit";
1309         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1310
1311         my (@types,@forms,@lang,@aud,@lit_form);
1312         my ($t_filter, $f_filter) = ('','');
1313         my ($a_filter, $l_filter, $lf_filter) = ('','','');
1314         my ($ot_filter, $of_filter) = ('','');
1315         my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1316
1317         if (my $a = $args{audience}) {
1318                 $a = [$a] if (!ref($a));
1319                 @aud = @$a;
1320                         
1321                 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1322                 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1323         }
1324
1325         if (my $l = $args{language}) {
1326                 $l = [$l] if (!ref($l));
1327                 @lang = @$l;
1328
1329                 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1330                 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1331         }
1332
1333         if (my $f = $args{lit_form}) {
1334                 $f = [$f] if (!ref($f));
1335                 @lit_form = @$f;
1336
1337                 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1338                 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1339         }
1340
1341         if (my $f = $args{item_form}) {
1342                 $f = [$f] if (!ref($f));
1343                 @forms = @$f;
1344
1345                 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1346                 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1347         }
1348
1349         if (my $t = $args{item_type}) {
1350                 $t = [$t] if (!ref($t));
1351                 @types = @$t;
1352
1353                 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1354                 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1355         }
1356
1357
1358         # XXX legacy format and item type support
1359         if ($args{format}) {
1360                 my ($t, $f) = split '-', $args{format};
1361                 @types = split '', $t;
1362                 @forms = split '', $f;
1363                 if (@types) {
1364                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1365                         $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1366                 }
1367
1368                 if (@forms) {
1369                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1370                         $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1371                 }
1372         }
1373
1374
1375
1376         my $descendants = defined($ou_type) ?
1377                                 "actor.org_unit_descendants($ou, $ou_type)" :
1378                                 "actor.org_unit_descendants($ou)";
1379
1380         my $search_table_list = '';
1381         my $fts_list = '';
1382         my $join_table_list = '';
1383         my @rank_list;
1384
1385         my @bonus_lists;
1386         my @bonus_values;
1387         my $prev_search_class;
1388         my $curr_search_class;
1389         for my $search_class (sort keys %{$args{searches}}) {
1390                 $prev_search_class = $curr_search_class if ($curr_search_class);
1391
1392                 $curr_search_class = $search_class;
1393
1394                 my $class = $_cdbi->{$search_class};
1395                 my $search_table = $class->table;
1396
1397                 my ($index_col) = $class->columns('FTS');
1398                 $index_col ||= 'value';
1399
1400                 
1401                 my $fts = OpenILS::Application::Storage::FTS->compile($args{searches}{$search_class}{term}, $search_class.'.value', "$search_class.$index_col");
1402
1403                 my $fts_where = $fts->sql_where_clause;
1404                 my @fts_ranks = $fts->fts_rank;
1405
1406                 my $SQLstring = join('%',map { lc($_) } $fts->words);
1407                 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1408                 my $first_word = lc(($fts->words)[0]).'%';
1409
1410                 my $rank = join(' + ', @fts_ranks);
1411
1412                 my %bonus = ();
1413                 $bonus{'keyword'} = [ { "CASE WHEN $search_class.value LIKE ? THEN 1.2 ELSE 1 END" => $SQLstring } ];
1414
1415                 $bonus{'series'} = [
1416                         { "CASE WHEN $search_class.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1417                         { "CASE WHEN $search_class.value ~ ? THEN 1000 ELSE 1 END" => $REstring },
1418                 ];
1419
1420                 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1421
1422                 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1423                 $bonus_list ||= '1';
1424
1425                 push @bonus_lists, $bonus_list;
1426                 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1427
1428
1429                 #---------------------
1430
1431                 $search_table_list .= "$search_table $search_class, ";
1432                 push @rank_list,$rank;
1433                 $fts_list .= " AND $fts_where AND m.source = $search_class.source";
1434
1435                 if ($prev_search_class) {
1436                         $join_table_list .= " AND $prev_search_class.source = $curr_search_class.source";
1437                 }
1438         }
1439
1440         my $metabib_record_descriptor = metabib::record_descriptor->table;
1441         my $metabib_full_rec = metabib::full_rec->table;
1442         my $metabib_metarecord = metabib::metarecord->table;
1443         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1444         my $asset_call_number_table = asset::call_number->table;
1445         my $asset_copy_table = asset::copy->table;
1446         my $cs_table = config::copy_status->table;
1447         my $cl_table = asset::copy_location->table;
1448         my $br_table = biblio::record_entry->table;
1449
1450         my $bonuses = join (' * ', @bonus_lists);
1451         my $relevance = join (' + ', @rank_list);
1452         $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT m.source)";
1453
1454
1455         my $secondary_sort = <<"        SORT";
1456                 ( FIRST ((
1457                         SELECT  COALESCE(LTRIM(SUBSTR( sfrt.value, sfrt.ind2::text::int )),'zzzzzzzz')
1458                           FROM  $metabib_full_rec sfrt,
1459                                 $metabib_metarecord mr
1460                           WHERE sfrt.record = mr.master_record
1461                                 AND sfrt.tag = '245'
1462                                 AND sfrt.subfield = 'a'
1463                           LIMIT 1
1464                 )) )
1465         SORT
1466
1467         my $rank = $relevance;
1468         if (lc($sort) eq 'pubdate') {
1469                 $rank = <<"             RANK";
1470                         ( FIRST ((
1471                                 SELECT  COALESCE(SUBSTRING(frp.value FROM '\\\\d+'),'9999')::INT
1472                                   FROM  $metabib_full_rec frp
1473                                   WHERE frp.record = mr.master_record
1474                                         AND frp.tag = '260'
1475                                         AND frp.subfield = 'c'
1476                                   LIMIT 1
1477                         )) )
1478                 RANK
1479         } elsif (lc($sort) eq 'create_date') {
1480                 $rank = <<"             RANK";
1481                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1482                 RANK
1483         } elsif (lc($sort) eq 'edit_date') {
1484                 $rank = <<"             RANK";
1485                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1486                 RANK
1487         } elsif (lc($sort) eq 'title') {
1488                 $rank = <<"             RANK";
1489                         ( FIRST ((
1490                                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, frt.ind2::text::int )),'zzzzzzzz')
1491                                   FROM  $metabib_full_rec frt
1492                                   WHERE frt.record = mr.master_record
1493                                         AND frt.tag = '245'
1494                                         AND frt.subfield = 'a'
1495                                   LIMIT 1
1496                         )) )
1497                 RANK
1498                 $secondary_sort = <<"           SORT";
1499                         ( FIRST ((
1500                                 SELECT  COALESCE(SUBSTRING(sfrp.value FROM '\\\\d+'),'9999')::INT
1501                                   FROM  $metabib_full_rec sfrp,
1502                                         $metabib_metarecord mr
1503                                   WHERE sfrp.record = mr.master_record
1504                                         AND sfrp.tag = '260'
1505                                         AND sfrp.subfield = 'c'
1506                                   LIMIT 1
1507                         )) )
1508                 SORT
1509         } elsif (lc($sort) eq 'author') {
1510                 $rank = <<"             RANK";
1511                         ( FIRST((
1512                                 SELECT  COALESCE(LTRIM(fra.value),'zzzzzzzz')
1513                                   FROM  $metabib_full_rec fra
1514                                   WHERE fra.record = mr.master_record
1515                                         AND fra.tag LIKE '1%'
1516                                         AND fra.subfield = 'a'
1517                                   ORDER BY fra.tag::text::int
1518                                   LIMIT 1
1519                         )) )
1520                 RANK
1521         } else {
1522                 push @bonus_values, @bonus_values;
1523                 $sort = undef;
1524         }
1525
1526
1527         my $select = <<"        SQL";
1528                 SELECT  m.metarecord,
1529                         $relevance,
1530                         CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1531                         $rank,
1532                         $secondary_sort
1533                 FROM    $search_table_list
1534                         $metabib_metarecord_source_map_table m,
1535                         $metabib_metarecord_source_map_table smrs
1536                 WHERE   m.metarecord = smrs.metarecord 
1537                         $fts_list
1538                         $join_table_list
1539                 GROUP BY m.metarecord
1540                 -- ORDER BY 4 $sort_dir
1541                 LIMIT 10000
1542         SQL
1543
1544         if ($self->api_name !~ /staff/o) {
1545                 $select = <<"           SQL";
1546
1547                         SELECT  s.*
1548                           FROM  ($select) s
1549                           WHERE EXISTS (
1550                                 SELECT  1
1551                                   FROM  $asset_call_number_table cn,
1552                                         $metabib_metarecord_source_map_table mrs,
1553                                         $asset_copy_table cp,
1554                                         $cs_table cs,
1555                                         $cl_table cl,
1556                                         $br_table br,
1557                                         $descendants d,
1558                                         $metabib_record_descriptor ord
1559                                   WHERE mrs.metarecord = s.metarecord
1560                                         AND br.id = mrs.source
1561                                         AND cn.record = mrs.source
1562                                         AND cp.status = cs.id
1563                                         AND cp.location = cl.id
1564                                         AND cn.owning_lib = d.id
1565                                         AND cp.call_number = cn.id
1566                                         AND cp.opac_visible IS TRUE
1567                                         AND cs.holdable IS TRUE
1568                                         AND cl.opac_visible IS TRUE
1569                                         AND br.active IS TRUE
1570                                         AND br.deleted IS FALSE
1571                                         AND ord.record = mrs.source
1572                                         $ot_filter
1573                                         $of_filter
1574                                         $oa_filter
1575                                         $ol_filter
1576                                         $olf_filter
1577                                   LIMIT 1
1578                                 )
1579                           ORDER BY 4 $sort_dir, 5
1580                 SQL
1581         } else {
1582                 $select = <<"           SQL";
1583
1584                         SELECT  s.*
1585                           FROM  ($select) s
1586                           WHERE EXISTS (
1587                                 SELECT  1
1588                                   FROM  $asset_call_number_table cn,
1589                                         $metabib_metarecord_source_map_table mrs,
1590                                         $descendants d,
1591                                         $br_table br,
1592                                         $metabib_record_descriptor ord
1593                                   WHERE mrs.metarecord = s.metarecord
1594                                         AND br.id = mrs.source
1595                                         AND cn.record = mrs.source
1596                                         AND cn.owning_lib = d.id
1597                                         AND ord.record = mrs.source
1598                                         AND br.deleted IS FALSE
1599                                         $ot_filter
1600                                         $of_filter
1601                                         $oa_filter
1602                                         $ol_filter
1603                                         $olf_filter
1604                                   LIMIT 1
1605                                 )
1606                                 OR NOT EXISTS (
1607                                 SELECT  1
1608                                   FROM  $asset_call_number_table cn,
1609                                         $metabib_metarecord_source_map_table mrs,
1610                                         $metabib_record_descriptor ord
1611                                   WHERE mrs.metarecord = s.metarecord
1612                                         AND cn.record = mrs.source
1613                                         AND ord.record = mrs.source
1614                                         $ot_filter
1615                                         $of_filter
1616                                         $oa_filter
1617                                         $ol_filter
1618                                         $olf_filter
1619                                   LIMIT 1
1620                                 )
1621                           ORDER BY 4 $sort_dir, 5
1622                 SQL
1623         }
1624
1625
1626         $log->debug("Field Search SQL :: [$select]",DEBUG);
1627
1628         my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1629                         $select, {},
1630                         @bonus_values,
1631                         @types, @forms, @aud, @lang, @lit_form,
1632                         # @types, @forms, @aud, @lang, @lit_form,
1633                         ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1634         );
1635         
1636         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1637
1638         my $max = 0;
1639         $max = 1 if (!@$recs);
1640         for (@$recs) {
1641                 $max = $$_[1] if ($$_[1] > $max);
1642         }
1643
1644         my $count = scalar(@$recs);
1645         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1646                 next unless ($$rec[0]);
1647                 my ($mrid,$rank,$skip) = @$rec;
1648                 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1649         }
1650         return undef;
1651 }
1652
1653 __PACKAGE__->register_method(
1654         api_name        => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1655         method          => 'postfilter_search_multi_class_fts',
1656         api_level       => 1,
1657         stream          => 1,
1658         cachable        => 1,
1659 );
1660 __PACKAGE__->register_method(
1661         api_name        => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1662         method          => 'postfilter_search_multi_class_fts',
1663         api_level       => 1,
1664         stream          => 1,
1665         cachable        => 1,
1666 );
1667
1668 __PACKAGE__->register_method(
1669         api_name        => "open-ils.storage.metabib.multiclass.search_fts",
1670         method          => 'postfilter_search_multi_class_fts',
1671         api_level       => 1,
1672         stream          => 1,
1673         cachable        => 1,
1674 );
1675 __PACKAGE__->register_method(
1676         api_name        => "open-ils.storage.metabib.multiclass.search_fts.staff",
1677         method          => 'postfilter_search_multi_class_fts',
1678         api_level       => 1,
1679         stream          => 1,
1680         cachable        => 1,
1681 );
1682
1683 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1684 sub biblio_search_multi_class_fts {
1685         my $self = shift;
1686         my $client = shift;
1687         my %args = @_;
1688         
1689         my $sort = $args{'sort'};
1690         my $sort_dir = $args{sort_dir} || 'DESC';
1691         my $ou = $args{org_unit};
1692         my $ou_type = $args{depth};
1693         my $limit = $args{limit} || 10;
1694         my $offset = $args{offset} || 0;
1695
1696         if (!$ou) {
1697                 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1698         }
1699
1700         if (!defined($args{org_unit})) {
1701                 die "No target organizational unit passed to ".$self->api_name;
1702         }
1703
1704         if (! scalar( keys %{$args{searches}} )) {
1705                 die "No search arguments were passed to ".$self->api_name;
1706         }
1707
1708         my $outer_limit = 1000;
1709
1710         my $limit_clause = '';
1711         my $offset_clause = '';
1712
1713         $limit_clause = "LIMIT $outer_limit";
1714         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1715
1716         my (@types,@forms,@lang,@aud,@lit_form);
1717         my ($t_filter, $f_filter) = ('','');
1718         my ($a_filter, $l_filter, $lf_filter) = ('','','');
1719         my ($ot_filter, $of_filter) = ('','');
1720         my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1721
1722         if (my $a = $args{audience}) {
1723                 $a = [$a] if (!ref($a));
1724                 @aud = @$a;
1725                         
1726                 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1727                 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1728         }
1729
1730         if (my $l = $args{language}) {
1731                 $l = [$l] if (!ref($l));
1732                 @lang = @$l;
1733
1734                 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1735                 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1736         }
1737
1738         if (my $f = $args{lit_form}) {
1739                 $f = [$f] if (!ref($f));
1740                 @lit_form = @$f;
1741
1742                 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1743                 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1744         }
1745
1746         if (my $f = $args{item_form}) {
1747                 $f = [$f] if (!ref($f));
1748                 @forms = @$f;
1749
1750                 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1751                 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1752         }
1753
1754         if (my $t = $args{item_type}) {
1755                 $t = [$t] if (!ref($t));
1756                 @types = @$t;
1757
1758                 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1759                 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1760         }
1761
1762
1763         # XXX legacy format and item type support
1764         if ($args{format}) {
1765                 my ($t, $f) = split '-', $args{format};
1766                 @types = split '', $t;
1767                 @forms = split '', $f;
1768                 if (@types) {
1769                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1770                         $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1771                 }
1772
1773                 if (@forms) {
1774                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1775                         $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1776                 }
1777         }
1778
1779
1780         my $descendants = defined($ou_type) ?
1781                                 "actor.org_unit_descendants($ou, $ou_type)" :
1782                                 "actor.org_unit_descendants($ou)";
1783
1784         my $search_table_list = '';
1785         my $fts_list = '';
1786         my $join_table_list = '';
1787         my @rank_list;
1788
1789
1790         my @bonus_lists;
1791         my @bonus_values;
1792         my $prev_search_class;
1793         my $curr_search_class;
1794         for my $search_class (sort keys %{$args{searches}}) {
1795                 $prev_search_class = $curr_search_class if ($curr_search_class);
1796
1797                 $curr_search_class = $search_class;
1798
1799                 my $class = $_cdbi->{$search_class};
1800                 my $search_table = $class->table;
1801
1802                 my ($index_col) = $class->columns('FTS');
1803                 $index_col ||= 'value';
1804
1805                 
1806                 my $fts = OpenILS::Application::Storage::FTS->compile($args{searches}{$search_class}{term}, $search_class.'.value', "$search_class.$index_col");
1807
1808                 my $fts_where = $fts->sql_where_clause;
1809                 my @fts_ranks = $fts->fts_rank;
1810
1811                 my $SQLstring = join('%',map { lc($_) } $fts->words);
1812                 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1813                 my $first_word = lc(($fts->words)[0]).'%';
1814
1815                 my $rank = join(' + ', @fts_ranks);
1816
1817                 my %bonus = ();
1818                 $bonus{'keyword'} = [ { "CASE WHEN $search_class.value ILIKE ? THEN 1.2 ELSE 1 END" => $SQLstring } ];
1819
1820                 $bonus{'series'} = [
1821                         { "CASE WHEN $search_class.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1822                         { "CASE WHEN $search_class.value ~ ? THEN 200 ELSE 1 END" => $REstring },
1823                 ];
1824
1825                 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1826
1827                 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1828                 $bonus_list ||= '1';
1829
1830                 push @bonus_lists, $bonus_list;
1831                 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1832
1833                 #---------------------
1834
1835                 $search_table_list .= "$search_table $search_class, ";
1836                 push @rank_list,$rank;
1837                 $fts_list .= " AND $fts_where AND b.id = $search_class.source";
1838
1839                 if ($prev_search_class) {
1840                         $join_table_list .= " AND $prev_search_class.source = $curr_search_class.source";
1841                 }
1842         }
1843
1844         my $metabib_record_descriptor = metabib::record_descriptor->table;
1845         my $metabib_full_rec = metabib::full_rec->table;
1846         my $metabib_metarecord = metabib::metarecord->table;
1847         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1848         my $asset_call_number_table = asset::call_number->table;
1849         my $asset_copy_table = asset::copy->table;
1850         my $cs_table = config::copy_status->table;
1851         my $cl_table = asset::copy_location->table;
1852         my $br_table = biblio::record_entry->table;
1853
1854
1855         my $bonuses = join (' * ', @bonus_lists);
1856         my $relevance = join (' + ', @rank_list);
1857         $relevance = "AVG( ($relevance) * ($bonuses) )";
1858
1859
1860         my $rank = $relevance;
1861         if (lc($sort) eq 'pubdate') {
1862                 $rank = <<"             RANK";
1863                         ( FIRST ((
1864                                 SELECT  COALESCE(SUBSTRING(frp.value FROM '\\\\d{4}'),'9999')::INT
1865                                   FROM  $metabib_full_rec frp
1866                                   WHERE frp.record = b.id
1867                                         AND frp.tag = '260'
1868                                         AND frp.subfield = 'c'
1869                                   LIMIT 1
1870                         )) )
1871                 RANK
1872         } elsif (lc($sort) eq 'create_date') {
1873                 $rank = <<"             RANK";
1874                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
1875                 RANK
1876         } elsif (lc($sort) eq 'edit_date') {
1877                 $rank = <<"             RANK";
1878                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
1879                 RANK
1880         } elsif (lc($sort) eq 'title') {
1881                 $rank = <<"             RANK";
1882                         ( FIRST ((
1883                                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, frt.ind2::text::int )),'zzzzzzzz')
1884                                   FROM  $metabib_full_rec frt
1885                                   WHERE frt.record = b.id
1886                                         AND frt.tag = '245'
1887                                         AND frt.subfield = 'a'
1888                                   LIMIT 1
1889                         )) )
1890                 RANK
1891         } elsif (lc($sort) eq 'author') {
1892                 $rank = <<"             RANK";
1893                         ( FIRST((
1894                                 SELECT  COALESCE(LTRIM(fra.value),'zzzzzzzz')
1895                                   FROM  $metabib_full_rec fra
1896                                   WHERE fra.record = b.id
1897                                         AND fra.tag LIKE '1%'
1898                                         AND fra.subfield = 'a'
1899                                   ORDER BY fra.tag::text::int
1900                                   LIMIT 1
1901                         )) )
1902                 RANK
1903         } else {
1904                 push @bonus_values, @bonus_values;
1905                 $sort = undef;
1906         }
1907
1908
1909         my $select = <<"        SQL";
1910                 SELECT  b.id,
1911                         $relevance AS rel,
1912                         $rank AS rank
1913                 FROM    $search_table_list
1914                         $metabib_record_descriptor rd,
1915                         $br_table b
1916                 WHERE   rd.record = b.id
1917                         AND b.active IS TRUE
1918                         AND b.deleted IS FALSE
1919                         $fts_list
1920                         $join_table_list
1921                         $t_filter
1922                         $f_filter
1923                         $a_filter
1924                         $l_filter
1925                         $lf_filter
1926                 GROUP BY b.id
1927                 ORDER BY 3 $sort_dir
1928                 LIMIT 10000
1929         SQL
1930
1931         if ($self->api_name !~ /staff/o) {
1932                 $select = <<"           SQL";
1933
1934                         SELECT  s.*
1935                           FROM  ($select) s
1936                           WHERE EXISTS (
1937                                 SELECT  1
1938                                   FROM  $asset_call_number_table cn,
1939                                         $asset_copy_table cp,
1940                                         $cs_table cs,
1941                                         $cl_table cl,
1942                                         $descendants d
1943                                   WHERE cn.record = s.id
1944                                         AND cp.status = cs.id
1945                                         AND cp.location = cl.id
1946                                         AND cn.owning_lib = d.id
1947                                         AND cp.call_number = cn.id
1948                                         AND cp.opac_visible IS TRUE
1949                                         AND cs.holdable IS TRUE
1950                                         AND cl.opac_visible IS TRUE
1951                                         AND cp.deleted IS FALSE
1952                                   LIMIT 1
1953                                 )
1954                           ORDER BY 3 $sort_dir
1955                 SQL
1956         } else {
1957                 $select = <<"           SQL";
1958
1959                         SELECT  s.*
1960                           FROM  ($select) s
1961                           WHERE EXISTS (
1962                                 SELECT  1
1963                                   FROM  $asset_call_number_table cn,
1964                                         $descendants d
1965                                   WHERE cn.record = s.id
1966                                         AND cn.owning_lib = d.id
1967                                   LIMIT 1
1968                                 )
1969                                 OR NOT EXISTS (
1970                                 SELECT  1
1971                                   FROM  $asset_call_number_table cn
1972                                   WHERE cn.record = s.id
1973                                   LIMIT 1
1974                                 )
1975                           ORDER BY 3 $sort_dir
1976                 SQL
1977         }
1978
1979
1980         $log->debug("Field Search SQL :: [$select]",DEBUG);
1981
1982         my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1983                         $select, {},
1984                         @bonus_values, @types, @forms, @aud, @lang, @lit_form
1985         );
1986         
1987         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1988
1989         my $max = 0;
1990         $max = 1 if (!@$recs);
1991         for (@$recs) {
1992                 $max = $$_[1] if ($$_[1] > $max);
1993         }
1994
1995         my $count = scalar(@$recs);
1996         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1997                 next unless ($$rec[0]);
1998                 my ($mrid,$rank) = @$rec;
1999                 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $count] );
2000         }
2001         return undef;
2002 }
2003
2004 __PACKAGE__->register_method(
2005         api_name        => "open-ils.storage.biblio.multiclass.search_fts.record",
2006         method          => 'biblio_search_multi_class_fts',
2007         api_level       => 1,
2008         stream          => 1,
2009         cachable        => 1,
2010 );
2011 __PACKAGE__->register_method(
2012         api_name        => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2013         method          => 'biblio_search_multi_class_fts',
2014         api_level       => 1,
2015         stream          => 1,
2016         cachable        => 1,
2017 );
2018
2019
2020
2021 __PACKAGE__->register_method(
2022         api_name        => "open-ils.storage.biblio.multiclass.search_fts",
2023         method          => 'biblio_search_multi_class_fts',
2024         api_level       => 1,
2025         stream          => 1,
2026         cachable        => 1,
2027 );
2028 __PACKAGE__->register_method(
2029         api_name        => "open-ils.storage.biblio.multiclass.search_fts.staff",
2030         method          => 'biblio_search_multi_class_fts',
2031         api_level       => 1,
2032         stream          => 1,
2033         cachable        => 1,
2034 );
2035
2036
2037 1;
2038