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