File Coverage

blib/lib/Interchange/Search/Solr.pm
Criterion Covered Total %
statement 74 560 13.2
branch 6 204 2.9
condition 0 44 0.0
subroutine 18 68 26.4
pod 27 27 100.0
total 125 903 13.8


line stmt bran cond sub pod time code
1             package Interchange::Search::Solr;
2              
3 14     14   114687 use strict;
  14         106  
  14         413  
4 14     14   76 use warnings;
  14         27  
  14         349  
5              
6 14     14   8039 use Moo;
  14         211021  
  14         69  
7 14     14   28506 use WebService::Solr;
  14         3074654  
  14         614  
8 14     14   8272 use WebService::Solr::Query;
  14         61740  
  14         514  
9 14     14   10120 use Data::Dumper;
  14         93537  
  14         1046  
10 14     14   8024 use POSIX qw//;
  14         90610  
  14         443  
11 14     14   115 use Encode qw//;
  14         36  
  14         240  
12 14     14   10251 use XML::LibXML;
  14         584048  
  14         109  
13 14     14   10036 use Interchange::Search::Solr::Response;
  14         50  
  14         518  
14 14     14   6944 use Interchange::Search::Solr::Builder;
  14         48  
  14         488  
15 14     14   7900 use Lingua::StopWords;
  14         5189  
  14         835  
16 14     14   199 use Types::Standard qw/ArrayRef HashRef Int Bool/;
  14         34  
  14         104  
17 14     14   20714 use namespace::clean;
  14         146311  
  14         105  
18 14     14   9210 use HTTP::Response;
  14         33  
  14         479  
19 14     14   89 use Scalar::Util;
  14         28  
  14         833  
20 14     14   93 use constant { DEBUG => 0 };
  14         41  
  14         101480  
