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