]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/metabib.pm
Remove dependence on search.query_parser_fts proc
[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                 @vformats = @$v;
2516         }
2517
2518         # XXX legacy format and item type support
2519         if ($args{format}) {
2520                 my ($t, $f) = split '-', $args{format};
2521                 @types = split '', $t;
2522                 @forms = split '', $f;
2523         }
2524
2525     my %stored_proc_search_args;
2526         for my $search_group (sort keys %{$args{searches}}) {
2527                 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2528                 my ($search_class,$search_field) = split /\|/, $search_group;
2529                 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2530
2531                 if ($search_field) {
2532                         unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2533                                 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2534                                 return undef;
2535                         }
2536                 }
2537
2538                 my $class = $_cdbi->{$search_class};
2539                 my $search_table = $class->table;
2540
2541                 my ($index_col) = $class->columns('FTS');
2542                 $index_col ||= 'value';
2543
2544                 
2545                 my $fts = OpenILS::Application::Storage::FTS->compile(
2546             $search_class => $args{searches}{$search_group}{term},
2547             $search_group_name.'.value',
2548             "$search_group_name.$index_col"
2549         );
2550                 $fts->sql_where_clause; # this builds the ranks for us
2551
2552                 my @fts_ranks   = $fts->fts_rank;
2553                 my @fts_queries = $fts->fts_query;
2554                 my @phrases = map { lc($_) } $fts->phrases;
2555                 my @words   = map { lc($_) } $fts->words;
2556
2557         $stored_proc_search_args{$search_group} = {
2558             fts_rank    => \@fts_ranks,
2559             fts_query   => \@fts_queries,
2560             phrase      => \@phrases,
2561             word        => \@words,
2562         };
2563
2564         }
2565
2566         my $param_search_ou = $ou;
2567         my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2568         my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2569         my $param_statuses  = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2570         my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2571         my $param_audience  = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud      ) . '}$$';
2572         my $param_language  = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang     ) . '}$$';
2573         my $param_lit_form  = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2574         my $param_types     = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types    ) . '}$$';
2575         my $param_forms     = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms    ) . '}$$';
2576         my $param_vformats  = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2577     my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2578         my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2579         my $param_after  = $args{after} ; $param_after  = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2580         my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2581     my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2582         my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2583         my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2584         my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2585         my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2586         my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2587         my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2588     my $param_rel_limit = $args{core_limit};  $param_rel_limit ||= 'NULL';
2589     my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2590     my $param_skip_chk  = $args{skip_check};  $param_skip_chk  ||= 'NULL';
2591
2592         my $sth = metabib::metarecord_source_map->db_Main->prepare(<<"    SQL");
2593         SELECT  *
2594           FROM  search.staged_fts(
2595                     $param_search_ou\:\:INT,
2596                     $param_depth\:\:INT,
2597                     $param_searches\:\:TEXT,
2598                     $param_statuses\:\:INT[],
2599                     $param_locations\:\:INT[],
2600                     $param_audience\:\:TEXT[],
2601                     $param_language\:\:TEXT[],
2602                     $param_lit_form\:\:TEXT[],
2603                     $param_types\:\:TEXT[],
2604                     $param_forms\:\:TEXT[],
2605                     $param_vformats\:\:TEXT[],
2606                     $param_bib_level\:\:TEXT[],
2607                     $param_before\:\:TEXT,
2608                     $param_after\:\:TEXT,
2609                     $param_during\:\:TEXT,
2610                     $param_between\:\:TEXT[],
2611                     $param_pref_lang\:\:TEXT,
2612                     $param_pref_lang_multiplier\:\:REAL,
2613                     $param_sort\:\:TEXT,
2614                     $param_sort_desc\:\:BOOL,
2615                     $metarecord\:\:BOOL,
2616                     $staff\:\:BOOL,
2617                     $param_rel_limit\:\:INT,
2618                     $param_chk_limit\:\:INT,
2619                     $param_skip_chk\:\:INT
2620                 );
2621     SQL
2622
2623     $sth->execute;
2624
2625     my $recs = $sth->fetchall_arrayref({});
2626     my $summary_row = pop @$recs;
2627
2628     my $total    = $$summary_row{total};
2629     my $checked  = $$summary_row{checked};
2630     my $visible  = $$summary_row{visible};
2631     my $deleted  = $$summary_row{deleted};
2632     my $excluded = $$summary_row{excluded};
2633
2634     my $estimate = $visible;
2635     if ( $total > $checked && $checked ) {
2636
2637         $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2638         $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2639
2640     }
2641
2642     delete $$summary_row{id};
2643     delete $$summary_row{rel};
2644     delete $$summary_row{record};
2645
2646     $client->respond( $summary_row );
2647
2648         $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2649
2650         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2651         delete $$rec{checked};
2652         delete $$rec{visible};
2653         delete $$rec{excluded};
2654         delete $$rec{deleted};
2655         delete $$rec{total};
2656         $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2657
2658                 $client->respond( $rec );
2659         }
2660         return undef;
2661 }
2662 __PACKAGE__->register_method(
2663         api_name        => "open-ils.storage.biblio.multiclass.staged.search_fts",
2664         method          => 'staged_fts',
2665         api_level       => 0,
2666         stream          => 1,
2667         cachable        => 1,
2668 );
2669 __PACKAGE__->register_method(
2670         api_name        => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2671         method          => 'staged_fts',
2672         api_level       => 0,
2673         stream          => 1,
2674         cachable        => 1,
2675 );
2676 __PACKAGE__->register_method(
2677         api_name        => "open-ils.storage.metabib.multiclass.staged.search_fts",
2678         method          => 'staged_fts',
2679         api_level       => 0,
2680         stream          => 1,
2681         cachable        => 1,
2682 );
2683 __PACKAGE__->register_method(
2684         api_name        => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2685         method          => 'staged_fts',
2686         api_level       => 0,
2687         stream          => 1,
2688         cachable        => 1,
2689 );
2690
2691 sub FTS_paging_estimate {
2692     my $self   = shift;
2693     my $client = shift;
2694
2695     my $checked  = shift;
2696     my $visible  = shift;
2697     my $excluded = shift;
2698     my $deleted  = shift;
2699     my $total    = shift;
2700
2701     my $deleted_ratio = $deleted / $checked;
2702     my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2703
2704     my $exclusion_ratio = $excluded / $checked;
2705     my $delete_adjusted_exclusion_ratio = $checked - $deleted ? $excluded / ($checked - $deleted) : 1;
2706
2707     my $inclusion_ratio = $visible / $checked;
2708     my $delete_adjusted_inclusion_ratio = $checked - $deleted ? $visible / ($checked - $deleted) : 0;
2709
2710     return {
2711         exclusion                   => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2712         inclusion                   => int($delete_adjusted_total * $inclusion_ratio),
2713         delete_adjusted_exclusion   => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2714         delete_adjusted_inclusion   => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2715     };
2716 }
2717 __PACKAGE__->register_method(
2718         api_name        => "open-ils.storage.fts_paging_estimate",
2719         method          => 'FTS_paging_estimate',
2720     argc        => 5,
2721     strict      => 1,
2722         api_level       => 1,
2723     signature   => {
2724         'return'=> q#
2725             Hash of estimation values based on four variant estimation strategies:
2726                 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2727                 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2728                 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2729                 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2730         #,
2731         desc    => q#
2732             Helper method used to determin the approximate number of
2733             hits for a search that spans multiple superpages.  For
2734             sparse superpages, the inclusion estimate will likely be the
2735             best estimate.  The exclusion strategy is the original, but
2736             inclusion is the default.
2737         #,
2738         params  => [
2739             {   name    => 'checked',
2740                 desc    => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2741                 type    => 'number'
2742             },
2743             {   name    => 'visible',
2744                 desc    => 'Number of records visible to the search location on the current superpage.',
2745                 type    => 'number'
2746             },
2747             {   name    => 'excluded',
2748                 desc    => 'Number of records excluded from the search location on the current superpage.',
2749                 type    => 'number'
2750             },
2751             {   name    => 'deleted',
2752                 desc    => 'Number of deleted records on the current superpage.',
2753                 type    => 'number'
2754             },
2755             {   name    => 'total',
2756                 desc    => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2757                 type    => 'number'
2758             }
2759         ]
2760     }
2761 );
2762
2763
2764 sub xref_count {
2765     my $self   = shift;
2766     my $client = shift;
2767     my $args   = shift;
2768
2769     my $term  = $$args{term};
2770     my $limit = $$args{max} || 1;
2771     my $min   = $$args{min} || 1;
2772         my @classes = @{$$args{class}};
2773
2774         $limit = $min if ($min > $limit);
2775
2776         if (!@classes) {
2777                 @classes = ( qw/ title author subject series keyword / );
2778         }
2779
2780         my %matches;
2781         my $bre_table = biblio::record_entry->table;
2782         my $cn_table  = asset::call_number->table;
2783         my $cp_table  = asset::copy->table;
2784
2785         for my $search_class ( @classes ) {
2786
2787                 my $class = $_cdbi->{$search_class};
2788                 my $search_table = $class->table;
2789
2790                 my ($index_col) = $class->columns('FTS');
2791                 $index_col ||= 'value';
2792
2793                 
2794                 my $where = OpenILS::Application::Storage::FTS
2795                         ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2796                         ->sql_where_clause;
2797
2798                 my $SQL = <<"           SQL";
2799                         SELECT  COUNT(DISTINCT X.source)
2800                           FROM  (SELECT $search_class.source
2801                                   FROM  $search_table $search_class
2802                                         JOIN $bre_table b ON (b.id = $search_class.source)
2803                                   WHERE $where
2804                                         AND NOT b.deleted
2805                                         AND b.active
2806                                   LIMIT $limit) X
2807                           HAVING COUNT(DISTINCT X.source) >= $min;
2808                 SQL
2809
2810                 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2811                 $matches{$search_class} = $res ? $res->[0] : 0;
2812         }
2813
2814         return \%matches;
2815 }
2816 __PACKAGE__->register_method(
2817     api_name  => "open-ils.storage.search.xref",
2818     method    => 'xref_count',
2819     api_level => 1,
2820 );
2821
2822 # Takes an abstract query object and recursively turns it back into a string
2823 # for QueryParser.
2824 sub abstract_query2str {
2825     my ($self, $conn, $query) = @_;
2826
2827     return QueryParser::Canonicalize::abstract_query2str_impl($query, 0);
2828 }
2829
2830 __PACKAGE__->register_method(
2831         api_name        => "open-ils.storage.query_parser.abstract_query.canonicalize",
2832         method          => "abstract_query2str",
2833         api_level       => 1,
2834     signature   => {
2835         params  => [
2836             {desc => q/
2837 Abstract query parser object, with complete config data. For example input,
2838 see the 'abstract_query' part of the output of an API call like
2839 open-ils.search.biblio.multiclass.query, when called with the return_abstract
2840 flag set to true./,
2841                 type => "object"}
2842         ],
2843         return => { type => "string", desc => "String representation of abstract query object" }
2844     }
2845 );
2846
2847 sub str2abstract_query {
2848     my ($self, $conn, $query, $qp_opts, $with_config) = @_;
2849
2850     my %use_opts = ( # reasonable defaults? should these even be hardcoded here?
2851         superpage => 1,
2852         superpage_size => 1000,
2853         core_limit => 25000,
2854         query => $query,
2855         (ref $opts eq 'HASH' ? %$opts : ())
2856     );
2857
2858     $with_config ||= 0;
2859
2860     # grab the query parser and initialize it
2861     my $parser = $OpenILS::Application::Storage::QParser;
2862     $parser->use;
2863
2864     _initialize_parser($parser) unless $parser->initialization_complete;
2865
2866     my $query = $parser->new(%use_opts)->parse;
2867
2868     return $query->parse_tree->to_abstract_query(with_config => $with_config);
2869 }
2870
2871 __PACKAGE__->register_method(
2872         api_name        => "open-ils.storage.query_parser.abstract_query.from_string",
2873         method          => "str2abstract_query",
2874         api_level       => 1,
2875     signature   => {
2876         params  => [
2877             {desc => "Query", type => "string"},
2878             {desc => q/Arguments for initializing QueryParser (optional)/,
2879                 type => "object"},
2880             {desc => q/Flag enabling inclusion of QP config in returned object (optional, default false)/,
2881                 type => "bool"}
2882         ],
2883         return => { type => "object", desc => "abstract representation of query parser query" }
2884     }
2885 );
2886
2887 sub query_parser_fts {
2888     my $self = shift;
2889     my $client = shift;
2890     my %args = @_;
2891
2892
2893     # grab the query parser and initialize it
2894     my $parser = $OpenILS::Application::Storage::QParser;
2895     $parser->use;
2896
2897     _initialize_parser($parser) unless $parser->initialization_complete;
2898
2899     # populate the locale/language map
2900     if (!$locale_map{COMPLETE}) {
2901
2902         my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2903         for my $locale ( @locales ) {
2904             $locale_map{lc($locale->code)} = $locale->marc_code;
2905         }
2906         $locale_map{COMPLETE} = 1;
2907
2908     }
2909
2910     # I hope we have a query!
2911         if (! $args{query} ) {
2912                 die "No query was passed to ".$self->api_name;
2913         }
2914
2915     my $default_CD_modifiers = OpenSRF::Utils::SettingsClient->new->config_value(
2916         apps => 'open-ils.search' => app_settings => 'default_CD_modifiers'
2917     );
2918
2919     # Protect against empty / missing default_CD_modifiers setting
2920     if ($default_CD_modifiers and !ref($default_CD_modifiers)) {
2921         $args{query} = "$default_CD_modifiers $args{query}";
2922     }
2923
2924     my $simple_plan = $args{_simple_plan};
2925     # remove bad chunks of the %args hash
2926     for my $bad ( grep { /^_/ } keys(%args)) {
2927         delete($args{$bad});
2928     }
2929
2930
2931     # parse the query and supply any query-level %arg-based defaults
2932     # we expect, and make use of, query, debug and core_limit args
2933     my $query = $parser->new( %args )->parse;
2934
2935     my $config = OpenSRF::Utils::SettingsClient->new();
2936
2937     # set the locale-based default preferred location
2938     if (!$query->parse_tree->find_filter('preferred_language')) {
2939         $parser->default_preferred_language( $args{preferred_language} );
2940
2941         if (!$parser->default_preferred_language) {
2942                     my $ses_locale = $client->session ? $client->session->session_locale : '';
2943             $parser->default_preferred_language( $locale_map{ lc($ses_locale) } );
2944         }
2945
2946         if (!$parser->default_preferred_language) { # still nothing...
2947             my $tmp_dpl = $config->config_value(
2948                 apps => 'open-ils.search' => app_settings => 'default_preferred_language'
2949             ) || $config->config_value(
2950                 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2951             );
2952
2953             $parser->default_preferred_language( $tmp_dpl )
2954         }
2955     }
2956
2957
2958     # set the global default language multiplier
2959     if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2960         my $tmp_dplw;
2961
2962         if ($tmp_dplw = $args{preferred_language_weight} || $args{preferred_language_multiplier} ) {
2963             $parser->default_preferred_language_multiplier($tmp_dplw);
2964
2965         } else {
2966             $tmp_dplw = $config->config_value(
2967                 apps => 'open-ils.search' => app_settings => 'default_preferred_language_weight'
2968             ) || $config->config_value(
2969                 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2970             );
2971
2972             $parser->default_preferred_language_multiplier( $tmp_dplw );
2973         }
2974     }
2975
2976         my $sth = metabib::metarecord_source_map->db_Main->prepare($query->parse_tree->toSQL);
2977     $sth->execute;
2978
2979     my $recs = $sth->fetchall_arrayref({});
2980         $log->debug("Search yielded ".scalar(@$recs)." checked, visible results.",DEBUG);
2981
2982     $client->respond({visible => scalar(@$recs)});
2983
2984         for my $rec (@$recs) {
2985         $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2986
2987                 $client->respond( $rec );
2988         }
2989         return undef;
2990 }
2991 __PACKAGE__->register_method(
2992         api_name        => "open-ils.storage.query_parser_search",
2993         method          => 'query_parser_fts',
2994         api_level       => 1,
2995         stream          => 1,
2996         cachable        => 1,
2997 );
2998
2999 sub query_parser_fts_wrapper {
3000         my $self = shift;
3001         my $client = shift;
3002         my %args = @_;
3003
3004         $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3005     # grab the query parser and initialize it
3006     my $parser = $OpenILS::Application::Storage::QParser;
3007     $parser->use;
3008
3009     if (!$parser->initialization_complete) {
3010         my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3011         $parser->initialize(
3012             config_record_attr_index_norm_map =>
3013                 $cstore->request(
3014                     'open-ils.cstore.direct.config.record_attr_index_norm_map.search.atomic',
3015                     { id => { "!=" => undef } },
3016                     { flesh => 1, flesh_fields => { crainm => [qw/norm/] }, order_by => [{ class => "crainm", field => "pos" }] }
3017                 )->gather(1),
3018             search_relevance_adjustment         =>
3019                 $cstore->request(
3020                     'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
3021                     { id => { "!=" => undef } }
3022                 )->gather(1),
3023             config_metabib_field                =>
3024                 $cstore->request(
3025                     'open-ils.cstore.direct.config.metabib_field.search.atomic',
3026                     { id => { "!=" => undef } }
3027                 )->gather(1),
3028             config_metabib_search_alias         =>
3029                 $cstore->request(
3030                     'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
3031                     { alias => { "!=" => undef } }
3032                 )->gather(1),
3033             config_metabib_field_index_norm_map =>
3034                 $cstore->request(
3035                     'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
3036                     { id => { "!=" => undef } },
3037                     { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
3038                 )->gather(1),
3039             config_record_attr_definition       =>
3040                 $cstore->request(
3041                     'open-ils.cstore.direct.config.record_attr_definition.search.atomic',
3042                     { name => { "!=" => undef } }
3043                 )->gather(1),
3044         );
3045
3046         $cstore->disconnect;
3047         die("Cannot initialize $parser!") unless ($parser->initialization_complete);
3048     }
3049
3050         if (! scalar( keys %{$args{searches}} )) {
3051                 die "No search arguments were passed to ".$self->api_name;
3052         }
3053
3054         $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3055     my $base_query = '';
3056     for my $sclass ( keys %{$args{searches}} ) {
3057             $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3058         $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3059     }
3060
3061     my $query = $base_query;
3062     $log->debug("Full base query: $base_query", DEBUG);
3063
3064     $query = "$args{facets} $query" if  ($args{facets});
3065
3066     if (!$locale_map{COMPLETE}) {
3067
3068         my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3069         for my $locale ( @locales ) {
3070             $locale_map{lc($locale->code)} = $locale->marc_code;
3071         }
3072         $locale_map{COMPLETE} = 1;
3073
3074     }
3075
3076     my $base_plan = $parser->new( query => $base_query )->parse;
3077
3078     $query = "$query preferred_language($args{preferred_language})"
3079         if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3080     $query = "$query preferred_language_weight($args{preferred_language_weight})"
3081         if ($args{preferred_language_weight} and !$base_plan->parse_tree->find_filter('preferred_language_weight') and !$base_plan->parse_tree->find_filter('preferred_language_multiplier'));
3082
3083
3084     # we add these to the end of the query (last-wins) because in wrapper mode we want to retain the behaviour
3085     # of separately specified options taking precidenc -- IOW, the user should not be able to cause a change in,
3086     # say, superpage size by adjusting the query string.
3087     $query = "$query estimation_strategy($args{estimation_strategy})" if ($args{estimation_strategy});
3088     $query = "$query site($args{org_unit})" if ($args{org_unit});
3089     $query = "$query depth($args{depth})" if (defined($args{depth}));
3090     $query = "$query sort($args{sort})" if ($args{sort});
3091     $query = "$query limit($args{limit})" if ($args{limit});
3092     $query = "$query core_limit($args{core_limit})" if ($args{core_limit});
3093     $query = "$query skip_check($args{skip_check})" if ($args{skip_check});
3094     $query = "$query superpage($args{superpage})" if ($args{superpage});
3095     $query = "$query offset($args{offset})" if ($args{offset});
3096     $query = "$query #metarecord" if ($self->api_name =~ /metabib/);
3097     $query = "$query #available" if ($args{available});
3098     $query = "$query #descending" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3099     $query = "$query #staff" if ($self->api_name =~ /staff/);
3100     $query = "$query before($args{before})" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3101     $query = "$query after($args{after})" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3102     $query = "$query during($args{during})" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3103     $query = "$query between($args{between}[0],$args{between}[1])"
3104         if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3105
3106
3107         my (@between,@statuses,@locations,@location_groups,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3108
3109         # XXX legacy format and item type support
3110         if ($args{format}) {
3111                 my ($t, $f) = split '-', $args{format};
3112                 $args{item_type} = [ split '', $t ];
3113                 $args{item_form} = [ split '', $f ];
3114         }
3115
3116     for my $filter ( qw/locations location_groups statuses between audience language lit_form item_form item_type bib_level vr_format/ ) {
3117         if (my $s = $args{$filter}) {
3118                 $s = [$s] if (!ref($s));
3119
3120                 my @filter_list = @$s;
3121
3122             next if ($filter eq 'between' and scalar(@filter_list) != 2);
3123             next if (@filter_list == 0);
3124
3125             my $filter_string = join ',', @filter_list;
3126             $query = "$query $filter($filter_string)";
3127             }
3128     }
3129
3130     $log->debug("Full QueryParser query: $query", DEBUG);
3131
3132     return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan );
3133 }
3134 __PACKAGE__->register_method(
3135         api_name        => "open-ils.storage.biblio.multiclass.staged.search_fts",
3136         method          => 'query_parser_fts_wrapper',
3137         api_level       => 1,
3138         stream          => 1,
3139         cachable        => 1,
3140 );
3141 __PACKAGE__->register_method(
3142         api_name        => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3143         method          => 'query_parser_fts_wrapper',
3144         api_level       => 1,
3145         stream          => 1,
3146         cachable        => 1,
3147 );
3148 __PACKAGE__->register_method(
3149         api_name        => "open-ils.storage.metabib.multiclass.staged.search_fts",
3150         method          => 'query_parser_fts_wrapper',
3151         api_level       => 1,
3152         stream          => 1,
3153         cachable        => 1,
3154 );
3155 __PACKAGE__->register_method(
3156         api_name        => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3157         method          => 'query_parser_fts_wrapper',
3158         api_level       => 1,
3159         stream          => 1,
3160         cachable        => 1,
3161 );
3162
3163
3164 1;
3165