]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Storage/Publisher/metabib.pm
speeding up common filtering
[working/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                 WHERE   m.metarecord = mr.id
1489                         AND smrs.metarecord = mr.id
1490                         $fts_list
1491                         $join_table_list
1492                 GROUP BY m.metarecord
1493                 ORDER BY 4 $sort_dir
1494                 LIMIT 10000
1495         SQL
1496
1497         if ($self->api_name !~ /staff/o) {
1498                 $select = <<"           SQL";
1499
1500                         SELECT  s.*
1501                           FROM  ($select) s
1502                           WHERE EXISTS (
1503                                 SELECT  1
1504                                   FROM  $asset_call_number_table cn,
1505                                         $metabib_metarecord_source_map_table mrs,
1506                                         $asset_copy_table cp,
1507                                         $cs_table cs,
1508                                         $cl_table cl,
1509                                         $br_table br,
1510                                         $descendants d,
1511                                         $metabib_record_descriptor ord
1512                                   WHERE mrs.metarecord = s.metarecord
1513                                         AND br.id = mrs.source
1514                                         AND cn.record = mrs.source
1515                                         AND cp.status = cs.id
1516                                         AND cp.location = cl.id
1517                                         AND cn.owning_lib = d.id
1518                                         AND cp.call_number = cn.id
1519                                         AND cp.opac_visible IS TRUE
1520                                         AND cs.holdable IS TRUE
1521                                         AND cl.opac_visible IS TRUE
1522                                         AND br.active IS TRUE
1523                                         AND br.deleted IS FALSE
1524                                         AND ord.record = mrs.source
1525                                         $ot_filter
1526                                         $of_filter
1527                                         $oa_filter
1528                                         $ol_filter
1529                                         $olf_filter
1530                                   LIMIT 1
1531                                 )
1532                           ORDER BY 4 $sort_dir
1533                 SQL
1534         } else {
1535                 $select = <<"           SQL";
1536
1537                         SELECT  s.*
1538                           FROM  ($select) s
1539                           WHERE EXISTS (
1540                                 SELECT  1
1541                                   FROM  $asset_call_number_table cn,
1542                                         $metabib_metarecord_source_map_table mrs,
1543                                         $descendants d,
1544                                         $br_table br,
1545                                         $metabib_record_descriptor ord
1546                                   WHERE mrs.metarecord = s.metarecord
1547                                         AND br.id = mrs.source
1548                                         AND cn.record = mrs.source
1549                                         AND cn.owning_lib = d.id
1550                                         AND ord.record = mrs.source
1551                                         AND br.deleted IS FALSE
1552                                         $ot_filter
1553                                         $of_filter
1554                                         $oa_filter
1555                                         $ol_filter
1556                                         $olf_filter
1557                                   LIMIT 1
1558                                 )
1559                                 OR NOT EXISTS (
1560                                 SELECT  1
1561                                   FROM  $asset_call_number_table cn,
1562                                         $metabib_metarecord_source_map_table mrs,
1563                                         $metabib_record_descriptor ord
1564                                   WHERE mrs.metarecord = s.metarecord
1565                                         AND cn.record = mrs.source
1566                                         AND ord.record = mrs.source
1567                                         $ot_filter
1568                                         $of_filter
1569                                         $oa_filter
1570                                         $ol_filter
1571                                         $olf_filter
1572                                   LIMIT 1
1573                                 )
1574                           ORDER BY 4 $sort_dir
1575                 SQL
1576         }
1577
1578
1579         $log->debug("Field Search SQL :: [$select]",DEBUG);
1580
1581         my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1582                         $select, {},
1583                         @bonus_values,
1584                         @types, @forms, @aud, @lang, @lit_form,
1585                         # @types, @forms, @aud, @lang, @lit_form,
1586                         ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1587         );
1588         
1589         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1590
1591         my $max = 0;
1592         $max = 1 if (!@$recs);
1593         for (@$recs) {
1594                 $max = $$_[1] if ($$_[1] > $max);
1595         }
1596
1597         my $count = scalar(@$recs);
1598         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1599                 next unless ($$rec[0]);
1600                 my ($mrid,$rank,$skip) = @$rec;
1601                 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1602         }
1603         return undef;
1604 }
1605
1606 __PACKAGE__->register_method(
1607         api_name        => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1608         method          => 'postfilter_search_multi_class_fts',
1609         api_level       => 1,
1610         stream          => 1,
1611         cachable        => 1,
1612 );
1613 __PACKAGE__->register_method(
1614         api_name        => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1615         method          => 'postfilter_search_multi_class_fts',
1616         api_level       => 1,
1617         stream          => 1,
1618         cachable        => 1,
1619 );
1620
1621 __PACKAGE__->register_method(
1622         api_name        => "open-ils.storage.metabib.multiclass.search_fts",
1623         method          => 'postfilter_search_multi_class_fts',
1624         api_level       => 1,
1625         stream          => 1,
1626         cachable        => 1,
1627 );
1628 __PACKAGE__->register_method(
1629         api_name        => "open-ils.storage.metabib.multiclass.search_fts.staff",
1630         method          => 'postfilter_search_multi_class_fts',
1631         api_level       => 1,
1632         stream          => 1,
1633         cachable        => 1,
1634 );
1635
1636 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1637 sub biblio_search_multi_class_fts {
1638         my $self = shift;
1639         my $client = shift;
1640         my %args = @_;
1641         
1642         my $sort = $args{'sort'};
1643         my $sort_dir = $args{sort_dir} || 'DESC';
1644         my $ou = $args{org_unit};
1645         my $ou_type = $args{depth};
1646         my $limit = $args{limit} || 10;
1647         my $offset = $args{offset} || 0;
1648
1649         if (!$ou) {
1650                 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1651         }
1652
1653         if (!defined($args{org_unit})) {
1654                 die "No target organizational unit passed to ".$self->api_name;
1655         }
1656
1657         if (! scalar( keys %{$args{searches}} )) {
1658                 die "No search arguments were passed to ".$self->api_name;
1659         }
1660
1661         my $outer_limit = 1000;
1662
1663         my $limit_clause = '';
1664         my $offset_clause = '';
1665
1666         $limit_clause = "LIMIT $outer_limit";
1667         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1668
1669         my (@types,@forms,@lang,@aud,@lit_form);
1670         my ($t_filter, $f_filter) = ('','');
1671         my ($a_filter, $l_filter, $lf_filter) = ('','','');
1672         my ($ot_filter, $of_filter) = ('','');
1673         my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1674
1675         if (my $a = $args{audience}) {
1676                 $a = [$a] if (!ref($a));
1677                 @aud = @$a;
1678                         
1679                 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1680                 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1681         }
1682
1683         if (my $l = $args{language}) {
1684                 $l = [$l] if (!ref($l));
1685                 @lang = @$l;
1686
1687                 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1688                 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1689         }
1690
1691         if (my $f = $args{lit_form}) {
1692                 $f = [$f] if (!ref($f));
1693                 @lit_form = @$f;
1694
1695                 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1696                 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1697         }
1698
1699         if (my $f = $args{item_form}) {
1700                 $f = [$f] if (!ref($f));
1701                 @forms = @$f;
1702
1703                 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1704                 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1705         }
1706
1707         if (my $t = $args{item_type}) {
1708                 $t = [$t] if (!ref($t));
1709                 @types = @$t;
1710
1711                 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1712                 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1713         }
1714
1715
1716         # XXX legacy format and item type support
1717         if ($args{format}) {
1718                 my ($t, $f) = split '-', $args{format};
1719                 @types = split '', $t;
1720                 @forms = split '', $f;
1721                 if (@types) {
1722                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1723                         $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1724                 }
1725
1726                 if (@forms) {
1727                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1728                         $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1729                 }
1730         }
1731
1732
1733         my $descendants = defined($ou_type) ?
1734                                 "actor.org_unit_descendants($ou, $ou_type)" :
1735                                 "actor.org_unit_descendants($ou)";
1736
1737         my $search_table_list = '';
1738         my $fts_list = '';
1739         my $join_table_list = '';
1740         my @rank_list;
1741
1742
1743         my @bonus_lists;
1744         my @bonus_values;
1745         my $prev_search_class;
1746         my $curr_search_class;
1747         for my $search_class (sort keys %{$args{searches}}) {
1748                 $prev_search_class = $curr_search_class if ($curr_search_class);
1749
1750                 $curr_search_class = $search_class;
1751
1752                 my $class = $_cdbi->{$search_class};
1753                 my $search_table = $class->table;
1754
1755                 my ($index_col) = $class->columns('FTS');
1756                 $index_col ||= 'value';
1757
1758                 
1759                 my $fts = OpenILS::Application::Storage::FTS->compile($args{searches}{$search_class}{term}, $search_class.'.value', "$search_class.$index_col");
1760
1761                 my $fts_where = $fts->sql_where_clause;
1762                 my @fts_ranks = $fts->fts_rank;
1763
1764                 my $SQLstring = join('%',$fts->words);
1765                 my $REstring = '^' . join('\s+',$fts->words) . '\W*$';
1766                 my $first_word = ($fts->words)[0].'%';
1767
1768                 my $rank = join(' + ', @fts_ranks);
1769
1770                 my %bonus = ();
1771                 $bonus{'keyword'} = [ { "CASE WHEN $search_class.value ILIKE ? THEN 1.2 ELSE 1 END" => $SQLstring } ];
1772                 $bonus{'title'} =
1773                         $bonus{'metabib::series_field_entry'} = [
1774                                 { "CASE WHEN $search_class.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1775                                 { "CASE WHEN $search_class.value ~* ? THEN 2 ELSE 1 END" => $REstring },
1776                                 @{ $bonus{'keyword'} }
1777                         ];
1778
1779                 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1780                 $bonus_list ||= '1';
1781
1782                 push @bonus_lists, $bonus_list;
1783                 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1784
1785                 #---------------------
1786
1787                 $search_table_list .= "$search_table $search_class, ";
1788                 push @rank_list,$rank;
1789                 $fts_list .= " AND $fts_where AND b.id = $search_class.source";
1790
1791                 if ($prev_search_class) {
1792                         $join_table_list .= " AND $prev_search_class.source = $curr_search_class.source";
1793                 }
1794         }
1795
1796         my $metabib_record_descriptor = metabib::record_descriptor->table;
1797         my $metabib_full_rec = metabib::full_rec->table;
1798         my $metabib_metarecord = metabib::metarecord->table;
1799         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1800         my $asset_call_number_table = asset::call_number->table;
1801         my $asset_copy_table = asset::copy->table;
1802         my $cs_table = config::copy_status->table;
1803         my $cl_table = asset::copy_location->table;
1804         my $br_table = biblio::record_entry->table;
1805
1806
1807         my $bonuses = join (' * ', @bonus_lists);
1808         my $relevance = join (' + ', @rank_list);
1809         $relevance = "AVG( ($relevance) * ($bonuses) )";
1810
1811
1812         my $rank = $relevance;
1813         if (lc($sort) eq 'pubdate') {
1814                 $rank = <<"             RANK";
1815                         ( FIRST ((
1816                                 SELECT  COALESCE(SUBSTRING(frp.value FROM '\\\\d{4}'),'9999')::INT
1817                                   FROM  $metabib_full_rec frp
1818                                   WHERE frp.record = b.id
1819                                         AND frp.tag = '260'
1820                                         AND frp.subfield = 'c'
1821                                   LIMIT 1
1822                         )) )
1823                 RANK
1824         } elsif (lc($sort) eq 'create_date') {
1825                 $rank = <<"             RANK";
1826                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
1827                 RANK
1828         } elsif (lc($sort) eq 'edit_date') {
1829                 $rank = <<"             RANK";
1830                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
1831                 RANK
1832         } elsif (lc($sort) eq 'title') {
1833                 $rank = <<"             RANK";
1834                         ( FIRST ((
1835                                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, frt.ind2::text::int )),'zzzzzzzz')
1836                                   FROM  $metabib_full_rec frt
1837                                   WHERE frt.record = b.id
1838                                         AND frt.tag = '245'
1839                                         AND frt.subfield = 'a'
1840                                   LIMIT 1
1841                         )) )
1842                 RANK
1843         } elsif (lc($sort) eq 'author') {
1844                 $rank = <<"             RANK";
1845                         ( FIRST((
1846                                 SELECT  COALESCE(LTRIM(fra.value),'zzzzzzzz')
1847                                   FROM  $metabib_full_rec fra
1848                                   WHERE fra.record = b.id
1849                                         AND fra.tag LIKE '1%'
1850                                         AND fra.subfield = 'a'
1851                                   ORDER BY fra.tag::text::int
1852                                   LIMIT 1
1853                         )) )
1854                 RANK
1855         } else {
1856                 push @bonus_values, @bonus_values;
1857                 $sort = undef;
1858         }
1859
1860
1861         my $select = <<"        SQL";
1862                 SELECT  b.id,
1863                         $relevance AS rel,
1864                         $rank AS rank
1865                 FROM    $search_table_list
1866                         $metabib_record_descriptor rd,
1867                         $br_table b
1868                 WHERE   rd.record = b.id
1869                         AND b.active IS TRUE
1870                         AND b.deleted IS FALSE
1871                         $fts_list
1872                         $join_table_list
1873                         $t_filter
1874                         $f_filter
1875                         $a_filter
1876                         $l_filter
1877                         $lf_filter
1878                 GROUP BY b.id
1879                 ORDER BY 3 $sort_dir
1880                 LIMIT 10000
1881         SQL
1882
1883         if ($self->api_name !~ /staff/o) {
1884                 $select = <<"           SQL";
1885
1886                         SELECT  s.*
1887                           FROM  ($select) s
1888                           WHERE EXISTS (
1889                                 SELECT  1
1890                                   FROM  $asset_call_number_table cn,
1891                                         $asset_copy_table cp,
1892                                         $cs_table cs,
1893                                         $cl_table cl,
1894                                         $descendants d
1895                                   WHERE cn.record = s.id
1896                                         AND cp.status = cs.id
1897                                         AND cp.location = cl.id
1898                                         AND cn.owning_lib = d.id
1899                                         AND cp.call_number = cn.id
1900                                         AND cp.opac_visible IS TRUE
1901                                         AND cs.holdable IS TRUE
1902                                         AND cl.opac_visible IS TRUE
1903                                         AND cp.deleted IS FALSE
1904                                   LIMIT 1
1905                                 )
1906                           ORDER BY 3 $sort_dir
1907                 SQL
1908         } else {
1909                 $select = <<"           SQL";
1910
1911                         SELECT  s.*
1912                           FROM  ($select) s
1913                           WHERE EXISTS (
1914                                 SELECT  1
1915                                   FROM  $asset_call_number_table cn,
1916                                         $descendants d
1917                                   WHERE cn.record = s.id
1918                                         AND cn.owning_lib = d.id
1919                                   LIMIT 1
1920                                 )
1921                                 OR NOT EXISTS (
1922                                 SELECT  1
1923                                   FROM  $asset_call_number_table cn
1924                                   WHERE cn.record = s.id
1925                                   LIMIT 1
1926                                 )
1927                           ORDER BY 3 $sort_dir
1928                 SQL
1929         }
1930
1931
1932         $log->debug("Field Search SQL :: [$select]",DEBUG);
1933
1934         my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1935                         $select, {},
1936                         @bonus_values, @types, @forms, @aud, @lang, @lit_form
1937         );
1938         
1939         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1940
1941         my $max = 0;
1942         $max = 1 if (!@$recs);
1943         for (@$recs) {
1944                 $max = $$_[1] if ($$_[1] > $max);
1945         }
1946
1947         my $count = scalar(@$recs);
1948         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1949                 next unless ($$rec[0]);
1950                 my ($mrid,$rank) = @$rec;
1951                 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $count] );
1952         }
1953         return undef;
1954 }
1955
1956 __PACKAGE__->register_method(
1957         api_name        => "open-ils.storage.biblio.multiclass.search_fts.record",
1958         method          => 'biblio_search_multi_class_fts',
1959         api_level       => 1,
1960         stream          => 1,
1961         cachable        => 1,
1962 );
1963 __PACKAGE__->register_method(
1964         api_name        => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
1965         method          => 'biblio_search_multi_class_fts',
1966         api_level       => 1,
1967         stream          => 1,
1968         cachable        => 1,
1969 );
1970
1971
1972
1973 __PACKAGE__->register_method(
1974         api_name        => "open-ils.storage.biblio.multiclass.search_fts",
1975         method          => 'biblio_search_multi_class_fts',
1976         api_level       => 1,
1977         stream          => 1,
1978         cachable        => 1,
1979 );
1980 __PACKAGE__->register_method(
1981         api_name        => "open-ils.storage.biblio.multiclass.search_fts.staff",
1982         method          => 'biblio_search_multi_class_fts',
1983         api_level       => 1,
1984         stream          => 1,
1985         cachable        => 1,
1986 );
1987
1988
1989 1;
1990
1991
1992 __END__
1993
1994
1995 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1996 sub postfilter_Z_search_class_fts {
1997         my $self = shift;
1998         my $client = shift;
1999         my %args = @_;
2000         
2001         my $term = $args{term};
2002         my $sort = $args{'sort'};
2003         my $sort_dir = $args{sort_dir} || 'DESC';
2004         my $ou = $args{org_unit};
2005         my $ou_type = $args{depth};
2006         my $limit = $args{limit} || 10;
2007         my $offset = $args{offset} || 0;
2008
2009         my (@types,@forms);
2010         my ($t_filter, $f_filter) = ('','');
2011         my ($ot_filter, $of_filter) = ('','');
2012
2013         if ($args{format}) {
2014                 my ($t, $f) = split '-', $args{format};
2015                 @types = split '', $t;
2016                 @forms = split '', $f;
2017                 if (@types) {
2018                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2019                         $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2020                 }
2021
2022                 if (@forms) {
2023                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2024                         $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2025                 }
2026         }
2027
2028
2029
2030         my $class = $self->{cdbi};
2031         my $search_table = $class->table;
2032         my $metabib_record_descriptor = metabib::record_descriptor->table;
2033         my $metabib_full_rec = metabib::full_rec->table;
2034         my $asset_call_number_table = asset::call_number->table;
2035         my $asset_copy_table = asset::copy->table;
2036         my $cs_table = config::copy_status->table;
2037         my $cl_table = asset::copy_location->table;
2038         my $br_table = biblio::record_entry->table;
2039
2040         my ($index_col) = $class->columns('FTS');
2041         $index_col ||= 'value';
2042
2043         my $fts = OpenILS::Application::Storage::FTS->compile($term, 'f.value', "f.$index_col");
2044
2045         my $fts_where = $fts->sql_where_clause;
2046         my @fts_ranks = $fts->fts_rank;
2047
2048         my $relevance = join(' + ', @fts_ranks);
2049
2050         $relevance = <<"        RANK";
2051                         (SUM(   ($relevance)
2052                                 * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
2053                                 * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
2054                                 * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
2055                         )/COUNT(f.id))
2056         RANK
2057
2058         my $rank = $relevance;
2059         if (lc($sort) eq 'pubdate') {
2060                 $rank = <<"             RANK";
2061                         (
2062                                 SELECT  FIRST(COALESCE(SUBSTRING(frp.value FROM '\\\\d+'),'9999')::INT)
2063                                   FROM  $metabib_full_rec frp
2064                                   WHERE frp.record = f.source
2065                                         AND frp.tag = '260'
2066                                         AND frp.subfield = 'c'
2067                         )
2068                 RANK
2069         } elsif (lc($sort) eq 'create_date') {
2070                 $rank = <<"             RANK";
2071                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.source)) )
2072                 RANK
2073         } elsif (lc($sort) eq 'edit_date') {
2074                 $rank = <<"             RANK";
2075                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.source)) )
2076                 RANK
2077         } elsif (lc($sort) eq 'title') {
2078                 $rank = <<"             RANK";
2079                         (
2080                                 SELECT  FIRST(COALESCE(LTRIM(SUBSTR( frt.value, frt.ind2::text::int )),'zzzzzzzz'))
2081                                   FROM  $metabib_full_rec frt
2082                                   WHERE frt.record = f.source
2083                                         AND frt.tag = '245'
2084                                         AND frt.subfield = 'a'
2085                         )
2086                 RANK
2087         } elsif (lc($sort) eq 'author') {
2088                 $rank = <<"             RANK";
2089                         ( FIRST ((
2090                                 SELECT  COALESCE(LTRIM(fra.value),'zzzzzzzz')
2091                                   FROM  $metabib_full_rec fra
2092                                   WHERE fra.record = f.source
2093                                         AND fra.tag LIKE '1%'
2094                                         AND fra.subfield = 'a'
2095                                   ORDER BY fra.tag::text::int
2096                                   LIMIT 1
2097                         )) )
2098                 RANK
2099         } else {
2100                 $sort = undef;
2101         }
2102
2103
2104
2105         my $select = <<"        SQL";
2106                 SELECT  f.source,
2107                         $rank,
2108                         $relevance
2109                 FROM    $search_table f,
2110                         $br_table br,
2111                         $descendants
2112                         $metabib_record_descriptor rd
2113                 WHERE   $fts_where
2114                         AND rd.record = f.source
2115                         AND br.id = f.source
2116                         AND br.deleted IS FALSE
2117                         $t_filter
2118                         $f_filter
2119                 GROUP BY 1
2120                 ORDER BY 2 $sort_dir, 3, MIN(COALESCE(CHAR_LENGTH(f.value),1))
2121         SQL
2122
2123
2124         if ($self->api_name !~ /Zsearch/o) {
2125
2126                 my $descendants = defined($ou_type) ?
2127                                 "actor.org_unit_descendants($ou, $ou_type)" :
2128                                 "actor.org_unit_descendants($ou)";
2129
2130                 if ($self->api_name !~ /staff/o) {
2131                         $select = <<"                   SQL";
2132
2133                         SELECT  DISTINCT s.*
2134                           FROM  ($select) s
2135                           WHERE EXISTS (
2136                                 SELECT  1
2137                                   FROM  $asset_call_number_table cn,
2138                                         $asset_copy_table cp,
2139                                         $cs_table cs,
2140                                         $cl_table cl,
2141                                         $br_table br,
2142                                         $descendants d
2143                                   WHERE br.id = s.source
2144                                         AND cn.record = s.source
2145                                         AND cp.status = cs.id
2146                                         AND cp.location = cl.id
2147                                         AND cn.owning_lib = d.id
2148                                         AND cp.call_number = cn.id
2149                                         AND cp.opac_visible IS TRUE
2150                                         AND cs.holdable IS TRUE
2151                                         AND cl.opac_visible IS TRUE
2152                                         AND br.active IS TRUE
2153                                         AND br.deleted IS FALSE
2154                                   LIMIT 1
2155                                 )
2156                           ORDER BY 2 $sort_dir, 3
2157
2158                         SQL
2159                 } else {
2160                         $select = <<"                   SQL";
2161
2162                         SELECT  DISTINCT s.*
2163                           FROM  ($select) s
2164                           WHERE EXISTS (
2165                                 SELECT  1
2166                                   FROM  $asset_call_number_table cn,
2167                                         $descendants d,
2168                                         $br_table br
2169                                   WHERE br.id = s.source
2170                                         AND cn.record = s.source
2171                                         AND cn.owning_lib = d.id
2172                                         AND br.deleted IS FALSE
2173                                   LIMIT 1
2174                                 )
2175                                 OR NOT EXISTS (
2176                                 SELECT  1
2177                                   FROM  $asset_call_number_table cn
2178                                   WHERE cn.record = s.source
2179                                   LIMIT 1
2180                                 )
2181                           ORDER BY 2 $sort_dir, 3
2182
2183                         SQL
2184                 }
2185         }
2186
2187
2188         $log->debug("Z39.50 (Record) Search SQL :: [$select]",DEBUG);
2189
2190         my $SQLstring = join('%',$fts->words);
2191         my $REstring = join('\\s+',$fts->words);
2192         my $first_word = ($fts->words)[0].'%';
2193         my $recs =
2194                 $class->db_Main->selectall_arrayref(
2195                         $select, {},
2196                         '%'.lc($SQLstring).'%',                 # phrase order match
2197                         lc($first_word),                        # first word match
2198                         '^\\s*'.lc($REstring).'\\s*/?\s*$',     # full exact match
2199                         ( !$sort ?
2200                                 ( '%'.lc($SQLstring).'%',                       # phrase order match
2201                                   lc($first_word),                              # first word match
2202                                   '^\\s*'.lc($REstring).'\\s*/?\s*$' ) :        # full exact match
2203                                 ()
2204                         ),
2205                         @types, @forms
2206                 );
2207         
2208         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2209
2210         my $max = 0;
2211         $max = 1 if (!@$recs);
2212         for (@$recs) {
2213                 $max = $$_[2] if ($$_[2] > $max);
2214         }
2215
2216         my $count = scalar(@$recs);
2217         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2218                 next unless ($rec);
2219                 my ($mrid,$junk,$rank) = @$rec;
2220                 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $count] );
2221         }
2222         return undef;
2223 }
2224
2225
2226 for my $class ( qw/title author subject keyword series/ ) {
2227         __PACKAGE__->register_method(
2228                 api_name        => "open-ils.storage.metabib.$class.Zsearch",
2229                 method          => 'postfilter_Z_search_class_fts',
2230                 api_level       => 1,
2231                 stream          => 1,
2232                 cdbi            => "metabib::${class}_field_entry",
2233                 cachable        => 1,
2234         );
2235         __PACKAGE__->register_method(
2236                 api_name        => "open-ils.storage.biblio.$class.search_fts.record",
2237                 method          => 'postfilter_Z_search_class_fts',
2238                 api_level       => 1,
2239                 stream          => 1,
2240                 cdbi            => "metabib::${class}_field_entry",
2241                 cachable        => 1,
2242         );
2243         __PACKAGE__->register_method(
2244                 api_name        => "open-ils.storage.biblio.$class.search_fts.record.staff",
2245                 method          => 'postfilter_Z_search_class_fts',
2246                 api_level       => 1,
2247                 stream          => 1,
2248                 cdbi            => "metabib::${class}_field_entry",
2249                 cachable        => 1,
2250         );
2251 }
2252
2253
2254 sub multi_Z_search_full_rec {
2255         my $self = shift;
2256         my $client = shift;
2257
2258         my %args = @_;  
2259         my $class_join = $args{class_join} || 'AND';
2260         my $limit = $args{limit} || 10;
2261         my $offset = $args{offset} || 0;
2262         my @binds;
2263         my @selects;
2264
2265         my $limiter_count = 0;
2266
2267         for my $arg (@{ $args{searches} }) {
2268                 my $term = $$arg{term};
2269                 my $limiters = $$arg{restrict};
2270
2271                 my ($index_col) = metabib::full_rec->columns('FTS');
2272                 $index_col ||= 'value';
2273                 my $search_table = metabib::full_rec->table;
2274
2275                 my $fts = OpenILS::Application::Storage::FTS->compile($term, 'value',"$index_col");
2276
2277                 my $fts_where = $fts->sql_where_clause();
2278                 my @fts_ranks = $fts->fts_rank;
2279
2280                 my $rank = join(' + ', @fts_ranks);
2281
2282                 my @wheres;
2283                 for my $limit (@$limiters) {
2284                         push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
2285                         push @binds, $$limit{tag}, ($$limit{subfield} ? $$limit{subfield} : '_');
2286                         $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
2287                 }
2288                 $limiter_count++;
2289                 my $where = join(' OR ', @wheres);
2290
2291                 push @selects, "SELECT FIRST(id), record, SUM($rank) as sum FROM $search_table WHERE $where GROUP BY 2";
2292
2293         }
2294
2295         my $metabib_record_descriptor = metabib::record_descriptor->table;
2296
2297         my $cj = 'HAVING COUNT(DISTINCT x.id) = ' . $limiter_count if ($class_join eq 'AND');
2298         my $search_table =
2299                 '(SELECT x.record, sum(x.sum) FROM (('.
2300                         join(') UNION ALL (', @selects).
2301                         ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
2302
2303         my ($t_filter, $f_filter) = ('','');
2304
2305         if ($args{format}) {
2306                 my ($t, $f) = split '-', $args{format};
2307                 my @types = split '', $t;
2308                 my @forms = split '', $f;
2309                 if (@types) {
2310                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2311                 }
2312
2313                 if (@forms) {
2314                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2315                 }
2316                 push @binds, @types, @forms;
2317         }
2318
2319
2320         my $select = <<"        SQL";
2321                 SELECT  f.record, f.sum
2322                 FROM    $search_table f,
2323                         $metabib_record_descriptor rd
2324                 WHERE   rd.record = f.record
2325                         $t_filter
2326                         $f_filter
2327                 ORDER BY 2
2328         SQL
2329
2330
2331         $log->debug("Search SQL :: [$select]",DEBUG);
2332
2333         my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
2334         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2335
2336         my $count = @$recs;
2337         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2338                 next unless ($$rec[0]);
2339                 my ($rid,$rank) = @$rec;
2340                 $client->respond( [$rid, sprintf('%0.3f',$rank), $count] );
2341         }
2342         return undef;
2343 }
2344 __PACKAGE__->register_method(
2345         api_name        => 'open-ils.storage.metabib.full_rec.Zmulti_search',
2346         method          => 'multi_Z_search_full_rec',
2347         api_level       => 1,
2348         stream          => 1,
2349         cachable        => 1,
2350 );
2351
2352 __PACKAGE__->register_method(
2353         api_name        => 'open-ils.storage.biblio.multiclass.search_fts.record',
2354         method          => 'multi_Z_search_full_rec',
2355         api_level       => 0,
2356         stream          => 1,
2357         cachable        => 1,
2358 );
2359
2360
2361 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2362 sub new_search_class_fts {
2363         my $self = shift;
2364         my $client = shift;
2365         my %args = @_;
2366         
2367         my $term = $args{term};
2368         my $ou = $args{org_unit};
2369         my $ou_type = $args{depth};
2370         my $limit = $args{limit};
2371         my $offset = $args{offset} || 0;
2372
2373         my $limit_clause = '';
2374         my $offset_clause = '';
2375
2376         $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
2377         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
2378
2379         my (@types,@forms);
2380         my ($t_filter, $f_filter) = ('','');
2381
2382         if ($args{format}) {
2383                 my ($t, $f) = split '-', $args{format};
2384                 @types = split '', $t;
2385                 @forms = split '', $f;
2386                 if (@types) {
2387                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2388                 }
2389
2390                 if (@forms) {
2391                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2392                 }
2393         }
2394
2395
2396
2397         my $descendants = defined($ou_type) ?
2398                                 "actor.org_unit_descendants($ou, $ou_type)" :
2399                                 "actor.org_unit_descendants($ou)";
2400
2401         my $class = $self->{cdbi};
2402         my $search_table = $class->table;
2403
2404         my $metabib_record_descriptor = metabib::record_descriptor->table;
2405         my $metabib_metarecord = metabib::metarecord->table;
2406         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2407         my $asset_call_number_table = asset::call_number->table;
2408         my $asset_copy_table = asset::copy->table;
2409         my $cs_table = config::copy_status->table;
2410         my $cl_table = asset::copy_location->table;
2411
2412         my ($index_col) = $class->columns('FTS');
2413         $index_col ||= 'value';
2414
2415         my $fts = OpenILS::Application::Storage::FTS->compile($term, 'f.value', "f.$index_col");
2416
2417         my $fts_where = $fts->sql_where_clause;
2418         my @fts_ranks = $fts->fts_rank;
2419
2420         my $rank = join(' + ', @fts_ranks);
2421
2422         if ($self->api_name !~ /staff/o) {
2423                 $select = <<"           SQL";
2424                         SELECT  m.metarecord, 
2425                                 (SUM(   $rank
2426                                         * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
2427                                         * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
2428                                         * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
2429                                 )/COUNT(m.source)),
2430                                 CASE WHEN COUNT(DISTINCT rd.record) = 1 THEN MIN(m.source) ELSE 0 END
2431                         FROM    $search_table f,
2432                                 $metabib_metarecord_source_map_table m,
2433                                 $metabib_metarecord_source_map_table mr,
2434                                 $metabib_record_descriptor rd
2435                         WHERE   $fts_where
2436                                 AND mr.source = f.source
2437                                 AND mr.metarecord = m.metarecord
2438                                 AND rd.record = m.source
2439                                 $t_filter
2440                                 $f_filter
2441                                 AND EXISTS (
2442                                         SELECT  TRUE
2443                                           FROM  $asset_call_number_table cn,
2444                                                 $asset_copy_table cp,
2445                                                 $cs_table cs,
2446                                                 $cl_table cl,
2447                                                 $descendants d
2448                                           WHERE cn.record = mr.source
2449                                                 AND cp.status = cs.id
2450                                                 AND cp.location = cl.id
2451                                                 AND cn.owning_lib = d.id
2452                                                 AND cp.call_number = cn.id
2453                                                 AND cp.opac_visible IS TRUE
2454                                                 AND cs.holdable IS TRUE
2455                                                 AND cl.opac_visible IS TRUE )
2456                         GROUP BY m.metarecord
2457                         ORDER BY 2 DESC, MIN(COALESCE(CHAR_LENGTH(f.value),1))
2458                 SQL
2459         } else {
2460                 $select = <<"           SQL";
2461                         SELECT  m.metarecord,
2462                                 (SUM(   $rank
2463                                         * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
2464                                         * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
2465                                         * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
2466                                 )/COUNT(m.source)),
2467                                 CASE WHEN COUNT(DISTINCT rd.record) = 1 THEN MIN(m.source) ELSE 0 END
2468                         FROM    $search_table f,
2469                                 $metabib_metarecord_source_map_table m,
2470                                 $metabib_metarecord_source_map_table mr,
2471                                 $metabib_record_descriptor rd
2472                         WHERE   $fts_where
2473                                 AND m.source = f.source
2474                                 AND m.metarecord = mr.metarecord
2475                                 AND rd.record = m.source
2476                                 $t_filter
2477                                 $f_filter
2478                         GROUP BY m.metarecord
2479                         ORDER BY 2 DESC, MIN(COALESCE(CHAR_LENGTH(f.value),1))
2480                 SQL
2481         }
2482
2483         $log->debug("Field Search SQL :: [$select]",DEBUG);
2484
2485         my $SQLstring = join('%',$fts->words);
2486         my $REstring = join('\\s+',$fts->words);
2487         my $first_word = ($fts->words)[0].'%';
2488         my $recs = ($self->api_name =~ /unordered/o) ? 
2489                         $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
2490                         $class->db_Main->selectall_arrayref($select, {},
2491                                 '%'.lc($SQLstring).'%',                 # phrase order match
2492                                 lc($first_word),                        # first word match
2493                                 '^\\s*'.lc($REstring).'\\s*/?\s*$',     # full exact match
2494                                 @types, @forms
2495                         );
2496         
2497         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2498
2499         my $count = scalar(@$recs);
2500         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2501                 my ($mrid,$rank,$skip) = @$rec;
2502                 $client->respond( [$mrid, sprintf('%0.3f',$rank), $skip, $count] );
2503         }
2504         return undef;
2505 }
2506
2507 for my $class ( qw/title author subject keyword series/ ) {
2508         __PACKAGE__->register_method(
2509                 api_name        => "open-ils.storage.metabib.$class.new_search_fts.metarecord",
2510                 method          => 'new_search_class_fts',
2511                 api_level       => 1,
2512                 stream          => 1,
2513                 cdbi            => "metabib::${class}_field_entry",
2514                 cachable        => 1,
2515         );
2516         __PACKAGE__->register_method(
2517                 api_name        => "open-ils.storage.metabib.$class.new_search_fts.metarecord.staff",
2518                 method          => 'new_search_class_fts',
2519                 api_level       => 1,
2520                 stream          => 1,
2521                 cdbi            => "metabib::${class}_field_entry",
2522                 cachable        => 1,
2523         );
2524 }
2525
2526
2527
2528 sub multi_search_full_rec {
2529         my $self = shift;
2530         my $client = shift;
2531
2532         my %args = @_;  
2533         my $class_join = $args{class_join} || 'AND';
2534         my $limit = $args{limit} || 100;
2535         my $offset = $args{offset} || 0;
2536         my @binds;
2537         my @selects;
2538
2539         for my $arg (@{ $args{searches} }) {
2540                 my $term = $$arg{term};
2541                 my $limiters = $$arg{restrict};
2542
2543                 my ($index_col) = metabib::full_rec->columns('FTS');
2544                 $index_col ||= 'value';
2545                 my $search_table = metabib::full_rec->table;
2546
2547                 my $fts = OpenILS::Application::Storage::FTS->compile($term, 'value',"$index_col");
2548
2549                 my $fts_where = $fts->sql_where_clause();
2550                 my @fts_ranks = $fts->fts_rank;
2551
2552                 my $rank = join(' + ', @fts_ranks);
2553
2554                 my @wheres;
2555                 for my $limit (@$limiters) {
2556                         push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
2557                         push @binds, $$limit{tag}, $$limit{subfield};
2558                         $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
2559                 }
2560                 my $where = join(' OR ', @wheres);
2561
2562                 push @selects, "SELECT id, record, $rank as sum FROM $search_table WHERE $where";
2563
2564         }
2565
2566         my $descendants = defined($args{depth}) ?
2567                                 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
2568                                 "actor.org_unit_descendants($args{org_unit})" ;
2569
2570
2571         my $metabib_record_descriptor = metabib::record_descriptor->table;
2572         my $metabib_metarecord = metabib::metarecord->table;
2573         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2574         my $asset_call_number_table = asset::call_number->table;
2575         my $asset_copy_table = asset::copy->table;
2576         my $cs_table = config::copy_status->table;
2577         my $cl_table = asset::copy_location->table;
2578
2579         my $cj = 'HAVING COUNT(x.id) = ' . scalar(@selects) if ($class_join eq 'AND');
2580         my $search_table =
2581                 '(SELECT x.record, sum(x.sum) FROM (('.
2582                         join(') UNION ALL (', @selects).
2583                         ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
2584
2585         my $has_vols = 'AND cn.owning_lib = d.id';
2586         my $has_copies = 'AND cp.call_number = cn.id';
2587         my $copies_visible = 'AND cp.opac_visible IS TRUE AND cs.holdable IS TRUE AND cl.opac_visible IS TRUE';
2588
2589         if ($self->api_name =~ /staff/o) {
2590                 $copies_visible = '';
2591                 $has_copies = '' if ($ou_type == 0);
2592                 $has_vols = '' if ($ou_type == 0);
2593         }
2594
2595         my ($t_filter, $f_filter) = ('','');
2596
2597         if ($args{format}) {
2598                 my ($t, $f) = split '-', $args{format};
2599                 my @types = split '', $t;
2600                 my @forms = split '', $f;
2601                 if (@types) {
2602                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
2603                 }
2604
2605                 if (@forms) {
2606                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
2607                 }
2608                 push @binds, @types, @forms;
2609         }
2610
2611
2612         if ($copies_visible) {
2613                 $select = <<"           SQL";
2614                         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
2615                         FROM    $search_table f,
2616                                 $metabib_metarecord_source_map_table m,
2617                                 $asset_call_number_table cn,
2618                                 $asset_copy_table cp,
2619                                 $cs_table cs,
2620                                 $cl_table cl,
2621                                 $metabib_record_descriptor rd,
2622                                 $descendants d
2623                         WHERE   m.source = f.record
2624                                 AND cn.record = m.source
2625                                 AND rd.record = m.source
2626                                 AND cp.status = cs.id
2627                                 AND cp.location = cl.id
2628                                 $has_vols
2629                                 $has_copies
2630                                 $copies_visible
2631                                 $t_filter
2632                                 $f_filter
2633                         GROUP BY m.metarecord HAVING count(DISTINCT cp.id) > 0
2634                         ORDER BY 2 DESC,3 DESC
2635                 SQL
2636         } else {
2637                 $select = <<"           SQL";
2638                         SELECT  m.metarecord, 1, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
2639                         FROM    $search_table f,
2640                                 $metabib_metarecord_source_map_table m,
2641                                 $metabib_record_descriptor rd
2642                         WHERE   m.source = f.record
2643                                 AND rd.record = m.source
2644                                 $t_filter
2645                                 $f_filter
2646                         GROUP BY 1,2,3 
2647                 SQL
2648         }
2649
2650
2651         $log->debug("Search SQL :: [$select]",DEBUG);
2652
2653         my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
2654         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2655
2656         my $count = @$recs;
2657         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2658                 next unless ($$rec[0]);
2659                 my ($mrid,$rank,$junk,$skip) = @$rec;
2660                 $client->respond( [$mrid, sprintf('%0.3f',$rank), $skip, $count] );
2661         }
2662         return undef;
2663 }
2664 __PACKAGE__->register_method(
2665         api_name        => 'open-ils.storage.metabib.full_rec.multi_search',
2666         method          => 'multi_search_full_rec',
2667         api_level       => 1,
2668         stream          => 1,
2669         cachable        => 1,
2670 );
2671 __PACKAGE__->register_method(
2672         api_name        => 'open-ils.storage.metabib.full_rec.multi_search.staff',
2673         method          => 'multi_search_full_rec',
2674         api_level       => 1,
2675         stream          => 1,
2676         cachable        => 1,
2677 );
2678
2679