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