package Data::SearchEngine::Solr; use Moose; use Clone qw(clone); use Data::SearchEngine::Paginator; use Data::SearchEngine::Item; use Data::SearchEngine::Results::Spellcheck::Suggestion; use Data::SearchEngine::Solr::Results; use Time::HiRes qw(time); use WebService::Solr; with ( 'Data::SearchEngine', 'Data::SearchEngine::Modifiable' ); our $VERSION = '0.18'; has options => ( is => 'ro', isa => 'HashRef', default => sub { { wt => 'json', fl => '*,score', } } ); has 'url' => ( is => 'ro', isa => 'Str', required => 1 ); has '_solr' => ( is => 'ro', isa => 'WebService::Solr', lazy_build => 1 ); sub _build__solr { my ($self) = @_; return WebService::Solr->new($self->url); } sub add { my ($self, $items, $options) = @_; my @docs; foreach my $item (@{ $items }) { my $doc = WebService::Solr::Document->new; $doc->add_fields(id => $item->id); foreach my $key ($item->keys) { my $val = $item->get_value($key); if(ref($val)) { foreach my $v (@{ $val }) { $doc->add_fields($key => $v); } } else { $doc->add_fields($key => $val); } } push(@docs, $doc); } $self->_solr->add(\@docs, $options); } sub optimize { my ($self) = @_; $self->_solr->optimize; } sub present { warn 'present is not implemented\n'; return 0; } sub remove { my ($self, $query, $id) = @_; $self->_solr->delete({ query => $query, id => $id }); } sub remove_by_id { my ($self, $id) = @_; return $self->_solr->delete_by_id($id); } sub find_by_id { die "Not implemented!"; } sub search { my ($self, $query) = @_; my $options = clone($self->options); $options->{rows} = $query->count; if($query->has_filters) { $options->{fq} = []; foreach my $filter (keys %{ $query->filters }) { push(@{ $options->{fq} }, $query->get_filter($filter)); } } if($query->has_order) { $options->{sort} = $query->order; } if($query->has_debug) { $options->{debug} = $query->debug; } $options->{start} = ($query->page - 1) * $query->count; my $start = time; my $resp = $self->_solr->search($query->query, $options); my $dpager = $resp->pager; # The response will have no pager if there were no results, so we handle # that here. my $pager = Data::SearchEngine::Paginator->new( current_page => defined($dpager) ? $dpager->current_page : 0, entries_per_page => defined($dpager) ? $dpager->entries_per_page : 0, total_entries => defined($dpager) ? $dpager->total_entries : 0 ); my $result = Data::SearchEngine::Solr::Results->new( query => $query, pager => $pager, elapsed => time - $start, raw => $resp ); my $facets = $resp->facet_counts; if(exists($facets->{facet_fields})) { foreach my $facet (keys %{ $facets->{facet_fields} }) { $result->set_facet($facet, $facets->{facet_fields}->{$facet}); } } if(exists($facets->{facet_queries})) { foreach my $facet (keys %{ $facets->{facet_queries} }) { $result->set_facet($facet, $facets->{facet_queries}->{$facet}); } } my $spellcheck = $resp->content->{spellcheck}; if(defined($spellcheck) && exists($spellcheck->{suggestions})) { my $suggs = $spellcheck->{suggestions}; for(my $i = 0; $i < scalar(@{ $suggs }); $i += 2) { my $sword = $suggs->[$i]; my $data = $suggs->[$i + 1]; if($sword eq 'collation') { $result->spell_collation($data); next; } elsif($sword eq 'correctlySpelled') { $result->spelled_correctly($data ? 1 : 0); } # Necessary to skip some of the non-hash pieces, like # correctlySpelled in the extended results next unless ref($data) eq 'HASH'; if(exists($data->{origFreq})) { # Only present in the extended results $result->set_spell_frequency($sword, $data->{origFreq}); } my $suggdata = $data->{suggestion}; if(defined($suggdata) && ref($suggdata) eq 'ARRAY') { foreach my $sugg (@{ $suggdata }) { if(ref($sugg) eq 'HASH') { # This handles "extended results" from the spellcheck # component... $result->set_spell_suggestion($sugg->{word}, Data::SearchEngine::Results::Spellcheck::Suggestion->new( original_word => $sword, word => $sugg->{word}, frequency => $sugg->{freq} ) ); } else { # This handles non-"extended results" from the spellcheck # component... $result->set_spell_suggestion($sugg, Data::SearchEngine::Results::Spellcheck::Suggestion->new( original_word => $sword, word => $sugg, ) ); } } } } } foreach my $doc ($resp->docs) { my %values; foreach my $fn ($doc->field_names) { my @n_values = $doc->values_for($fn); if (scalar(@n_values) > 1) { @{$values{$fn}} = @n_values; } else { $values{$fn} = $n_values[0]; } } $result->add(Data::SearchEngine::Item->new( id => $doc->value_for('id'), values => \%values, )); } return $result; } sub update { my $self = shift; $self->add(@_); } 1; =pod =head1 NAME Data::SearchEngine::Solr =head1 VERSION version 0.20 =head1 SYNOPSIS my $solr = Data::SearchEngine::Solr->new( url => 'http://localhost:8983/solr', options => { fq => 'category:Foo', facets => 'true' } ); my $query = Data::SearchEngine::Query->new( count => 10, page => 1, query => 'ice cream', ); my $results = $solr->search($query); foreach my $item ($results->items) { print $item->get_value('name')."\n"; } =head1 DESCRIPTION Data::SearchEngine::Solr is a L backend for the Solr search server. =head1 NAME Data::SearchEngine::Solr - Data::SearchEngine backend for Solr =head1 SOLR FEATURES =head2 FILTERS This module uses the values from Data::SearchEngine::Query's C to populate the C parameter. Before talking to Solr we iterate over the filters and add the filter's value to C. $query->filters->{'last name watson'} = 'last_name:watson'; Will results in fq=name:watson. Multiple filters will be appended. =head2 FACETS Facets may be enabled thusly: $solr->options->{facets} = 'true'; $solr->options->{facet.field} = 'somefield'; You may also use other C parameters, as defined by Solr. To access facet data, consult the documentation for L and it's C method. =head2 SPELLCHECK Queries may be spellchecked using Solr's spellcheck component. If you supply the correct parameters through the URL or to your URI handler then Data::SearchEngine::Solr will see it in the results and populate the bits from L. Note that some of the features may not work properly unless C is true in your query. =head1 ATTRIBUTES =head2 options HashRef that is passed to L. Please see the above documentation on filters and facets before using this directly. =head2 url The URL at which to contact the Solr instance. =head1 METHODS =head2 add (\@items) Adds a list of Ls to the Solr index. The Items are converted into Ls using the follow means: =over 4 =item C is used as the bonus. =item C is used as the document's id. =item Multiple-value fields are broken up into multiple L objects per L's convention. This is merely a formality, it has no real affect. =back =head2 optimize Calls WebService::Solr's C method. =head2 remove Deletes an item from the index. A straight dispatch to L's C. =head2 remove_by_id Delete a specific document by it's id. =head2 search ($query) Accepts a L and returns a L object containing the data from Solr. =head2 update Alias for C. =head1 AUTHOR Cory G Watson, C<< >> =head1 COPYRIGHT & LICENSE Copyright 2009 - 2011 Cory G Watson. This program is free software; you can redistribute it and/or modify it under the terms of either: the GNU General Public License as published by the Free Software Foundation; or the Artistic License. See http://dev.perl.org/licenses/ for more information. =head1 AUTHOR Cory G Watson =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2011 by Cold Hard Code, LLC. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut __END__