21              
22             =head1 NAME
23              
24             Interchange::Search::Solr -- Solr query encapsulation
25              
26             =head1 VERSION
27              
28             Version 0.21
29              
30             =cut
31              
32             our $VERSION = '0.21';
33              
34             =head1 DESCRIPTION
35              
36             Exposes Solr search API in a programmer-friendly way.
37              
38             =head1 SYNOPSIS
39              
40             Quick summary of what the module does.
41              
42             Perhaps a little code snippet.
43              
44             use Interchange::Search::Solr;
45             my $solr = Interchange::Search::Solr->new(solr_url => $url);
46             $solr->rows(10);
47             $solr->start(0);
48             $solr->search('shirts');
49             $results = $solr->results;
50              
51             =head2 INDEX HANDLERS
52              
53             # Clear the index
54             $solr->delete( ['*:*'] );
55              
56             # Add a document
57             $solr->add( [
58             { sku => 'foo', title => 'My Foo' },
59             { sku => 'bar', title => 'My Bar' },
60             ] )
61              
62             # Commit (only needed if autocommit is disabled)
63             $solr->commit;
64              
65             =head1 ACCESSORS
66              
67             =head2 solr_url
68              
69             Url of the solr instance. Read-only.
70              
71             =head2 input_encoding [DEPRECATED]
72              
73             Assume the urls to be in this encoding, so decode it before parsing
74             it. This is basically a (probably bugged) workaround when you have all
75             the shop in latin1. If the search keep crashing on non-ascii
76             characters, try to set this to iso-8859-1.
77              
78             =head2 rows
79              
80             Number of results to return. Read-write (so you can reuse the object).
81              
82             =head2 page_scope
83              
84             The number of paging items for the paginator
85              
86             =head2 search_fields
87              
88             An arrayref with the indexed fields to search. Defaults to:
89              
90             [qw/sku name description/]
91              
92             You can add boost to fields appending a float with a caret.
93              
94             [qw/ sku^5.0 name^1.0 description^0.4 /]
95              
96              
97             =head2 return_fields
98              
99             An arrayref of indexed fields to return. All by default.
100              
101             =head2 facets
102              
103             An arrayref with the fields which will generate a facet.
104             Defaults to
105              
106             [qw/suchbegriffe manufacturer/]
107              
108             =head2 facet_ranges
109              
110             An optional arrayref of hashrefs with the facet ranges (tipically, a price).
111              
112             The structure must have these these keys:
113              
114             {
115             name => String,
116             start => Int,
117             end => Int,
118             gap => Int,
119             }
120              
121             Name is the name of the field which needs to generate a range facet.
122             start, end and gap are, in this order, lower bound, upper bound, span
123             of each range.
124              
125             See: L<https://solr.apache.org/guide/8_8/faceting.html#range-faceting>
126              
127             When searching, the field should have exactly two numeric arguments,
128             e.g. "/price/1/100" (from 1 to 100).
129              
130             So far only integers are supported.
131              
132             =head2 start
133              
134             Start of pagination. Read-write.
135              
136             =head2 page
137              
138             Current page. Read-write.
139              
140             =head2 filters
141              
142             An hashref with the filters. E.g.
143              
144             {
145             suchbegriffe => [qw/xxxxx yyyy/],
146             manufacturer => [qw/pikeur/],
147             }
148              
149             The keys of the hashref, to have any effect, must be one of the facets.
150              
151             =head2 response
152              
153             Read-only accessor to the response object of the current search.
154              
155             =head2 facets_found
156              
157             After a search, the structure with the facets can be retrieved with
158             this accessor, with this structure:
159              
160             {
161             field => [ { name => "name", count => 11 }, { name => "name", count => 9 }, ... ],
162             field2 => [ { name => "name", count => 7 }, { name => "name", count => 6 }, ... ],
163             ...
164             }
165              
166             Each hashref in each field's arrayref has the following keys:
167              
168             =over 4
169              
170             =item name
171              
172             The name to display
173              
174             =item count
175              
176             The count of item
177              
178             =item query_url
179              
180             The url fragment to toggle this filter.
181              
182             =item active
183              
184             True if currently in use (to be used for, e.g., checkboxes)
185              
186             =back
187              
188             =head2 facet_ranges_found
189              
190             Return the range facets, same as for the C<facets_found> method, but
191             in the structure only name and count are found.
192              
193             =head2 search_string
194              
195             Debug only. The search string produced by the query.
196              
197             =head2 search_terms
198              
199             The terms used for the current search.
200              
201             =head2 search_structure
202              
203             The perl data structure used for the current search. It's passed to
204             L<Webservice::Solr::Query> for stringification.
205              
206             =head2 sorting
207              
208             The field used to sort the result (optional and defaults to score, as
209             per Solr doc).
210              
211             You can set it to a scalar with a field name or instead you can use
212             the L<SQL::Abstract> syntax (all the cases documented there are
213             supported). E.g.
214              
215             $solr->sorting([{ -asc => 'created_date' }, {-desc => [qw/updated_date sku/] }]);
216              
217             If you pass a reference, the C<sorting_direction> setting is ignored.
218              
219             =head2 sorting_direction
220              
221             The direction used by the sorting, when C<sorting> is specified and is a plain scalar.
222             Default to 'desc'.
223              
224             =cut
225              
226             has solr_url => (is => 'ro',
227             required => 1);
228              
229             has input_encoding => (is => 'ro');
230              
231              
232             =head2 wild_matching
233              
234             By default, a search term produce a query with a wildcard appended. So
235             searching for 1234 will query 1234*. With this option set to true, a
236             wildcard is prepended as well, querying for *1234* instead).
237              
238             =cut
239              
240              
241             has wild_matching => (is => 'ro',
242             default => sub { 0 });
243              
244             =head2 stop_words_langs
245              
246             The languages for which we should build the stop word list. It
247             defaults to:
248              
249             [ 'en' ]
250              
251             New in 0.10. To revert to the old behaviour (no filtering of
252             stopwords), pass an empty arrayref.
253              
254             =cut
255              
256             has stop_words => (is => 'lazy', isa => HashRef);
257              
258             has stop_words_langs => (is => 'ro', default => sub { [qw/en/ ] }, isa => ArrayRef);
259              
260             sub _build_stop_words {
261 0     0   0 my $self = shift;
262 0         0 my @stopwords;
263 0         0 foreach my $lang (@{ $self->stop_words_langs }) {
  0         0  
264 0 0       0 if (my $stops = Lingua::StopWords::getStopWords($lang, 'UTF-8')) {
265 0         0 push @stopwords, keys %$stops;
266             }
267             }
268 0         0 my %out = map { $_ => 1 } @stopwords;
  0         0  
269 0         0 return \%out;
270             }
271              
272             =head2 min_chars
273              
274             Minimum characters for filtering the search terms. Default to 3.
275              
276             New in 0.10. To revert to the old behaviour, set it to 0.
277              
278             =head2 permit_empty_search
279              
280             By default, empty searches are not executed. You can permit them
281             setting this accessor to 1. The module will reset it to 0 when the
282             search is executed.
283              
284             =cut
285              
286             has min_chars => (is => 'ro', isa => Int, default => sub { 3 });
287              
288             has permit_empty_search => (is => 'rw', isa => Bool, default => sub { 0 });
289              
290             has search_fields => (is => 'ro',
291             default => sub {
292             return [
293             qw/sku
294             name
295             description/
296             ]
297             },
298             isa => sub { die unless ref($_[0]) eq 'ARRAY' });
299              
300             has facets => (is => 'rw',
301             isa => sub { die "not an arrayref" unless ref($_[0]) eq 'ARRAY' },
302             default => sub {
303             return [qw/suchbegriffe manufacturer/];
304             });
305              
306             has facet_ranges => (is => 'rw',
307             isa => sub {
308             die "not an arrayref" unless ref($_[0]) eq 'ARRAY';
309             foreach my $i (@{$_[0]}) {
310             die "Missing name in facet range definition" unless $i->{name};
311             }
312             },
313             default => sub { [] },
314             );
315              
316             sub _facet_range_names {
317 0     0   0 return map { $_->{name} } @{ shift->facet_ranges };
  0         0  
  0         0  
318             }
319              
320             has rows => (is => 'rw',
321             default => sub { 10 });
322              
323             has page_scope => (is => 'rw',
324             default => sub { 5 });
325              
326             has start => (is => 'rw',
327             default => sub { 0 });
328              
329             has page => (is => 'rw',
330             default => sub { 1 });
331              
332             has filters => (is => 'rw',
333             isa => HashRef,
334             default => sub { return {} },
335             );
336              
337             has response => (is => 'rwp');
338              
339             has search_string => (is => 'rwp');
340              
341             has search_terms => (is => 'rw',
342             isa => sub { die unless ref($_[0]) eq 'ARRAY' },
343             default => sub { return [] },
344             );
345              
346             has search_structure => (is => 'rw');
347              
348             has sorting => (is => 'rw');
349             has sorting_direction => (is => 'rw',
350             isa => sub { die unless $_[0] =~ m/\A(asc|desc)\z/ },
351             default => sub { 'desc' },
352             );
353              
354             has return_fields => (is => 'rw',
355             isa => sub { die unless ref($_[0]) eq 'ARRAY' },
356             );
357              
358             has global_conditions => (is => 'rw',
359             isa => sub { die unless ref($_[0]) eq 'HASH' }
360             );
361              
362             sub results {
363 0     0 1 0 my $self = shift;
364 0         0 my @matches;
365 0 0       0 if ($self->response->ok) {
366 0         0 for my $doc ( $self->response->docs ) {
367 0         0 my (%record, $name);
368              
369 0         0 for my $fld ($doc->fields) {
370 0         0 $name = $fld->name;
371 0 0       0 next if $name =~ /^_/;
372              
373 0         0 $record{$name} = $fld->value;
374             }
375              
376 0         0 push @matches, \%record;
377             }
378             }
379 0         0 return \@matches;
380             }
381              
382             =head1 INTERNAL ACCESSORS
383              
384             =head2 solr_object
385              
386             The L<WebService::Solr> instance.
387              
388             =cut
389              
390             has solr_object => (is => 'lazy');
391              
392             sub _build_solr_object {
393 0     0   0 my $self = shift;
394 0         0 my @args = $self->solr_url;
395             # if (my $enc = $self->solr_encoding) {
396             # my %options = (
397             # default_params => {
398             # ie => $enc,
399             # },
400             # );
401             # push @args, \%options;
402             # }
403 0         0 return WebService::Solr->new(@args);
404             }
405              
406             =head2 builder_object(\@terms, \%filters, $page)
407              
408             Creates Interchange::Search::Solr::Builder instance.
409              
410             =cut
411              
412             sub builder_object {
413 0     0 1 0 my ($self, $terms, $filters, $page) = @_;
414             return Interchange::Search::Solr::Builder->new(
415             terms => $terms,
416             filters => $filters,
417 0         0 facets => [ @{$self->facets}, $self->_facet_range_names ],
  0         0  
418             page => $page
419             );
420             }
421              
422             =head1 METHODS
423              
424             =head2 search( [ $string_or$structure ] ])
425              
426             Run a search and return a L<WebService::Solr::Response> object.
427              
428             The method accept zero or one argument.
429              
430             With no arguments, run a full wildcard search.
431              
432             With one argument, if it's a string, run the search against all the
433             indexed fields. If it's a structure, build a query for it. The syntax
434             of the structure is described at L<WebService::Solr::Query>.
435              
436             After calling this method you can inspect the response using the
437             following methods:
438              
439             =head2 results
440              
441             Returns reference to list of results, each result is a hash
442             reference.
443              
444             =head2 skus_found
445              
446             Returns just a plain list of skus.
447              
448             =head2 num_found
449              
450             Return the number of items found
451              
452             =head2 has_more
453              
454             Return true if there are more pages
455              
456             =cut
457              
458             sub search {
459 0     0 1 0 my ($self, $string) = (shift, shift);
460 0 0       0 die "Extra parameter found" if (@_);
461 0         0 my $structure;
462             # here we just split the terms, set C<search_terms>, and call
463             # _do_search.
464 0 0 0     0 if ($string && ref($string)) {
465 0         0 $structure = $string;
466 0         0 $string = undef;
467             }
468 0         0 my @terms;
469 0 0       0 if ($string) {
470 0         0 @terms = grep { $self->_term_is_good($_) } split(/\s+/, $string);
  0         0  
471             }
472 0         0 $self->search_terms(\@terms);
473 0         0 $self->search_structure($structure);
474 0         0 return $self->_do_search;
475             }
476              
477             sub _do_search {
478 0     0   0 my $self = shift;
479              
480 0         0 my @terms = grep { $self->_term_is_good($_) } @{ $self->search_terms };
  0         0  
  0         0  
481              
482 0         0 my $query = '';
483 0         0 my $wild_match = '';
484 0 0       0 if ($self->wild_matching) {
485 0         0 $wild_match = '*';
486             }
487              
488 0 0       0 if (@terms) {
489 0         0 my @escaped = map { $wild_match . WebService::Solr::Query->escape($_) . '*' } @terms;
  0         0  
490 0         0 $query = '(' . join(' AND ', @escaped) . ')';
491             # even if the structure looks correct, the query isn't build properly
492             # print Dumper($query);
493             }
494             else {
495             # catch all
496 0 0       0 if (my $structure = $self->search_structure) {
497 0         0 $query = WebService::Solr::Query->new($structure);
498             }
499              
500             }
501 0         0 return $self->execute_query($query);
502             }
503              
504             =head2 execute_query($query)
505              
506             Accept either a raw string with the query or a WebService::Solr::Query
507             object and run the query against the Solr service.
508              
509             If no query is provided, a wildcard search is performed.
510              
511             =cut
512              
513             sub _search_is_empty {
514 0     0   0 my $self = shift;
515 0         0 my @terms = @{ $self->search_terms };
  0         0  
516 0         0 my %filters = %{ $self->filters };
  0         0  
517 0         0 my $structure = $self->search_structure;
518 0 0 0     0 if (@terms || %filters || $structure) {
      0        
519 0         0 return 0;
520             }
521             else {
522 0         0 return 1;
523             }
524             }
525              
526             sub execute_query {
527 0     0 1 0 my ($self, $query) = @_;
528 0         0 my $querystring = '*';
529 0 0       0 if (ref($query)) {
    0          
530 0         0 $querystring = $query->stringify;
531             }
532             elsif ($query) {
533 0         0 $querystring = $query;
534             }
535              
536 0 0       0 if (my $global = $self->global_conditions) {
537 0         0 my @conditions = ($querystring);
538 0         0 foreach my $condition (keys %$global) {
539 0         0 my $string;
540 0 0       0 if (ref($global->{$condition}) eq 'SCALAR') {
541 0         0 $string = ${$global->{$condition}};
  0         0  
542             }
543             else {
544             $string = '"' .
545 0         0 WebService::Solr::Query->escape($global->{$condition}) . '"';
546             }
547 0         0 push @conditions, qq{($condition:$string)};
548             }
549 0         0 $querystring = '(' . join(' AND ', @conditions) . ')';
550             }
551             # save the debug info
552 0         0 $self->_set_search_string($querystring);
553              
554 0         0 my $our_res;
555 0 0       0 unless ($self->permit_empty_search) {
556 0 0       0 if ($self->_search_is_empty) {
557 0         0 $our_res = Interchange::Search::Solr::Response->new(HTTP::Response->new(404));
558 0         0 $our_res->error('empty_search');
559             }
560             }
561 0 0       0 unless ($our_res) {
562 0         0 my %params = $self->construct_params;
563             # print Dumper(\%params) if DEBUG;
564 0         0 my $res = $self->solr_object->search($querystring, \%params);
565 0         0 $our_res = Interchange::Search::Solr::Response->new($res->raw_response);
566              
567 0 0       0 unless ( $our_res->success ) {
568 0         0 die "Solr failure: " . $our_res->exception_message;
569             }
570             }
571 0         0 $self->_set_response($our_res);
572 0         0 $self->permit_empty_search(0);
573 0         0 return $our_res;
574             }
575              
576             =head2 construct_params
577              
578             Constructs parameters for the search.
579              
580             =cut
581              
582             sub construct_params {
583             # set start and rows
584 0     0 1 0 my $self = shift;
585 0         0 my %params = (
586             start => $self->_start_row,
587             rows => $self->_rows
588             );
589              
590 0         0 my @fq;
591 0 0       0 if (my $facet_field = $self->facets) {
592 0         0 $params{facet} = 'true';
593 0         0 $params{'facet.field'} = $facet_field;
594 0         0 $params{'facet.mincount'} = 1;
595              
596             # see if we have filters set
597 0 0       0 if (my $filters = $self->filters) {
598 0         0 foreach my $facet (@{ $self->facets }) {
  0         0  
599 0 0       0 if (my $condition = $filters->{$facet}) {
600 0         0 push @fq,
601             WebService::Solr::Query->new({
602             $facet => $condition,
603             });
604             }
605             }
606             }
607             }
608              
609 0 0       0 if (my $facet_ranges = $self->facet_ranges) {
610             # these are the settings for the ranges.
611 0         0 my @ranges;
612 0         0 foreach my $fr (@$facet_ranges) {
613 0         0 my $f = $fr->{name};
614 0 0       0 die "Facet range must have a name!" unless $f;
615 0         0 foreach my $k (qw/start end gap method/) {
616 0 0       0 if (defined $fr->{$k}) {
617 0         0 $params{"f.$f.facet.range.$k"} = $fr->{$k};
618             }
619             }
620 0         0 push @ranges, $f;
621             }
622 0 0       0 if (@ranges) {
623 0         0 $params{facet} = 'true';
624 0         0 $params{'facet.range'} = \@ranges;
625             # see if we have filters set
626 0 0       0 if (my $filters = $self->filters) {
627 0         0 foreach my $facet (@ranges) {
628 0 0       0 if (my $condition = $filters->{$facet}) {
629 0         0 my ($from, $to) = @$condition;
630 0 0 0     0 if (defined $from and
      0        
      0        
631             defined $to and
632             $from =~ m/\A[1-9][0-9]*\z/ and
633             $to =~ m/\A[1-9][0-9]*\z/) {
634 0         0 my $range = "[${from} TO ${to}]";
635 0         0 push @fq, "$facet:$range";
636             }
637             }
638             }
639             }
640             }
641             }
642 0 0       0 if (@fq) {
643 0         0 $params{fq} = \@fq;
644             }
645              
646              
647 0 0       0 if (my $sort_by = $self->sorting) {
648 0         0 my $sort_by_struct;
649 0 0       0 if (ref($sort_by)) {
650 0         0 $sort_by_struct = $sort_by;
651             }
652             else {
653 0         0 $sort_by_struct = { '-' . $self->sorting_direction => $sort_by };
654             }
655 0         0 $params{sort} = join(', ', $self->_build_sort_field($sort_by_struct));
656             }
657 0 0       0 if (my $fl = $self->return_fields) {
658 0         0 $params{fl} = join(',', @$fl);
659             }
660             # if using edifmax
661 0         0 $params{qf} = join(' ', @{ $self->search_fields });
  0         0  
662 0         0 $params{defType} = 'edismax';
663 0         0 return %params;
664             }
665              
666             sub _start_row {
667 0     0   0 my $self = shift;
668 0   0     0 return $self->_convert_to_int($self->start) || 0;
669             }
670              
671             sub _rows {
672 0     0   0 my $self = shift;
673 0   0     0 return $self->_convert_to_int($self->rows) || 10;
674             }
675              
676             sub _convert_to_int {
677 0     0   0 my ($self, $maybe_num) = @_;
678 0 0       0 return 0 unless $maybe_num;
679 0 0       0 if ($maybe_num =~ m/([1-9][0-9]*)/) {
680 0         0 return $1;
681             }
682             else {
683 0         0 return 0;
684             }
685             }
686              
687             sub num_found {
688 0     0 1 0 my $self = shift;
689 0 0       0 if (my $res = $self->response) {
690 0   0     0 return $res->content->{response}->{numFound} || 0;
691             }
692             else {
693 0         0 return 0;
694             }
695             }
696              
697             sub skus_found {
698 0     0 1 0 my $self = shift;
699 0         0 my @skus;
700 0 0       0 if ($self->response->ok) {
701 0         0 foreach my $item ($self->response->docs) {
702 0         0 push @skus, $item->value_for('sku');
703             }
704             }
705 0         0 return @skus;
706             }
707              
708             sub facets_found {
709 0     0 1 0 my $self = shift;
710 0         0 my $res = $self->response;
711 0         0 my $facets = $res->content->{facet_counts}->{facet_fields};
712 0         0 my %out;
713 0         0 foreach my $field (keys %$facets) {
714 0         0 my @list = @{$facets->{$field}};
  0         0  
715 0         0 my @items;
716 0         0 while (@list > 1) {
717 0         0 my $name = shift @list;
718 0         0 my $count = shift @list;
719 0         0 push @items, {
720             name => $name,
721             count => $count,
722             query_url => $self->_build_facet_url($field, $name),
723             active => $self->_filter_is_active($field, $name),
724             };
725             }
726 0         0 $out{$field} = \@items;
727             }
728 0         0 return \%out;
729             }
730              
731             sub facet_ranges_found {
732 0     0 1 0 my $self = shift;
733 0         0 my %out;
734 0 0       0 if (my $facets = $self->response->content->{facet_counts}->{facet_ranges}) {
735 0         0 foreach my $field (keys %$facets) {
736 0 0       0 if (my $range = $facets->{$field}) {
737 0 0       0 if (my $counts = $range->{counts}) {
738 0 0       0 if (ref($counts) eq 'ARRAY') {
739 0         0 my @items;
740 0         0 my @list = @$counts;
741 0         0 while (@list > 1) {
742 0         0 my $value = shift @list;
743 0         0 my $count = shift @list;
744 0         0 push @items, {
745             name => $value,
746             count => $count,
747             };
748             }
749 0         0 $out{$field} = [ sort { $a->{name} <=> $b->{name} } @items ];
  0         0  
750             }
751             }
752             }
753             }
754             }
755 0         0 return \%out;
756             }
757              
758             sub has_more {
759 0     0 1 0 my $self = shift;
760 0 0       0 if ($self->num_found > ($self->_start_row + $self->_rows)) {
761 0         0 return 1;
762             }
763             else {
764 0         0 return 0;
765             }
766             }
767              
768             =head2 num_docs
769              
770             Returns the number of documents in the index.
771              
772             =cut
773              
774             sub num_docs {
775 0     0 1 0 my $self = shift;
776 0         0 my $response;
777              
778 0         0 $self->permit_empty_search(1);
779 0         0 $response = $self->search('*:*');
780              
781 0         0 return $self->num_found;
782             }
783              
784             =head2 add $data
785              
786             Adds the documents in $data to the index. $data is a reference to an array
787             of hash references, e.g.:
788              
789             [ { sku => 'foo', title => 'My Foo' }, { sku => 'bar', title => 'My Bar' } ]
790              
791             =cut
792              
793             sub add {
794 0     0 1 0 my ($self, $data) = @_;
795 0         0 my ($res, $our_res, $xml);
796              
797 0         0 $xml = $self->_build_xml_add_op($data);
798 0         0 $res = $self->solr_object->_send_update($xml);
799 0         0 $our_res = Interchange::Search::Solr::Response->new($res->raw_response);
800              
801 0         0 return $our_res;
802             }
803              
804             =head2 delete $data
805              
806             Deletes documents by queries in $data.
807              
808             =cut
809              
810             sub delete {
811 0     0 1 0 my ($self, $data) = @_;
812 0         0 my ($res, $our_res, $xml);
813              
814 0         0 $xml = $self->_build_xml_del_op($data);
815 0         0 $res = $self->solr_object->_send_update($xml);
816 0         0 $our_res = Interchange::Search::Solr::Response->new($res->raw_response);
817              
818 0         0 return $our_res;
819             }
820              
821             =head2 commit
822              
823             Commits recently indexed content. This is necessary to make these changes visible
824             for searches unless autocommit is enabled in the configuration.
825              
826             =cut
827              
828             sub commit {
829 0     0 1 0 my $self = shift;
830 0         0 my ($res, $our_res);
831 0         0 my $xml = '<commit/>';
832              
833 0         0 $res = $self->solr_object->_send_update($xml);
834 0         0 $our_res = Interchange::Search::Solr::Response->new($res->raw_response);
835              
836 0         0 return $our_res;
837             }
838              
839             =head2 maintainer_update($mode)
840              
841             Perform a maintainer update and return a L<WebService::Solr::Response>
842             object.
843              
844             =cut
845              
846             sub maintainer_update {
847 0     0 1 0 my ($self, $mode, $data) = @_;
848 0 0       0 die "Missing argument" unless $mode;
849 0         0 my (@query, %params, $xml, $wss_res, $res);
850              
851 0 0       0 if ($mode eq 'add') {
    0          
    0          
    0          
852 0         0 $res = $self->add($data);
853             }
854             elsif ($mode eq 'clear') {
855 0         0 $res = $self->delete(['*:*']);
856             }
857             elsif ($mode eq 'full') {
858 0         0 @query = ('dataimport', { command => 'full-import' });
859             }
860             elsif ($mode eq 'delta') {
861 0         0 @query = ('dataimport', { command => 'delta-import' });
862             }
863             else {
864 0         0 die "Unrecognized mode $mode!";
865             }
866              
867 0 0       0 unless ( $res ) {
868 0         0 $wss_res = $self->solr_object->generic_solr_request(@query);
869 0         0 $res = Interchange::Search::Solr::Response->new($wss_res->raw_response);
870             }
871              
872 0 0       0 if ( $res->success ) {
873             # commit the changes
874 0         0 $res = $self->commit;
875             }
876              
877 0         0 return $res;
878             }
879              
880             # builds XML for add index handler
881              
882             sub _build_xml_add_op {
883 3     3   1871 my ($self, $input) = @_;
884 3         33 my $doc = XML::LibXML::Document->new;
885 3         34 my $el_add = $doc->createElement('add');
886 3         4 my $list;
887 3         45 $doc->addChild($el_add);
888              
889 3 100       15 if (ref($input) eq 'ARRAY') {
    50          
890 1         17 $list = $input;
891             }
892             elsif (ref($input) eq 'HASH') {
893 2         115 $list = [ $input ];
894             }
895             else {
896 0         0 die "Bad usage: input should be an arrayref or an hashref";
897             }
898              
899 3         9 foreach my $data (@$list) {
900 4         47 my $el_doc = $doc->createElement('doc');
901 4         17 $el_add->addChild($el_doc);
902 4         10 while (my ($name, $value) = each %$data) {
903 4 50       60 if (defined $value) {
904 4         9 my @values;
905 4 100       11 if (ref($value) eq 'ARRAY') {
906 2         7 @values = @$value;
907             } else {
908 2         5 @values = ($value);
909             }
910 4         9 foreach my $v (@values) {
911 6         56 my $el_field = $doc->createElement('field');
912 6         20 $el_field->setAttribute(name => $name);
913 6         71 $el_field->appendText($v);
914 6         23 $el_doc->addChild($el_field);
915             }
916             }
917             }
918             }
919 3         196 return $doc->firstChild->toString;
920             }
921              
922             # builds XML for delete index handler
923              
924             sub _build_xml_del_op {
925 0     0     my ($self, $input) = @_;
926 0           my $doc = XML::LibXML::Document->new;
927 0           my $el_del = $doc->createElement('delete');
928 0           my $list;
929 0           $doc->addChild($el_del);
930              
931 0 0         if (ref($input) eq 'ARRAY') {
932 0           $list = $input;
933             }
934             else {
935 0           die "Bad usage: input should be an arrayref";
936             }
937              
938 0           foreach my $query (@$list) {
939 0           my $el_q = $doc->createElement('query');
940 0           $el_q->appendText($query);
941 0           $el_del->addChild($el_q);
942             }
943              
944 0           return $doc->firstChild->toString;
945             }
946              
947             =head2 reset_object
948              
949             Reset the leftovers of a possible previous search.
950              
951             =head2 search_from_url($url)
952              
953             Parse the url provided and do the search.
954              
955             =head2 safe_search_from_url($url)
956              
957             Same as above, but safe from crashes (wrapped in eval)
958              
959             =cut
960              
961             sub reset_object {
962 0     0 1   my $self = shift;
963 0           $self->start(0);
964 0           $self->page(1);
965 0           $self->_set_response(undef);
966 0           $self->_set_search_string(undef);
967 0           $self->filters({});
968 0           $self->search_terms([]);
969 0           $self->search_structure(undef);
970             }
971              
972             sub search_from_url {
973 0     0 1   my ($self, $url) = @_;
974 0 0         if (my $enc = $self->input_encoding) {
975 0           $url = Encode::decode($enc, $url);
976             }
977 0           $self->_parse_url($url);
978             # at this point, all the parameters are set after the url parsing
979 0           return $self->_do_search;
980             }
981              
982             sub safe_search_from_url {
983 0     0 1   my ($self, $url) = @_;
984 0           my $res;
985 0           eval { $res = $self->search_from_url($url) };
  0            
986 0           return $res;
987             }
988              
989              
990             =head2 reset_facet_url($facet_name)
991              
992             Return an URL which should reset the facet passed as argument.
993              
994             See you searched for "/color/blue/price/1/12", this would return
995             "/color/blue"
996              
997             =head2 add_terms_to_url($url, $string)
998              
999             Parse the url, and return a new one with the additional words added.
1000             The page is discarded, while the filters are retained.
1001              
1002             =cut
1003              
1004             sub add_terms_to_url {
1005 0     0 1   my ($self, $url, @other_terms) = @_;
1006 0 0         die "Bad usage" unless defined $url;
1007 0           $self->_parse_url($url);
1008 0 0         return $url unless @other_terms;
1009 0           my @additional_terms = grep { $self->_term_is_good($_) } @other_terms;
  0            
1010 0           my @terms = @{ $self->search_terms };
  0            
1011 0           push @terms, @additional_terms;
1012 0           $self->search_terms(\@terms);
1013 0           my $builder = $self->builder_object(
1014             $self->search_terms,
1015             $self->filters
1016             );
1017 0           return $builder->url_builder;
1018            
1019             }
1020              
1021              
1022             sub _parse_url {
1023 0     0     my ($self, $url) = @_;
1024 0           $self->reset_object;
1025 0 0         return unless $url;
1026 0           my @fragments = grep { $_ } split('/', $url);
  0            
1027              
1028             # nothing to do if there are no fragments
1029 0 0         return unless @fragments;
1030              
1031 0           my (@terms, %filters);
1032             # the first keyword we need is the optional "words"
1033 0 0         if ($fragments[0] eq 'words') {
1034             # just discards and check if we have something. This could
1035             # also mean that the next word is not a keyword.
1036 0           shift @fragments;
1037 0 0         if (@fragments) {
1038 0           push @terms, shift @fragments;
1039             }
1040             }
1041              
1042             # the page is the last fragment, so check that
1043 0 0         if (@fragments > 1) {
1044 0           my $page = $#fragments;
1045 0 0         if ($fragments[$page - 1] eq 'page') {
1046 0           $page = pop @fragments;
1047             # and remove the page
1048 0           pop @fragments;
1049             # but assert it is a number, 1 otherwise
1050 0 0         if ($page =~ s/^([1-9][0-9]*)$/)/) {
1051 0           $self->page($1);
1052             }
1053             else {
1054 0           $self->page(1);
1055             }
1056 0           $self->_set_start_from_page;
1057             }
1058             }
1059 0           my $current_filter;
1060 0           while (@fragments) {
1061              
1062 0           my $chunk = shift @fragments;
1063              
1064             # we lookup until the first keyword, but only if there is
1065             # non-keywords after that.
1066 0 0 0       if ($self->_fragment_is_keyword($chunk) and
      0        
1067             @fragments and
1068             !$self->_fragment_is_keyword($fragments[0])) {
1069             # chunk is actually a keyword. Set the flag, prepare the
1070             # array and move on.
1071 0           $current_filter = $chunk;
1072 0           $filters{$current_filter} = [];
1073 0           next;
1074             }
1075              
1076             # are we inside a filter?
1077 0 0         if ($current_filter) {
1078 0           push @{ $filters{$current_filter} }, $chunk;
  0            
1079             }
1080             # if not, it's a term
1081             else {
1082 0           push @terms, $chunk;
1083             }
1084             }
1085             # filter the terms
1086 0           $self->search_terms([ grep { $self->_term_is_good($_) } @terms ]);
  0            
