]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Storage/Publisher/metabib.pm
sort records within a metarecord by type/quality
[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
27         my (@types,@forms);
28
29         if ($formats) {
30                 my ($t, $f) = split '-', $formats;
31                 @types = split '', $t;
32                 @forms = split '', $f;
33         }
34
35         my $copies_visible = 'AND cp.opac_visible IS TRUE AND cs.holdable IS TRUE AND cl.opac_visible IS TRUE';
36         $copies_visible = '' if ($self->api_name =~ /staff/o);
37
38         my $sm_table = metabib::metarecord_source_map->table;
39         my $rd_table = metabib::record_descriptor->table;
40         my $cn_table = asset::call_number->table;
41         my $cl_table = asset::copy_location->table;
42         my $cp_table = asset::copy->table;
43         my $cs_table = config::copy_status->table;
44         my $out_table = actor::org_unit_type->table;
45         my $br_table = biblio::record_entry->table;
46
47         my $sql = <<"   SQL";
48          SELECT record, item_type, item_form, count
49            FROM (
50                 SELECT  rd.record,
51                         rd.item_type,
52                         rd.item_form,
53                         br.quality,
54         SQL
55
56         if ($copies_visible) {
57                 $sql .= <<"             SQL"; 
58                         sum((SELECT     count(cp.id)
59                                FROM     $cp_table cp
60                                         JOIN $cs_table cs ON (cp.status = cs.id)
61                                         JOIN $cl_table cl ON (cp.location = cl.id)
62                                WHERE    cn.id = cp.call_number
63                                         $copies_visible
64                           )) AS count
65                 SQL
66         } else {
67                 $sql .= '0 AS count';
68         }
69
70         if ($copies_visible) {
71                 $sql .= <<"             SQL";
72                   FROM  $cn_table cn,
73                         $sm_table sm,
74                         $br_table br,
75                         $rd_table rd
76                   WHERE rd.record = sm.source
77                         AND br.id = rd.record
78                         AND cn.record = rd.record
79                         AND sm.metarecord = ?
80                 SQL
81         } else {
82                 $sql .= <<"             SQL";
83                   FROM  $sm_table sm,
84                         $br_table br,
85                         $rd_table rd
86                   WHERE rd.record = sm.source
87                         AND br.id = rd.record
88                         AND sm.metarecord = ?
89                 SQL
90         }
91
92         $sql .= <<"     SQL";
93                   GROUP BY rd.record, rd.item_type, rd.item_form, br.quality
94                   ORDER BY
95                         CASE
96                                 WHEN rd.item_type IS NULL -- default
97                                         THEN 0
98                                 WHEN rd.item_type = '' -- default
99                                         THEN 0
100                                 WHEN rd.item_type IN ('a','t') -- books
101                                         THEN 1
102                                 WHEN rd.item_type = 'g' -- movies
103                                         THEN 2
104                                 WHEN rd.item_type IN ('i','j') -- sound recordings
105                                         THEN 3
106                                 WHEN rd.item_type = 'm' -- software
107                                         THEN 4
108                                 WHEN rd.item_type = 'k' -- images
109                                         THEN 5
110                                 WHEN rd.item_type IN ('e','f') -- maps
111                                         THEN 6
112                                 WHEN rd.item_type IN ('o','p') -- mixed
113                                         THEN 7
114                                 WHEN rd.item_type IN ('c','d') -- music
115                                         THEN 8
116                                 WHEN rd.item_type = 'r' -- 3d
117                                         THEN 9
118                         END,
119                         br.quality DESC,
120                         count DESC
121                 ) x
122         SQL
123
124         if ($copies_visible) {
125                 $sql .= ' WHERE x.count > 0'
126         }
127
128         if (@types) {
129                 $sql .= ' AND x.item_type IN ('.join(',',map{'?'}@types).')';
130         }
131
132         if (@forms) {
133                 $sql .= ' AND x.item_form IN ('.join(',',map{'?'}@forms).')';
134         }
135
136         my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
137         $sth->execute("$mr", @types, @forms);
138         while ( my $row = $sth->fetchrow_arrayref ) {
139                 $client->respond( $$row[0] );
140         }
141         return undef;
142
143 }
144 __PACKAGE__->register_method(
145         api_name        => 'open-ils.storage.ordered.metabib.metarecord.records',
146         method          => 'ordered_records_from_metarecord',
147         api_level       => 1,
148         stream          => 1,
149         cachable        => 1,
150 );
151 __PACKAGE__->register_method(
152         api_name        => 'open-ils.storage.ordered.metabib.metarecord.records.staff',
153         method          => 'ordered_records_from_metarecord',
154         api_level       => 1,
155         stream          => 1,
156         cachable        => 1,
157 );
158
159
160 sub metarecord_copy_count {
161         my $self = shift;
162         my $client = shift;
163
164         my %args = @_;
165
166         my $sm_table = metabib::metarecord_source_map->table;
167         my $rd_table = metabib::record_descriptor->table;
168         my $cn_table = asset::call_number->table;
169         my $cp_table = asset::copy->table;
170         my $cl_table = asset::copy_location->table;
171         my $cs_table = config::copy_status->table;
172         my $out_table = actor::org_unit_type->table;
173         my $descendants = "actor.org_unit_descendants(u.id)";
174         my $ancestors = "actor.org_unit_ancestors(?)";
175
176         my $copies_visible = 'AND cp.opac_visible IS TRUE AND cs.holdable IS TRUE AND cl.opac_visible IS TRUE';
177         $copies_visible = '' if ($self->api_name =~ /staff/o);
178
179         my (@types,@forms);
180         my ($t_filter, $f_filter) = ('','');
181
182         if ($args{format}) {
183                 my ($t, $f) = split '-', $args{format};
184                 @types = split '', $t;
185                 @forms = split '', $f;
186                 if (@types) {
187                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
188                 }
189
190                 if (@forms) {
191                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
192                 }
193         }
194
195         my $sql = <<"   SQL";
196                 SELECT  t.depth,
197                         u.id AS org_unit,
198                         sum(
199                                 (SELECT count(cp.id)
200                                   FROM  $sm_table r
201                                         JOIN $cn_table cn ON (cn.record = r.source)
202                                         JOIN $rd_table rd ON (cn.record = rd.record)
203                                         JOIN $cp_table cp ON (cn.id = cp.call_number)
204                                         JOIN $cs_table cs ON (cp.status = cs.id)
205                                         JOIN $cl_table cl ON (cp.location = cl.id)
206                                         JOIN $descendants a ON (cp.circ_lib = a.id)
207                                   WHERE r.metarecord = ?
208                                         $copies_visible
209                                         $t_filter
210                                         $f_filter
211                                 )
212                         ) AS count,
213                         sum(
214                                 (SELECT count(cp.id)
215                                   FROM  $sm_table r
216                                         JOIN $cn_table cn ON (cn.record = r.source)
217                                         JOIN $rd_table rd ON (cn.record = rd.record)
218                                         JOIN $cp_table cp ON (cn.id = cp.call_number)
219                                         JOIN $cs_table cs ON (cp.status = cs.id)
220                                         JOIN $cl_table cl ON (cp.location = cl.id)
221                                         JOIN $descendants a ON (cp.circ_lib = a.id)
222                                   WHERE r.metarecord = ?
223                                         AND cp.status = 0
224                                         $copies_visible
225                                         $t_filter
226                                         $f_filter
227                                 )
228                         ) AS available
229
230                   FROM  $ancestors u
231                         JOIN $out_table t ON (u.ou_type = t.id)
232                   GROUP BY 1,2
233         SQL
234
235         my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
236         $sth->execute(  ''.$args{metarecord},
237                         @types, 
238                         @forms,
239                         ''.$args{metarecord},
240                         @types, 
241                         @forms,
242                         ''.$args{org_unit}, 
243         ); 
244
245         while ( my $row = $sth->fetchrow_hashref ) {
246                 $client->respond( $row );
247         }
248         return undef;
249 }
250 __PACKAGE__->register_method(
251         api_name        => 'open-ils.storage.metabib.metarecord.copy_count',
252         method          => 'metarecord_copy_count',
253         api_level       => 1,
254         stream          => 1,
255         cachable        => 1,
256 );
257 __PACKAGE__->register_method(
258         api_name        => 'open-ils.storage.metabib.metarecord.copy_count.staff',
259         method          => 'metarecord_copy_count',
260         api_level       => 1,
261         stream          => 1,
262         cachable        => 1,
263 );
264
265 sub biblio_multi_search_full_rec {
266         my $self = shift;
267         my $client = shift;
268
269         my %args = @_;  
270         my $class_join = $args{class_join} || 'AND';
271         my $limit = $args{limit} || 100;
272         my $offset = $args{offset} || 0;
273         my $sort = $args{'sort'};
274         my $sort_dir = $args{sort_dir} || 'DESC';
275
276         my @binds;
277         my @selects;
278
279         for my $arg (@{ $args{searches} }) {
280                 my $term = $$arg{term};
281                 my $limiters = $$arg{restrict};
282
283                 my ($index_col) = metabib::full_rec->columns('FTS');
284                 $index_col ||= 'value';
285                 my $search_table = metabib::full_rec->table;
286
287                 my $fts = OpenILS::Application::Storage::FTS->compile($term, 'value',"$index_col");
288
289                 my $fts_where = $fts->sql_where_clause();
290                 my @fts_ranks = $fts->fts_rank;
291
292                 my $rank = join(' + ', @fts_ranks);
293
294                 my @wheres;
295                 for my $limit (@$limiters) {
296                         push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
297                         push @binds, $$limit{tag}, $$limit{subfield};
298                         $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
299                 }
300                 my $where = join(' OR ', @wheres);
301
302                 push @selects, "SELECT id, record, $rank as sum FROM $search_table WHERE $where";
303
304         }
305
306         my $descendants = defined($args{depth}) ?
307                                 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
308                                 "actor.org_unit_descendants($args{org_unit})" ;
309
310
311         my $metabib_record_descriptor = metabib::record_descriptor->table;
312         my $metabib_full_rec = metabib::full_rec->table;
313         my $asset_call_number_table = asset::call_number->table;
314         my $asset_copy_table = asset::copy->table;
315         my $cs_table = config::copy_status->table;
316         my $cl_table = asset::copy_location->table;
317         my $br_table = biblio::record_entry->table;
318
319         my $cj = 'HAVING COUNT(x.id) = ' . scalar(@selects) if ($class_join eq 'AND');
320         my $search_table =
321                 '(SELECT x.record, sum(x.sum) FROM (('.
322                         join(') UNION ALL (', @selects).
323                         ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
324
325         my $has_vols = 'AND cn.owning_lib = d.id';
326         my $has_copies = 'AND cp.call_number = cn.id';
327         my $copies_visible = 'AND cp.opac_visible IS TRUE AND cs.holdable IS TRUE AND cl.opac_visible IS TRUE';
328
329         if ($self->api_name =~ /staff/o) {
330                 $copies_visible = '';
331                 $has_copies = '' if ($ou_type == 0);
332                 $has_vols = '' if ($ou_type == 0);
333         }
334
335         my ($t_filter, $f_filter) = ('','');
336         my ($a_filter, $l_filter, $lf_filter) = ('','','');
337
338         if (my $a = $args{audience}) {
339                 $a = [$a] if (!ref($a));
340                 my @aud = @$a;
341                         
342                 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
343                 push @binds, @aud;
344         }
345
346         if (my $l = $args{language}) {
347                 $l = [$l] if (!ref($l));
348                 my @lang = @$l;
349
350                 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
351                 push @binds, @lang;
352         }
353
354         if (my $f = $args{lit_form}) {
355                 $f = [$f] if (!ref($f));
356                 my @lit_form = @$f;
357
358                 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
359                 push @binds, @lit_form;
360         }
361
362         if (my $f = $args{item_form}) {
363                 $f = [$f] if (!ref($f));
364                 my @forms = @$f;
365
366                 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
367                 push @binds, @forms;
368         }
369
370         if (my $t = $args{item_type}) {
371                 $t = [$t] if (!ref($t));
372                 my @types = @$t;
373
374                 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
375                 push @binds, @types;
376         }
377
378
379         if ($args{format}) {
380                 my ($t, $f) = split '-', $args{format};
381                 my @types = split '', $t;
382                 my @forms = split '', $f;
383                 if (@types) {
384                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
385                 }
386
387                 if (@forms) {
388                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
389                 }
390                 push @binds, @types, @forms;
391         }
392
393         my $relevance = 'sum(f.sum)';
394         $relevance = 1 if (!$copies_visible);
395
396         my $rank = $relevance;
397         if (lc($sort) eq 'pubdate') {
398                 $rank = <<"             RANK";
399                         ( FIRST ((
400                                 SELECT  COALESCE(SUBSTRING(frp.value FROM '\\\\d+'),'9999')::INT
401                                   FROM  $metabib_full_rec frp
402                                   WHERE frp.record = f.record
403                                         AND frp.tag = '260'
404                                         AND frp.subfield = 'c'
405                                   LIMIT 1
406                         )) )
407                 RANK
408         } elsif (lc($sort) eq 'create_date') {
409                 $rank = <<"             RANK";
410                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
411                 RANK
412         } elsif (lc($sort) eq 'edit_date') {
413                 $rank = <<"             RANK";
414                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
415                 RANK
416         } elsif (lc($sort) eq 'title') {
417                 $rank = <<"             RANK";
418                         ( FIRST ((
419                                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, frt.ind2::text::int )),'zzzzzzzz')
420                                   FROM  $metabib_full_rec frt
421                                   WHERE frt.record = f.record
422                                         AND frt.tag = '245'
423                                         AND frt.subfield = 'a'
424                                   LIMIT 1
425                         )) )
426                 RANK
427         } elsif (lc($sort) eq 'author') {
428                 $rank = <<"             RANK";
429                         ( FIRST((
430                                 SELECT  COALESCE(LTRIM(fra.value),'zzzzzzzz')
431                                   FROM  $metabib_full_rec fra
432                                   WHERE fra.record = f.record
433                                         AND fra.tag LIKE '1%'
434                                         AND fra.subfield = 'a'
435                                   ORDER BY fra.tag::text::int
436                                   LIMIT 1
437                         )) )
438                 RANK
439         } else {
440                 $sort = undef;
441         }
442
443
444         if ($copies_visible) {
445                 $select = <<"           SQL";
446                         SELECT  f.record, $relevance, count(DISTINCT cp.id), $rank
447                         FROM    $search_table f,
448                                 $asset_call_number_table cn,
449                                 $asset_copy_table cp,
450                                 $cs_table cs,
451                                 $cl_table cl,
452                                 $br_table br,
453                                 $metabib_record_descriptor rd,
454                                 $descendants d
455                         WHERE   br.id = f.record
456                                 AND cn.record = f.record
457                                 AND rd.record = f.record
458                                 AND cp.status = cs.id
459                                 AND cp.location = cl.id
460                                 AND br.deleted IS FALSE
461                                 AND cn.deleted IS FALSE
462                                 AND cp.deleted IS FALSE
463                                 $has_vols
464                                 $has_copies
465                                 $copies_visible
466                                 $t_filter
467                                 $f_filter
468                                 $a_filter
469                                 $l_filter
470                                 $lf_filter
471                         GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
472                         ORDER BY 4 $sort_dir,3 DESC
473                 SQL
474         } else {
475                 $select = <<"           SQL";
476                         SELECT  f.record, 1, 1, $rank
477                         FROM    $search_table f,
478                                 $br_table br,
479                                 $metabib_record_descriptor rd
480                         WHERE   br.id = f.record
481                                 AND rd.record = f.record
482                                 AND br.deleted IS FALSE
483                                 $t_filter
484                                 $f_filter
485                                 $a_filter
486                                 $l_filter
487                                 $lf_filter
488                         GROUP BY 1,2,3 
489                         ORDER BY 4 $sort_dir
490                 SQL
491         }
492
493
494         $log->debug("Search SQL :: [$select]",DEBUG);
495
496         my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
497         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
498
499         my $max = 0;
500         $max = 1 if (!@$recs);
501         for (@$recs) {
502                 $max = $$_[1] if ($$_[1] > $max);
503         }
504
505         my $count = @$recs;
506         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
507                 next unless ($$rec[0]);
508                 my ($rid,$rank,$junk,$skip) = @$rec;
509                 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
510         }
511         return undef;
512 }
513 __PACKAGE__->register_method(
514         api_name        => 'open-ils.storage.biblio.full_rec.multi_search',
515         method          => 'biblio_multi_search_full_rec',
516         api_level       => 1,
517         stream          => 1,
518         cachable        => 1,
519 );
520 __PACKAGE__->register_method(
521         api_name        => 'open-ils.storage.biblio.full_rec.multi_search.staff',
522         method          => 'biblio_multi_search_full_rec',
523         api_level       => 1,
524         stream          => 1,
525         cachable        => 1,
526 );
527
528 sub search_full_rec {
529         my $self = shift;
530         my $client = shift;
531
532         my %args = @_;
533         
534         my $term = $args{term};
535         my $limiters = $args{restrict};
536
537         my ($index_col) = metabib::full_rec->columns('FTS');
538         $index_col ||= 'value';
539         my $search_table = metabib::full_rec->table;
540
541         my $fts = OpenILS::Application::Storage::FTS->compile($term, 'value',"$index_col");
542
543         my $fts_where = $fts->sql_where_clause();
544         my @fts_ranks = $fts->fts_rank;
545
546         my $rank = join(' + ', @fts_ranks);
547
548         my @binds;
549         my @wheres;
550         for my $limit (@$limiters) {
551                 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
552                 push @binds, $$limit{tag}, $$limit{subfield};
553                 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
554         }
555         my $where = join(' OR ', @wheres);
556
557         my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
558
559         $log->debug("Search SQL :: [$select]",DEBUG);
560
561         my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
562         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
563
564         $client->respond($_) for (@$recs);
565         return undef;
566 }
567 __PACKAGE__->register_method(
568         api_name        => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
569         method          => 'search_full_rec',
570         api_level       => 1,
571         stream          => 1,
572         cachable        => 1,
573 );
574 __PACKAGE__->register_method(
575         api_name        => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
576         method          => 'search_full_rec',
577         api_level       => 1,
578         stream          => 1,
579         cachable        => 1,
580 );
581
582
583 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
584 sub search_class_fts {
585         my $self = shift;
586         my $client = shift;
587         my %args = @_;
588         
589         my $term = $args{term};
590         my $ou = $args{org_unit};
591         my $ou_type = $args{depth};
592         my $limit = $args{limit};
593         my $offset = $args{offset};
594
595         my $limit_clause = '';
596         my $offset_clause = '';
597
598         $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
599         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
600
601         my (@types,@forms);
602         my ($t_filter, $f_filter) = ('','');
603
604         if ($args{format}) {
605                 my ($t, $f) = split '-', $args{format};
606                 @types = split '', $t;
607                 @forms = split '', $f;
608                 if (@types) {
609                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
610                 }
611
612                 if (@forms) {
613                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
614                 }
615         }
616
617
618
619         my $descendants = defined($ou_type) ?
620                                 "actor.org_unit_descendants($ou, $ou_type)" :
621                                 "actor.org_unit_descendants($ou)";
622
623         my $class = $self->{cdbi};
624         my $search_table = $class->table;
625
626         my $metabib_record_descriptor = metabib::record_descriptor->table;
627         my $metabib_metarecord = metabib::metarecord->table;
628         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
629         my $asset_call_number_table = asset::call_number->table;
630         my $asset_copy_table = asset::copy->table;
631         my $cs_table = config::copy_status->table;
632         my $cl_table = asset::copy_location->table;
633
634         my ($index_col) = $class->columns('FTS');
635         $index_col ||= 'value';
636
637         my $fts = OpenILS::Application::Storage::FTS->compile($term, 'f.value', "f.$index_col");
638
639         my $fts_where = $fts->sql_where_clause;
640         my @fts_ranks = $fts->fts_rank;
641
642         my $rank = join(' + ', @fts_ranks);
643
644         my $has_vols = 'AND cn.owning_lib = d.id';
645         my $has_copies = 'AND cp.call_number = cn.id';
646         my $copies_visible = 'AND cp.opac_visible IS TRUE AND cs.holdable IS TRUE AND cl.opac_visible IS TRUE';
647
648         my $visible_count = ', count(DISTINCT cp.id)';
649         my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
650
651         if ($self->api_name =~ /staff/o) {
652                 $copies_visible = '';
653                 $visible_count_test = '';
654                 $has_copies = '' if ($ou_type == 0);
655                 $has_vols = '' if ($ou_type == 0);
656         }
657
658         my $rank_calc = <<"     RANK";
659                 , (SUM( $rank
660                         * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
661                         * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
662                         * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
663                 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
664         RANK
665
666         $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
667
668         if ($copies_visible) {
669                 $select = <<"           SQL";
670                         SELECT  m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
671                         FROM    $search_table f,
672                                 $metabib_metarecord_source_map_table m,
673                                 $asset_call_number_table cn,
674                                 $asset_copy_table cp,
675                                 $cs_table cs,
676                                 $cl_table cl,
677                                 $metabib_record_descriptor rd,
678                                 $descendants d
679                         WHERE   $fts_where
680                                 AND m.source = f.source
681                                 AND cn.record = m.source
682                                 AND rd.record = m.source
683                                 AND cp.status = cs.id
684                                 AND cp.location = cl.id
685                                 $has_vols
686                                 $has_copies
687                                 $copies_visible
688                                 $t_filter
689                                 $f_filter
690                         GROUP BY 1 $visible_count_test
691                         ORDER BY 2 DESC,3
692                         $limit_clause $offset_clause
693                 SQL
694         } else {
695                 $select = <<"           SQL";
696                         SELECT  m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
697                         FROM    $search_table f,
698                                 $metabib_metarecord_source_map_table m,
699                                 $metabib_record_descriptor rd
700                         WHERE   $fts_where
701                                 AND m.source = f.source
702                                 AND rd.record = m.source
703                                 $t_filter
704                                 $f_filter
705                         GROUP BY 1, 4
706                         ORDER BY 2 DESC,3
707                         $limit_clause $offset_clause
708                 SQL
709         }
710
711         $log->debug("Field Search SQL :: [$select]",DEBUG);
712
713         my $SQLstring = join('%',$fts->words);
714         my $REstring = join('\\s+',$fts->words);
715         my $first_word = ($fts->words)[0].'%';
716         my $recs = ($self->api_name =~ /unordered/o) ? 
717                         $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
718                         $class->db_Main->selectall_arrayref($select, {},
719                                 '%'.lc($SQLstring).'%',                 # phrase order match
720                                 lc($first_word),                        # first word match
721                                 '^\\s*'.lc($REstring).'\\s*/?\s*$',     # full exact match
722                                 @types, @forms
723                         );
724         
725         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
726
727         $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
728         return undef;
729 }
730
731 for my $class ( qw/title author subject keyword series/ ) {
732         __PACKAGE__->register_method(
733                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord",
734                 method          => 'search_class_fts',
735                 api_level       => 1,
736                 stream          => 1,
737                 cdbi            => "metabib::${class}_field_entry",
738                 cachable        => 1,
739         );
740         __PACKAGE__->register_method(
741                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
742                 method          => 'search_class_fts',
743                 api_level       => 1,
744                 stream          => 1,
745                 cdbi            => "metabib::${class}_field_entry",
746                 cachable        => 1,
747         );
748         __PACKAGE__->register_method(
749                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
750                 method          => 'search_class_fts',
751                 api_level       => 1,
752                 stream          => 1,
753                 cdbi            => "metabib::${class}_field_entry",
754                 cachable        => 1,
755         );
756         __PACKAGE__->register_method(
757                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
758                 method          => 'search_class_fts',
759                 api_level       => 1,
760                 stream          => 1,
761                 cdbi            => "metabib::${class}_field_entry",
762                 cachable        => 1,
763         );
764 }
765
766 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
767 sub search_class_fts_count {
768         my $self = shift;
769         my $client = shift;
770         my %args = @_;
771         
772         my $term = $args{term};
773         my $ou = $args{org_unit};
774         my $ou_type = $args{depth};
775         my $limit = $args{limit} || 100;
776         my $offset = $args{offset} || 0;
777
778         my $descendants = defined($ou_type) ?
779                                 "actor.org_unit_descendants($ou, $ou_type)" :
780                                 "actor.org_unit_descendants($ou)";
781                 
782         my (@types,@forms);
783         my ($t_filter, $f_filter) = ('','');
784
785         if ($args{format}) {
786                 my ($t, $f) = split '-', $args{format};
787                 @types = split '', $t;
788                 @forms = split '', $f;
789                 if (@types) {
790                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
791                 }
792
793                 if (@forms) {
794                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
795                 }
796         }
797
798
799         (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
800
801         my $class = $self->{cdbi};
802         my $search_table = $class->table;
803
804         my $metabib_record_descriptor = metabib::record_descriptor->table;
805         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
806         my $asset_call_number_table = asset::call_number->table;
807         my $asset_copy_table = asset::copy->table;
808         my $cs_table = config::copy_status->table;
809         my $cl_table = asset::copy_location->table;
810
811         my ($index_col) = $class->columns('FTS');
812         $index_col ||= 'value';
813
814         my $fts = OpenILS::Application::Storage::FTS->compile($term, 'value',"$index_col");
815
816         my $fts_where = $fts->sql_where_clause;
817
818         my $has_vols = 'AND cn.owning_lib = d.id';
819         my $has_copies = 'AND cp.call_number = cn.id';
820         my $copies_visible = 'AND cp.opac_visible IS TRUE AND cs.holdable IS TRUE AND cl.opac_visible IS TRUE';
821         if ($self->api_name =~ /staff/o) {
822                 $copies_visible = '';
823                 $has_vols = '' if ($ou_type == 0);
824                 $has_copies = '' if ($ou_type == 0);
825         }
826
827         # XXX test an "EXISTS version of descendant checking...
828         my $select;
829         if ($copies_visible) {
830                 $select = <<"           SQL";
831                 SELECT  count(distinct  m.metarecord)
832                   FROM  $search_table f,
833                         $metabib_metarecord_source_map_table m,
834                         $metabib_metarecord_source_map_table mr,
835                         $asset_call_number_table cn,
836                         $asset_copy_table cp,
837                         $cs_table cs,
838                         $cl_table cl,
839                         $metabib_record_descriptor rd,
840                         $descendants d
841                   WHERE $fts_where
842                         AND mr.source = f.source
843                         AND mr.metarecord = m.metarecord
844                         AND cn.record = m.source
845                         AND rd.record = m.source
846                         AND cp.status = cs.id
847                         AND cp.location = cl.id
848                         $has_vols
849                         $has_copies
850                         $copies_visible
851                         $t_filter
852                         $f_filter
853                 SQL
854         } else {
855                 $select = <<"           SQL";
856                 SELECT  count(distinct  m.metarecord)
857                   FROM  $search_table f,
858                         $metabib_metarecord_source_map_table m,
859                         $metabib_metarecord_source_map_table mr,
860                         $metabib_record_descriptor rd
861                   WHERE $fts_where
862                         AND mr.source = f.source
863                         AND mr.metarecord = m.metarecord
864                         AND rd.record = m.source
865                         $t_filter
866                         $f_filter
867                 SQL
868         }
869
870         $log->debug("Field Search Count SQL :: [$select]",DEBUG);
871
872         my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
873         
874         $log->debug("Count Search yielded $recs results.",DEBUG);
875
876         return $recs;
877
878 }
879 for my $class ( qw/title author subject keyword series/ ) {
880         __PACKAGE__->register_method(
881                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
882                 method          => 'search_class_fts_count',
883                 api_level       => 1,
884                 stream          => 1,
885                 cdbi            => "metabib::${class}_field_entry",
886                 cachable        => 1,
887         );
888         __PACKAGE__->register_method(
889                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
890                 method          => 'search_class_fts_count',
891                 api_level       => 1,
892                 stream          => 1,
893                 cdbi            => "metabib::${class}_field_entry",
894                 cachable        => 1,
895         );
896 }
897
898
899 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
900 sub postfilter_search_class_fts {
901         my $self = shift;
902         my $client = shift;
903         my %args = @_;
904         
905         my $term = $args{term};
906         my $sort = $args{'sort'};
907         my $sort_dir = $args{sort_dir} || 'DESC';
908         my $ou = $args{org_unit};
909         my $ou_type = $args{depth};
910         my $limit = $args{limit} || 10;
911         my $offset = $args{offset} || 0;
912
913         my $outer_limit = 1000;
914
915         my $limit_clause = '';
916         my $offset_clause = '';
917
918         $limit_clause = "LIMIT $outer_limit";
919         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
920
921         my (@types,@forms,@lang,@aud,@lit_form);
922         my ($t_filter, $f_filter) = ('','');
923         my ($a_filter, $l_filter, $lf_filter) = ('','','');
924         my ($ot_filter, $of_filter) = ('','');
925         my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
926
927         if (my $a = $args{audience}) {
928                 $a = [$a] if (!ref($a));
929                 @aud = @$a;
930                         
931                 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
932                 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
933         }
934
935         if (my $l = $args{language}) {
936                 $l = [$l] if (!ref($l));
937                 @lang = @$l;
938
939                 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
940                 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
941         }
942
943         if (my $f = $args{lit_form}) {
944                 $f = [$f] if (!ref($f));
945                 @lit_form = @$f;
946
947                 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
948                 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
949         }
950
951         if ($args{format}) {
952                 my ($t, $f) = split '-', $args{format};
953                 @types = split '', $t;
954                 @forms = split '', $f;
955                 if (@types) {
956                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
957                         $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
958                 }
959
960                 if (@forms) {
961                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
962                         $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
963                 }
964         }
965
966
967         my $descendants = defined($ou_type) ?
968                                 "actor.org_unit_descendants($ou, $ou_type)" :
969                                 "actor.org_unit_descendants($ou)";
970
971         my $class = $self->{cdbi};
972         my $search_table = $class->table;
973
974         my $metabib_full_rec = metabib::full_rec->table;
975         my $metabib_record_descriptor = metabib::record_descriptor->table;
976         my $metabib_metarecord = metabib::metarecord->table;
977         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
978         my $asset_call_number_table = asset::call_number->table;
979         my $asset_copy_table = asset::copy->table;
980         my $cs_table = config::copy_status->table;
981         my $cl_table = asset::copy_location->table;
982         my $br_table = biblio::record_entry->table;
983
984         my ($index_col) = $class->columns('FTS');
985         $index_col ||= 'value';
986
987         my $fts = OpenILS::Application::Storage::FTS->compile($term, 'f.value', "f.$index_col");
988
989         my $SQLstring = join('%',$fts->words);
990         my $REstring = '^' . join('\s+',$fts->words) . '\W*$';
991         my $first_word = ($fts->words)[0].'%';
992
993         my $fts_where = $fts->sql_where_clause;
994         my @fts_ranks = $fts->fts_rank;
995
996         my %bonus = ();
997         $bonus{'metabib::keyword_field_entry'} = [ { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring } ];
998         $bonus{'metabib::title_field_entry'} =
999                 $bonus{'metabib::series_field_entry'} = [
1000                         { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1001                         { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1002                         @{ $bonus{'metabib::keyword_field_entry'} }
1003                 ];
1004
1005         my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1006         $bonus_list ||= '1';
1007
1008         my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1009
1010         my $relevance = join(' + ', @fts_ranks);
1011         $relevance = <<"        RANK";
1012                         (SUM( ( $relevance )  * ( $bonus_list ) )/COUNT(m.source))
1013         RANK
1014
1015         my $rank = $relevance;
1016         if (lc($sort) eq 'pubdate') {
1017                 $rank = <<"             RANK";
1018                         ( FIRST ((
1019                                 SELECT  COALESCE(SUBSTRING(frp.value FROM '\\\\d+'),'9999')::INT
1020                                   FROM  $metabib_full_rec frp
1021                                   WHERE frp.record = mr.master_record
1022                                         AND frp.tag = '260'
1023                                         AND frp.subfield = 'c'
1024                                   LIMIT 1
1025                         )) )
1026                 RANK
1027         } elsif (lc($sort) eq 'create_date') {
1028                 $rank = <<"             RANK";
1029                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1030                 RANK
1031         } elsif (lc($sort) eq 'edit_date') {
1032                 $rank = <<"             RANK";
1033                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1034                 RANK
1035         } elsif (lc($sort) eq 'title') {
1036                 $rank = <<"             RANK";
1037                         ( FIRST ((
1038                                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, frt.ind2::text::int )),'zzzzzzzz')
1039                                   FROM  $metabib_full_rec frt
1040                                   WHERE frt.record = mr.master_record
1041                                         AND frt.tag = '245'
1042                                         AND frt.subfield = 'a'
1043                                   LIMIT 1
1044                         )) )
1045                 RANK
1046         } elsif (lc($sort) eq 'author') {
1047                 $rank = <<"             RANK";
1048                         ( FIRST((
1049                                 SELECT  COALESCE(LTRIM(fra.value),'zzzzzzzz')
1050                                   FROM  $metabib_full_rec fra
1051                                   WHERE fra.record = mr.master_record
1052                                         AND fra.tag LIKE '1%'
1053                                         AND fra.subfield = 'a'
1054                                   ORDER BY fra.tag::text::int
1055                                   LIMIT 1
1056                         )) )
1057                 RANK
1058         } else {
1059                 $sort = undef;
1060         }
1061
1062         my $select = <<"        SQL";
1063                 SELECT  m.metarecord,
1064                         $relevance,
1065                         CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1066                         $rank
1067                 FROM    $search_table f,
1068                         $metabib_metarecord_source_map_table m,
1069                         $metabib_metarecord_source_map_table smrs,
1070                         $metabib_metarecord mr,
1071                         $metabib_record_descriptor rd
1072                 WHERE   $fts_where
1073                         AND smrs.metarecord = mr.id
1074                         AND m.source = f.source
1075                         AND m.metarecord = mr.id
1076                         AND rd.record = smrs.source
1077                         $t_filter
1078                         $f_filter
1079                         $a_filter
1080                         $l_filter
1081                         $lf_filter
1082                 GROUP BY m.metarecord
1083                 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1084                 LIMIT 10000
1085         SQL
1086
1087         if (0) {
1088                 $select = <<"           SQL";
1089
1090                         SELECT  DISTINCT s.*
1091                           FROM  $asset_call_number_table cn,
1092                                 $metabib_metarecord_source_map_table mrs,
1093                                 $asset_copy_table cp,
1094                                 $cs_table cs,
1095                                 $cl_table cl,
1096                                 $br_table br,
1097                                 $descendants d,
1098                                 $metabib_record_descriptor ord,
1099                                 ($select) s
1100                           WHERE mrs.metarecord = s.metarecord
1101                                 AND br.id = mrs.source
1102                                 AND cn.record = mrs.source
1103                                 AND cp.status = cs.id
1104                                 AND cp.location = cl.id
1105                                 AND cn.owning_lib = d.id
1106                                 AND cp.call_number = cn.id
1107                                 AND cp.opac_visible IS TRUE
1108                                 AND cs.holdable IS TRUE
1109                                 AND cl.opac_visible IS TRUE
1110                                 AND br.active IS TRUE
1111                                 AND br.deleted IS FALSE
1112                                 AND ord.record = mrs.source
1113                                 $ot_filter
1114                                 $of_filter
1115                                 $oa_filter
1116                                 $ol_filter
1117                                 $olf_filter
1118                           ORDER BY 4 $sort_dir
1119                 SQL
1120         } elsif ($self->api_name !~ /staff/o) {
1121                 $select = <<"           SQL";
1122
1123                         SELECT  DISTINCT s.*
1124                           FROM  ($select) s
1125                           WHERE EXISTS (
1126                                 SELECT  1
1127                                   FROM  $asset_call_number_table cn,
1128                                         $metabib_metarecord_source_map_table mrs,
1129                                         $asset_copy_table cp,
1130                                         $cs_table cs,
1131                                         $cl_table cl,
1132                                         $br_table br,
1133                                         $descendants d,
1134                                         $metabib_record_descriptor ord
1135                                 
1136                                   WHERE mrs.metarecord = s.metarecord
1137                                         AND br.id = mrs.source
1138                                         AND cn.record = mrs.source
1139                                         AND cp.status = cs.id
1140                                         AND cp.location = cl.id
1141                                         AND cn.owning_lib = d.id
1142                                         AND cp.call_number = cn.id
1143                                         AND cp.opac_visible IS TRUE
1144                                         AND cs.holdable IS TRUE
1145                                         AND cl.opac_visible IS TRUE
1146                                         AND br.active IS TRUE
1147                                         AND br.deleted IS FALSE
1148                                         AND ord.record = mrs.source
1149                                         $ot_filter
1150                                         $of_filter
1151                                         $oa_filter
1152                                         $ol_filter
1153                                         $olf_filter
1154                                   LIMIT 1
1155                                 )
1156                           ORDER BY 4 $sort_dir
1157                 SQL
1158         } else {
1159                 $select = <<"           SQL";
1160
1161                         SELECT  DISTINCT s.*
1162                           FROM  ($select) s
1163                           WHERE EXISTS (
1164                                 SELECT  1
1165                                   FROM  $asset_call_number_table cn,
1166                                         $metabib_metarecord_source_map_table mrs,
1167                                         $br_table br,
1168                                         $descendants d,
1169                                         $metabib_record_descriptor ord
1170                                 
1171                                   WHERE mrs.metarecord = s.metarecord
1172                                         AND br.id = mrs.source
1173                                         AND cn.record = mrs.source
1174                                         AND cn.owning_lib = d.id
1175                                         AND br.deleted IS FALSE
1176                                         AND ord.record = mrs.source
1177                                         $ot_filter
1178                                         $of_filter
1179                                         $oa_filter
1180                                         $ol_filter
1181                                         $olf_filter
1182                                   LIMIT 1
1183                                 )
1184                                 OR NOT EXISTS (
1185                                 SELECT  1
1186                                   FROM  $asset_call_number_table cn,
1187                                         $metabib_metarecord_source_map_table mrs,
1188                                         $metabib_record_descriptor ord
1189                                   WHERE mrs.metarecord = s.metarecord
1190                                         AND cn.record = mrs.source
1191                                         AND ord.record = mrs.source
1192                                         $ot_filter
1193                                         $of_filter
1194                                         $oa_filter
1195                                         $ol_filter
1196                                         $olf_filter
1197                                   LIMIT 1
1198                                 )
1199                           ORDER BY 4 $sort_dir
1200                 SQL
1201         }
1202
1203
1204         $log->debug("Field Search SQL :: [$select]",DEBUG);
1205
1206         my $recs = $class->db_Main->selectall_arrayref(
1207                         $select, {},
1208                         (@bonus_values > 0 ? @bonus_values : () ),
1209                         ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1210                         @types, @forms, @aud, @lang, @lit_form,
1211                         @types, @forms, @aud, @lang, @lit_form,
1212                         ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1213         
1214         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1215
1216         my $max = 0;
1217         $max = 1 if (!@$recs);
1218         for (@$recs) {
1219                 $max = $$_[1] if ($$_[1] > $max);
1220         }
1221
1222         my $count = scalar(@$recs);
1223         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1224                 my ($mrid,$rank,$skip) = @$rec;
1225                 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1226         }
1227         return undef;
1228 }
1229
1230 for my $class ( qw/title author subject keyword series/ ) {
1231         __PACKAGE__->register_method(
1232                 api_name        => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1233                 method          => 'postfilter_search_class_fts',
1234                 api_level       => 1,
1235                 stream          => 1,
1236                 cdbi            => "metabib::${class}_field_entry",
1237                 cachable        => 1,
1238         );
1239         __PACKAGE__->register_method(
1240                 api_name        => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1241                 method          => 'postfilter_search_class_fts',
1242                 api_level       => 1,
1243                 stream          => 1,
1244                 cdbi            => "metabib::${class}_field_entry",
1245                 cachable        => 1,
1246         );
1247 }
1248
1249
1250
1251 my $_cdbi = {   title   => "metabib::title_field_entry",
1252                 author  => "metabib::author_field_entry",
1253                 subject => "metabib::subject_field_entry",
1254                 keyword => "metabib::keyword_field_entry",
1255                 series  => "metabib::series_field_entry",
1256 };
1257
1258 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1259 sub postfilter_search_multi_class_fts {
1260         my $self = shift;
1261         my $client = shift;
1262         my %args = @_;
1263         
1264         my $sort = $args{'sort'};
1265         my $sort_dir = $args{sort_dir} || 'DESC';
1266         my $ou = $args{org_unit};
1267         my $ou_type = $args{depth};
1268         my $limit = $args{limit} || 10;;
1269         my $offset = $args{offset} || 0;
1270
1271         if (!$ou) {
1272                 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1273         }
1274
1275         if (!defined($args{org_unit})) {
1276                 die "No target organizational unit passed to ".$self->api_name;
1277         }
1278
1279         if (! scalar( keys %{$args{searches}} )) {
1280                 die "No search arguments were passed to ".$self->api_name;
1281         }
1282
1283         my $outer_limit = 1000;
1284
1285         my $limit_clause = '';
1286         my $offset_clause = '';
1287
1288         $limit_clause = "LIMIT $outer_limit";
1289         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1290
1291         my (@types,@forms,@lang,@aud,@lit_form);
1292         my ($t_filter, $f_filter) = ('','');
1293         my ($a_filter, $l_filter, $lf_filter) = ('','','');
1294         my ($ot_filter, $of_filter) = ('','');
1295         my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1296
1297         if (my $a = $args{audience}) {
1298                 $a = [$a] if (!ref($a));
1299                 @aud = @$a;
1300                         
1301                 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1302                 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1303         }
1304
1305         if (my $l = $args{language}) {
1306                 $l = [$l] if (!ref($l));
1307                 @lang = @$l;
1308
1309                 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1310                 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1311         }
1312
1313         if (my $f = $args{lit_form}) {
1314                 $f = [$f] if (!ref($f));
1315                 @lit_form = @$f;
1316
1317                 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1318                 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1319         }
1320
1321         if (my $f = $args{item_form}) {
1322                 $f = [$f] if (!ref($f));
1323                 @forms = @$f;
1324
1325                 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1326                 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1327         }
1328
1329         if (my $t = $args{item_type}) {
1330                 $t = [$t] if (!ref($t));
1331                 @types = @$t;
1332
1333                 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1334                 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1335         }
1336
1337
1338         # XXX legacy format and item type support
1339         if ($args{format}) {
1340                 my ($t, $f) = split '-', $args{format};
1341                 @types = split '', $t;
1342                 @forms = split '', $f;
1343                 if (@types) {
1344                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1345                         $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1346                 }
1347
1348                 if (@forms) {
1349                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1350                         $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1351                 }
1352         }
1353
1354
1355
1356         my $descendants = defined($ou_type) ?
1357                                 "actor.org_unit_descendants($ou, $ou_type)" :
1358                                 "actor.org_unit_descendants($ou)";
1359
1360         my $search_table_list = '';
1361         my $fts_list = '';
1362         my $join_table_list = '';
1363         my @rank_list;
1364
1365         my @bonus_lists;
1366         my @bonus_values;
1367         my $prev_search_class;
1368         my $curr_search_class;
1369         for my $search_class (sort keys %{$args{searches}}) {
1370                 $prev_search_class = $curr_search_class if ($curr_search_class);
1371
1372                 $curr_search_class = $search_class;
1373
1374                 my $class = $_cdbi->{$search_class};
1375                 my $search_table = $class->table;
1376
1377                 my ($index_col) = $class->columns('FTS');
1378                 $index_col ||= 'value';
1379
1380                 
1381                 my $fts = OpenILS::Application::Storage::FTS->compile($args{searches}{$search_class}{term}, $search_class.'.value', "$search_class.$index_col");
1382
1383                 my $fts_where = $fts->sql_where_clause;
1384                 my @fts_ranks = $fts->fts_rank;
1385
1386                 my $rank = join(' + ', @fts_ranks);
1387
1388                 my %bonus = ();
1389                 $bonus{'keyword'} = [ { "CASE WHEN $search_class.value ILIKE ? THEN 1.2 ELSE 1 END" => $SQLstring } ];
1390                 $bonus{'title'} =
1391                         $bonus{'metabib::series_field_entry'} = [
1392                                 { "CASE WHEN $search_class.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1393                                 { "CASE WHEN $search_class.value ~* ? THEN 2 ELSE 1 END" => $REstring },
1394                                 @{ $bonus{'keyword'} }
1395                         ];
1396
1397                 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1398                 $bonus_list ||= '1';
1399
1400                 push @bonus_lists, $bonus_list;
1401                 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1402
1403
1404                 #---------------------
1405
1406                 $search_table_list .= "$search_table $search_class, ";
1407                 push @rank_list,$rank;
1408                 $fts_list .= " AND $fts_where AND m.source = $search_class.source";
1409
1410                 if ($prev_search_class) {
1411                         $join_table_list .= " AND $prev_search_class.source = $curr_search_class.source";
1412                 }
1413         }
1414
1415         my $metabib_record_descriptor = metabib::record_descriptor->table;
1416         my $metabib_full_rec = metabib::full_rec->table;
1417         my $metabib_metarecord = metabib::metarecord->table;
1418         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1419         my $asset_call_number_table = asset::call_number->table;
1420         my $asset_copy_table = asset::copy->table;
1421         my $cs_table = config::copy_status->table;
1422         my $cl_table = asset::copy_location->table;
1423         my $br_table = biblio::record_entry->table;
1424
1425         my $bonuses = join (' * ', @bonus_lists);
1426         my $relevance = join (' + ', @rank_list);
1427         $relevance = "AVG( ($relevance) * ($bonuses) )";
1428
1429
1430         my $rank = $relevance;
1431         if (lc($sort) eq 'pubdate') {
1432                 $rank = <<"             RANK";
1433                         ( FIRST ((
1434                                 SELECT  COALESCE(SUBSTRING(frp.value FROM '\\\\d+'),'9999')::INT
1435                                   FROM  $metabib_full_rec frp
1436                                   WHERE frp.record = mr.master_record
1437                                         AND frp.tag = '260'
1438                                         AND frp.subfield = 'c'
1439                                   LIMIT 1
1440                         )) )
1441                 RANK
1442         } elsif (lc($sort) eq 'create_date') {
1443                 $rank = <<"             RANK";
1444                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1445                 RANK
1446         } elsif (lc($sort) eq 'edit_date') {
1447                 $rank = <<"             RANK";
1448                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1449                 RANK
1450         } elsif (lc($sort) eq 'title') {
1451                 $rank = <<"             RANK";
1452                         ( FIRST ((
1453                                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, frt.ind2::text::int )),'zzzzzzzz')
1454                                   FROM  $metabib_full_rec frt
1455                                   WHERE frt.record = mr.master_record
1456                                         AND frt.tag = '245'
1457                                         AND frt.subfield = 'a'
1458                                   LIMIT 1
1459                         )) )
1460                 RANK
1461         } elsif (lc($sort) eq 'author') {
1462                 $rank = <<"             RANK";
1463                         ( FIRST((
1464                                 SELECT  COALESCE(LTRIM(fra.value),'zzzzzzzz')
1465                                   FROM  $metabib_full_rec fra
1466                                   WHERE fra.record = mr.master_record
1467                                         AND fra.tag LIKE '1%'
1468                                         AND fra.subfield = 'a'
1469                                   ORDER BY fra.tag::text::int
1470                                   LIMIT 1
1471                         )) )
1472                 RANK
1473         } else {
1474                 push @bonus_values, @bonus_values;
1475                 $sort = undef;
1476         }
1477
1478
1479         my $select = <<"        SQL";
1480                 SELECT  m.metarecord,
1481                         $relevance,
1482                         CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1483                         $rank
1484                 FROM    $search_table_list
1485                         $metabib_metarecord_source_map_table m,
1486                         $metabib_metarecord_source_map_table smrs,
1487                         $metabib_metarecord mr,
1488                         $metabib_record_descriptor rd
1489                 WHERE   m.metarecord = mr.id
1490                         AND smrs.metarecord = mr.id
1491                         $fts_list
1492                         $join_table_list
1493                         AND rd.record = smrs.source
1494                         $t_filter
1495                         $f_filter
1496                         $a_filter
1497                         $l_filter
1498                         $lf_filter
1499                 GROUP BY m.metarecord
1500                 ORDER BY 4 $sort_dir
1501                 LIMIT 10000
1502         SQL
1503
1504         if ($self->api_name !~ /staff/o) {
1505                 $select = <<"           SQL";
1506
1507                         SELECT  s.*
1508                           FROM  ($select) s
1509                           WHERE EXISTS (
1510                                 SELECT  1
1511                                   FROM  $asset_call_number_table cn,
1512                                         $metabib_metarecord_source_map_table mrs,
1513                                         $asset_copy_table cp,
1514                                         $cs_table cs,
1515                                         $cl_table cl,
1516                                         $br_table br,
1517                                         $descendants d,
1518                                         $metabib_record_descriptor ord
1519                                   WHERE mrs.metarecord = s.metarecord
1520                                         AND br.id = mrs.source
1521                                         AND cn.record = mrs.source
1522                                         AND cp.status = cs.id
1523                                         AND cp.location = cl.id
1524                                         AND cn.owning_lib = d.id
1525                                         AND cp.call_number = cn.id
1526                                         AND cp.opac_visible IS TRUE
1527                                         AND cs.holdable IS TRUE
1528                                         AND cl.opac_visible IS TRUE
1529                                         AND br.active IS TRUE
1530                                         AND br.deleted IS FALSE
1531                                         AND ord.record = mrs.source
1532                                         $ot_filter
1533                                         $of_filter
1534                                         $oa_filter
1535                                         $ol_filter
1536                                         $olf_filter
1537                                   LIMIT 1
1538                                 )
1539                           ORDER BY 4 $sort_dir
1540                 SQL
1541         } else {
1542                 $select = <<"           SQL";
1543
1544                         SELECT  s.*
1545                           FROM  ($select) s
1546                           WHERE EXISTS (
1547                                 SELECT  1
1548                                   FROM  $asset_call_number_table cn,
1549                                         $metabib_metarecord_source_map_table mrs,
1550                                         $descendants d,
1551                                         $br_table br,
1552                                         $metabib_record_descriptor ord
1553                                   WHERE mrs.metarecord = s.metarecord
1554                                         AND br.id = mrs.source
1555                                         AND cn.record = mrs.source
1556                                         AND cn.owning_lib = d.id
1557                                         AND ord.record = mrs.source
1558                                         AND br.deleted IS FALSE
1559                                         $ot_filter
1560                                         $of_filter
1561                                         $oa_filter
1562                                         $ol_filter
1563                                         $olf_filter
1564                                   LIMIT 1
1565                                 )
1566                                 OR NOT EXISTS (
1567                                 SELECT  1
1568                                   FROM  $asset_call_number_table cn,
1569                                         $metabib_metarecord_source_map_table mrs,
1570                                         $metabib_record_descriptor ord
1571                                   WHERE mrs.metarecord = s.metarecord
1572                                         AND cn.record = mrs.source
1573                                         AND ord.record = mrs.source
1574                                         $ot_filter
1575                                         $of_filter
1576                                         $oa_filter
1577                                         $ol_filter
1578                                         $olf_filter
1579                                   LIMIT 1
1580                                 )
1581                           ORDER BY 4 $sort_dir
1582                 SQL
1583         }
1584
1585
1586         $log->debug("Field Search SQL :: [$select]",DEBUG);
1587
1588         my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1589                         $select, {},
1590                         @bonus_values,
1591                         @types, @forms, @aud, @lang, @lit_form,
1592                         @types, @forms, @aud, @lang, @lit_form,
1593                         ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1594         );
1595         
1596         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1597
1598         my $max = 0;
1599         $max = 1 if (!@$recs);
1600         for (@$recs) {
1601                 $max = $$_[1] if ($$_[1] > $max);
1602         }
1603
1604         my $count = scalar(@$recs);
1605         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1606                 next unless ($$rec[0]);
1607                 my ($mrid,$rank,$skip) = @$rec;
1608                 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1609         }
1610         return undef;
1611 }
1612
1613 __PACKAGE__->register_method(
1614         api_name        => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1615         method          => 'postfilter_search_multi_class_fts',
1616         api_level       => 1,
1617         stream          => 1,
1618         cachable        => 1,
1619 );
1620 __PACKAGE__->register_method(
1621         api_name        => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1622         method          => 'postfilter_search_multi_class_fts',
1623         api_level       => 1,
1624         stream          => 1,
1625         cachable        => 1,
1626 );
1627
1628 __PACKAGE__->register_method(
1629         api_name        => "open-ils.storage.metabib.multiclass.search_fts",
1630         method          => 'postfilter_search_multi_class_fts',
1631         api_level       => 1,
1632         stream          => 1,
1633         cachable        => 1,
1634 );
1635 __PACKAGE__->register_method(
1636         api_name        => "open-ils.storage.metabib.multiclass.search_fts.staff",
1637         method          => 'postfilter_search_multi_class_fts',
1638         api_level       => 1,
1639         stream          => 1,
1640         cachable        => 1,
1641 );
1642
1643 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1644 sub biblio_search_multi_class_fts {
1645         my $self = shift;
1646         my $client = shift;
1647         my %args = @_;
1648         
1649         my $sort = $args{'sort'};
1650         my $sort_dir = $args{sort_dir} || 'DESC';
1651         my $ou = $args{org_unit};
1652         my $ou_type = $args{depth};
1653         my $limit = $args{limit} || 10;
1654         my $offset = $args{offset} || 0;
1655
1656         if (!$ou) {
1657                 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1658         }
1659
1660         if (!defined($args{org_unit})) {
1661                 die "No target organizational unit passed to ".$self->api_name;
1662         }
1663
1664         if (! scalar( keys %{$args{searches}} )) {
1665                 die "No search arguments were passed to ".$self->api_name;
1666         }
1667
1668         my $outer_limit = 1000;
1669
1670         my $limit_clause = '';
1671         my $offset_clause = '';
1672
1673         $limit_clause = "LIMIT $outer_limit";
1674         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1675
1676         my (@types,@forms,@lang,@aud,@lit_form);
1677         my ($t_filter, $f_filter) = ('','');
1678         my ($a_filter, $l_filter, $lf_filter) = ('','','');
1679         my ($ot_filter, $of_filter) = ('','');
1680         my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1681
1682         if (my $a = $args{audience}) {
1683                 $a = [$a] if (!ref($a));
1684                 @aud = @$a;
1685                         
1686                 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1687                 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1688         }
1689
1690         if (my $l = $args{language}) {
1691                 $l = [$l] if (!ref($l));
1692                 @lang = @$l;
1693
1694                 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1695                 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1696         }
1697
1698         if (my $f = $args{lit_form}) {
1699                 $f = [$f] if (!ref($f));
1700                 @lit_form = @$f;
1701
1702                 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1703                 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1704         }
1705
1706         if (my $f = $args{item_form}) {
1707                 $f = [$f] if (!ref($f));
1708                 @forms = @$f;
1709
1710                 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1711                 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1712         }
1713
1714         if (my $t = $args{item_type}) {
1715                 $t = [$t] if (!ref($t));
1716                 @types = @$t;
1717
1718                 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1719                 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1720         }
1721
1722
1723         # XXX legacy format and item type support
1724         if ($args{format}) {
1725                 my ($t, $f) = split '-', $args{format};
1726                 @types = split '', $t;
1727                 @forms = split '', $f;
1728                 if (@types) {
1729                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1730                         $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1731                 }
1732
1733                 if (@forms) {
1734                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1735                         $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1736                 }
1737         }
1738
1739
1740         my $descendants = defined($ou_type) ?
1741                                 "actor.org_unit_descendants($ou, $ou_type)" :
1742                                 "actor.org_unit_descendants($ou)";
1743
1744         my $search_table_list = '';
1745         my $fts_list = '';
1746         my $join_table_list = '';
1747         my @rank_list;
1748
1749
1750         my @bonus_lists;
1751         my @bonus_values;
1752         my $prev_search_class;
1753         my $curr_search_class;
1754         for my $search_class (sort keys %{$args{searches}}) {
1755                 $prev_search_class = $curr_search_class if ($curr_search_class);
1756
1757                 $curr_search_class = $search_class;
1758
1759                 my $class = $_cdbi->{$search_class};
1760                 my $search_table = $class->table;
1761
1762                 my ($index_col) = $class->columns('FTS');
1763                 $index_col ||= 'value';
1764
1765                 
1766                 my $fts = OpenILS::Application::Storage::FTS->compile($args{searches}{$search_class}{term}, $search_class.'.value', "$search_class.$index_col");
1767
1768                 my $fts_where = $fts->sql_where_clause;
1769                 my @fts_ranks = $fts->fts_rank;
1770
1771                 my $SQLstring = join('%',$fts->words);
1772                 my $REstring = '^' . join('\s+',$fts->words) . '\W*$';
1773                 my $first_word = ($fts->words)[0].'%';
1774
1775                 my $rank = join(' + ', @fts_ranks);
1776
1777                 my %bonus = ();
1778                 $bonus{'keyword'} = [ { "CASE WHEN $search_class.value ILIKE ? THEN 1.2 ELSE 1 END" => $SQLstring } ];
1779                 $bonus{'title'} =
1780                         $bonus{'metabib::series_field_entry'} = [
1781                                 { "CASE WHEN $search_class.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1782                                 { "CASE WHEN $search_class.value ~* ? THEN 2 ELSE 1 END" => $REstring },
1783                                 @{ $bonus{'keyword'} }
1784                         ];
1785
1786                 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1787                 $bonus_list ||= '1';
1788
1789                 push @bonus_lists, $bonus_list;
1790                 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1791
1792                 #---------------------
1793
1794                 $search_table_list .= "$search_table $search_class, ";
1795                 push @rank_list,$rank;
1796                 $fts_list .= " AND $fts_where AND b.id = $search_class.source";
1797
1798                 if ($prev_search_class) {
1799                         $join_table_list .= " AND $prev_search_class.source = $curr_search_class.source";
1800                 }
1801         }
1802
1803         my $metabib_record_descriptor = metabib::record_descriptor->table;
1804         my $metabib_full_rec = metabib::full_rec->table;
1805         my $metabib_metarecord = metabib::metarecord->table;
1806         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1807         my $asset_call_number_table = asset::call_number->table;
1808         my $asset_copy_table = asset::copy->table;
1809         my $cs_table = config::copy_status->table;
1810         my $cl_table = asset::copy_location->table;
1811         my $br_table = biblio::record_entry->table;
1812
1813
1814         my $bonuses = join (' * ', @bonus_lists);
1815         my $relevance = join (' + ', @rank_list);
1816         $relevance = "AVG( ($relevance) * ($bonuses) )";
1817
1818
1819         my $rank = $relevance;
1820         if (lc($sort) eq 'pubdate') {
1821                 $rank = <<"             RANK";
1822                         ( FIRST ((
1823                                 SELECT  COALESCE(SUBSTRING(frp.value FROM '\\\\d{4}'),'9999')::INT
1824                                   FROM  $metabib_full_rec frp
1825                                   WHERE frp.record = b.id
1826                                         AND frp.tag = '260'
1827                                         AND frp.subfield = 'c'
1828                                   LIMIT 1
1829                         )) )
1830                 RANK
1831         } elsif (lc($sort) eq 'create_date') {
1832                 $rank = <<"             RANK";
1833                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
1834                 RANK
1835         } elsif (lc($sort) eq 'edit_date') {
1836                 $rank = <<"             RANK";
1837                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
1838                 RANK
1839         } elsif (lc($sort) eq 'title') {
1840                 $rank = <<"             RANK";
1841                         ( FIRST ((
1842                                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, frt.ind2::text::int )),'zzzzzzzz')
1843                                   FROM  $metabib_full_rec frt
1844                                   WHERE frt.record = b.id
1845                                         AND frt.tag = '245'
1846                                         AND frt.subfield = 'a'
1847                                   LIMIT 1
1848                         )) )
1849                 RANK
1850         } elsif (lc($sort) eq 'author') {
1851                 $rank = <<"             RANK";
1852                         ( FIRST((
1853                                 SELECT  COALESCE(LTRIM(fra.value),'zzzzzzzz')
1854                                   FROM  $metabib_full_rec fra
1855                                   WHERE fra.record = b.id
1856                                         AND fra.tag LIKE '1%'
1857                                         AND fra.subfield = 'a'
1858                                   ORDER BY fra.tag::text::int
1859                                   LIMIT 1
1860                         )) )
1861                 RANK
1862         } else {
1863                 push @bonus_values, @bonus_values;
1864                 $sort = undef;
1865         }
1866
1867
1868         my $select = <<"        SQL";
1869                 SELECT  b.id,
1870                         $relevance AS rel,
1871                         $rank AS rank
1872                 FROM    $search_table_list
1873                         $metabib_record_descriptor rd,
1874                         $br_table b
1875                 WHERE   rd.record = b.id
1876                         AND b.active IS TRUE
1877                         AND b.deleted IS FALSE
1878                         $fts_list
1879                         $join_table_list
1880                         $t_filter
1881                         $f_filter
1882                         $a_filter
1883                         $l_filter
1884                         $lf_filter
1885                 GROUP BY b.id
1886                 ORDER BY 3 $sort_dir
1887                 LIMIT 10000
1888         SQL
1889
1890         if ($self->api_name !~ /staff/o) {
1891                 $select = <<"           SQL";
1892
1893                         SELECT  s.*
1894                           FROM  ($select) s
1895                           WHERE EXISTS (
1896                                 SELECT  1
1897                                   FROM  $asset_call_number_table cn,
1898                                         $asset_copy_table cp,
1899                                         $cs_table cs,
1900                                         $cl_table cl,
1901                                         $descendants d
1902                                   WHERE cn.record = s.id
1903                                         AND cp.status = cs.id
1904                                         AND cp.location = cl.id
1905                                         AND cn.owning_lib = d.id
1906                                         AND cp.call_number = cn.id
1907                                         AND cp.opac_visible IS TRUE
1908                                         AND cs.holdable IS TRUE
1909                                         AND cl.opac_visible IS TRUE
1910                                         AND cp.deleted IS FALSE
1911                                   LIMIT 1
1912                                 )
1913                           ORDER BY 3 $sort_dir
1914                 SQL
1915         } else {
1916                 $select = <<"           SQL";
1917
1918                         SELECT  s.*
1919                           FROM  ($select) s
1920                           WHERE EXISTS (
1921                                 SELECT  1
1922                                   FROM  $asset_call_number_table cn,
1923                                         $descendants d
1924                                   WHERE cn.record = s.id
1925                                         AND cn.owning_lib = d.id
1926                                   LIMIT 1
1927                                 )
1928                                 OR NOT EXISTS (
1929                                 SELECT  1
1930                                   FROM  $asset_call_number_table cn
1931                                   WHERE cn.record = s.id
1932                                   LIMIT 1
1933                                 )
1934                           ORDER BY 3 $sort_dir
1935                 SQL
1936         }
1937
1938
1939         $log->debug("Field Search SQL :: [$select]",DEBUG);
1940
1941         my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1942                         $select, {},
1943                         @bonus_values, @types, @forms, @aud, @lang, @lit_form
1944         );
1945         
1946         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1947
1948         my $max = 0;
1949         $max = 1 if (!@$recs);
1950         for (@$recs) {
1951                 $max = $$_[1] if ($$_[1] > $max);
1952         }
1953
1954         my $count = scalar(@$recs);
1955         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1956                 next unless ($$rec[0]);
1957                 my ($mrid,$rank) = @$rec;
1958                 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $count] );
1959         }
1960         return undef;
1961 }
1962
1963 __PACKAGE__->register_method(
1964         api_name        => "open-ils.storage.biblio.multiclass.search_fts.record",
1965         method          => 'biblio_search_multi_class_fts',
1966         api_level       => 1,
1967         stream          => 1,
1968         cachable        => 1,
1969 );
1970 __PACKAGE__->register_method(
1971         api_name        => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
1972         method          => 'biblio_search_multi_class_fts',
1973         api_level       => 1,
1974         stream          => 1,
1975         cachable        => 1,
1976 );
1977
1978
1979
1980 __PACKAGE__->register_method(
1981         api_name        => "open-ils.storage.biblio.multiclass.search_fts",
1982         method          => 'biblio_search_multi_class_fts',
1983         api_level       => 1,
1984         stream          => 1,
1985         cachable        => 1,
1986 );
1987 __PACKAGE__->register_method(
1988         api_name        => "open-ils.storage.biblio.multiclass.search_fts.staff",
1989         method          => 'biblio_search_multi_class_fts',
1990         api_level       => 1,
1991         stream          => 1,
1992         cachable        => 1,
1993 );
1994
1995
1996 1;
1997
1998
1999 __END__
2000
2001
2002 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2003 sub postfilter_Z_search_class_fts {
2004         my $self = shift;
2005         my $client = shift;
2006         my %args = @_;
2007         
2008         my $term = $args{term};
2009         my $sort = $args{'sort'};
2010         my $sort_dir = $args{sort_dir} || 'DESC';
2011         my $ou = $args{org_unit};
2012         my $ou_type = $args{depth};
2013         my $limit = $args{limit} || 10;
2014         my $offset = $args{offset} || 0;
2015
2016         my (@types,@forms);
2017         my ($t_filter, $f_filter) = ('','');
2018         my ($ot_filter, $of_filter) = ('','');
2019
2020         if ($args{format}) {
2021                 my ($t, $f) = split '-', $args{format};
2022                 @types = split '', $t;
2023                 @forms = split '', $f;
2024                 if (@types) {
2025                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2026                         $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2027                 }
2028
2029                 if (@forms) {
2030                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2031                         $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2032                 }
2033         }
2034
2035
2036
2037         my $class = $self->{cdbi};
2038         my $search_table = $class->table;
2039         my $metabib_record_descriptor = metabib::record_descriptor->table;
2040         my $metabib_full_rec = metabib::full_rec->table;
2041         my $asset_call_number_table = asset::call_number->table;
2042         my $asset_copy_table = asset::copy->table;
2043         my $cs_table = config::copy_status->table;
2044         my $cl_table = asset::copy_location->table;
2045         my $br_table = biblio::record_entry->table;
2046
2047         my ($index_col) = $class->columns('FTS');
2048         $index_col ||= 'value';
2049
2050         my $fts = OpenILS::Application::Storage::FTS->compile($term, 'f.value', "f.$index_col");
2051
2052         my $fts_where = $fts->sql_where_clause;
2053         my @fts_ranks = $fts->fts_rank;
2054
2055         my $relevance = join(' + ', @fts_ranks);
2056
2057         $relevance = <<"        RANK";
2058                         (SUM(   ($relevance)
2059                                 * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
2060                                 * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
2061                                 * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
2062                         )/COUNT(f.id))
2063         RANK
2064
2065         my $rank = $relevance;
2066         if (lc($sort) eq 'pubdate') {
2067                 $rank = <<"             RANK";
2068                         (
2069                                 SELECT  FIRST(COALESCE(SUBSTRING(frp.value FROM '\\\\d+'),'9999')::INT)
2070                                   FROM  $metabib_full_rec frp
2071                                   WHERE frp.record = f.source
2072                                         AND frp.tag = '260'
2073                                         AND frp.subfield = 'c'
2074                         )
2075                 RANK
2076         } elsif (lc($sort) eq 'create_date') {
2077                 $rank = <<"             RANK";
2078                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.source)) )
2079                 RANK
2080         } elsif (lc($sort) eq 'edit_date') {
2081                 $rank = <<"             RANK";
2082                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.source)) )
2083                 RANK
2084         } elsif (lc($sort) eq 'title') {
2085                 $rank = <<"             RANK";
2086                         (
2087                                 SELECT  FIRST(COALESCE(LTRIM(SUBSTR( frt.value, frt.ind2::text::int )),'zzzzzzzz'))
2088                                   FROM  $metabib_full_rec frt
2089                                   WHERE frt.record = f.source
2090                                         AND frt.tag = '245'
2091                                         AND frt.subfield = 'a'
2092                         )
2093                 RANK
2094         } elsif (lc($sort) eq 'author') {
2095                 $rank = <<"             RANK";
2096                         ( FIRST ((
2097                                 SELECT  COALESCE(LTRIM(fra.value),'zzzzzzzz')
2098                                   FROM  $metabib_full_rec fra
2099                                   WHERE fra.record = f.source
2100                                         AND fra.tag LIKE '1%'
2101                                         AND fra.subfield = 'a'
2102                                   ORDER BY fra.tag::text::int
2103                                   LIMIT 1
2104                         )) )
2105                 RANK
2106         } else {
2107                 $sort = undef;
2108         }
2109
2110
2111
2112         my $select = <<"        SQL";
2113                 SELECT  f.source,
2114                         $rank,
2115                         $relevance
2116                 FROM    $search_table f,
2117                         $br_table br,
2118                         $descendants
2119                         $metabib_record_descriptor rd
2120                 WHERE   $fts_where
2121                         AND rd.record = f.source
2122                         AND br.id = f.source
2123                         AND br.deleted IS FALSE
2124                         $t_filter
2125                         $f_filter
2126                 GROUP BY 1
2127                 ORDER BY 2 $sort_dir, 3, MIN(COALESCE(CHAR_LENGTH(f.value),1))
2128         SQL
2129
2130
2131         if ($self->api_name !~ /Zsearch/o) {
2132
2133                 my $descendants = defined($ou_type) ?
2134                                 "actor.org_unit_descendants($ou, $ou_type)" :
2135                                 "actor.org_unit_descendants($ou)";
2136
2137                 if ($self->api_name !~ /staff/o) {
2138                         $select = <<"                   SQL";
2139
2140                         SELECT  DISTINCT s.*
2141                           FROM  ($select) s
2142                           WHERE EXISTS (
2143                                 SELECT  1
2144                                   FROM  $asset_call_number_table cn,
2145                                         $asset_copy_table cp,
2146                                         $cs_table cs,
2147                                         $cl_table cl,
2148                                         $br_table br,
2149                                         $descendants d
2150                                   WHERE br.id = s.source
2151                                         AND cn.record = s.source
2152                                         AND cp.status = cs.id
2153                                         AND cp.location = cl.id
2154                                         AND cn.owning_lib = d.id
2155                                         AND cp.call_number = cn.id
2156                                         AND cp.opac_visible IS TRUE
2157                                         AND cs.holdable IS TRUE
2158                                         AND cl.opac_visible IS TRUE
2159                                         AND br.active IS TRUE
2160                                         AND br.deleted IS FALSE
2161                                   LIMIT 1
2162                                 )
2163                           ORDER BY 2 $sort_dir, 3
2164
2165                         SQL
2166                 } else {
2167                         $select = <<"                   SQL";
2168
2169                         SELECT  DISTINCT s.*
2170                           FROM  ($select) s
2171                           WHERE EXISTS (
2172                                 SELECT  1
2173                                   FROM  $asset_call_number_table cn,
2174                                         $descendants d,
2175                                         $br_table br
2176                                   WHERE br.id = s.source
2177                                         AND cn.record = s.source
2178                                         AND cn.owning_lib = d.id
2179                                         AND br.deleted IS FALSE
2180                                   LIMIT 1
2181                                 )
2182                                 OR NOT EXISTS (
2183                                 SELECT  1
2184                                   FROM  $asset_call_number_table cn
2185                                   WHERE cn.record = s.source
2186                                   LIMIT 1
2187                                 )
2188                           ORDER BY 2 $sort_dir, 3
2189
2190                         SQL
2191                 }
2192         }
2193
2194
2195         $log->debug("Z39.50 (Record) Search SQL :: [$select]",DEBUG);
2196
2197         my $SQLstring = join('%',$fts->words);
2198         my $REstring = join('\\s+',$fts->words);
2199         my $first_word = ($fts->words)[0].'%';
2200         my $recs =
2201                 $class->db_Main->selectall_arrayref(
2202                         $select, {},
2203                         '%'.lc($SQLstring).'%',                 # phrase order match
2204                         lc($first_word),                        # first word match
2205                         '^\\s*'.lc($REstring).'\\s*/?\s*$',     # full exact match
2206                         ( !$sort ?
2207                                 ( '%'.lc($SQLstring).'%',                       # phrase order match
2208                                   lc($first_word),                              # first word match
2209                                   '^\\s*'.lc($REstring).'\\s*/?\s*$' ) :        # full exact match
2210                                 ()
2211                         ),
2212                         @types, @forms
2213                 );
2214         
2215         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2216
2217         my $max = 0;
2218         $max = 1 if (!@$recs);
2219         for (@$recs) {
2220                 $max = $$_[2] if ($$_[2] > $max);
2221         }
2222
2223         my $count = scalar(@$recs);
2224         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2225                 next unless ($rec);
2226                 my ($mrid,$junk,$rank) = @$rec;
2227                 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $count] );
2228         }
2229         return undef;
2230 }
2231
2232
2233 for my $class ( qw/title author subject keyword series/ ) {
2234         __PACKAGE__->register_method(
2235                 api_name        => "open-ils.storage.metabib.$class.Zsearch",
2236                 method          => 'postfilter_Z_search_class_fts',
2237                 api_level       => 1,
2238                 stream          => 1,
2239                 cdbi            => "metabib::${class}_field_entry",
2240                 cachable        => 1,
2241         );
2242         __PACKAGE__->register_method(
2243                 api_name        => "open-ils.storage.biblio.$class.search_fts.record",
2244                 method          => 'postfilter_Z_search_class_fts',
2245                 api_level       => 1,
2246                 stream          => 1,
2247                 cdbi            => "metabib::${class}_field_entry",
2248                 cachable        => 1,
2249         );
2250         __PACKAGE__->register_method(
2251                 api_name        => "open-ils.storage.biblio.$class.search_fts.record.staff",
2252                 method          => 'postfilter_Z_search_class_fts',
2253                 api_level       => 1,
2254                 stream          => 1,
2255                 cdbi            => "metabib::${class}_field_entry",
2256                 cachable        => 1,
2257         );
2258 }
2259
2260
2261 sub multi_Z_search_full_rec {
2262         my $self = shift;
2263         my $client = shift;
2264
2265         my %args = @_;  
2266         my $class_join = $args{class_join} || 'AND';
2267         my $limit = $args{limit} || 10;
2268         my $offset = $args{offset} || 0;
2269         my @binds;
2270         my @selects;
2271
2272         my $limiter_count = 0;
2273
2274         for my $arg (@{ $args{searches} }) {
2275                 my $term = $$arg{term};
2276                 my $limiters = $$arg{restrict};
2277
2278                 my ($index_col) = metabib::full_rec->columns('FTS');
2279                 $index_col ||= 'value';
2280                 my $search_table = metabib::full_rec->table;
2281
2282                 my $fts = OpenILS::Application::Storage::FTS->compile($term, 'value',"$index_col");
2283
2284                 my $fts_where = $fts->sql_where_clause();
2285                 my @fts_ranks = $fts->fts_rank;
2286
2287                 my $rank = join(' + ', @fts_ranks);
2288
2289                 my @wheres;
2290                 for my $limit (@$limiters) {
2291                         push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
2292                         push @binds, $$limit{tag}, ($$limit{subfield} ? $$limit{subfield} : '_');
2293                         $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
2294                 }
2295                 $limiter_count++;
2296                 my $where = join(' OR ', @wheres);
2297
2298                 push @selects, "SELECT FIRST(id), record, SUM($rank) as sum FROM $search_table WHERE $where GROUP BY 2";
2299
2300         }
2301
2302         my $metabib_record_descriptor = metabib::record_descriptor->table;
2303
2304         my $cj = 'HAVING COUNT(DISTINCT x.id) = ' . $limiter_count if ($class_join eq 'AND');
2305         my $search_table =
2306                 '(SELECT x.record, sum(x.sum) FROM (('.
2307                         join(') UNION ALL (', @selects).
2308                         ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
2309
2310         my ($t_filter, $f_filter) = ('','');
2311
2312         if ($args{format}) {
2313                 my ($t, $f) = split '-', $args{format};
2314                 my @types = split '', $t;
2315                 my @forms = split '', $f;
2316                 if (@types) {
2317                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2318                 }
2319
2320                 if (@forms) {
2321                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2322                 }
2323                 push @binds, @types, @forms;
2324         }
2325
2326
2327         my $select = <<"        SQL";
2328                 SELECT  f.record, f.sum
2329                 FROM    $search_table f,
2330                         $metabib_record_descriptor rd
2331                 WHERE   rd.record = f.record
2332                         $t_filter
2333                         $f_filter
2334                 ORDER BY 2
2335         SQL
2336
2337
2338         $log->debug("Search SQL :: [$select]",DEBUG);
2339
2340         my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
2341         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2342
2343         my $count = @$recs;
2344         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2345                 next unless ($$rec[0]);
2346                 my ($rid,$rank) = @$rec;
2347                 $client->respond( [$rid, sprintf('%0.3f',$rank), $count] );
2348         }
2349         return undef;
2350 }
2351 __PACKAGE__->register_method(
2352         api_name        => 'open-ils.storage.metabib.full_rec.Zmulti_search',
2353         method          => 'multi_Z_search_full_rec',
2354         api_level       => 1,
2355         stream          => 1,
2356         cachable        => 1,
2357 );
2358
2359 __PACKAGE__->register_method(
2360         api_name        => 'open-ils.storage.biblio.multiclass.search_fts.record',
2361         method          => 'multi_Z_search_full_rec',
2362         api_level       => 0,
2363         stream          => 1,
2364         cachable        => 1,
2365 );
2366
2367
2368 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2369 sub new_search_class_fts {
2370         my $self = shift;
2371         my $client = shift;
2372         my %args = @_;
2373         
2374         my $term = $args{term};
2375         my $ou = $args{org_unit};
2376         my $ou_type = $args{depth};
2377         my $limit = $args{limit};
2378         my $offset = $args{offset} || 0;
2379
2380         my $limit_clause = '';
2381         my $offset_clause = '';
2382
2383         $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
2384         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
2385
2386         my (@types,@forms);
2387         my ($t_filter, $f_filter) = ('','');
2388
2389         if ($args{format}) {
2390                 my ($t, $f) = split '-', $args{format};
2391                 @types = split '', $t;
2392                 @forms = split '', $f;
2393                 if (@types) {
2394                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2395                 }
2396
2397                 if (@forms) {
2398                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2399                 }
2400         }
2401
2402
2403
2404         my $descendants = defined($ou_type) ?
2405                                 "actor.org_unit_descendants($ou, $ou_type)" :
2406                                 "actor.org_unit_descendants($ou)";
2407
2408         my $class = $self->{cdbi};
2409         my $search_table = $class->table;
2410
2411         my $metabib_record_descriptor = metabib::record_descriptor->table;
2412         my $metabib_metarecord = metabib::metarecord->table;
2413         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2414         my $asset_call_number_table = asset::call_number->table;
2415         my $asset_copy_table = asset::copy->table;
2416         my $cs_table = config::copy_status->table;
2417         my $cl_table = asset::copy_location->table;
2418
2419         my ($index_col) = $class->columns('FTS');
2420         $index_col ||= 'value';
2421
2422         my $fts = OpenILS::Application::Storage::FTS->compile($term, 'f.value', "f.$index_col");
2423
2424         my $fts_where = $fts->sql_where_clause;
2425         my @fts_ranks = $fts->fts_rank;
2426
2427         my $rank = join(' + ', @fts_ranks);
2428
2429         if ($self->api_name !~ /staff/o) {
2430                 $select = <<"           SQL";
2431                         SELECT  m.metarecord, 
2432                                 (SUM(   $rank
2433                                         * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
2434                                         * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
2435                                         * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
2436                                 )/COUNT(m.source)),
2437                                 CASE WHEN COUNT(DISTINCT rd.record) = 1 THEN MIN(m.source) ELSE 0 END
2438                         FROM    $search_table f,
2439                                 $metabib_metarecord_source_map_table m,
2440                                 $metabib_metarecord_source_map_table mr,
2441                                 $metabib_record_descriptor rd
2442                         WHERE   $fts_where
2443                                 AND mr.source = f.source
2444                                 AND mr.metarecord = m.metarecord
2445                                 AND rd.record = m.source
2446                                 $t_filter
2447                                 $f_filter
2448                                 AND EXISTS (
2449                                         SELECT  TRUE
2450                                           FROM  $asset_call_number_table cn,
2451                                                 $asset_copy_table cp,
2452                                                 $cs_table cs,
2453                                                 $cl_table cl,
2454                                                 $descendants d
2455                                           WHERE cn.record = mr.source
2456                                                 AND cp.status = cs.id
2457                                                 AND cp.location = cl.id
2458                                                 AND cn.owning_lib = d.id
2459                                                 AND cp.call_number = cn.id
2460                                                 AND cp.opac_visible IS TRUE
2461                                                 AND cs.holdable IS TRUE
2462                                                 AND cl.opac_visible IS TRUE )
2463                         GROUP BY m.metarecord
2464                         ORDER BY 2 DESC, MIN(COALESCE(CHAR_LENGTH(f.value),1))
2465                 SQL
2466         } else {
2467                 $select = <<"           SQL";
2468                         SELECT  m.metarecord,
2469                                 (SUM(   $rank
2470                                         * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
2471                                         * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
2472                                         * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
2473                                 )/COUNT(m.source)),
2474                                 CASE WHEN COUNT(DISTINCT rd.record) = 1 THEN MIN(m.source) ELSE 0 END
2475                         FROM    $search_table f,
2476                                 $metabib_metarecord_source_map_table m,
2477                                 $metabib_metarecord_source_map_table mr,
2478                                 $metabib_record_descriptor rd
2479                         WHERE   $fts_where
2480                                 AND m.source = f.source
2481                                 AND m.metarecord = mr.metarecord
2482                                 AND rd.record = m.source
2483                                 $t_filter
2484                                 $f_filter
2485                         GROUP BY m.metarecord
2486                         ORDER BY 2 DESC, MIN(COALESCE(CHAR_LENGTH(f.value),1))
2487                 SQL
2488         }
2489
2490         $log->debug("Field Search SQL :: [$select]",DEBUG);
2491
2492         my $SQLstring = join('%',$fts->words);
2493         my $REstring = join('\\s+',$fts->words);
2494         my $first_word = ($fts->words)[0].'%';
2495         my $recs = ($self->api_name =~ /unordered/o) ? 
2496                         $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
2497                         $class->db_Main->selectall_arrayref($select, {},
2498                                 '%'.lc($SQLstring).'%',                 # phrase order match
2499                                 lc($first_word),                        # first word match
2500                                 '^\\s*'.lc($REstring).'\\s*/?\s*$',     # full exact match
2501                                 @types, @forms
2502                         );
2503         
2504         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2505
2506         my $count = scalar(@$recs);
2507         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2508                 my ($mrid,$rank,$skip) = @$rec;
2509                 $client->respond( [$mrid, sprintf('%0.3f',$rank), $skip, $count] );
2510         }
2511         return undef;
2512 }
2513
2514 for my $class ( qw/title author subject keyword series/ ) {
2515         __PACKAGE__->register_method(
2516                 api_name        => "open-ils.storage.metabib.$class.new_search_fts.metarecord",
2517                 method          => 'new_search_class_fts',
2518                 api_level       => 1,
2519                 stream          => 1,
2520                 cdbi            => "metabib::${class}_field_entry",
2521                 cachable        => 1,
2522         );
2523         __PACKAGE__->register_method(
2524                 api_name        => "open-ils.storage.metabib.$class.new_search_fts.metarecord.staff",
2525                 method          => 'new_search_class_fts',
2526                 api_level       => 1,
2527                 stream          => 1,
2528                 cdbi            => "metabib::${class}_field_entry",
2529                 cachable        => 1,
2530         );
2531 }
2532
2533
2534
2535 sub multi_search_full_rec {
2536         my $self = shift;
2537         my $client = shift;
2538
2539         my %args = @_;  
2540         my $class_join = $args{class_join} || 'AND';
2541         my $limit = $args{limit} || 100;
2542         my $offset = $args{offset} || 0;
2543         my @binds;
2544         my @selects;
2545
2546         for my $arg (@{ $args{searches} }) {
2547                 my $term = $$arg{term};
2548                 my $limiters = $$arg{restrict};
2549
2550                 my ($index_col) = metabib::full_rec->columns('FTS');
2551                 $index_col ||= 'value';
2552                 my $search_table = metabib::full_rec->table;
2553
2554                 my $fts = OpenILS::Application::Storage::FTS->compile($term, 'value',"$index_col");
2555
2556                 my $fts_where = $fts->sql_where_clause();
2557                 my @fts_ranks = $fts->fts_rank;
2558
2559                 my $rank = join(' + ', @fts_ranks);
2560
2561                 my @wheres;
2562                 for my $limit (@$limiters) {
2563                         push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
2564                         push @binds, $$limit{tag}, $$limit{subfield};
2565                         $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
2566                 }
2567                 my $where = join(' OR ', @wheres);
2568
2569                 push @selects, "SELECT id, record, $rank as sum FROM $search_table WHERE $where";
2570
2571         }
2572
2573         my $descendants = defined($args{depth}) ?
2574                                 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
2575                                 "actor.org_unit_descendants($args{org_unit})" ;
2576
2577
2578         my $metabib_record_descriptor = metabib::record_descriptor->table;
2579         my $metabib_metarecord = metabib::metarecord->table;
2580         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2581         my $asset_call_number_table = asset::call_number->table;
2582         my $asset_copy_table = asset::copy->table;
2583         my $cs_table = config::copy_status->table;
2584         my $cl_table = asset::copy_location->table;
2585
2586         my $cj = 'HAVING COUNT(x.id) = ' . scalar(@selects) if ($class_join eq 'AND');
2587         my $search_table =
2588                 '(SELECT x.record, sum(x.sum) FROM (('.
2589                         join(') UNION ALL (', @selects).
2590                         ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
2591
2592         my $has_vols = 'AND cn.owning_lib = d.id';
2593         my $has_copies = 'AND cp.call_number = cn.id';
2594         my $copies_visible = 'AND cp.opac_visible IS TRUE AND cs.holdable IS TRUE AND cl.opac_visible IS TRUE';
2595
2596         if ($self->api_name =~ /staff/o) {
2597                 $copies_visible = '';
2598                 $has_copies = '' if ($ou_type == 0);
2599                 $has_vols = '' if ($ou_type == 0);
2600         }
2601
2602         my ($t_filter, $f_filter) = ('','');
2603
2604         if ($args{format}) {
2605                 my ($t, $f) = split '-', $args{format};
2606                 my @types = split '', $t;
2607                 my @forms = split '', $f;
2608                 if (@types) {
2609                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2610                 }
2611
2612                 if (@forms) {
2613                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2614                 }
2615                 push @binds, @types, @forms;
2616         }
2617
2618
2619         if ($copies_visible) {
2620                 $select = <<"           SQL";
2621                         SELECT  m.metarecord, sum(f.sum), count(DISTINCT cp.id), CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
2622                         FROM    $search_table f,
2623                                 $metabib_metarecord_source_map_table m,
2624                                 $asset_call_number_table cn,
2625                                 $asset_copy_table cp,
2626                                 $cs_table cs,
2627                                 $cl_table cl,
2628                                 $metabib_record_descriptor rd,
2629                                 $descendants d
2630                         WHERE   m.source = f.record
2631                                 AND cn.record = m.source
2632                                 AND rd.record = m.source
2633                                 AND cp.status = cs.id
2634                                 AND cp.location = cl.id
2635                                 $has_vols
2636                                 $has_copies
2637                                 $copies_visible
2638                                 $t_filter
2639                                 $f_filter
2640                         GROUP BY m.metarecord HAVING count(DISTINCT cp.id) > 0
2641                         ORDER BY 2 DESC,3 DESC
2642                 SQL
2643         } else {
2644                 $select = <<"           SQL";
2645                         SELECT  m.metarecord, 1, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
2646                         FROM    $search_table f,
2647                                 $metabib_metarecord_source_map_table m,
2648                                 $metabib_record_descriptor rd
2649                         WHERE   m.source = f.record
2650                                 AND rd.record = m.source
2651                                 $t_filter
2652                                 $f_filter
2653                         GROUP BY 1,2,3 
2654                 SQL
2655         }
2656
2657
2658         $log->debug("Search SQL :: [$select]",DEBUG);
2659
2660         my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
2661         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2662
2663         my $count = @$recs;
2664         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2665                 next unless ($$rec[0]);
2666                 my ($mrid,$rank,$junk,$skip) = @$rec;
2667                 $client->respond( [$mrid, sprintf('%0.3f',$rank), $skip, $count] );
2668         }
2669         return undef;
2670 }
2671 __PACKAGE__->register_method(
2672         api_name        => 'open-ils.storage.metabib.full_rec.multi_search',
2673         method          => 'multi_search_full_rec',
2674         api_level       => 1,
2675         stream          => 1,
2676         cachable        => 1,
2677 );
2678 __PACKAGE__->register_method(
2679         api_name        => 'open-ils.storage.metabib.full_rec.multi_search.staff',
2680         method          => 'multi_search_full_rec',
2681         api_level       => 1,
2682         stream          => 1,
2683         cachable        => 1,
2684 );
2685
2686