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