1087 0           $self->filters(\%filters);
1088             }
1089              
1090             sub _fragment_is_keyword {
1091 0     0     my ($self, $fragment) = @_;
1092 0 0         return unless defined $fragment;
1093 0           return grep { $_ eq $fragment } (@{ $self->facets }, $self->_facet_range_names);
  0            
  0            
1094             }
1095              
1096              
1097             sub _set_start_from_page {
1098 0     0     my $self = shift;
1099 0           $self->start($self->rows * ($self->page - 1));
1100             }
1101              
1102              
1103             =head2 current_search_to_url
1104              
1105             Return the url for the current search.
1106              
1107             =cut
1108              
1109             sub current_search_to_url {
1110 0     0 1   my ($self, %args) = @_;
1111 0           my $page;
1112              
1113 0 0         if (! $args{hide_page}) {
1114 0           $page = $self->page;
1115             }
1116              
1117 0           my $builder = $self->builder_object($self->search_terms,
1118             $self->filters,
1119             $page);
1120              
1121 0           return $builder->url_builder;
1122             }
1123              
1124             sub reset_facet_url {
1125 0     0 1   my ($self, $facet) = @_;
1126 0           my @terms = @{ $self->search_terms };
  0            
1127 0           my $filters = $self->filters;
1128             # copy
1129 0           my %toggled_filters = %{ $self->filters };
  0            
1130 0           delete $toggled_filters{$facet};
1131 0           return $self->builder_object(\@terms, \%toggled_filters)->url_builder;
1132             }
1133              
1134             sub _build_facet_url {
1135 0     0     my ($self, $field, $name) = @_;
1136             # get the current filters
1137             # print "Building $field $name\n";
1138 0           my @terms = @{ $self->search_terms };
  0            
1139             # page is not needed
1140              
1141             # the hash for the url builder
1142 0           my %toggled_filters;
1143              
1144             # the current filters
1145 0           my $filters = $self->filters;
1146              
1147             # loop over the facets we defined
1148 0           foreach my $facet (@{ $self->facets }, $self->_facet_range_names) {
  0            
1149              
1150             # copy of the active filters
1151 0 0         my @active = @{ $filters->{$facet} || [] };
  0            
1152              
1153             # filter is active: remove
1154 0 0         if ($self->_filter_is_active($facet, $name)) {
    0          
1155 0           @active = grep { $_ ne $name } @active;
  0            
1156             }
1157             # it's not active, but we're building an url for this facet
1158             elsif ($facet eq $field) {
1159 0           push @active, $name;
1160             }
1161             # and store
1162 0 0         $toggled_filters{$facet} = \@active if @active;
1163             }
1164             # print Dumper(\@terms, \%toggled_filters);
1165 0           my $builder = $self->builder_object(\@terms, \%toggled_filters);
1166 0           return $builder->url_builder;
1167             }
1168              
1169             sub _filter_is_active {
1170 0     0     my ($self, $field, $name) = @_;
1171 0           my $filters = $self->filters;
1172 0 0         if (my $list = $self->filters->{$field}) {
1173 0 0         if (my @active = @$list) {
1174 0 0         if (grep { $_ eq $name } @active) {
  0            
1175 0           return 1;
1176             }
1177             }
1178             }
1179 0           return 0 ;
1180             }
1181              
1182             =head2 paginator
1183              
1184             Return an hashref suitable to be turned into a paginator, undef if
1185             there is no need for a paginator.
1186              
1187             Be careful that a defined empty string in the first/last/next/previous
1188             keys is perfectly legit and points to an unfiltered search which will
1189             return all the items, so concatenating it to the prefix is perfectly
1190             fine (e.g. "products/" . ''). When rendering this structure to HTML,
1191             just check if the value is defined, not if it's true.
1192              
1193             The structure looks like this:
1194              
1195             {
1196             first => 'words/bla' || undef,
1197             first_page => 1 || undef,
1198             last => 'words/bla/page/5' || undef,
1199             last_page => 5 || undef,
1200             next => 'words/bla/page/5' || undef,
1201             next_page => 5 || undef
1202             previous => 'words/bla/page/3' || undef,
1203             previous_page => 3 || undef,
1204             pages => [
1205             {
1206             name => 1,
1207             url => 'words/bla/page/1',
1208             },
1209             {
1210             name => 2,
1211             url => 'words/bla/page/2',
1212             },
1213             {
1214             name => 3,
1215             url => 'words/bla/page/3',
1216             },
1217             {
1218             name => 4,
1219             url => 'words/bla/page/4',
1220             current => 1,
1221             },
1222             {
1223             name => 5,
1224             url => 'words/bla/page/5',
1225             },
1226             ]
1227             total_pages => 5
1228             }
1229              
1230             =cut
1231              
1232             sub paginator {
1233 0     0 1   my $self = shift;
1234 0   0       my $page = $self->page || 1;
1235 0           my $page_size = $self->rows;
1236 0           my $page_scope = $self->page_scope;
1237 0           my $total = $self->num_found;
1238 0 0         return undef unless $total;
1239 0           my $total_pages = POSIX::ceil($total / $page_size);
1240 0 0         return undef if $total_pages < 2;
1241              
1242             # compute the scope
1243 0 0         my $start = ($page - $page_scope > 0) ? ($page - $page_scope) : 1;
1244 0 0         my $end = ($page + $page_scope < $total_pages) ? ($page + $page_scope) : $total_pages;
1245              
1246 0           my %pager = (items => []);
1247 0           my $builder = $self->builder_object($self->search_terms, $self->filters);
1248              
1249 0           for (my $count = $start; $count <= $end ; $count++) {
1250             # create the link
1251 0           $builder->page($count);
1252 0           my $url = $builder->url_builder;
1253 0           my $item = {
1254             url => $url,
1255             name => $count,
1256             };
1257 0           my $position = $count - $page;
1258 0 0         if ($position == 0) {
    0          
    0          
1259 0           $item->{current} = 1;
1260             }
1261             elsif ($position == 1) {
1262 0           $pager{next} = $url;
1263 0           $pager{next_page} = $count;
1264             }
1265             elsif ($position == -1) {
1266 0           $pager{previous} = $url;
1267 0           $pager{previous_page} = $count;
1268             }
1269 0           push @{$pager{items}}, $item;
  0            
1270             }
1271 0 0         if ($page != $total_pages) {
1272 0           $builder->page($total_pages);
1273 0           $pager{last} = $builder->url_builder;
1274 0           $pager{last_page} = $total_pages;
1275             }
1276 0 0         if ($page != 1) {
1277 0           $builder->page(1);
1278 0           $pager{first} = $builder->url_builder;
1279 0           $pager{first_page} = 1;
1280             }
1281 0           $pager{total_pages} = $total_pages;
1282 0           return \%pager;
1283             }
1284              
1285             =head2 terms_found
1286              
1287             Returns an hashref suitable to build a widget with the terms used and
1288             the links to toggle them. Return undef if no terms were used in the search.
1289              
1290             The structure looks like this:
1291              
1292             {
1293             reset => '',
1294             terms => [
1295             { term => 'first', url => 'words/second' },
1296             { term => 'second', url => 'words/first' },
1297             ],
1298             }
1299              
1300             See also:
1301              
1302             =over 4
1303              
1304             =item clear_words_link
1305              
1306             Which return the reset link
1307              
1308             =item remove_word_links
1309              
1310             Which returns a list of hashrefs with C<uri> and C<label> for each
1311             word to remove.
1312              
1313             =back
1314              
1315             =cut
1316              
1317             sub terms_found {
1318 0     0 1   my $self = shift;
1319 0           my @terms = @{ $self->search_terms };
  0            
1320 0 0         return unless @terms;
1321 0           my %out = (
1322             reset => $self->builder_object([], $self->filters)->url_builder,
1323             terms => [],
1324             );
1325 0           my @toggled;
1326 0           my $builder = $self->builder_object(\@toggled, $self->filters);
1327 0           foreach my $term (@terms) {
1328 0           @toggled = grep { $_ ne $term } @terms;
  0            
1329 0           $builder->terms(\@toggled);
1330 0           push @{ $out{terms} }, {
  0            
1331             term => $term,
1332             url => $builder->url_builder,
1333             };
1334             }
1335 0           return \%out;
1336            
1337             }
1338              
1339             =head2 version
1340              
1341             Return the version of this module.
1342              
1343             =cut
1344              
1345             sub version {
1346 0     0 1   return $VERSION;
1347             }
1348              
1349              
1350             =head2 breadcrumbs
1351              
1352             Return a list of hashrefs with C<uri> and C<label> suitable to compose
1353             a breadcrumb for the current search.
1354              
1355             If the breadcrumb points to a facet, the facet name is stored in the
1356             C<facet> key.
1357              
1358             =cut
1359              
1360             sub breadcrumbs {
1361 0     0 1   my $self = shift;
1362 0           my $words = $self->search_terms;
1363 0           my $filters = $self->filters;
1364             # always add the root
1365 0           my @pieces;
1366 0           my $current_uri = 'words';
1367 0           foreach my $word (@$words) {
1368 0           $current_uri .= "/$word";
1369 0           push @pieces, {
1370             uri => $current_uri,
1371             label => $word,
1372             };
1373             }
1374 0 0         if (%$filters) {
1375 0           foreach my $facet (@{ $self->facets }) {
  0            
1376 0 0         if (my $terms = $filters->{$facet}) {
1377 0           $current_uri .= "/$facet";
1378 0           foreach my $term (@$terms) {
1379 0           $current_uri .= "/$term";
1380 0           push @pieces, {
1381             uri => $current_uri,
1382             label => $term,
1383             facet => $facet,
1384             };
1385             }
1386             }
1387             }
1388 0           foreach my $facet ($self->_facet_range_names) {
1389 0 0         if (my $terms = $filters->{$facet}) {
1390 0           my ($start, $end) = @$terms;
1391 0   0       $start ||= '*';
1392 0   0       $end ||= '*';
1393 0           $current_uri .= "/$facet/$start/$end";
1394 0           push @pieces, {
1395             uri => $current_uri,
1396             label => "$start-$end",
1397             facet => $facet,
1398             };
1399             }
1400             }
1401             }
1402 0           return @pieces;
1403             }
1404              
1405             sub clear_words_link {
1406 0     0 1   my $self = shift;
1407 0 0         if (my $struct = $self->terms_found) {
1408 0           return $struct->{reset};
1409             }
1410             else {
1411 0           return;
1412             }
1413             }
1414              
1415             sub remove_word_links {
1416 0     0 1   my $self = shift;
1417 0           my @out;
1418 0 0         if (my $struct = $self->terms_found) {
1419 0 0         if (my $terms = $struct->{terms}) {
1420 0           foreach my $term (@$terms) {
1421             push @out, {
1422             uri => $term->{url},
1423             label => $term->{term},
1424 0           };
1425             }
1426             }
1427             }
1428 0           return @out;
1429             }
1430              
1431             sub _term_is_good {
1432 0     0     my ($self, $term) = @_;
1433 0 0 0       if ($term && $term =~ /\w/) {
1434 0 0         if ($self->stop_words->{lc($term)}) {
1435 0           return 0;
1436             }
1437 0 0         if ($self->min_chars > 1) {
1438 0           my $re = "\\w.*" x $self->min_chars;
1439 0 0         if ($term =~ m/$re/) {
1440 0           return 1;
1441             }
1442             else {
1443 0           return 0;
1444             }
1445             }
1446 0           return 1;
1447             }
1448 0           return 0;
1449             }
1450              
1451             # stolen from SQL::Abstract
1452              
1453             sub _build_sort_field {
1454 0     0     my ($self, $arg) = @_;
1455             return $self->_SWITCH_refkind($arg,
1456             {
1457             ARRAYREF => sub {
1458 0     0     map { $self->_build_sort_field($_) } @$arg;
  0            
1459             },
1460             SCALAR => sub {
1461 0     0     return "$arg";
1462             },
1463             UNDEF => sub {
1464 0     0     return;
1465             },
1466             SCALARREF => sub {
1467 0     0     $$arg;
1468             },
1469             HASHREF => sub {
1470 0     0     my ($key, $val, @rest) = %$arg;
1471 0 0         return () unless $key;
1472 0 0 0       if ( @rest or not $key =~ /^-(desc|asc)/i ) {
1473 0           die "hash passed to sorting must have exactly ".
1474             "one key (-desc or -asc)";
1475             }
1476 0           my $direction = $1;
1477 0           my @ret;
1478 0           for my $c ($self->_build_sort_field($val)) {
1479 0           my $query;
1480             $self->_SWITCH_refkind ($c,
1481             {
1482             SCALAR => sub {
1483 0           $query = $c;
1484             },
1485 0           });
1486 0           $query = $query . ' ' . lc($direction);
1487 0           push @ret, $query;
1488             }
1489 0           return @ret;
1490             },
1491 0           });
1492             }
1493              
1494              
1495             sub _refkind {
1496 0     0     my ($self, $data) = @_;
1497 0 0         return 'UNDEF' unless defined $data;
1498             # blessed objects are treated like scalars
1499 0 0         my $ref = (Scalar::Util::blessed $data) ? '' : ref $data;
1500 0 0         return 'SCALAR' unless $ref;
1501 0           my $n_steps = 1;
1502 0           while ($ref eq 'REF') {
1503 0           $data = $$data;
1504 0 0         $ref = (Scalar::Util::blessed $data) ? '' : ref $data;
1505 0 0         $n_steps++ if $ref;
1506             }
1507 0   0       return ($ref||'SCALAR') . ('REF' x $n_steps);
1508             }
1509              
1510             sub _SWITCH_refkind {
1511 0     0     my ($self, $data, $dispatch_table) = @_;
1512 0           my $type = $self->_refkind($data);
1513 0           my $coderef = $dispatch_table->{$self->_refkind($data)};
1514 0 0         die "Unsupported structure $type" unless $coderef;
1515 0           $coderef->();
1516             }
1517              
1518              
1519             =head1 AUTHORS
1520              
1521             Marco Pessotto, C<< <melmothx at gmail.com> >>
1522              
1523             Stefan Hornburg (Racke), C<< <racke at linuxia.de> >>
1524              
1525             =head1 BUGS
1526              
1527             Please report any bugs or feature requests to
1528             L<https://github.com/interchange/Interchange-Search-Solr/issues>.
1529              
1530             =head1 SUPPORT
1531              
1532             You can find documentation for this module with the perldoc command.
1533              
1534             perldoc Interchange::Search::Solr
1535              
1536              
1537             You can also look for information at:
1538              
1539             =over 4
1540              
1541             =item * Github
1542              
1543             L<https://github.com/interchange/Interchange-Search-Solr>
1544              
1545             =item * AnnoCPAN: Annotated CPAN documentation
1546              
1547             L<http://annocpan.org/dist/Interchange-Search-Solr>
1548              
1549             =item * CPAN Ratings
1550              
1551             L<http://cpanratings.perl.org/d/Interchange-Search-Solr>
1552              
1553             =item * META CPAN
1554              
1555             L<https://metacpan.org/pod/Interchange::Search::Solr>
1556              
1557             =back
1558              
1559              
1560             =head1 ACKNOWLEDGEMENTS
1561              
1562             Mohammad S Anwar (GH #14).
1563              
1564             =head1 LICENSE AND COPYRIGHT
1565              
1566             Copyright 2014-2021 Marco Pessotto, Stefan Hornburg (Racke).
1567              
1568             This program is free software; you can redistribute it and/or modify it
1569             under the terms of the the Artistic License (2.0). You may obtain a
1570             copy of the full license at:
1571              
1572             L<http://www.perlfoundation.org/artistic_license_2_0>
1573              
1574             Any use, modification, and distribution of the Standard or Modified
1575             Versions is governed by this Artistic License. By using, modifying or
1576             distributing the Package, you accept this license. Do not use, modify,
1577             or distribute the Package, if you do not accept this license.
1578              
1579             If your Modified Version has been derived from a Modified Version made
1580             by someone other than you, you are nevertheless required to ensure that
1581             your Modified Version complies with the requirements of this license.
1582              
1583             This license does not grant you the right to use any trademark, service
1584             mark, tradename, or logo of the Copyright Holder.
1585              
1586             This license includes the non-exclusive, worldwide, free-of-charge
1587             patent license to make, have made, use, offer to sell, sell, import and
1588             otherwise transfer the Package with respect to any patent claims
1589             licensable by the Copyright Holder that are necessarily infringed by the
1590             Package. If you institute patent litigation (including a cross-claim or
1591             counterclaim) against any party alleging that the Package constitutes
1592             direct or contributory patent infringement, then this Artistic License
1593             to you shall terminate on the date that such litigation is filed.
1594              
1595             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
1596             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
1597             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
1598             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
1599             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
1600             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
1601             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
1602             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1603              
1604              
1605             =cut
1606              
1607             1; # End of Interchange::Search::Solr