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