]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Storage/Publisher/metabib.pm
identifier search class, including some (I believe) sane default index defs
[working/Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Storage / Publisher / metabib.pm
1 package OpenILS::Application::Storage::Publisher::metabib;
2 use base qw/OpenILS::Application::Storage::Publisher/;
3 use vars qw/$VERSION/;
4 use OpenSRF::EX qw/:try/;
5 use OpenILS::Application::Storage::FTS;
6 use OpenILS::Utils::Fieldmapper;
7 use OpenSRF::Utils::Logger qw/:level/;
8 use OpenSRF::Utils::Cache;
9 use OpenSRF::Utils::JSON;
10 use Data::Dumper;
11 use Digest::MD5 qw/md5_hex/;
12
13
14 my $log = 'OpenSRF::Utils::Logger';
15
16 $VERSION = 1;
17
18 sub ordered_records_from_metarecord {
19         my $self = shift;
20         my $client = shift;
21         my $mr = shift;
22         my $formats = shift;
23         my $org = shift || 1;
24         my $depth = shift;
25
26         my (@types,@forms,@blvl);
27
28         if ($formats) {
29                 my ($t, $f, $b) = split '-', $formats;
30                 @types = split '', $t;
31                 @forms = split '', $f;
32                 @blvl = split '', $b;
33         }
34
35         my $descendants =
36                 defined($depth) ?
37                         "actor.org_unit_descendants($org, $depth)" :
38                         "actor.org_unit_descendants($org)" ;
39
40
41         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';
42         $copies_visible = '' if ($self->api_name =~ /staff/o);
43
44         my $sm_table = metabib::metarecord_source_map->table;
45         my $rd_table = metabib::record_descriptor->table;
46         my $fr_table = metabib::full_rec->table;
47         my $cn_table = asset::call_number->table;
48         my $cl_table = asset::copy_location->table;
49         my $cp_table = asset::copy->table;
50         my $cs_table = config::copy_status->table;
51         my $src_table = config::bib_source->table;
52         my $out_table = actor::org_unit_type->table;
53         my $br_table = biblio::record_entry->table;
54
55         my $sql = <<"   SQL";
56                 SELECT  record,
57                         item_type,
58                         item_form,
59                         quality,
60                         FIRST(COALESCE(LTRIM(SUBSTR( value, COALESCE(SUBSTRING(ind2 FROM '\\\\d+'),'0')::INT + 1 )),'zzzzzzzz')) AS title
61                 FROM    (
62                         SELECT  rd.record,
63                                 rd.item_type,
64                                 rd.item_form,
65                                 br.quality,
66                                 fr.tag,
67                                 fr.subfield,
68                                 fr.value,
69                                 fr.ind2
70         SQL
71
72         if ($copies_visible) {
73                 $sql .= <<"             SQL";
74                           FROM  $sm_table sm,
75                                 $br_table br,
76                                 $fr_table fr,
77                                 $rd_table rd
78                           WHERE rd.record = sm.source
79                                 AND fr.record = sm.source
80                                 AND br.id = sm.source
81                                 AND sm.metarecord = ?
82                                 AND (EXISTS ((SELECT    1
83                                                 FROM    $cp_table cp
84                                                         JOIN $cn_table cn ON (cp.call_number = cn.id)
85                                                         JOIN $cs_table cs ON (cp.status = cs.id)
86                                                         JOIN $cl_table cl ON (cp.location = cl.id)
87                                                         JOIN $descendants d ON (cp.circ_lib = d.id)
88                                                 WHERE   cn.record = sm.source
89                                                         $copies_visible
90                                                 LIMIT 1))
91                                         OR EXISTS ((
92                                             SELECT      1
93                                               FROM      $src_table src
94                                               WHERE     src.id = br.source
95                                                         AND src.transcendant IS TRUE))
96                                 )
97                                           
98                 SQL
99         } else {
100                 $sql .= <<"             SQL";
101                           FROM  $sm_table sm
102                                 JOIN $br_table br ON (sm.source = br.id)
103                                 JOIN $fr_table fr ON (fr.record = br.id)
104                                 JOIN $rd_table rd ON (rd.record = br.id)
105                           WHERE sm.metarecord = ?
106                                 AND ((  EXISTS (
107                                                 SELECT  1
108                                                   FROM  $cp_table cp,
109                                                         $cn_table cn,
110                                                         $descendants d
111                                                   WHERE cn.record = br.id
112                                                         AND cn.deleted = FALSE
113                                                         AND cp.deleted = FALSE
114                                                         AND cp.circ_lib = d.id
115                                                         AND cn.id = cp.call_number
116                                                   LIMIT 1
117                                         ) OR NOT EXISTS (
118                                                 SELECT  1
119                                                   FROM  $cp_table cp,
120                                                         $cn_table cn
121                                                   WHERE cn.record = br.id
122                                                         AND cn.deleted = FALSE
123                                                         AND cp.deleted = FALSE
124                                                         AND cn.id = cp.call_number
125                                                   LIMIT 1
126                                         ))
127                                         OR EXISTS ((
128                                             SELECT      1
129                                               FROM      $src_table src
130                                               WHERE     src.id = br.source
131                                                         AND src.transcendant IS TRUE))
132                                 )
133                 SQL
134         }
135
136         if (@types) {
137                 $sql .= '                               AND rd.item_type IN ('.join(',',map{'?'}@types).')';
138         }
139
140         if (@forms) {
141                 $sql .= '                               AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
142         }
143
144         if (@blvl) {
145                 $sql .= '                               AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
146         }
147
148
149
150         $sql .= <<"     SQL";
151                           OFFSET 0
152                         ) AS x
153           WHERE tag = '245'
154                 AND subfield = 'a'
155           GROUP BY record, item_type, item_form, quality
156           ORDER BY
157                 CASE
158                         WHEN item_type IS NULL -- default
159                                 THEN 0
160                         WHEN item_type = '' -- default
161                                 THEN 0
162                         WHEN item_type IN ('a','t') -- books
163                                 THEN 1
164                         WHEN item_type = 'g' -- movies
165                                 THEN 2
166                         WHEN item_type IN ('i','j') -- sound recordings
167                                 THEN 3
168                         WHEN item_type = 'm' -- software
169                                 THEN 4
170                         WHEN item_type = 'k' -- images
171                                 THEN 5
172                         WHEN item_type IN ('e','f') -- maps
173                                 THEN 6
174                         WHEN item_type IN ('o','p') -- mixed
175                                 THEN 7
176                         WHEN item_type IN ('c','d') -- music
177                                 THEN 8
178                         WHEN item_type = 'r' -- 3d
179                                 THEN 9
180                 END,
181                 title ASC,
182                 quality DESC
183         SQL
184
185         my $ids = metabib::metarecord_source_map->db_Main->selectcol_arrayref($sql, {}, "$mr", @types, @forms, @blvl);
186         return $ids if ($self->api_name =~ /atomic$/o);
187
188         $client->respond( $_ ) for ( @$ids );
189         return undef;
190
191 }
192 __PACKAGE__->register_method(
193         api_name        => 'open-ils.storage.ordered.metabib.metarecord.records',
194         method          => 'ordered_records_from_metarecord',
195         api_level       => 1,
196         cachable        => 1,
197 );
198 __PACKAGE__->register_method(
199         api_name        => 'open-ils.storage.ordered.metabib.metarecord.records.staff',
200         method          => 'ordered_records_from_metarecord',
201         api_level       => 1,
202         cachable        => 1,
203 );
204
205 __PACKAGE__->register_method(
206         api_name        => 'open-ils.storage.ordered.metabib.metarecord.records.atomic',
207         method          => 'ordered_records_from_metarecord',
208         api_level       => 1,
209         cachable        => 1,
210 );
211 __PACKAGE__->register_method(
212         api_name        => 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic',
213         method          => 'ordered_records_from_metarecord',
214         api_level       => 1,
215         cachable        => 1,
216 );
217
218 sub isxn_search {
219         my $self = shift;
220         my $client = shift;
221         my $isxn = lc(shift());
222
223         $isxn =~ s/^\s*//o;
224         $isxn =~ s/\s*$//o;
225         $isxn =~ s/-//o if ($self->api_name =~ /isbn/o);
226
227         my $tag = ($self->api_name =~ /isbn/o) ? "'020' OR f.tag = '024'" : "'022'";
228
229         my $fr_table = metabib::full_rec->table;
230         my $bib_table = biblio::record_entry->table;
231
232         my $sql = <<"   SQL";
233                 SELECT  DISTINCT f.record
234                   FROM  $fr_table f
235                         JOIN $bib_table b ON (b.id = f.record)
236                   WHERE (f.tag = $tag)
237                         AND f.value LIKE ?
238                         AND b.deleted IS FALSE
239         SQL
240
241         my $list = metabib::full_rec->db_Main->selectcol_arrayref($sql, {}, "$isxn%");
242         $client->respond($_) for (@$list);
243         return undef;
244 }
245 __PACKAGE__->register_method(
246         api_name        => 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
247         method          => 'isxn_search',
248         api_level       => 1,
249         stream          => 1,
250 );
251 __PACKAGE__->register_method(
252         api_name        => 'open-ils.storage.id_list.biblio.record_entry.search.issn',
253         method          => 'isxn_search',
254         api_level       => 1,
255         stream          => 1,
256 );
257
258 sub metarecord_copy_count {
259         my $self = shift;
260         my $client = shift;
261
262         my %args = @_;
263
264         my $sm_table = metabib::metarecord_source_map->table;
265         my $rd_table = metabib::record_descriptor->table;
266         my $cn_table = asset::call_number->table;
267         my $cp_table = asset::copy->table;
268         my $br_table = biblio::record_entry->table;
269         my $src_table = config::bib_source->table;
270         my $cl_table = asset::copy_location->table;
271         my $cs_table = config::copy_status->table;
272         my $out_table = actor::org_unit_type->table;
273
274         my $descendants = "actor.org_unit_descendants(u.id)";
275         my $ancestors = "actor.org_unit_ancestors(?) u JOIN $out_table t ON (u.ou_type = t.id)";
276
277     if ($args{org_unit} < 0) {
278         $args{org_unit} *= -1;
279         $ancestors = "(select org_unit as id from actor.org_lasso_map where lasso = ?) u CROSS JOIN (SELECT -1 AS depth) t";
280     }
281
282         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';
283         $copies_visible = '' if ($self->api_name =~ /staff/o);
284
285         my (@types,@forms,@blvl);
286         my ($t_filter, $f_filter, $b_filter) = ('','','');
287
288         if ($args{format}) {
289                 my ($t, $f, $b) = split '-', $args{format};
290                 @types = split '', $t;
291                 @forms = split '', $f;
292                 @blvl = split '', $b;
293
294                 if (@types) {
295                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
296                 }
297
298                 if (@forms) {
299                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
300                 }
301
302                 if (@blvl) {
303                         $b_filter .= ' AND rd.bib_level IN ('.join(',',map{'?'}@blvl).')';
304                 }
305         }
306
307         my $sql = <<"   SQL";
308                 SELECT  t.depth,
309                         u.id AS org_unit,
310                         sum(
311                                 (SELECT count(cp.id)
312                                   FROM  $sm_table r
313                                         JOIN $cn_table cn ON (cn.record = r.source)
314                                         JOIN $rd_table rd ON (cn.record = rd.record)
315                                         JOIN $cp_table cp ON (cn.id = cp.call_number)
316                                         JOIN $cs_table cs ON (cp.status = cs.id)
317                                         JOIN $cl_table cl ON (cp.location = cl.id)
318                                         JOIN $descendants a ON (cp.circ_lib = a.id)
319                                   WHERE r.metarecord = ?
320                                         AND cn.deleted IS FALSE
321                                         AND cp.deleted IS FALSE
322                                         $copies_visible
323                                         $t_filter
324                                         $f_filter
325                                         $b_filter
326                                 )
327                         ) AS count,
328                         sum(
329                                 (SELECT count(cp.id)
330                                   FROM  $sm_table r
331                                         JOIN $cn_table cn ON (cn.record = r.source)
332                                         JOIN $rd_table rd ON (cn.record = rd.record)
333                                         JOIN $cp_table cp ON (cn.id = cp.call_number)
334                                         JOIN $cs_table cs ON (cp.status = cs.id)
335                                         JOIN $cl_table cl ON (cp.location = cl.id)
336                                         JOIN $descendants a ON (cp.circ_lib = a.id)
337                                   WHERE r.metarecord = ?
338                                         AND cp.status IN (0,7,12)
339                                         AND cn.deleted IS FALSE
340                                         AND cp.deleted IS FALSE
341                                         $copies_visible
342                                         $t_filter
343                                         $f_filter
344                                         $b_filter
345                                 )
346                         ) AS available,
347                         sum(
348                                 (SELECT count(cp.id)
349                                   FROM  $sm_table r
350                                         JOIN $cn_table cn ON (cn.record = r.source)
351                                         JOIN $rd_table rd ON (cn.record = rd.record)
352                                         JOIN $cp_table cp ON (cn.id = cp.call_number)
353                                         JOIN $cs_table cs ON (cp.status = cs.id)
354                                         JOIN $cl_table cl ON (cp.location = cl.id)
355                                   WHERE r.metarecord = ?
356                                         AND cn.deleted IS FALSE
357                                         AND cp.deleted IS FALSE
358                                         AND cp.opac_visible IS TRUE
359                                         AND cs.opac_visible IS TRUE
360                                         AND cl.opac_visible IS TRUE
361                                         $t_filter
362                                         $f_filter
363                                         $b_filter
364                                 )
365                         ) AS unshadow,
366                         sum(    
367                                 (SELECT sum(1)
368                                   FROM  $sm_table r
369                                         JOIN $br_table br ON (br.id = r.source)
370                                         JOIN $src_table src ON (src.id = br.source)
371                                   WHERE r.metarecord = ?
372                                         AND src.transcendant IS TRUE
373                                 )
374                         ) AS transcendant
375
376                   FROM  $ancestors
377                   GROUP BY 1,2
378         SQL
379
380         my $sth = metabib::metarecord_source_map->db_Main->prepare_cached($sql);
381         $sth->execute(  ''.$args{metarecord},
382                         @types, 
383                         @forms,
384                         @blvl,
385                         ''.$args{metarecord},
386                         @types, 
387                         @forms,
388                         @blvl,
389                         ''.$args{metarecord},
390                         @types, 
391                         @forms,
392                         @blvl,
393                         ''.$args{metarecord},
394                         ''.$args{org_unit}, 
395         ); 
396
397         while ( my $row = $sth->fetchrow_hashref ) {
398                 $client->respond( $row );
399         }
400         return undef;
401 }
402 __PACKAGE__->register_method(
403         api_name        => 'open-ils.storage.metabib.metarecord.copy_count',
404         method          => 'metarecord_copy_count',
405         api_level       => 1,
406         stream          => 1,
407         cachable        => 1,
408 );
409 __PACKAGE__->register_method(
410         api_name        => 'open-ils.storage.metabib.metarecord.copy_count.staff',
411         method          => 'metarecord_copy_count',
412         api_level       => 1,
413         stream          => 1,
414         cachable        => 1,
415 );
416
417 sub biblio_multi_search_full_rec {
418     my $self   = shift;
419     my $client = shift;
420     my %args   = @_;
421
422     my $class_join = $args{class_join} || 'AND';
423     my $limit      = $args{limit}      || 100;
424     my $offset     = $args{offset}     || 0;
425     my $sort       = $args{'sort'};
426     my $sort_dir   = $args{sort_dir}   || 'DESC';
427
428         my @binds;
429         my @selects;
430
431         for my $arg (@{ $args{searches} }) {
432                 my $term     = $$arg{term};
433                 my $limiters = $$arg{restrict};
434
435                 my ($index_col)  = metabib::full_rec->columns('FTS') || 'value';
436                 my $search_table = metabib::full_rec->table;
437
438                 my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
439
440                 my $fts_where = $fts->sql_where_clause();
441                 my @fts_ranks = $fts->fts_rank;
442
443                 my $rank = join(' + ', @fts_ranks);
444
445                 my @wheres;
446                 for my $limit (@$limiters) {
447                         push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
448                         push @binds, $$limit{tag}, $$limit{subfield};
449                         $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
450                 }
451                 my $where = join(' OR ', @wheres);
452
453                 push @selects, "SELECT id, record, $rank as sum FROM $search_table WHERE $where";
454
455         }
456
457         my $descendants = defined($args{depth}) ?
458                                 "actor.org_unit_descendants($args{org_unit}, $args{depth})" :
459                                 "actor.org_unit_descendants($args{org_unit})" ;
460
461
462         my $metabib_record_descriptor = metabib::record_descriptor->table;
463         my $metabib_full_rec = metabib::full_rec->table;
464         my $asset_call_number_table = asset::call_number->table;
465         my $asset_copy_table = asset::copy->table;
466         my $cs_table = config::copy_status->table;
467         my $cl_table = asset::copy_location->table;
468         my $br_table = biblio::record_entry->table;
469
470         my $cj = 'HAVING COUNT(x.id) = ' . scalar(@selects) if ($class_join eq 'AND');
471         my $search_table =
472                 '(SELECT x.record, sum(x.sum) FROM (('.
473                         join(') UNION ALL (', @selects).
474                         ")) x GROUP BY 1 $cj ORDER BY 2 DESC )";
475
476         my $has_vols = 'AND cn.owning_lib = d.id';
477         my $has_copies = 'AND cp.call_number = cn.id';
478         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';
479
480         if ($self->api_name =~ /staff/o) {
481                 $copies_visible = '';
482                 $has_copies     = '' if ($ou_type == 0);
483                 $has_vols       = '' if ($ou_type == 0);
484         }
485
486         my ($t_filter, $f_filter) = ('','');
487         my ($a_filter, $l_filter, $lf_filter) = ('','','');
488
489         if (my $a = $args{audience}) {
490                 $a = [$a] if (!ref($a));
491                 my @aud = @$a;
492                         
493                 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
494                 push @binds, @aud;
495         }
496
497         if (my $l = $args{language}) {
498                 $l = [$l] if (!ref($l));
499                 my @lang = @$l;
500
501                 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
502                 push @binds, @lang;
503         }
504
505         if (my $f = $args{lit_form}) {
506                 $f = [$f] if (!ref($f));
507                 my @lit_form = @$f;
508
509                 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
510                 push @binds, @lit_form;
511         }
512
513         if (my $f = $args{item_form}) {
514                 $f = [$f] if (!ref($f));
515                 my @forms = @$f;
516
517                 $f_filter = ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
518                 push @binds, @forms;
519         }
520
521         if (my $t = $args{item_type}) {
522                 $t = [$t] if (!ref($t));
523                 my @types = @$t;
524
525                 $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
526                 push @binds, @types;
527         }
528
529
530         if ($args{format}) {
531                 my ($t, $f) = split '-', $args{format};
532                 my @types = split '', $t;
533                 my @forms = split '', $f;
534                 if (@types) {
535                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
536                 }
537
538                 if (@forms) {
539                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
540                 }
541                 push @binds, @types, @forms;
542         }
543
544         my $relevance = 'sum(f.sum)';
545         $relevance = 1 if (!$copies_visible);
546
547         my $rank = $relevance;
548         if (lc($sort) eq 'pubdate') {
549                 $rank = <<"             RANK";
550                         ( FIRST ((
551                                 SELECT  COALESCE(SUBSTRING(frp.value FROM '\\\\d+'),'9999')::INT
552                                   FROM  $metabib_full_rec frp
553                                   WHERE frp.record = f.record
554                                         AND frp.tag = '260'
555                                         AND frp.subfield = 'c'
556                                   LIMIT 1
557                         )) )
558                 RANK
559         } elsif (lc($sort) eq 'create_date') {
560                 $rank = <<"             RANK";
561                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = f.record)) )
562                 RANK
563         } elsif (lc($sort) eq 'edit_date') {
564                 $rank = <<"             RANK";
565                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = f.record)) )
566                 RANK
567         } elsif (lc($sort) eq 'title') {
568                 $rank = <<"             RANK";
569                         ( FIRST ((
570                                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM '\\\\d+'),'0')::INT + 1 )),'zzzzzzzz')
571                                   FROM  $metabib_full_rec frt
572                                   WHERE frt.record = f.record
573                                         AND frt.tag = '245'
574                                         AND frt.subfield = 'a'
575                                   LIMIT 1
576                         )) )
577                 RANK
578         } elsif (lc($sort) eq 'author') {
579                 $rank = <<"             RANK";
580                         ( FIRST((
581                                 SELECT  COALESCE(LTRIM(fra.value),'zzzzzzzz')
582                                   FROM  $metabib_full_rec fra
583                                   WHERE fra.record = f.record
584                                         AND fra.tag LIKE '1%'
585                                         AND fra.subfield = 'a'
586                                   ORDER BY fra.tag::text::int
587                                   LIMIT 1
588                         )) )
589                 RANK
590         } else {
591                 $sort = undef;
592         }
593
594
595         if ($copies_visible) {
596                 $select = <<"           SQL";
597                         SELECT  f.record, $relevance, count(DISTINCT cp.id), $rank
598                         FROM    $search_table f,
599                                 $asset_call_number_table cn,
600                                 $asset_copy_table cp,
601                                 $cs_table cs,
602                                 $cl_table cl,
603                                 $br_table br,
604                                 $metabib_record_descriptor rd,
605                                 $descendants d
606                         WHERE   br.id = f.record
607                                 AND cn.record = f.record
608                                 AND rd.record = f.record
609                                 AND cp.status = cs.id
610                                 AND cp.location = cl.id
611                                 AND br.deleted IS FALSE
612                                 AND cn.deleted IS FALSE
613                                 AND cp.deleted IS FALSE
614                                 $has_vols
615                                 $has_copies
616                                 $copies_visible
617                                 $t_filter
618                                 $f_filter
619                                 $a_filter
620                                 $l_filter
621                                 $lf_filter
622                         GROUP BY f.record HAVING count(DISTINCT cp.id) > 0
623                         ORDER BY 4 $sort_dir,3 DESC
624                 SQL
625         } else {
626                 $select = <<"           SQL";
627                         SELECT  f.record, 1, 1, $rank
628                         FROM    $search_table f,
629                                 $br_table br,
630                                 $metabib_record_descriptor rd
631                         WHERE   br.id = f.record
632                                 AND rd.record = f.record
633                                 AND br.deleted IS FALSE
634                                 $t_filter
635                                 $f_filter
636                                 $a_filter
637                                 $l_filter
638                                 $lf_filter
639                         GROUP BY 1,2,3 
640                         ORDER BY 4 $sort_dir
641                 SQL
642         }
643
644
645         $log->debug("Search SQL :: [$select]",DEBUG);
646
647         my $recs = metabib::full_rec->db_Main->selectall_arrayref("$select;", {}, @binds);
648         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
649
650         my $max = 0;
651         $max = 1 if (!@$recs);
652         for (@$recs) {
653                 $max = $$_[1] if ($$_[1] > $max);
654         }
655
656         my $count = @$recs;
657         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
658                 next unless ($$rec[0]);
659                 my ($rid,$rank,$junk,$skip) = @$rec;
660                 $client->respond( [$rid, sprintf('%0.3f',$rank/$max), $count] );
661         }
662         return undef;
663 }
664 __PACKAGE__->register_method(
665         api_name        => 'open-ils.storage.biblio.full_rec.multi_search',
666         method          => 'biblio_multi_search_full_rec',
667         api_level       => 1,
668         stream          => 1,
669         cachable        => 1,
670 );
671 __PACKAGE__->register_method(
672         api_name        => 'open-ils.storage.biblio.full_rec.multi_search.staff',
673         method          => 'biblio_multi_search_full_rec',
674         api_level       => 1,
675         stream          => 1,
676         cachable        => 1,
677 );
678
679 sub search_full_rec {
680         my $self = shift;
681         my $client = shift;
682
683         my %args = @_;
684         
685         my $term = $args{term};
686         my $limiters = $args{restrict};
687
688         my ($index_col) = metabib::full_rec->columns('FTS');
689         $index_col ||= 'value';
690         my $search_table = metabib::full_rec->table;
691
692         my $fts = OpenILS::Application::Storage::FTS->compile('default' => $term, 'value',"$index_col");
693
694         my $fts_where = $fts->sql_where_clause();
695         my @fts_ranks = $fts->fts_rank;
696
697         my $rank = join(' + ', @fts_ranks);
698
699         my @binds;
700         my @wheres;
701         for my $limit (@$limiters) {
702                 push @wheres, "( tag = ? AND subfield LIKE ? AND $fts_where )";
703                 push @binds, $$limit{tag}, $$limit{subfield};
704                 $log->debug("Limiting query using { tag => $$limit{tag}, subfield => $$limit{subfield} }", DEBUG);
705         }
706         my $where = join(' OR ', @wheres);
707
708         my $select = "SELECT record, sum($rank) FROM $search_table WHERE $where GROUP BY 1 ORDER BY 2 DESC;";
709
710         $log->debug("Search SQL :: [$select]",DEBUG);
711
712         my $recs = metabib::full_rec->db_Main->selectall_arrayref($select, {}, @binds);
713         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
714
715         $client->respond($_) for (@$recs);
716         return undef;
717 }
718 __PACKAGE__->register_method(
719         api_name        => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
720         method          => 'search_full_rec',
721         api_level       => 1,
722         stream          => 1,
723         cachable        => 1,
724 );
725 __PACKAGE__->register_method(
726         api_name        => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
727         method          => 'search_full_rec',
728         api_level       => 1,
729         stream          => 1,
730         cachable        => 1,
731 );
732
733
734 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
735 sub search_class_fts {
736         my $self = shift;
737         my $client = shift;
738         my %args = @_;
739         
740         my $term = $args{term};
741         my $ou = $args{org_unit};
742         my $ou_type = $args{depth};
743         my $limit = $args{limit};
744         my $offset = $args{offset};
745
746         my $limit_clause = '';
747         my $offset_clause = '';
748
749         $limit_clause = "LIMIT $limit" if (defined $limit and int($limit) > 0);
750         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
751
752         my (@types,@forms);
753         my ($t_filter, $f_filter) = ('','');
754
755         if ($args{format}) {
756                 my ($t, $f) = split '-', $args{format};
757                 @types = split '', $t;
758                 @forms = split '', $f;
759                 if (@types) {
760                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
761                 }
762
763                 if (@forms) {
764                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
765                 }
766         }
767
768
769
770         my $descendants = defined($ou_type) ?
771                                 "actor.org_unit_descendants($ou, $ou_type)" :
772                                 "actor.org_unit_descendants($ou)";
773
774         my $class = $self->{cdbi};
775         my $search_table = $class->table;
776
777         my $metabib_record_descriptor = metabib::record_descriptor->table;
778         my $metabib_metarecord = metabib::metarecord->table;
779         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
780         my $asset_call_number_table = asset::call_number->table;
781         my $asset_copy_table = asset::copy->table;
782         my $cs_table = config::copy_status->table;
783         my $cl_table = asset::copy_location->table;
784
785         my ($index_col) = $class->columns('FTS');
786         $index_col ||= 'value';
787
788         (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
789         my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
790
791         my $fts_where = $fts->sql_where_clause;
792         my @fts_ranks = $fts->fts_rank;
793
794         my $rank = join(' + ', @fts_ranks);
795
796         my $has_vols = 'AND cn.owning_lib = d.id';
797         my $has_copies = 'AND cp.call_number = cn.id';
798         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';
799
800         my $visible_count = ', count(DISTINCT cp.id)';
801         my $visible_count_test = 'HAVING count(DISTINCT cp.id) > 0';
802
803         if ($self->api_name =~ /staff/o) {
804                 $copies_visible = '';
805                 $visible_count_test = '';
806                 $has_copies = '' if ($ou_type == 0);
807                 $has_vols = '' if ($ou_type == 0);
808         }
809
810         my $rank_calc = <<"     RANK";
811                 , (SUM( $rank
812                         * CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END -- phrase order
813                         * CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END -- first word match
814                         * CASE WHEN f.value ~* ? THEN 2 ELSE 1 END -- only word match
815                 )/COUNT(m.source)), MIN(COALESCE(CHAR_LENGTH(f.value),1))
816         RANK
817
818         $rank_calc = ',1 , 1' if ($self->api_name =~ /unordered/o);
819
820         if ($copies_visible) {
821                 $select = <<"           SQL";
822                         SELECT  m.metarecord $rank_calc $visible_count, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
823                         FROM    $search_table f,
824                                 $metabib_metarecord_source_map_table m,
825                                 $asset_call_number_table cn,
826                                 $asset_copy_table cp,
827                                 $cs_table cs,
828                                 $cl_table cl,
829                                 $metabib_record_descriptor rd,
830                                 $descendants d
831                         WHERE   $fts_where
832                                 AND m.source = f.source
833                                 AND cn.record = m.source
834                                 AND rd.record = m.source
835                                 AND cp.status = cs.id
836                                 AND cp.location = cl.id
837                                 $has_vols
838                                 $has_copies
839                                 $copies_visible
840                                 $t_filter
841                                 $f_filter
842                         GROUP BY 1 $visible_count_test
843                         ORDER BY 2 DESC,3
844                         $limit_clause $offset_clause
845                 SQL
846         } else {
847                 $select = <<"           SQL";
848                         SELECT  m.metarecord $rank_calc, 0, CASE WHEN COUNT(DISTINCT m.source) = 1 THEN MAX(m.source) ELSE MAX(0) END
849                         FROM    $search_table f,
850                                 $metabib_metarecord_source_map_table m,
851                                 $metabib_record_descriptor rd
852                         WHERE   $fts_where
853                                 AND m.source = f.source
854                                 AND rd.record = m.source
855                                 $t_filter
856                                 $f_filter
857                         GROUP BY 1, 4
858                         ORDER BY 2 DESC,3
859                         $limit_clause $offset_clause
860                 SQL
861         }
862
863         $log->debug("Field Search SQL :: [$select]",DEBUG);
864
865         my $SQLstring = join('%',$fts->words);
866         my $REstring = join('\\s+',$fts->words);
867         my $first_word = ($fts->words)[0].'%';
868         my $recs = ($self->api_name =~ /unordered/o) ? 
869                         $class->db_Main->selectall_arrayref($select, {}, @types, @forms) :
870                         $class->db_Main->selectall_arrayref($select, {},
871                                 '%'.lc($SQLstring).'%',                 # phrase order match
872                                 lc($first_word),                        # first word match
873                                 '^\\s*'.lc($REstring).'\\s*/?\s*$',     # full exact match
874                                 @types, @forms
875                         );
876         
877         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
878
879         $client->respond($_) for (map { [@$_[0,1,3,4]] } @$recs);
880         return undef;
881 }
882
883 for my $class ( qw/title author subject keyword series identifier/ ) {
884         __PACKAGE__->register_method(
885                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord",
886                 method          => 'search_class_fts',
887                 api_level       => 1,
888                 stream          => 1,
889                 cdbi            => "metabib::${class}_field_entry",
890                 cachable        => 1,
891         );
892         __PACKAGE__->register_method(
893                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
894                 method          => 'search_class_fts',
895                 api_level       => 1,
896                 stream          => 1,
897                 cdbi            => "metabib::${class}_field_entry",
898                 cachable        => 1,
899         );
900         __PACKAGE__->register_method(
901                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
902                 method          => 'search_class_fts',
903                 api_level       => 1,
904                 stream          => 1,
905                 cdbi            => "metabib::${class}_field_entry",
906                 cachable        => 1,
907         );
908         __PACKAGE__->register_method(
909                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
910                 method          => 'search_class_fts',
911                 api_level       => 1,
912                 stream          => 1,
913                 cdbi            => "metabib::${class}_field_entry",
914                 cachable        => 1,
915         );
916 }
917
918 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
919 sub search_class_fts_count {
920         my $self = shift;
921         my $client = shift;
922         my %args = @_;
923         
924         my $term = $args{term};
925         my $ou = $args{org_unit};
926         my $ou_type = $args{depth};
927         my $limit = $args{limit} || 100;
928         my $offset = $args{offset} || 0;
929
930         my $descendants = defined($ou_type) ?
931                                 "actor.org_unit_descendants($ou, $ou_type)" :
932                                 "actor.org_unit_descendants($ou)";
933                 
934         my (@types,@forms);
935         my ($t_filter, $f_filter) = ('','');
936
937         if ($args{format}) {
938                 my ($t, $f) = split '-', $args{format};
939                 @types = split '', $t;
940                 @forms = split '', $f;
941                 if (@types) {
942                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
943                 }
944
945                 if (@forms) {
946                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
947                 }
948         }
949
950
951         (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).search_fts.*/$1/o;
952
953         my $class = $self->{cdbi};
954         my $search_table = $class->table;
955
956         my $metabib_record_descriptor = metabib::record_descriptor->table;
957         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
958         my $asset_call_number_table = asset::call_number->table;
959         my $asset_copy_table = asset::copy->table;
960         my $cs_table = config::copy_status->table;
961         my $cl_table = asset::copy_location->table;
962
963         my ($index_col) = $class->columns('FTS');
964         $index_col ||= 'value';
965
966         my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'value',"$index_col");
967
968         my $fts_where = $fts->sql_where_clause;
969
970         my $has_vols = 'AND cn.owning_lib = d.id';
971         my $has_copies = 'AND cp.call_number = cn.id';
972         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';
973         if ($self->api_name =~ /staff/o) {
974                 $copies_visible = '';
975                 $has_vols = '' if ($ou_type == 0);
976                 $has_copies = '' if ($ou_type == 0);
977         }
978
979         # XXX test an "EXISTS version of descendant checking...
980         my $select;
981         if ($copies_visible) {
982                 $select = <<"           SQL";
983                 SELECT  count(distinct  m.metarecord)
984                   FROM  $search_table f,
985                         $metabib_metarecord_source_map_table m,
986                         $metabib_metarecord_source_map_table mr,
987                         $asset_call_number_table cn,
988                         $asset_copy_table cp,
989                         $cs_table cs,
990                         $cl_table cl,
991                         $metabib_record_descriptor rd,
992                         $descendants d
993                   WHERE $fts_where
994                         AND mr.source = f.source
995                         AND mr.metarecord = m.metarecord
996                         AND cn.record = m.source
997                         AND rd.record = m.source
998                         AND cp.status = cs.id
999                         AND cp.location = cl.id
1000                         $has_vols
1001                         $has_copies
1002                         $copies_visible
1003                         $t_filter
1004                         $f_filter
1005                 SQL
1006         } else {
1007                 $select = <<"           SQL";
1008                 SELECT  count(distinct  m.metarecord)
1009                   FROM  $search_table f,
1010                         $metabib_metarecord_source_map_table m,
1011                         $metabib_metarecord_source_map_table mr,
1012                         $metabib_record_descriptor rd
1013                   WHERE $fts_where
1014                         AND mr.source = f.source
1015                         AND mr.metarecord = m.metarecord
1016                         AND rd.record = m.source
1017                         $t_filter
1018                         $f_filter
1019                 SQL
1020         }
1021
1022         $log->debug("Field Search Count SQL :: [$select]",DEBUG);
1023
1024         my $recs = $class->db_Main->selectrow_arrayref($select, {}, @types, @forms)->[0];
1025         
1026         $log->debug("Count Search yielded $recs results.",DEBUG);
1027
1028         return $recs;
1029
1030 }
1031 for my $class ( qw/title author subject keyword series identifier/ ) {
1032         __PACKAGE__->register_method(
1033                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
1034                 method          => 'search_class_fts_count',
1035                 api_level       => 1,
1036                 stream          => 1,
1037                 cdbi            => "metabib::${class}_field_entry",
1038                 cachable        => 1,
1039         );
1040         __PACKAGE__->register_method(
1041                 api_name        => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
1042                 method          => 'search_class_fts_count',
1043                 api_level       => 1,
1044                 stream          => 1,
1045                 cdbi            => "metabib::${class}_field_entry",
1046                 cachable        => 1,
1047         );
1048 }
1049
1050
1051 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1052 sub postfilter_search_class_fts {
1053         my $self = shift;
1054         my $client = shift;
1055         my %args = @_;
1056         
1057         my $term = $args{term};
1058         my $sort = $args{'sort'};
1059         my $sort_dir = $args{sort_dir} || 'DESC';
1060         my $ou = $args{org_unit};
1061         my $ou_type = $args{depth};
1062         my $limit = $args{limit} || 10;
1063         my $visibility_limit = $args{visibility_limit} || 5000;
1064         my $offset = $args{offset} || 0;
1065
1066         my $outer_limit = 1000;
1067
1068         my $limit_clause = '';
1069         my $offset_clause = '';
1070
1071         $limit_clause = "LIMIT $outer_limit";
1072         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1073
1074         my (@types,@forms,@lang,@aud,@lit_form);
1075         my ($t_filter, $f_filter) = ('','');
1076         my ($a_filter, $l_filter, $lf_filter) = ('','','');
1077         my ($ot_filter, $of_filter) = ('','');
1078         my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1079
1080         if (my $a = $args{audience}) {
1081                 $a = [$a] if (!ref($a));
1082                 @aud = @$a;
1083                         
1084                 $a_filter = ' AND rd.audience IN ('.join(',',map{'?'}@aud).')';
1085                 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1086         }
1087
1088         if (my $l = $args{language}) {
1089                 $l = [$l] if (!ref($l));
1090                 @lang = @$l;
1091
1092                 $l_filter = ' AND rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1093                 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1094         }
1095
1096         if (my $f = $args{lit_form}) {
1097                 $f = [$f] if (!ref($f));
1098                 @lit_form = @$f;
1099
1100                 $lf_filter = ' AND rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1101                 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1102         }
1103
1104         if ($args{format}) {
1105                 my ($t, $f) = split '-', $args{format};
1106                 @types = split '', $t;
1107                 @forms = split '', $f;
1108                 if (@types) {
1109                         $t_filter = ' AND rd.item_type IN ('.join(',',map{'?'}@types).')';
1110                         $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1111                 }
1112
1113                 if (@forms) {
1114                         $f_filter .= ' AND rd.item_form IN ('.join(',',map{'?'}@forms).')';
1115                         $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1116                 }
1117         }
1118
1119
1120         my $descendants = defined($ou_type) ?
1121                                 "actor.org_unit_descendants($ou, $ou_type)" :
1122                                 "actor.org_unit_descendants($ou)";
1123
1124         my $class = $self->{cdbi};
1125         my $search_table = $class->table;
1126
1127         my $metabib_full_rec = metabib::full_rec->table;
1128         my $metabib_record_descriptor = metabib::record_descriptor->table;
1129         my $metabib_metarecord = metabib::metarecord->table;
1130         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1131         my $asset_call_number_table = asset::call_number->table;
1132         my $asset_copy_table = asset::copy->table;
1133         my $cs_table = config::copy_status->table;
1134         my $cl_table = asset::copy_location->table;
1135         my $br_table = biblio::record_entry->table;
1136
1137         my ($index_col) = $class->columns('FTS');
1138         $index_col ||= 'value';
1139
1140         (my $search_class = $self->api_name) =~ s/.*metabib.(\w+).post_filter.*/$1/o;
1141
1142         my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $term, 'f.value', "f.$index_col");
1143
1144         my $SQLstring = join('%',map { lc($_) } $fts->words);
1145         my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1146         my $first_word = lc(($fts->words)[0]).'%';
1147
1148         my $fts_where = $fts->sql_where_clause;
1149         my @fts_ranks = $fts->fts_rank;
1150
1151         my %bonus = ();
1152         $bonus{'metabib::identifier_field_entry'} =
1153         $bonus{'metabib::keyword_field_entry'} = [
1154             { 'CASE WHEN f.value ILIKE ? THEN 1.2 ELSE 1 END' => $SQLstring }
1155         ];
1156
1157         $bonus{'metabib::title_field_entry'} =
1158                 $bonus{'metabib::series_field_entry'} = [
1159                         { 'CASE WHEN f.value ILIKE ? THEN 1.5 ELSE 1 END' => $first_word },
1160                         { 'CASE WHEN f.value ~* ? THEN 2 ELSE 1 END' => $REstring },
1161                         @{ $bonus{'metabib::keyword_field_entry'} }
1162                 ];
1163
1164         my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$class} };
1165         $bonus_list ||= '1';
1166
1167         my @bonus_values = map { values %$_ } @{ $bonus{$class} };
1168
1169         my $relevance = join(' + ', @fts_ranks);
1170         $relevance = <<"        RANK";
1171                         (SUM( ( $relevance )  * ( $bonus_list ) )/COUNT(m.source))
1172         RANK
1173
1174         my $string_default_sort = 'zzzz';
1175         $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1176
1177         my $number_default_sort = '9999';
1178         $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1179
1180         my $rank = $relevance;
1181         if (lc($sort) eq 'pubdate') {
1182                 $rank = <<"             RANK";
1183                         ( FIRST ((
1184                                 SELECT  COALESCE(SUBSTRING(frp.value FROM '\\\\d+'),'$number_default_sort')::INT
1185                                   FROM  $metabib_full_rec frp
1186                                   WHERE frp.record = mr.master_record
1187                                         AND frp.tag = '260'
1188                                         AND frp.subfield = 'c'
1189                                   LIMIT 1
1190                         )) )
1191                 RANK
1192         } elsif (lc($sort) eq 'create_date') {
1193                 $rank = <<"             RANK";
1194                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1195                 RANK
1196         } elsif (lc($sort) eq 'edit_date') {
1197                 $rank = <<"             RANK";
1198                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1199                 RANK
1200         } elsif (lc($sort) eq 'title') {
1201                 $rank = <<"             RANK";
1202                         ( FIRST ((
1203                                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM '\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1204                                   FROM  $metabib_full_rec frt
1205                                   WHERE frt.record = mr.master_record
1206                                         AND frt.tag = '245'
1207                                         AND frt.subfield = 'a'
1208                                   LIMIT 1
1209                         )) )
1210                 RANK
1211         } elsif (lc($sort) eq 'author') {
1212                 $rank = <<"             RANK";
1213                         ( FIRST((
1214                                 SELECT  COALESCE(LTRIM(fra.value),'$string_default_sort')
1215                                   FROM  $metabib_full_rec fra
1216                                   WHERE fra.record = mr.master_record
1217                                         AND fra.tag LIKE '1%'
1218                                         AND fra.subfield = 'a'
1219                                   ORDER BY fra.tag::text::int
1220                                   LIMIT 1
1221                         )) )
1222                 RANK
1223         } else {
1224                 $sort = undef;
1225         }
1226
1227         my $select = <<"        SQL";
1228                 SELECT  m.metarecord,
1229                         $relevance,
1230                         CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN MIN(m.source) ELSE 0 END,
1231                         $rank
1232                 FROM    $search_table f,
1233                         $metabib_metarecord_source_map_table m,
1234                         $metabib_metarecord_source_map_table smrs,
1235                         $metabib_metarecord mr,
1236                         $metabib_record_descriptor rd
1237                 WHERE   $fts_where
1238                         AND smrs.metarecord = mr.id
1239                         AND m.source = f.source
1240                         AND m.metarecord = mr.id
1241                         AND rd.record = smrs.source
1242                         $t_filter
1243                         $f_filter
1244                         $a_filter
1245                         $l_filter
1246                         $lf_filter
1247                 GROUP BY m.metarecord
1248                 ORDER BY 4 $sort_dir, MIN(COALESCE(CHAR_LENGTH(f.value),1))
1249                 LIMIT $visibility_limit
1250         SQL
1251
1252         if (0) {
1253                 $select = <<"           SQL";
1254
1255                         SELECT  DISTINCT s.*
1256                           FROM  $asset_call_number_table cn,
1257                                 $metabib_metarecord_source_map_table mrs,
1258                                 $asset_copy_table cp,
1259                                 $cs_table cs,
1260                                 $cl_table cl,
1261                                 $br_table br,
1262                                 $descendants d,
1263                                 $metabib_record_descriptor ord,
1264                                 ($select) s
1265                           WHERE mrs.metarecord = s.metarecord
1266                                 AND br.id = mrs.source
1267                                 AND cn.record = mrs.source
1268                                 AND cp.status = cs.id
1269                                 AND cp.location = cl.id
1270                                 AND cn.owning_lib = d.id
1271                                 AND cp.call_number = cn.id
1272                                 AND cp.opac_visible IS TRUE
1273                                 AND cs.opac_visible IS TRUE
1274                                 AND cl.opac_visible IS TRUE
1275                                 AND d.opac_visible IS TRUE
1276                                 AND br.active IS TRUE
1277                                 AND br.deleted IS FALSE
1278                                 AND ord.record = mrs.source
1279                                 $ot_filter
1280                                 $of_filter
1281                                 $oa_filter
1282                                 $ol_filter
1283                                 $olf_filter
1284                           ORDER BY 4 $sort_dir
1285                 SQL
1286         } elsif ($self->api_name !~ /staff/o) {
1287                 $select = <<"           SQL";
1288
1289                         SELECT  DISTINCT s.*
1290                           FROM  ($select) s
1291                           WHERE EXISTS (
1292                                 SELECT  1
1293                                   FROM  $asset_call_number_table cn,
1294                                         $metabib_metarecord_source_map_table mrs,
1295                                         $asset_copy_table cp,
1296                                         $cs_table cs,
1297                                         $cl_table cl,
1298                                         $br_table br,
1299                                         $descendants d,
1300                                         $metabib_record_descriptor ord
1301                                 
1302                                   WHERE mrs.metarecord = s.metarecord
1303                                         AND br.id = mrs.source
1304                                         AND cn.record = mrs.source
1305                                         AND cp.status = cs.id
1306                                         AND cp.location = cl.id
1307                                         AND cp.circ_lib = d.id
1308                                         AND cp.call_number = cn.id
1309                                         AND cp.opac_visible IS TRUE
1310                                         AND cs.opac_visible IS TRUE
1311                                         AND cl.opac_visible IS TRUE
1312                                         AND d.opac_visible IS TRUE
1313                                         AND br.active IS TRUE
1314                                         AND br.deleted IS FALSE
1315                                         AND ord.record = mrs.source
1316                                         $ot_filter
1317                                         $of_filter
1318                                         $oa_filter
1319                                         $ol_filter
1320                                         $olf_filter
1321                                   LIMIT 1
1322                                 )
1323                           ORDER BY 4 $sort_dir
1324                 SQL
1325         } else {
1326                 $select = <<"           SQL";
1327
1328                         SELECT  DISTINCT s.*
1329                           FROM  ($select) s
1330                           WHERE EXISTS (
1331                                 SELECT  1
1332                                   FROM  $asset_call_number_table cn,
1333                                         $asset_copy_table cp,
1334                                         $metabib_metarecord_source_map_table mrs,
1335                                         $br_table br,
1336                                         $descendants d,
1337                                         $metabib_record_descriptor ord
1338                                 
1339                                   WHERE mrs.metarecord = s.metarecord
1340                                         AND br.id = mrs.source
1341                                         AND cn.record = mrs.source
1342                                         AND cn.id = cp.call_number
1343                                         AND br.deleted IS FALSE
1344                                         AND cn.deleted IS FALSE
1345                                         AND ord.record = mrs.source
1346                                         AND (   cn.owning_lib = d.id
1347                                                 OR (    cp.circ_lib = d.id
1348                                                         AND cp.deleted IS FALSE
1349                                                 )
1350                                         )
1351                                         $ot_filter
1352                                         $of_filter
1353                                         $oa_filter
1354                                         $ol_filter
1355                                         $olf_filter
1356                                   LIMIT 1
1357                                 )
1358                                 OR NOT EXISTS (
1359                                 SELECT  1
1360                                   FROM  $asset_call_number_table cn,
1361                                         $metabib_metarecord_source_map_table mrs,
1362                                         $metabib_record_descriptor ord
1363                                   WHERE mrs.metarecord = s.metarecord
1364                                         AND cn.record = mrs.source
1365                                         AND ord.record = mrs.source
1366                                         $ot_filter
1367                                         $of_filter
1368                                         $oa_filter
1369                                         $ol_filter
1370                                         $olf_filter
1371                                   LIMIT 1
1372                                 )
1373                           ORDER BY 4 $sort_dir
1374                 SQL
1375         }
1376
1377
1378         $log->debug("Field Search SQL :: [$select]",DEBUG);
1379
1380         my $recs = $class->db_Main->selectall_arrayref(
1381                         $select, {},
1382                         (@bonus_values > 0 ? @bonus_values : () ),
1383                         ( (!$sort && @bonus_values > 0) ? @bonus_values : () ),
1384                         @types, @forms, @aud, @lang, @lit_form,
1385                         @types, @forms, @aud, @lang, @lit_form,
1386                         ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () ) );
1387         
1388         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1389
1390         my $max = 0;
1391         $max = 1 if (!@$recs);
1392         for (@$recs) {
1393                 $max = $$_[1] if ($$_[1] > $max);
1394         }
1395
1396         my $count = scalar(@$recs);
1397         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1398                 my ($mrid,$rank,$skip) = @$rec;
1399                 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1400         }
1401         return undef;
1402 }
1403
1404 for my $class ( qw/title author subject keyword series identifier/ ) {
1405         __PACKAGE__->register_method(
1406                 api_name        => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
1407                 method          => 'postfilter_search_class_fts',
1408                 api_level       => 1,
1409                 stream          => 1,
1410                 cdbi            => "metabib::${class}_field_entry",
1411                 cachable        => 1,
1412         );
1413         __PACKAGE__->register_method(
1414                 api_name        => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
1415                 method          => 'postfilter_search_class_fts',
1416                 api_level       => 1,
1417                 stream          => 1,
1418                 cdbi            => "metabib::${class}_field_entry",
1419                 cachable        => 1,
1420         );
1421 }
1422
1423
1424
1425 my $_cdbi = {   title   => "metabib::title_field_entry",
1426                 author  => "metabib::author_field_entry",
1427                 subject => "metabib::subject_field_entry",
1428                 keyword => "metabib::keyword_field_entry",
1429                 series  => "metabib::series_field_entry",
1430                 identifier      => "metabib::identifier_field_entry",
1431 };
1432
1433 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1434 sub postfilter_search_multi_class_fts {
1435     my $self   = shift;
1436     my $client = shift;
1437     my %args   = @_;
1438         
1439     my $sort             = $args{'sort'};
1440     my $sort_dir         = $args{sort_dir} || 'DESC';
1441     my $ou               = $args{org_unit};
1442     my $ou_type          = $args{depth};
1443     my $limit            = $args{limit}  || 10;
1444     my $offset           = $args{offset} ||  0;
1445     my $visibility_limit = $args{visibility_limit} || 5000;
1446
1447         if (!$ou) {
1448                 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1449         }
1450
1451         if (!defined($args{org_unit})) {
1452                 die "No target organizational unit passed to ".$self->api_name;
1453         }
1454
1455         if (! scalar( keys %{$args{searches}} )) {
1456                 die "No search arguments were passed to ".$self->api_name;
1457         }
1458
1459         my $outer_limit = 1000;
1460
1461         my $limit_clause  = '';
1462         my $offset_clause = '';
1463
1464         $limit_clause  = "LIMIT $outer_limit";
1465         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1466
1467         my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1468         my ($t_filter,   $f_filter,   $v_filter) = ('','','');
1469         my ($a_filter,   $l_filter,  $lf_filter) = ('','','');
1470         my ($ot_filter, $of_filter,  $ov_filter) = ('','','');
1471         my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1472
1473         if ($args{available}) {
1474                 $avail_filter = ' AND cp.status IN (0,7,12)';
1475         }
1476
1477         if (my $a = $args{audience}) {
1478                 $a = [$a] if (!ref($a));
1479                 @aud = @$a;
1480                         
1481                 $a_filter  = ' AND  rd.audience IN ('.join(',',map{'?'}@aud).')';
1482                 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1483         }
1484
1485         if (my $l = $args{language}) {
1486                 $l = [$l] if (!ref($l));
1487                 @lang = @$l;
1488
1489                 $l_filter  = ' AND  rd.item_lang IN ('.join(',',map{'?'}@lang).')';
1490                 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1491         }
1492
1493         if (my $f = $args{lit_form}) {
1494                 $f = [$f] if (!ref($f));
1495                 @lit_form = @$f;
1496
1497                 $lf_filter  = ' AND  rd.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1498                 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1499         }
1500
1501         if (my $f = $args{item_form}) {
1502                 $f = [$f] if (!ref($f));
1503                 @forms = @$f;
1504
1505                 $f_filter  = ' AND  rd.item_form IN ('.join(',',map{'?'}@forms).')';
1506                 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1507         }
1508
1509         if (my $t = $args{item_type}) {
1510                 $t = [$t] if (!ref($t));
1511                 @types = @$t;
1512
1513                 $t_filter  = ' AND  rd.item_type IN ('.join(',',map{'?'}@types).')';
1514                 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1515         }
1516
1517         if (my $v = $args{vr_format}) {
1518                 $v = [$v] if (!ref($v));
1519                 @vformats = @$v;
1520
1521                 $v_filter  = ' AND  rd.vr_format IN ('.join(',',map{'?'}@vformats).')';
1522                 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
1523         }
1524
1525
1526         # XXX legacy format and item type support
1527         if ($args{format}) {
1528                 my ($t, $f) = split '-', $args{format};
1529                 @types = split '', $t;
1530                 @forms = split '', $f;
1531                 if (@types) {
1532                         $t_filter  = ' AND  rd.item_type IN ('.join(',',map{'?'}@types).')';
1533                         $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
1534                 }
1535
1536                 if (@forms) {
1537                         $f_filter  .= ' AND  rd.item_form IN ('.join(',',map{'?'}@forms).')';
1538                         $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1539                 }
1540         }
1541
1542
1543
1544         my $descendants = defined($ou_type) ?
1545                                 "actor.org_unit_descendants($ou, $ou_type)" :
1546                                 "actor.org_unit_descendants($ou)";
1547
1548     my $search_table_list = '';
1549     my $fts_list          = '';
1550     my $join_table_list   = '';
1551     my @rank_list;
1552
1553         my $field_table = config::metabib_field->table;
1554
1555         my @bonus_lists;
1556         my @bonus_values;
1557         my $prev_search_group;
1558         my $curr_search_group;
1559         my $search_class;
1560         my $search_field;
1561         my $metabib_field;
1562         for my $search_group (sort keys %{$args{searches}}) {
1563                 (my $search_group_name = $search_group) =~ s/\|/_/gso;
1564                 ($search_class,$search_field) = split /\|/, $search_group;
1565                 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
1566
1567                 if ($search_field) {
1568                         unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
1569                                 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
1570                                 return undef;
1571                         }
1572                 }
1573
1574                 $prev_search_group = $curr_search_group if ($curr_search_group);
1575
1576                 $curr_search_group = $search_group_name;
1577
1578                 my $class = $_cdbi->{$search_class};
1579                 my $search_table = $class->table;
1580
1581                 my ($index_col) = $class->columns('FTS');
1582                 $index_col ||= 'value';
1583
1584                 
1585                 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
1586
1587                 my $fts_where = $fts->sql_where_clause;
1588                 my @fts_ranks = $fts->fts_rank;
1589
1590                 my $SQLstring = join('%',map { lc($_) } $fts->words);
1591                 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
1592                 my $first_word = lc(($fts->words)[0]).'%';
1593
1594                 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
1595                 my $rank = join(' + ', @fts_ranks);
1596
1597                 my %bonus = ();
1598                 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value LIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
1599                 $bonus{'author'}  = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $first_word } ];
1600
1601                 $bonus{'series'} = [
1602                         { "CASE WHEN $search_group_name.value LIKE ? THEN 1.5 ELSE 1 END" => $first_word },
1603                         { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
1604                 ];
1605
1606                 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
1607
1608                 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
1609                 $bonus_list ||= '1';
1610
1611                 push @bonus_lists, $bonus_list;
1612                 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
1613
1614
1615                 #---------------------
1616
1617                 $search_table_list .= "$search_table $search_group_name, ";
1618                 push @rank_list,$rank;
1619                 $fts_list .= " AND $fts_where AND m.source = $search_group_name.source";
1620
1621                 if ($metabib_field) {
1622                         $join_table_list .= " AND $search_group_name.field = " . $metabib_field->id;
1623                         $metabib_field = undef;
1624                 }
1625
1626                 if ($prev_search_group) {
1627                         $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
1628                 }
1629         }
1630
1631         my $metabib_record_descriptor = metabib::record_descriptor->table;
1632         my $metabib_full_rec = metabib::full_rec->table;
1633         my $metabib_metarecord = metabib::metarecord->table;
1634         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
1635         my $asset_call_number_table = asset::call_number->table;
1636         my $asset_copy_table = asset::copy->table;
1637         my $cs_table = config::copy_status->table;
1638         my $cl_table = asset::copy_location->table;
1639         my $br_table = biblio::record_entry->table;
1640         my $source_table = config::bib_source->table;
1641
1642         my $bonuses = join (' * ', @bonus_lists);
1643         my $relevance = join (' + ', @rank_list);
1644         $relevance = "SUM( ($relevance) * ($bonuses) )/COUNT(DISTINCT smrs.source)";
1645
1646         my $string_default_sort = 'zzzz';
1647         $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
1648
1649         my $number_default_sort = '9999';
1650         $number_default_sort = '0000' if ($sort_dir eq 'DESC');
1651
1652
1653
1654         my $secondary_sort = <<"        SORT";
1655                 ( FIRST ((
1656                         SELECT  COALESCE(LTRIM(SUBSTR( sfrt.value, COALESCE(SUBSTRING(sfrt.ind2 FROM '\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1657                           FROM  $metabib_full_rec sfrt,
1658                                 $metabib_metarecord mr
1659                           WHERE sfrt.record = mr.master_record
1660                                 AND sfrt.tag = '245'
1661                                 AND sfrt.subfield = 'a'
1662                           LIMIT 1
1663                 )) )
1664         SORT
1665
1666         my $rank = $relevance;
1667         if (lc($sort) eq 'pubdate') {
1668                 $rank = <<"             RANK";
1669                         ( FIRST ((
1670                                 SELECT  COALESCE(SUBSTRING(frp.value FROM '\\\\d+'),'$number_default_sort')::INT
1671                                   FROM  $metabib_full_rec frp
1672                                   WHERE frp.record = mr.master_record
1673                                         AND frp.tag = '260'
1674                                         AND frp.subfield = 'c'
1675                                   LIMIT 1
1676                         )) )
1677                 RANK
1678         } elsif (lc($sort) eq 'create_date') {
1679                 $rank = <<"             RANK";
1680                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1681                 RANK
1682         } elsif (lc($sort) eq 'edit_date') {
1683                 $rank = <<"             RANK";
1684                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = mr.master_record)) )
1685                 RANK
1686         } elsif (lc($sort) eq 'title') {
1687                 $rank = <<"             RANK";
1688                         ( FIRST ((
1689                                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM '\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
1690                                   FROM  $metabib_full_rec frt
1691                                   WHERE frt.record = mr.master_record
1692                                         AND frt.tag = '245'
1693                                         AND frt.subfield = 'a'
1694                                   LIMIT 1
1695                         )) )
1696                 RANK
1697                 $secondary_sort = <<"           SORT";
1698                         ( FIRST ((
1699                                 SELECT  COALESCE(SUBSTRING(sfrp.value FROM '\\\\d+'),'$number_default_sort')::INT
1700                                   FROM  $metabib_full_rec sfrp
1701                                   WHERE sfrp.record = mr.master_record
1702                                         AND sfrp.tag = '260'
1703                                         AND sfrp.subfield = 'c'
1704                                   LIMIT 1
1705                         )) )
1706                 SORT
1707         } elsif (lc($sort) eq 'author') {
1708                 $rank = <<"             RANK";
1709                         ( FIRST((
1710                                 SELECT  COALESCE(LTRIM(fra.value),'$string_default_sort')
1711                                   FROM  $metabib_full_rec fra
1712                                   WHERE fra.record = mr.master_record
1713                                         AND fra.tag LIKE '1%'
1714                                         AND fra.subfield = 'a'
1715                                   ORDER BY fra.tag::text::int
1716                                   LIMIT 1
1717                         )) )
1718                 RANK
1719         } else {
1720                 push @bonus_values, @bonus_values;
1721                 $sort = undef;
1722         }
1723
1724
1725         my $select = <<"        SQL";
1726                 SELECT  m.metarecord,
1727                         $relevance,
1728                         CASE WHEN COUNT(DISTINCT smrs.source) = 1 THEN FIRST(m.source) ELSE 0 END,
1729                         $rank,
1730                         $secondary_sort
1731                 FROM    $search_table_list
1732                         $metabib_metarecord mr,
1733                         $metabib_metarecord_source_map_table m,
1734                         $metabib_metarecord_source_map_table smrs
1735                 WHERE   m.metarecord = smrs.metarecord 
1736                         AND mr.id = m.metarecord
1737                         $fts_list
1738                         $join_table_list
1739                 GROUP BY m.metarecord
1740                 -- ORDER BY 4 $sort_dir
1741                 LIMIT $visibility_limit
1742         SQL
1743
1744         if ($self->api_name !~ /staff/o) {
1745                 $select = <<"           SQL";
1746
1747                         SELECT  s.*
1748                           FROM  ($select) s
1749                           WHERE EXISTS (
1750                                 SELECT  1
1751                                   FROM  $asset_call_number_table cn,
1752                                         $metabib_metarecord_source_map_table mrs,
1753                                         $asset_copy_table cp,
1754                                         $cs_table cs,
1755                                         $cl_table cl,
1756                                         $br_table br,
1757                                         $descendants d,
1758                                         $metabib_record_descriptor ord
1759                                   WHERE mrs.metarecord = s.metarecord
1760                                         AND br.id = mrs.source
1761                                         AND cn.record = mrs.source
1762                                         AND cp.status = cs.id
1763                                         AND cp.location = cl.id
1764                                         AND cp.circ_lib = d.id
1765                                         AND cp.call_number = cn.id
1766                                         AND cp.opac_visible IS TRUE
1767                                         AND cs.opac_visible IS TRUE
1768                                         AND cl.opac_visible IS TRUE
1769                                         AND d.opac_visible IS TRUE
1770                                         AND br.active IS TRUE
1771                                         AND br.deleted IS FALSE
1772                                         AND cp.deleted IS FALSE
1773                                         AND cn.deleted IS FALSE
1774                                         AND ord.record = mrs.source
1775                                         $ot_filter
1776                                         $of_filter
1777                                         $ov_filter
1778                                         $oa_filter
1779                                         $ol_filter
1780                                         $olf_filter
1781                                         $avail_filter
1782                                   LIMIT 1
1783                                 )
1784                                 OR EXISTS (
1785                                 SELECT  1
1786                                   FROM  $br_table br,
1787                                         $metabib_metarecord_source_map_table mrs,
1788                                         $metabib_record_descriptor ord,
1789                                         $source_table src
1790                                   WHERE mrs.metarecord = s.metarecord
1791                                         AND ord.record = mrs.source
1792                                         AND br.id = mrs.source
1793                                         AND br.source = src.id
1794                                         AND src.transcendant IS TRUE
1795                                         $ot_filter
1796                                         $of_filter
1797                                         $ov_filter
1798                                         $oa_filter
1799                                         $ol_filter
1800                                         $olf_filter
1801                                 )
1802                           ORDER BY 4 $sort_dir, 5
1803                 SQL
1804         } else {
1805                 $select = <<"           SQL";
1806
1807                         SELECT  DISTINCT s.*
1808                           FROM  ($select) s,
1809                                 $metabib_metarecord_source_map_table omrs,
1810                                 $metabib_record_descriptor ord
1811                           WHERE omrs.metarecord = s.metarecord
1812                                 AND ord.record = omrs.source
1813                                 AND (   EXISTS (
1814                                                 SELECT  1
1815                                                   FROM  $asset_call_number_table cn,
1816                                                         $asset_copy_table cp,
1817                                                         $descendants d,
1818                                                         $br_table br
1819                                                   WHERE br.id = omrs.source
1820                                                         AND cn.record = omrs.source
1821                                                         AND br.deleted IS FALSE
1822                                                         AND cn.deleted IS FALSE
1823                                                         AND cp.call_number = cn.id
1824                                                         AND (   cn.owning_lib = d.id
1825                                                                 OR (    cp.circ_lib = d.id
1826                                                                         AND cp.deleted IS FALSE
1827                                                                 )
1828                                                         )
1829                                                         $avail_filter
1830                                                   LIMIT 1
1831                                         )
1832                                         OR NOT EXISTS (
1833                                                 SELECT  1
1834                                                   FROM  $asset_call_number_table cn
1835                                                   WHERE cn.record = omrs.source
1836                                                         AND cn.deleted IS FALSE
1837                                                   LIMIT 1
1838                                         )
1839                                         OR EXISTS (
1840                                         SELECT  1
1841                                           FROM  $br_table br,
1842                                                 $metabib_metarecord_source_map_table mrs,
1843                                                 $metabib_record_descriptor ord,
1844                                                 $source_table src
1845                                           WHERE mrs.metarecord = s.metarecord
1846                                                 AND br.id = mrs.source
1847                                                 AND br.source = src.id
1848                                                 AND src.transcendant IS TRUE
1849                                                 $ot_filter
1850                                                 $of_filter
1851                                                 $ov_filter
1852                                                 $oa_filter
1853                                                 $ol_filter
1854                                                 $olf_filter
1855                                         )
1856                                 )
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         }
1867
1868
1869         $log->debug("Field Search SQL :: [$select]",DEBUG);
1870
1871         my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
1872                         $select, {},
1873                         @bonus_values,
1874                         @types, @forms, @vformats, @aud, @lang, @lit_form,
1875                         @types, @forms, @vformats, @aud, @lang, @lit_form,
1876                         # ($self->api_name =~ /staff/o ? (@types, @forms, @aud, @lang, @lit_form) : () )
1877         );
1878         
1879         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
1880
1881         my $max = 0;
1882         $max = 1 if (!@$recs);
1883         for (@$recs) {
1884                 $max = $$_[1] if ($$_[1] > $max);
1885         }
1886
1887         my $count = scalar(@$recs);
1888         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
1889                 next unless ($$rec[0]);
1890                 my ($mrid,$rank,$skip) = @$rec;
1891                 $client->respond( [$mrid, sprintf('%0.3f',$rank/$max), $skip, $count] );
1892         }
1893         return undef;
1894 }
1895
1896 __PACKAGE__->register_method(
1897         api_name        => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
1898         method          => 'postfilter_search_multi_class_fts',
1899         api_level       => 1,
1900         stream          => 1,
1901         cachable        => 1,
1902 );
1903 __PACKAGE__->register_method(
1904         api_name        => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
1905         method          => 'postfilter_search_multi_class_fts',
1906         api_level       => 1,
1907         stream          => 1,
1908         cachable        => 1,
1909 );
1910
1911 __PACKAGE__->register_method(
1912         api_name        => "open-ils.storage.metabib.multiclass.search_fts",
1913         method          => 'postfilter_search_multi_class_fts',
1914         api_level       => 1,
1915         stream          => 1,
1916         cachable        => 1,
1917 );
1918 __PACKAGE__->register_method(
1919         api_name        => "open-ils.storage.metabib.multiclass.search_fts.staff",
1920         method          => 'postfilter_search_multi_class_fts',
1921         api_level       => 1,
1922         stream          => 1,
1923         cachable        => 1,
1924 );
1925
1926 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
1927 sub biblio_search_multi_class_fts {
1928         my $self = shift;
1929         my $client = shift;
1930         my %args = @_;
1931         
1932     my $sort             = $args{'sort'};
1933     my $sort_dir         = $args{sort_dir} || 'DESC';
1934     my $ou               = $args{org_unit};
1935     my $ou_type          = $args{depth};
1936     my $limit            = $args{limit}  || 10;
1937     my $offset           = $args{offset} ||  0;
1938     my $pref_lang        = $args{prefered_language} || 'eng';
1939     my $visibility_limit = $args{visibility_limit}  || 5000;
1940
1941         if (!$ou) {
1942                 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
1943         }
1944
1945         if (! scalar( keys %{$args{searches}} )) {
1946                 die "No search arguments were passed to ".$self->api_name;
1947         }
1948
1949         my $outer_limit = 1000;
1950
1951         my $limit_clause  = '';
1952         my $offset_clause = '';
1953
1954         $limit_clause  = "LIMIT $outer_limit";
1955         $offset_clause = "OFFSET $offset" if (defined $offset and int($offset) > 0);
1956
1957         my ($avail_filter,@types,@forms,@lang,@aud,@lit_form,@vformats) = ('');
1958         my ($t_filter,   $f_filter,   $v_filter) = ('','','');
1959         my ($a_filter,   $l_filter,  $lf_filter) = ('','','');
1960         my ($ot_filter, $of_filter,  $ov_filter) = ('','','');
1961         my ($oa_filter, $ol_filter, $olf_filter) = ('','','');
1962
1963         if ($args{available}) {
1964                 $avail_filter = ' AND cp.status IN (0,7,12)';
1965         }
1966
1967         if (my $a = $args{audience}) {
1968                 $a = [$a] if (!ref($a));
1969                 @aud = @$a;
1970                         
1971                 $a_filter  = ' AND rd.audience  IN ('.join(',',map{'?'}@aud).')';
1972                 $oa_filter = ' AND ord.audience IN ('.join(',',map{'?'}@aud).')';
1973         }
1974
1975         if (my $l = $args{language}) {
1976                 $l = [$l] if (!ref($l));
1977                 @lang = @$l;
1978
1979                 $l_filter  = ' AND rd.item_lang  IN ('.join(',',map{'?'}@lang).')';
1980                 $ol_filter = ' AND ord.item_lang IN ('.join(',',map{'?'}@lang).')';
1981         }
1982
1983         if (my $f = $args{lit_form}) {
1984                 $f = [$f] if (!ref($f));
1985                 @lit_form = @$f;
1986
1987                 $lf_filter  = ' AND rd.lit_form  IN ('.join(',',map{'?'}@lit_form).')';
1988                 $olf_filter = ' AND ord.lit_form IN ('.join(',',map{'?'}@lit_form).')';
1989         }
1990
1991         if (my $f = $args{item_form}) {
1992                 $f = [$f] if (!ref($f));
1993                 @forms = @$f;
1994
1995                 $f_filter  = ' AND rd.item_form  IN ('.join(',',map{'?'}@forms).')';
1996                 $of_filter = ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
1997         }
1998
1999         if (my $t = $args{item_type}) {
2000                 $t = [$t] if (!ref($t));
2001                 @types = @$t;
2002
2003                 $t_filter  = ' AND rd.item_type  IN ('.join(',',map{'?'}@types).')';
2004                 $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2005         }
2006
2007         if (my $v = $args{vr_format}) {
2008                 $v = [$v] if (!ref($v));
2009                 @vformats = @$v;
2010
2011                 $v_filter  = ' AND rd.vr_format  IN ('.join(',',map{'?'}@vformats).')';
2012                 $ov_filter = ' AND ord.vr_format IN ('.join(',',map{'?'}@vformats).')';
2013         }
2014
2015         # XXX legacy format and item type support
2016         if ($args{format}) {
2017                 my ($t, $f) = split '-', $args{format};
2018                 @types = split '', $t;
2019                 @forms = split '', $f;
2020                 if (@types) {
2021                         $t_filter  = ' AND rd.item_type  IN ('.join(',',map{'?'}@types).')';
2022                         $ot_filter = ' AND ord.item_type IN ('.join(',',map{'?'}@types).')';
2023                 }
2024
2025                 if (@forms) {
2026                         $f_filter  .= ' AND rd.item_form  IN ('.join(',',map{'?'}@forms).')';
2027                         $of_filter .= ' AND ord.item_form IN ('.join(',',map{'?'}@forms).')';
2028                 }
2029         }
2030
2031
2032         my $descendants = defined($ou_type) ?
2033                                 "actor.org_unit_descendants($ou, $ou_type)" :
2034                                 "actor.org_unit_descendants($ou)";
2035
2036         my $search_table_list = '';
2037         my $fts_list = '';
2038         my $join_table_list = '';
2039         my @rank_list;
2040
2041         my $field_table = config::metabib_field->table;
2042
2043         my @bonus_lists;
2044         my @bonus_values;
2045         my $prev_search_group;
2046         my $curr_search_group;
2047         my $search_class;
2048         my $search_field;
2049         my $metabib_field;
2050         for my $search_group (sort keys %{$args{searches}}) {
2051                 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2052                 ($search_class,$search_field) = split /\|/, $search_group;
2053                 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2054
2055                 if ($search_field) {
2056                         unless ( $metabib_field = config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2057                                 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2058                                 return undef;
2059                         }
2060                 }
2061
2062                 $prev_search_group = $curr_search_group if ($curr_search_group);
2063
2064                 $curr_search_group = $search_group_name;
2065
2066                 my $class = $_cdbi->{$search_class};
2067                 my $search_table = $class->table;
2068
2069                 my ($index_col) = $class->columns('FTS');
2070                 $index_col ||= 'value';
2071
2072                 
2073                 my $fts = OpenILS::Application::Storage::FTS->compile($search_class => $args{searches}{$search_group}{term}, $search_group_name.'.value', "$search_group_name.$index_col");
2074
2075                 my $fts_where = $fts->sql_where_clause;
2076                 my @fts_ranks = $fts->fts_rank;
2077
2078                 my $SQLstring = join('%',map { lc($_) } $fts->words) .'%';
2079                 my $REstring = '^' . join('\s+',map { lc($_) } $fts->words) . '\W*$';
2080                 my $first_word = lc(($fts->words)[0]).'%';
2081
2082                 $_.=" * (SELECT weight FROM $field_table WHERE $search_group_name.field = id)" for (@fts_ranks);
2083                 my $rank = join('  + ', @fts_ranks);
2084
2085                 my %bonus = ();
2086                 $bonus{'subject'} = [];
2087                 $bonus{'author'}  = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word } ];
2088
2089                 $bonus{'keyword'} = [ { "CASE WHEN $search_group_name.value ILIKE ? THEN 10 ELSE 1 END" => $SQLstring } ];
2090
2091                 $bonus{'series'} = [
2092                         { "CASE WHEN $search_group_name.value ILIKE ? THEN 1.5 ELSE 1 END" => $first_word },
2093                         { "CASE WHEN $search_group_name.value ~ ? THEN 20 ELSE 1 END" => $REstring },
2094                 ];
2095
2096                 $bonus{'title'} = [ @{ $bonus{'series'} }, @{ $bonus{'keyword'} } ];
2097
2098                 if ($pref_lang) {
2099                         push @{ $bonus{'title'}   }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2100                         push @{ $bonus{'author'}  }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2101                         push @{ $bonus{'subject'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2102                         push @{ $bonus{'keyword'} }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2103                         push @{ $bonus{'series'}  }, { "CASE WHEN rd.item_lang = ? THEN 10 ELSE 1 END" => $pref_lang };
2104                 }
2105
2106                 my $bonus_list = join ' * ', map { keys %$_ } @{ $bonus{$search_class} };
2107                 $bonus_list ||= '1';
2108
2109                 push @bonus_lists, $bonus_list;
2110                 push @bonus_values, map { values %$_ } @{ $bonus{$search_class} };
2111
2112                 #---------------------
2113
2114                 $search_table_list .= "$search_table $search_group_name, ";
2115                 push @rank_list,$rank;
2116                 $fts_list .= " AND $fts_where AND b.id = $search_group_name.source";
2117
2118                 if ($metabib_field) {
2119                         $fts_list .= " AND $curr_search_group.field = " . $metabib_field->id;
2120                         $metabib_field = undef;
2121                 }
2122
2123                 if ($prev_search_group) {
2124                         $join_table_list .= " AND $prev_search_group.source = $curr_search_group.source";
2125                 }
2126         }
2127
2128         my $metabib_record_descriptor = metabib::record_descriptor->table;
2129         my $metabib_full_rec = metabib::full_rec->table;
2130         my $metabib_metarecord = metabib::metarecord->table;
2131         my $metabib_metarecord_source_map_table = metabib::metarecord_source_map->table;
2132         my $asset_call_number_table = asset::call_number->table;
2133         my $asset_copy_table = asset::copy->table;
2134         my $cs_table = config::copy_status->table;
2135         my $cl_table = asset::copy_location->table;
2136         my $br_table = biblio::record_entry->table;
2137         my $source_table = config::bib_source->table;
2138
2139
2140         my $bonuses = join (' * ', @bonus_lists);
2141         my $relevance = join (' + ', @rank_list);
2142         $relevance = "AVG( ($relevance) * ($bonuses) )";
2143
2144         my $string_default_sort = 'zzzz';
2145         $string_default_sort = 'AAAA' if ($sort_dir eq 'DESC');
2146
2147         my $number_default_sort = '9999';
2148         $number_default_sort = '0000' if ($sort_dir eq 'DESC');
2149
2150         my $rank = $relevance;
2151         if (lc($sort) eq 'pubdate') {
2152                 $rank = <<"             RANK";
2153                         ( FIRST ((
2154                                 SELECT  COALESCE(SUBSTRING(frp.value FROM '\\\\d{4}'),'$number_default_sort')::INT
2155                                   FROM  $metabib_full_rec frp
2156                                   WHERE frp.record = b.id
2157                                         AND frp.tag = '260'
2158                                         AND frp.subfield = 'c'
2159                                   LIMIT 1
2160                         )) )
2161                 RANK
2162         } elsif (lc($sort) eq 'create_date') {
2163                 $rank = <<"             RANK";
2164                         ( FIRST (( SELECT create_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2165                 RANK
2166         } elsif (lc($sort) eq 'edit_date') {
2167                 $rank = <<"             RANK";
2168                         ( FIRST (( SELECT edit_date FROM $br_table rbr WHERE rbr.id = b.id)) )
2169                 RANK
2170         } elsif (lc($sort) eq 'title') {
2171                 $rank = <<"             RANK";
2172                         ( FIRST ((
2173                                 SELECT  COALESCE(LTRIM(SUBSTR( frt.value, COALESCE(SUBSTRING(frt.ind2 FROM '\\\\d+'),'0')::INT + 1 )),'$string_default_sort')
2174                                   FROM  $metabib_full_rec frt
2175                                   WHERE frt.record = b.id
2176                                         AND frt.tag = '245'
2177                                         AND frt.subfield = 'a'
2178                                   LIMIT 1
2179                         )) )
2180                 RANK
2181         } elsif (lc($sort) eq 'author') {
2182                 $rank = <<"             RANK";
2183                         ( FIRST((
2184                                 SELECT  COALESCE(LTRIM(fra.value),'$string_default_sort')
2185                                   FROM  $metabib_full_rec fra
2186                                   WHERE fra.record = b.id
2187                                         AND fra.tag LIKE '1%'
2188                                         AND fra.subfield = 'a'
2189                                   ORDER BY fra.tag::text::int
2190                                   LIMIT 1
2191                         )) )
2192                 RANK
2193         } else {
2194                 push @bonus_values, @bonus_values;
2195                 $sort = undef;
2196         }
2197
2198
2199         my $select = <<"        SQL";
2200                 SELECT  b.id,
2201                         $relevance AS rel,
2202                         $rank AS rank,
2203                         b.source
2204                 FROM    $search_table_list
2205                         $metabib_record_descriptor rd,
2206                         $source_table src,
2207                         $br_table b
2208                 WHERE   rd.record = b.id
2209                         AND b.active IS TRUE
2210                         AND b.deleted IS FALSE
2211                         $fts_list
2212                         $join_table_list
2213                         $t_filter
2214                         $f_filter
2215                         $v_filter
2216                         $a_filter
2217                         $l_filter
2218                         $lf_filter
2219                 GROUP BY b.id, b.source
2220                 ORDER BY 3 $sort_dir
2221                 LIMIT $visibility_limit
2222         SQL
2223
2224         if ($self->api_name !~ /staff/o) {
2225                 $select = <<"           SQL";
2226
2227                         SELECT  s.*
2228                           FROM  ($select) s
2229                                 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2230                           WHERE EXISTS (
2231                                 SELECT  1
2232                                   FROM  $asset_call_number_table cn,
2233                                         $asset_copy_table cp,
2234                                         $cs_table cs,
2235                                         $cl_table cl,
2236                                         $descendants d
2237                                   WHERE cn.record = s.id
2238                                         AND cp.status = cs.id
2239                                         AND cp.location = cl.id
2240                                         AND cp.call_number = cn.id
2241                                         AND cp.opac_visible IS TRUE
2242                                         AND cs.opac_visible IS TRUE
2243                                         AND cl.opac_visible IS TRUE
2244                                         AND d.opac_visible IS TRUE
2245                                         AND cp.deleted IS FALSE
2246                                         AND cn.deleted IS FALSE
2247                                         AND cp.circ_lib = d.id
2248                                         $avail_filter
2249                                   LIMIT 1
2250                                 )
2251                                 OR src.transcendant IS TRUE
2252                           ORDER BY 3 $sort_dir
2253                 SQL
2254         } else {
2255                 $select = <<"           SQL";
2256
2257                         SELECT  s.*
2258                           FROM  ($select) s
2259                                 LEFT OUTER JOIN $source_table src ON (s.source = src.id)
2260                           WHERE EXISTS (
2261                                 SELECT  1
2262                                   FROM  $asset_call_number_table cn,
2263                                         $asset_copy_table cp,
2264                                         $descendants d
2265                                   WHERE cn.record = s.id
2266                                         AND cp.call_number = cn.id
2267                                         AND cn.deleted IS FALSE
2268                                         AND cp.circ_lib = d.id
2269                                         AND cp.deleted IS FALSE
2270                                         $avail_filter
2271                                   LIMIT 1
2272                                 )
2273                                 OR NOT EXISTS (
2274                                 SELECT  1
2275                                   FROM  $asset_call_number_table cn
2276                                   WHERE cn.record = s.id
2277                                   LIMIT 1
2278                                 )
2279                                 OR src.transcendant IS TRUE
2280                           ORDER BY 3 $sort_dir
2281                 SQL
2282         }
2283
2284
2285         $log->debug("Field Search SQL :: [$select]",DEBUG);
2286
2287         my $recs = $_cdbi->{title}->db_Main->selectall_arrayref(
2288                         $select, {},
2289                         @bonus_values, @types, @forms, @vformats, @aud, @lang, @lit_form
2290         );
2291         
2292         $log->debug("Search yielded ".scalar(@$recs)." results.",DEBUG);
2293
2294         my $count = scalar(@$recs);
2295         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2296                 next unless ($$rec[0]);
2297                 my ($mrid,$rank) = @$rec;
2298                 $client->respond( [$mrid, sprintf('%0.3f',$rank), $count] );
2299         }
2300         return undef;
2301 }
2302
2303 __PACKAGE__->register_method(
2304         api_name        => "open-ils.storage.biblio.multiclass.search_fts.record",
2305         method          => 'biblio_search_multi_class_fts',
2306         api_level       => 1,
2307         stream          => 1,
2308         cachable        => 1,
2309 );
2310 __PACKAGE__->register_method(
2311         api_name        => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
2312         method          => 'biblio_search_multi_class_fts',
2313         api_level       => 1,
2314         stream          => 1,
2315         cachable        => 1,
2316 );
2317 __PACKAGE__->register_method(
2318         api_name        => "open-ils.storage.biblio.multiclass.search_fts",
2319         method          => 'biblio_search_multi_class_fts',
2320         api_level       => 1,
2321         stream          => 1,
2322         cachable        => 1,
2323 );
2324 __PACKAGE__->register_method(
2325         api_name        => "open-ils.storage.biblio.multiclass.search_fts.staff",
2326         method          => 'biblio_search_multi_class_fts',
2327         api_level       => 1,
2328         stream          => 1,
2329         cachable        => 1,
2330 );
2331
2332
2333 my %locale_map;
2334 my $default_preferred_language;
2335 my $default_preferred_language_weight;
2336
2337 # XXX factored most of the PG dependant stuff out of here... need to find a way to do "dependants".
2338 sub staged_fts {
2339     my $self   = shift;
2340     my $client = shift;
2341     my %args   = @_;
2342
2343     if (!$locale_map{COMPLETE}) {
2344
2345         my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2346         for my $locale ( @locales ) {
2347             $locale_map{$locale->code} = $locale->marc_code;
2348         }
2349         $locale_map{COMPLETE} = 1;
2350
2351     }
2352
2353     if (!$default_preferred_language) {
2354
2355         $default_preferred_language = OpenSRF::Utils::SettingsClient
2356             ->new
2357             ->config_value(
2358                 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2359         );
2360
2361     }
2362
2363     if (!$default_preferred_language_weight) {
2364
2365         $default_preferred_language_weight = OpenSRF::Utils::SettingsClient
2366             ->new
2367             ->config_value(
2368                 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2369         );
2370
2371     }
2372
2373     # inclusion, exclusion, delete_adjusted_inclusion, delete_adjusted_exclusion
2374     my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2375
2376     my $ou     = $args{org_unit};
2377     my $limit  = $args{limit}  || 10;
2378     my $offset = $args{offset} ||  0;
2379
2380         if (!$ou) {
2381                 $ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
2382         }
2383
2384         if (! scalar( keys %{$args{searches}} )) {
2385                 die "No search arguments were passed to ".$self->api_name;
2386         }
2387
2388         my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
2389
2390     if (!defined($args{preferred_language})) {
2391                 my $ses_locale = $client->session ? $client->session->session_locale : $default_preferred_language;
2392         $args{preferred_language} =
2393             $locale_map{ $ses_locale } || 'eng';
2394     }
2395
2396     if (!defined($args{preferred_language_weight})) {
2397         $args{preferred_language_weight} = $default_preferred_language_weight || 2;
2398     }
2399
2400         if ($args{available}) {
2401                 @statuses = (0,7,12);
2402         }
2403
2404         if (my $s = $args{locations}) {
2405                 $s = [$s] if (!ref($s));
2406                 @locations = @$s;
2407         }
2408
2409         if (my $b = $args{between}) {
2410                 if (ref($b) && @$b == 2) {
2411                     @between = @$b;
2412         }
2413         }
2414
2415         if (my $s = $args{statuses}) {
2416                 $s = [$s] if (!ref($s));
2417                 @statuses = @$s;
2418         }
2419
2420         if (my $a = $args{audience}) {
2421                 $a = [$a] if (!ref($a));
2422                 @aud = @$a;
2423         }
2424
2425         if (my $l = $args{language}) {
2426                 $l = [$l] if (!ref($l));
2427                 @lang = @$l;
2428         }
2429
2430         if (my $f = $args{lit_form}) {
2431                 $f = [$f] if (!ref($f));
2432                 @lit_form = @$f;
2433         }
2434
2435         if (my $f = $args{item_form}) {
2436                 $f = [$f] if (!ref($f));
2437                 @forms = @$f;
2438         }
2439
2440         if (my $t = $args{item_type}) {
2441                 $t = [$t] if (!ref($t));
2442                 @types = @$t;
2443         }
2444
2445         if (my $b = $args{bib_level}) {
2446                 $b = [$b] if (!ref($b));
2447                 @bib_level = @$b;
2448         }
2449
2450         if (my $v = $args{vr_format}) {
2451                 $v = [$v] if (!ref($v));
2452                 @vformats = @$v;
2453         }
2454
2455         # XXX legacy format and item type support
2456         if ($args{format}) {
2457                 my ($t, $f) = split '-', $args{format};
2458                 @types = split '', $t;
2459                 @forms = split '', $f;
2460         }
2461
2462     my %stored_proc_search_args;
2463         for my $search_group (sort keys %{$args{searches}}) {
2464                 (my $search_group_name = $search_group) =~ s/\|/_/gso;
2465                 my ($search_class,$search_field) = split /\|/, $search_group;
2466                 $log->debug("Searching class [$search_class] and field [$search_field]",DEBUG);
2467
2468                 if ($search_field) {
2469                         unless ( config::metabib_field->search( field_class => $search_class, name => $search_field )->next ) {
2470                                 $log->warn("Requested class [$search_class] or field [$search_field] does not exist!");
2471                                 return undef;
2472                         }
2473                 }
2474
2475                 my $class = $_cdbi->{$search_class};
2476                 my $search_table = $class->table;
2477
2478                 my ($index_col) = $class->columns('FTS');
2479                 $index_col ||= 'value';
2480
2481                 
2482                 my $fts = OpenILS::Application::Storage::FTS->compile(
2483             $search_class => $args{searches}{$search_group}{term},
2484             $search_group_name.'.value',
2485             "$search_group_name.$index_col"
2486         );
2487                 $fts->sql_where_clause; # this builds the ranks for us
2488
2489                 my @fts_ranks   = $fts->fts_rank;
2490                 my @fts_queries = $fts->fts_query;
2491                 my @phrases = map { lc($_) } $fts->phrases;
2492                 my @words   = map { lc($_) } $fts->words;
2493
2494         $stored_proc_search_args{$search_group} = {
2495             fts_rank    => \@fts_ranks,
2496             fts_query   => \@fts_queries,
2497             phrase      => \@phrases,
2498             word        => \@words,
2499         };
2500
2501         }
2502
2503         my $param_search_ou = $ou;
2504         my $param_depth = $args{depth}; $param_depth = 'NULL' unless (defined($param_depth) and length($param_depth) > 0 );
2505         my $param_searches = OpenSRF::Utils::JSON->perl2JSON( \%stored_proc_search_args ); $param_searches =~ s/\$//go; $param_searches = '$$'.$param_searches.'$$';
2506         my $param_statuses  = '$${' . join(',', map { s/\$//go; "\"$_\"" } @statuses ) . '}$$';
2507         my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\"" } @locations) . '}$$';
2508         my $param_audience  = '$${' . join(',', map { s/\$//go; "\"$_\"" } @aud      ) . '}$$';
2509         my $param_language  = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lang     ) . '}$$';
2510         my $param_lit_form  = '$${' . join(',', map { s/\$//go; "\"$_\"" } @lit_form ) . '}$$';
2511         my $param_types     = '$${' . join(',', map { s/\$//go; "\"$_\"" } @types    ) . '}$$';
2512         my $param_forms     = '$${' . join(',', map { s/\$//go; "\"$_\"" } @forms    ) . '}$$';
2513         my $param_vformats  = '$${' . join(',', map { s/\$//go; "\"$_\"" } @vformats ) . '}$$';
2514     my $param_bib_level = '$${' . join(',', map { s/\$//go; "\"$_\"" } @bib_level) . '}$$';
2515         my $param_before = $args{before}; $param_before = 'NULL' unless (defined($param_before) and length($param_before) > 0 );
2516         my $param_after  = $args{after} ; $param_after  = 'NULL' unless (defined($param_after ) and length($param_after ) > 0 );
2517         my $param_during = $args{during}; $param_during = 'NULL' unless (defined($param_during) and length($param_during) > 0 );
2518     my $param_between = '$${"' . join('","', map { int($_) } @between) . '"}$$';
2519         my $param_pref_lang = $args{preferred_language}; $param_pref_lang =~ s/\$//go; $param_pref_lang = '$$'.$param_pref_lang.'$$';
2520         my $param_pref_lang_multiplier = $args{preferred_language_weight}; $param_pref_lang_multiplier ||= 'NULL';
2521         my $param_sort = $args{'sort'}; $param_sort =~ s/\$//go; $param_sort = '$$'.$param_sort.'$$';
2522         my $param_sort_desc = defined($args{sort_dir}) && $args{sort_dir} =~ /^d/io ? "'t'" : "'f'";
2523         my $metarecord = $self->api_name =~ /metabib/o ? "'t'" : "'f'";
2524         my $staff = $self->api_name =~ /staff/o ? "'t'" : "'f'";
2525     my $param_rel_limit = $args{core_limit};  $param_rel_limit ||= 'NULL';
2526     my $param_chk_limit = $args{check_limit}; $param_chk_limit ||= 'NULL';
2527     my $param_skip_chk  = $args{skip_check};  $param_skip_chk  ||= 'NULL';
2528
2529         my $sth = metabib::metarecord_source_map->db_Main->prepare(<<"    SQL");
2530         SELECT  *
2531           FROM  search.staged_fts(
2532                     $param_search_ou\:\:INT,
2533                     $param_depth\:\:INT,
2534                     $param_searches\:\:TEXT,
2535                     $param_statuses\:\:INT[],
2536                     $param_locations\:\:INT[],
2537                     $param_audience\:\:TEXT[],
2538                     $param_language\:\:TEXT[],
2539                     $param_lit_form\:\:TEXT[],
2540                     $param_types\:\:TEXT[],
2541                     $param_forms\:\:TEXT[],
2542                     $param_vformats\:\:TEXT[],
2543                     $param_bib_level\:\:TEXT[],
2544                     $param_before\:\:TEXT,
2545                     $param_after\:\:TEXT,
2546                     $param_during\:\:TEXT,
2547                     $param_between\:\:TEXT[],
2548                     $param_pref_lang\:\:TEXT,
2549                     $param_pref_lang_multiplier\:\:REAL,
2550                     $param_sort\:\:TEXT,
2551                     $param_sort_desc\:\:BOOL,
2552                     $metarecord\:\:BOOL,
2553                     $staff\:\:BOOL,
2554                     $param_rel_limit\:\:INT,
2555                     $param_chk_limit\:\:INT,
2556                     $param_skip_chk\:\:INT
2557                 );
2558     SQL
2559
2560     $sth->execute;
2561
2562     my $recs = $sth->fetchall_arrayref({});
2563     my $summary_row = pop @$recs;
2564
2565     my $total    = $$summary_row{total};
2566     my $checked  = $$summary_row{checked};
2567     my $visible  = $$summary_row{visible};
2568     my $deleted  = $$summary_row{deleted};
2569     my $excluded = $$summary_row{excluded};
2570
2571     my $estimate = $visible;
2572     if ( $total > $checked && $checked ) {
2573
2574         $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2575         $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2576
2577     }
2578
2579     delete $$summary_row{id};
2580     delete $$summary_row{rel};
2581     delete $$summary_row{record};
2582
2583     $client->respond( $summary_row );
2584
2585         $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
2586
2587         for my $rec (@$recs[$offset .. $offset + $limit - 1]) {
2588         delete $$rec{checked};
2589         delete $$rec{visible};
2590         delete $$rec{excluded};
2591         delete $$rec{deleted};
2592         delete $$rec{total};
2593         $$rec{rel} = sprintf('%0.3f',$$rec{rel});
2594
2595                 $client->respond( $rec );
2596         }
2597         return undef;
2598 }
2599 __PACKAGE__->register_method(
2600         api_name        => "open-ils.storage.biblio.multiclass.staged.search_fts",
2601         method          => 'staged_fts',
2602         api_level       => 0,
2603         stream          => 1,
2604         cachable        => 1,
2605 );
2606 __PACKAGE__->register_method(
2607         api_name        => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
2608         method          => 'staged_fts',
2609         api_level       => 0,
2610         stream          => 1,
2611         cachable        => 1,
2612 );
2613 __PACKAGE__->register_method(
2614         api_name        => "open-ils.storage.metabib.multiclass.staged.search_fts",
2615         method          => 'staged_fts',
2616         api_level       => 0,
2617         stream          => 1,
2618         cachable        => 1,
2619 );
2620 __PACKAGE__->register_method(
2621         api_name        => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
2622         method          => 'staged_fts',
2623         api_level       => 0,
2624         stream          => 1,
2625         cachable        => 1,
2626 );
2627
2628 sub FTS_paging_estimate {
2629     my $self   = shift;
2630     my $client = shift;
2631
2632     my $checked  = shift;
2633     my $visible  = shift;
2634     my $excluded = shift;
2635     my $deleted  = shift;
2636     my $total    = shift;
2637
2638     my $deleted_ratio = $deleted / $checked;
2639     my $delete_adjusted_total = $total - ( $total * $deleted_ratio );
2640
2641     my $exclusion_ratio = $excluded / $checked;
2642     my $delete_adjusted_exclusion_ratio = $excluded / ($checked - $deleted);
2643
2644     my $inclusion_ratio = $visible / $checked;
2645     my $delete_adjusted_inclusion_ratio = $visible / ($checked - $deleted);
2646
2647     return {
2648         exclusion                   => int($delete_adjusted_total - ( $delete_adjusted_total * $exclusion_ratio )),
2649         inclusion                   => int($delete_adjusted_total * $inclusion_ratio),
2650         delete_adjusted_exclusion   => int($delete_adjusted_total - ( $delete_adjusted_total * $delete_adjusted_exclusion_ratio )),
2651         delete_adjusted_inclusion   => int($delete_adjusted_total * $delete_adjusted_inclusion_ratio)
2652     };
2653 }
2654 __PACKAGE__->register_method(
2655         api_name        => "open-ils.storage.fts_paging_estimate",
2656         method          => 'FTS_paging_estimate',
2657     argc        => 5,
2658     strict      => 1,
2659         api_level       => 1,
2660     signature   => {
2661         'return'=> q#
2662             Hash of estimation values based on four variant estimation strategies:
2663                 exclusion -- Estimate based on the ratio of excluded records on the current superpage;
2664                 inclusion -- Estimate based on the ratio of visible records on the current superpage;
2665                 delete_adjusted_exclusion -- Same as exclusion strategy, but the ratio is adjusted by deleted count;
2666                 delete_adjusted_inclusion -- Same as inclusion strategy, but the ratio is adjusted by deleted count;
2667         #,
2668         desc    => q#
2669             Helper method used to determin the approximate number of
2670             hits for a search that spans multiple superpages.  For
2671             sparse superpages, the inclusion estimate will likely be the
2672             best estimate.  The exclusion strategy is the original, but
2673             inclusion is the default.
2674         #,
2675         params  => [
2676             {   name    => 'checked',
2677                 desc    => 'Number of records check -- nominally the size of a superpage, or a remaining amount from the last superpage.',
2678                 type    => 'number'
2679             },
2680             {   name    => 'visible',
2681                 desc    => 'Number of records visible to the search location on the current superpage.',
2682                 type    => 'number'
2683             },
2684             {   name    => 'excluded',
2685                 desc    => 'Number of records excluded from the search location on the current superpage.',
2686                 type    => 'number'
2687             },
2688             {   name    => 'deleted',
2689                 desc    => 'Number of deleted records on the current superpage.',
2690                 type    => 'number'
2691             },
2692             {   name    => 'total',
2693                 desc    => 'Total number of records up to check_limit (superpage_size * max_superpages).',
2694                 type    => 'number'
2695             }
2696         ]
2697     }
2698 );
2699
2700
2701 sub xref_count {
2702     my $self   = shift;
2703     my $client = shift;
2704     my $args   = shift;
2705
2706     my $term  = $$args{term};
2707     my $limit = $$args{max} || 1;
2708     my $min   = $$args{min} || 1;
2709         my @classes = @{$$args{class}};
2710
2711         $limit = $min if ($min > $limit);
2712
2713         if (!@classes) {
2714                 @classes = ( qw/ title author subject series keyword / );
2715         }
2716
2717         my %matches;
2718         my $bre_table = biblio::record_entry->table;
2719         my $cn_table  = asset::call_number->table;
2720         my $cp_table  = asset::copy->table;
2721
2722         for my $search_class ( @classes ) {
2723
2724                 my $class = $_cdbi->{$search_class};
2725                 my $search_table = $class->table;
2726
2727                 my ($index_col) = $class->columns('FTS');
2728                 $index_col ||= 'value';
2729
2730                 
2731                 my $where = OpenILS::Application::Storage::FTS
2732                         ->compile($search_class => $term, $search_class.'.value', "$search_class.$index_col")
2733                         ->sql_where_clause;
2734
2735                 my $SQL = <<"           SQL";
2736                         SELECT  COUNT(DISTINCT X.source)
2737                           FROM  (SELECT $search_class.source
2738                                   FROM  $search_table $search_class
2739                                         JOIN $bre_table b ON (b.id = $search_class.source)
2740                                   WHERE $where
2741                                         AND NOT b.deleted
2742                                         AND b.active
2743                                   LIMIT $limit) X
2744                           HAVING COUNT(DISTINCT X.source) >= $min;
2745                 SQL
2746
2747                 my $res = $class->db_Main->selectrow_arrayref( $SQL );
2748                 $matches{$search_class} = $res ? $res->[0] : 0;
2749         }
2750
2751         return \%matches;
2752 }
2753 __PACKAGE__->register_method(
2754     api_name  => "open-ils.storage.search.xref",
2755     method    => 'xref_count',
2756     api_level => 1,
2757 );
2758
2759 sub query_parser_fts {
2760     my $self = shift;
2761     my $client = shift;
2762     my %args = @_;
2763
2764
2765     # grab the query parser and initialize it
2766     my $parser = $OpenILS::Application::Storage::QParser;
2767     $parser->use;
2768
2769     if (!$parser->initialization_complete) {
2770         my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
2771         $parser->initialize(
2772             config_metabib_field_index_norm_map =>
2773                 $cstore->request(
2774                     'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
2775                     { id => { "!=" => undef } },
2776                     { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
2777                 )->gather(1),
2778             search_relevance_adjustment         =>
2779                 $cstore->request(
2780                     'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
2781                     { id => { "!=" => undef } }
2782                 )->gather(1),
2783             config_metabib_field                =>
2784                 $cstore->request(
2785                     'open-ils.cstore.direct.config.metabib_field.search.atomic',
2786                     { id => { "!=" => undef } }
2787                 )->gather(1),
2788             config_metabib_search_alias         =>
2789                 $cstore->request(
2790                     'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
2791                     { alias => { "!=" => undef } }
2792                 )->gather(1),
2793         );
2794
2795         $cstore->disconnect;
2796         die("Cannot initialize $parser!") unless ($parser->initialization_complete);
2797     }
2798
2799
2800     # populate the locale/language map
2801     if (!$locale_map{COMPLETE}) {
2802
2803         my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
2804         for my $locale ( @locales ) {
2805             $locale_map{$locale->code} = $locale->marc_code;
2806         }
2807         $locale_map{COMPLETE} = 1;
2808
2809     }
2810
2811     # I hope we have a query!
2812         if (! $args{query} ) {
2813                 die "No query was passed to ".$self->api_name;
2814         }
2815
2816
2817     my $simple_plan = $args{_simple_plan};
2818     # remove bad chunks of the %args hash
2819     for my $bad ( grep { /^_/ } keys(%args)) {
2820         delete($args{$bad});
2821     }
2822
2823
2824     # parse the query and supply any query-level %arg-based defaults
2825     # we expect, and make use of, query, superpage, superpage_size, debug and core_limit args
2826     my $query = $parser->new( %args )->parse;
2827
2828
2829     # set the locale-based default prefered location
2830     if (!$query->parse_tree->find_filter('preferred_language')) {
2831         $parser->default_preferred_language( $args{preferred_language} );
2832         if (!$parser->default_preferred_language) {
2833                     my $ses_locale = $client->session ? $client->session->session_locale : '';
2834             $parser->default_preferred_language( $locale_map{ $ses_locale } );
2835         }
2836         $parser->default_preferred_language(
2837             OpenSRF::Utils::SettingsClient->new->config_value(
2838                 apps => 'open-ils.storage' => app_settings => 'default_preferred_language'
2839             )
2840         ) if (!$parser->default_preferred_language);
2841     }
2842
2843
2844     # set the global default language multiplier
2845     if (!$query->parse_tree->find_filter('preferred_language_weight') and !$query->parse_tree->find_filter('preferred_language_multiplier')) {
2846         $parser->default_preferred_language_multiplier($args{preferred_language_weight});
2847         $parser->default_preferred_language_multiplier($args{preferred_language_multiplier});
2848         $parser->default_preferred_language_multiplier(
2849             OpenSRF::Utils::SettingsClient->new->config_value(
2850                 apps => 'open-ils.storage' => app_settings => 'default_preferred_language_weight'
2851             )
2852         ) if (!$parser->default_preferred_language_multiplier);
2853     }
2854
2855     # gather the site, if one is specified, defaulting to the in-query version
2856         my $ou = $args{org_unit};
2857         if (my ($filter) = $query->parse_tree->find_filter('site')) {
2858             $ou = $filter->args->[0] if (@{$filter->args});
2859     }
2860         $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^\d+$/);
2861
2862
2863     # gather lasso, as with $ou
2864         my $lasso = $args{lasso};
2865         if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
2866             $lasso = $filter->args->[0] if (@{$filter->args});
2867     }
2868         $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
2869     $lasso = -$lasso if ($lasso);
2870
2871
2872 #    # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
2873 #    # gather user lasso, as with $ou and lasso
2874 #    my $mylasso = $args{my_lasso};
2875 #    if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
2876 #            $mylasso = $filter->args->[0] if (@{$filter->args});
2877 #    }
2878 #    $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
2879
2880
2881     # if we have a lasso, go with that, otherwise ... ou
2882     $ou = $lasso if ($lasso);
2883
2884
2885     # get the default $ou if we have nothing
2886         $ou = actor::org_unit->search( { parent_ou => undef } )->next->id if (!$ou and !$lasso and !$mylasso);
2887
2888
2889     # XXX when user lassos are here, check to make sure we don't have one -- it'll be passed in the depth, with an ou of 0
2890     # gather the depth, if one is specified, defaulting to the in-query version
2891         my $depth = $args{depth};
2892         if (my ($filter) = $query->parse_tree->find_filter('depth')) {
2893             $depth = $filter->args->[0] if (@{$filter->args});
2894     }
2895         $depth = actor::org_unit->search_where( [{ name => $depth },{ opac_label => $depth }], {limit => 1} )->next->id if ($depth and $depth !~ /^\d+$/);
2896
2897
2898     # gather the limit or default to 10
2899         my $limit = $args{check_limit} || 'NULL';
2900         if (my ($filter) = $query->parse_tree->find_filter('limit')) {
2901             $limit = $filter->args->[0] if (@{$filter->args});
2902     }
2903         if (my ($filter) = $query->parse_tree->find_filter('check_limit')) {
2904             $limit = $filter->args->[0] if (@{$filter->args});
2905     }
2906
2907
2908     # gather the offset or default to 0
2909         my $offset = $args{skip_check} || $args{offset} || 0;
2910         if (my ($filter) = $query->parse_tree->find_filter('offset')) {
2911             $offset = $filter->args->[0] if (@{$filter->args});
2912     }
2913         if (my ($filter) = $query->parse_tree->find_filter('skip_check')) {
2914             $offset = $filter->args->[0] if (@{$filter->args});
2915     }
2916
2917
2918     # gather the estimation strategy or default to inclusion
2919     my $estimation_strategy = $args{estimation_strategy} || 'inclusion';
2920         if (my ($filter) = $query->parse_tree->find_filter('estimation_strategy')) {
2921             $estimation_strategy = $filter->args->[0] if (@{$filter->args});
2922     }
2923
2924
2925     # gather the estimation strategy or default to inclusion
2926     my $core_limit = $args{core_limit};
2927         if (my ($filter) = $query->parse_tree->find_filter('core_limit')) {
2928             $core_limit = $filter->args->[0] if (@{$filter->args});
2929     }
2930
2931
2932     # gather statuses, and then forget those if we have an #available modifier
2933     my @statuses;
2934     if (my ($filter) = $query->parse_tree->find_filter('statuses')) {
2935         @statuses = @{$filter->args} if (@{$filter->args});
2936     }
2937     @statuses = (0,7,12) if ($query->parse_tree->find_modifier('available'));
2938
2939
2940     # gather locations
2941     my @location;
2942     if (my ($filter) = $query->parse_tree->find_filter('locations')) {
2943         @location = @{$filter->args} if (@{$filter->args});
2944     }
2945
2946
2947     my $param_check = $limit || $query->superpage_size || 'NULL';
2948     my $param_offset = $offset || 'NULL';
2949     my $param_limit = $core_limit || 'NULL';
2950
2951     my $sp = $query->superpage || 1;
2952     if ($sp > 1) {
2953         $param_offset = ($sp - 1) * $sp_size;
2954     }
2955
2956         my $param_search_ou = $ou;
2957         my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
2958         my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
2959         my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
2960         my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
2961         my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
2962         my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
2963
2964         my $sth = metabib::metarecord_source_map->db_Main->prepare(<<"    SQL");
2965         SELECT  *
2966           FROM  search.query_parser_fts(
2967                     $param_search_ou\:\:INT,
2968                     $param_depth\:\:INT,
2969                     $param_core_query\:\:TEXT,
2970                     $param_statuses\:\:INT[],
2971                     $param_locations\:\:INT[],
2972                     $param_offset\:\:INT,
2973                     $param_check\:\:INT,
2974                     $param_limit\:\:INT,
2975                     $metarecord\:\:BOOL,
2976                     $staff\:\:BOOL
2977                 );
2978     SQL
2979
2980     $sth->execute;
2981
2982     my $recs = $sth->fetchall_arrayref({});
2983     my $summary_row = pop @$recs;
2984
2985     my $total    = $$summary_row{total};
2986     my $checked  = $$summary_row{checked};
2987     my $visible  = $$summary_row{visible};
2988     my $deleted  = $$summary_row{deleted};
2989     my $excluded = $$summary_row{excluded};
2990
2991     my $estimate = $visible;
2992     if ( $total > $checked && $checked ) {
2993
2994         $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
2995         $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
2996
2997     }
2998
2999     delete $$summary_row{id};
3000     delete $$summary_row{rel};
3001     delete $$summary_row{record};
3002
3003     if (defined($simple_plan)) {
3004         $$summary_row{complex_query} = $simple_plan ? 0 : 1;
3005     } else {
3006         $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
3007     }
3008
3009     $client->respond( $summary_row );
3010
3011         $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
3012
3013         for my $rec (@$recs) {
3014         delete $$rec{checked};
3015         delete $$rec{visible};
3016         delete $$rec{excluded};
3017         delete $$rec{deleted};
3018         delete $$rec{total};
3019         $$rec{rel} = sprintf('%0.3f',$$rec{rel});
3020
3021                 $client->respond( $rec );
3022         }
3023         return undef;
3024 }
3025
3026 sub query_parser_fts_wrapper {
3027         my $self = shift;
3028         my $client = shift;
3029         my %args = @_;
3030
3031         $log->debug("Entering compatability wrapper function for old-style staged search", DEBUG);
3032     # grab the query parser and initialize it
3033     my $parser = $OpenILS::Application::Storage::QParser;
3034     $parser->use;
3035
3036     if (!$parser->initialization_complete) {
3037         my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
3038         $parser->initialize(
3039             config_metabib_field_index_norm_map =>
3040                 $cstore->request(
3041                     'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
3042                     { id => { "!=" => undef } },
3043                     { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
3044                 )->gather(1),
3045             search_relevance_adjustment         =>
3046                 $cstore->request(
3047                     'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
3048                     { id => { "!=" => undef } }
3049                 )->gather(1),
3050             config_metabib_field                =>
3051                 $cstore->request(
3052                     'open-ils.cstore.direct.config.metabib_field.search.atomic',
3053                     { id => { "!=" => undef } }
3054                 )->gather(1),
3055             config_metabib_search_alias         =>
3056                 $cstore->request(
3057                     'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
3058                     { alias => { "!=" => undef } }
3059                 )->gather(1),
3060         );
3061
3062         $cstore->disconnect;
3063         die("Cannot initialize $parser!") unless ($parser->initialization_complete);
3064     }
3065
3066         if (! scalar( keys %{$args{searches}} )) {
3067                 die "No search arguments were passed to ".$self->api_name;
3068         }
3069
3070         $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
3071     my $base_query = '';
3072     for my $sclass ( keys %{$args{searches}} ) {
3073             $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
3074         $base_query .= " $sclass: $args{searches}{$sclass}{term}";
3075     }
3076
3077     my $query = $base_query;
3078     $log->debug("Full base query: $base_query", DEBUG);
3079
3080     if (!$locale_map{COMPLETE}) {
3081
3082         my @locales = config::i18n_locale->search_where({ code => { '<>' => '' } });
3083         for my $locale ( @locales ) {
3084             $locale_map{$locale->code} = $locale->marc_code;
3085         }
3086         $locale_map{COMPLETE} = 1;
3087
3088     }
3089
3090     my $base_plan = $parser->new( query => $base_query )->parse;
3091
3092     $query = "preferred_language($args{preferred_language}) $query"
3093         if ($args{preferred_language} and !$base_plan->parse_tree->find_filter('preferred_language'));
3094     $query = "preferred_language_weight($args{preferred_language_weight}) $query"
3095         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'));
3096
3097     $query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
3098     $query = "site($args{org_unit}) $query" if ($args{org_unit});
3099     $query = "sort($args{sort}) $query" if ($args{sort});
3100     $query = "limit($args{limit}) $query" if ($args{limit});
3101     $query = "core_limit($args{core_limit}) $query" if ($args{core_limit});
3102     $query = "skip_check($args{skip_check}) $query" if ($args{skip_check});
3103     $query = "superpage($args{superpage}) $query" if ($args{superpage});
3104     $query = "offset($args{offset}) $query" if ($args{offset});
3105     $query = "#metarecord $query" if ($self->api_name =~ /metabib/);
3106     $query = "#available $query" if ($args{available});
3107     $query = "#descending $query" if ($args{sort_dir} && $args{sort_dir} =~ /^d/i);
3108     $query = "#staff $query" if ($self->api_name =~ /staff/);
3109     $query = "before($args{before}) $query" if (defined($args{before}) and $args{before} =~ /^\d+$/);
3110     $query = "after($args{after}) $query" if (defined($args{after}) and $args{after} =~ /^\d+$/);
3111     $query = "during($args{during}) $query" if (defined($args{during}) and $args{during} =~ /^\d+$/);
3112     $query = "between($args{between}[0],$args{between}[1]) $query"
3113         if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
3114
3115
3116         my (@between,@statuses,@locations,@types,@forms,@lang,@aud,@lit_form,@vformats,@bib_level);
3117
3118         # XXX legacy format and item type support
3119         if ($args{format}) {
3120                 my ($t, $f) = split '-', $args{format};
3121                 $args{item_type} = [ split '', $t ];
3122                 $args{item_form} = [ split '', $f ];
3123         }
3124
3125     for my $filter ( qw/locations statuses between audience language lit_form item_form item_type bib_level vr_format/ ) {
3126         if (my $s = $args{$filter}) {
3127                 $s = [$s] if (!ref($s));
3128
3129                 my @filter_list = @$s;
3130
3131             next if ($filter eq 'between' and scalar(@filter_list) != 2);
3132             next if (@filter_list == 0);
3133
3134             my $filter_string = join ',', @filter_list;
3135             $query = "$filter($filter_string) $query";
3136             }
3137     }
3138
3139     $log->debug("Full QueryParser query: $query", DEBUG);
3140
3141     return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan );
3142 }
3143 __PACKAGE__->register_method(
3144         api_name        => "open-ils.storage.biblio.multiclass.staged.search_fts",
3145         method          => 'query_parser_fts_wrapper',
3146         api_level       => 1,
3147         stream          => 1,
3148         cachable        => 1,
3149 );
3150 __PACKAGE__->register_method(
3151         api_name        => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
3152         method          => 'query_parser_fts_wrapper',
3153         api_level       => 1,
3154         stream          => 1,
3155         cachable        => 1,
3156 );
3157 __PACKAGE__->register_method(
3158         api_name        => "open-ils.storage.metabib.multiclass.staged.search_fts",
3159         method          => 'query_parser_fts_wrapper',
3160         api_level       => 1,
3161         stream          => 1,
3162         cachable        => 1,
3163 );
3164 __PACKAGE__->register_method(
3165         api_name        => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
3166         method          => 'query_parser_fts_wrapper',
3167         api_level       => 1,
3168         stream          => 1,
3169         cachable        => 1,
3170 );
3171
3172
3173 1;
3174