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