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