#
# This file is part of ElasticSearchX-Model
#
# This software is Copyright (c) 2013 by Moritz Onken.
#
# This is free software, licensed under:
#
# The (three-clause) BSD License
#
package ElasticSearchX::Model::Document::Set;
{
$ElasticSearchX::Model::Document::Set::VERSION = '0.1.6';
}
# ABSTRACT: Represents a query used for fetching a set of results
use Moose;
use MooseX::Attribute::Chained;
use MooseX::Attribute::ChainedClone;
use ElasticSearchX::Model::Scroll;
use ElasticSearchX::Model::Document::Types qw(:all);
has type => ( is => 'ro', required => 1 );
has index => ( is => 'ro', required => 1, handles => [qw(es model)] );
has query => (
isa => 'HashRef',
is => 'rw',
traits => [qw(ChainedClone)]
);
has filter => (
isa => 'HashRef',
is => 'rw',
traits => [qw(ChainedClone)]
);
has [qw(from size)] =>
( isa => 'Int', is => 'rw', traits => [qw(ChainedClone)] );
has [qw(fields sort)] => (
isa => 'ArrayRef',
is => 'rw',
traits => [qw(ChainedClone)]
);
sub add_sort { push( @{ $_[0]->sort }, $_[1] ); return $_[0]; }
sub add_field { push( @{ $_[0]->fields }, $_[1] ); return $_[0]; }
has query_type =>
( isa => QueryType, is => 'rw', traits => [qw(ChainedClone)] );
has mixin => ( is => 'ro', isa => 'HashRef', traits => [qw(ChainedClone)] );
has inflate =>
( isa => 'Bool', default => 1, is => 'rw', traits => [qw(ChainedClone)] );
sub raw {
shift->inflate(0);
}
has _refresh =>
( isa => 'Bool', default => 0, is => 'rw', traits => [qw(ChainedClone)] );
sub refresh {
shift->_refresh(1);
}
sub _build_qs {
my ( $self, $qs ) = @_;
$qs ||= {};
# we only want to set qs if they are not the default
$qs->{refresh} = 1 if ( $self->_refresh );
$qs->{query_type} = $self->query_type if ( $self->query_type );
return $qs;
}
sub _build_query {
my $self = shift;
my $query
= { query => $self->query ? $self->query : { match_all => {} } };
$query->{filter} = $self->filter if ( $self->filter );
$query = { query => { filtered => $query } }
if ( $self->filter && !$self->query );
my $q = {
%$query,
$self->size ? ( size => $self->size ) : (),
$self->from ? ( from => $self->from ) : (),
$self->fields ? ( fields => $self->fields ) : (),
$self->sort ? ( sort => $self->sort ) : (),
$self->mixin ? ( %{ $self->mixin } ) : (),
};
return $q;
}
sub put {
my ( $self, $args, $qs ) = @_;
my $doc = $self->new_document($args);
$doc->put( $self->_build_qs($qs) );
return $doc;
}
sub new_document {
my ( $self, $args ) = @_;
return $self->type->name->new( %$args, index => $self->index );
}
sub inflate_result {
my ( $self, $res ) = @_;
my ( $type, $index ) = ( $res->{_type}, $res->{_index} );
$index = $index ? $self->model->index($index) : $self->index;
$type = $type ? $index->get_type($type) : $self->type;
my $doc = $type->inflate_result( $index, $res );
unless($res->{_source}) {
$doc->_loaded_attributes({ map { $_ => 1 } @{$self->fields} });
}
return $doc;
}
sub get {
my ( $self, $args, $qs ) = @_;
$qs = $self->_build_qs($qs);
my ($id);
my ( $index, $type ) = ( $self->index->name, $self->type->short_name );
if ( !ref $args ) {
$id = $args;
}
elsif ( my $pk = $self->type->get_id_attribute ) {
my $found = 0;
my @fields
= map { $self->type->find_attribute_by_name($_) } @{ $pk->id };
map { $found++ } grep { exists $args->{ $_->name } } @fields;
die "All id fields need to be supplied to get: @fields"
unless ( @fields == $found );
$id = ElasticSearchX::Model::Util::digest(
map {
$_->has_deflator
? $_->deflate( $self, $args->{ $_->name } )
: $args->{ $_->name }
} @fields
);
}
my $res = $self->es->get(
index => $index,
type => $type,
id => $id,
$self->fields ? ( fields => $self->fields ) : (),
ignore_missing => 1,
%{ $qs || {} },
);
return undef unless ($res);
return $self->inflate ? $self->inflate_result($res) : $res;
}
sub all {
my ( $self, $qs ) = @_;
$qs = $self->_build_qs($qs);
my ( $index, $type ) = ( $self->index->name, $self->type->short_name );
my $res = $self->es->transport->request(
{ method => 'POST',
cmd => "/$index/$type/_search",
data => $self->_build_query,
qs => { version => 1, %{ $qs || {} } },
}
);
return $res unless ( $self->inflate );
return () unless ( $res->{hits}->{total} );
return map { $self->inflate_result($_) } @{ $res->{hits}->{hits} };
}
sub first {
my ( $self, $qs ) = @_;
$qs = $self->_build_qs($qs);
my @data = $self->size(1)->all($qs);
return undef unless (@data);
return $data[0] if ( $self->inflate );
return $data[0]->{hits}->{hits}->[0];
}
sub count {
my ( $self, $qs ) = @_;
$qs = $self->_build_qs($qs);
my ( $index, $type ) = ( $self->index->name, $self->type->short_name );
my $res = $self->es->transport->request(
{ method => 'POST',
cmd => "/$index/$type/_search",
data => { %{ $self->_build_query }, size => 0 },
qs => $qs,
}
);
return $res->{hits}->{total};
}
sub delete {
my ( $self, $qs ) = @_;
$qs = $self->_build_qs($qs);
my $query = $self->_build_query;
return $self->es->delete_by_query(
index => $self->index->name,
type => $self->type->short_name,
query => $query->{filter} ? { filtered => $query } : $query->{query},
%$qs,
);
}
sub scroll {
my ( $self, $scroll, $qs ) = @_;
return ElasticSearchX::Model::Scroll->new(
set => $self,
scroll => $scroll || '1m',
qs => $self->_build_qs( { version => 1, %{ $qs || {} } } ),
);
}
__PACKAGE__->meta->make_immutable;
__END__
=pod
=head1 NAME
ElasticSearchX::Model::Document::Set - Represents a query used for fetching a set of results
=head1 VERSION
version 0.1.6
=head1 SYNOPSIS
my $type = $model->index('default')->type('tweet');
my $all = $type->all;
my $result = $type->filter( { term => { message => 'hello' } } )->first;
my $tweet
= $type->get( { user => 'mo', post_date => DateTime->now->iso8601 } );
package MyModel::Tweet::Set;
use Moose;
extends 'ElasticSearchX::Model::Document::Set';
sub hello {
my $self = shift;
return $self->filter({
term => { message => 'hello' }
});
}
__PACKAGE__->meta->make_immutable;
my $result = $type->hello->first;
=head1 DESCRIPTION
Whenever a type is accessed by calling L<ElasticSearchX::Model::Index/type>
you will receive an instance of this class. The instance can then be used
to build new objects (L</new_document>), put new documents in the index
(L</put>), do search and so on.
=head1 SUBCLASSING
If you define a C<::Set> class on top of your document class, this class
will be used as set class. This allows you to put most of your business
logic in this class.
=head1 ATTRIBUTES
All attributes have the L<MooseX::Attribute::ChainedClone> trait applied.
That means that you can chain calls to these attributes and that a cloned
instance is returned whenever you set an attribute. This pattern is inspired
by L<DBIx::Class::ResultSet/search>.
my $type = $model->index('default')->type('tweet');
my @documents = $type->fields(['user'])->all;
# $type->fields has not been touched, instead a cloned instance of $type
# has been created with "fields" set to ['user']
$type = $type->fields(['user']);
# this will set $type to a cloned instance of $type with fields
# set to ['user']
@documents = $type->all;
# same result as above
=head2 filter
Adds a filter to the query. If no L</query> is given, it will automatically
build a C<filtered> query, which performs far better.
=head2 query
=head2 size
=head2 from
=head2 fields
=head2 sort
=head2 query_type
These attributes are passed directly to the ElasticSearch search request.
=head2 mixin
The previously mentioned attributes don't cover all of
ElasticSearch's options for searching. You can set the
L</mixin> attribute to a HashRef which is then merged with
the attributes.
=head2 inflate
Inflate the returned results to the appropriate document
object. Defaults to C<1>. You can either use C<< $type->inflate(0) >>
to disable this behaviour for extra speed, or you can
use the L</raw> convenience method.
=head2 index
=head2 type
=head1 METHODS
=head2 all
=head2 all( { %qs } )
Returns all results as a list, limited by L</size> and L</from>.
=head2 scroll
=head2 scroll( $scroll, { %qs } )
my $iterator = $twitter->type('tweet')->scroll;
while ( my $tweet = $iterator->next ) {
# do something
}
Large results should be scrolled thorugh using this iterator.
It will return an instance of L<ElasticSearchX::Model::Scroll>.
The C<$scroll> parameter is a time value parameter (for example: C<5m>),
indicating for how long the nodes that participate in the search will
maintain relevant resources in order to continue and support it.
C<$scroll> defaults to C<1m>.
Scrolling is executed by pulling in L</size> number of documents.
=head2 first
=head2 first( { %qs } )
Returns the first result only. It automatically sets
L</size> to C<1> to speed up the retrieval. However,
it doesn't touch L</from>. In order to get the second
result, you would do:
my $second = $type->from(2)->first;
=head2 count
Returns the number of results.
=head2 delete
=head2 delete( { %qs } )
Delete all documents that match the query. Issues a call to
L<ElasticSearch/delete_by_query()>.
=head2 get
=head2 get( { %qs } )
$type->get('fd_ZGWupT2KOxw3w9Q7VSA');
$type->get({
user => 'mo',
post_date => $dt->iso8601,
});
Get a document by its id from ElasticSearch. You can either
pass the id as a string or you can pass a HashRef of
the values that make up the id.
=head2 put
=head2 put( { %qs } )
my $doc = $type->put({
message => 'hello',
});
This methods builds a new document using L</new_document> and
pushes it to the index. It returns the created document. If
no id was supplied, the id will be fetched from ElasticSearch
and set on the object in the C<_id> attribute.
=head2 new_document
my $doc = $type->new_document({
message => 'hello',
});
Builds a new document but doesn't commit it just yet. You
can manually commit the new document by calling
L<ElasticSearchX::Model::Document/put> on the document
object.
=head2 raw
Don't inflate returned results. This is a convenience
method around L</inflate>.
=head2 refresh
This will add the C<refresh> query parameter to all requests.
$users->refresh->put( { nickname => 'mo' } );
=head1 AUTHOR
Moritz Onken
=head1 COPYRIGHT AND LICENSE
This software is Copyright (c) 2013 by Moritz Onken.
This is free software, licensed under:
The (three-clause) BSD License
=cut