File Coverage

blib/lib/ElasticSearchX/Model/Document/Set.pm
Criterion Covered Total %
statement 17 110 15.4
branch 0 50 0.0
condition 0 6 0.0
subroutine 6 21 28.5
pod 10 14 71.4
total 33 201 16.4


line stmt bran cond sub pod time code
1             #
2             # This file is part of ElasticSearchX-Model
3             #
4             # This software is Copyright (c) 2019 by Moritz Onken.
5             #
6             # This is free software, licensed under:
7             #
8             # The (three-clause) BSD License
9             #
10             package ElasticSearchX::Model::Document::Set;
11             $ElasticSearchX::Model::Document::Set::VERSION = '2.0.1';
12             # ABSTRACT: Represents a query used for fetching a set of results
13 2     2   1500 use Moose;
  2         6  
  2         14  
14 2     2   14545 use MooseX::Attribute::Chained;
  2         13967  
  2         59  
15 2     2   964 use MooseX::Attribute::ChainedClone;
  2         12070  
  2         69  
16 2     2   943 use ElasticSearchX::Model::Scroll;
  2         825  
  2         104  
17 2     2   21 use ElasticSearchX::Model::Document::Types qw(:all);
  2         5  
  2         25  
18              
19             has type => ( is => 'ro', required => 1 );
20             has index => ( is => 'ro', required => 1, handles => [qw(es model)] );
21              
22             has query => (
23             isa => 'HashRef',
24             is => 'rw',
25             traits => [qw(ChainedClone)]
26             );
27              
28             has filter => (
29             isa => 'HashRef',
30             is => 'rw',
31             traits => [qw(ChainedClone)]
32             );
33              
34             has [qw(from size)] =>
35             ( isa => 'Int', is => 'rw', traits => [qw(ChainedClone)] );
36              
37             has [qw(fields sort)] => (
38             isa => 'ArrayRef',
39             is => 'rw',
40             traits => [qw(ChainedClone)]
41             );
42              
43             has source => (
44             is => 'rw',
45             traits => [qw(ChainedClone)],
46             default => sub { \1 },
47             );
48              
49 0     0 0 0 sub add_sort { push( @{ $_[0]->sort }, $_[1] ); return $_[0]; }
  0         0  
  0         0  
50              
51 0     0 0 0 sub add_field { push( @{ $_[0]->fields }, $_[1] ); return $_[0]; }
  0         0  
  0         0  
52              
53             has search_type =>
54             ( isa => QueryType, is => 'rw', traits => [qw(ChainedClone)] );
55              
56 0     0 0 0 sub query_type { shift->search_type(@_) }
57              
58             has mixin => ( is => 'ro', isa => 'HashRef', traits => [qw(ChainedClone)] );
59              
60             has inflate =>
61             ( isa => 'Bool', default => 1, is => 'rw', traits => [qw(ChainedClone)] );
62              
63             sub raw {
64 0     0 1 0 shift->inflate(0);
65             }
66              
67             has _refresh =>
68             ( isa => 'Bool', default => 0, is => 'rw', traits => [qw(ChainedClone)] );
69              
70             sub refresh {
71 0     0 1 0 shift->_refresh(1);
72             }
73              
74             sub _build_qs {
75 0     0   0 my ( $self, $qs ) = @_;
76 0   0     0 $qs ||= {};
77              
78             # we only want to set qs if they are not the default
79 0 0       0 $qs->{refresh} = 1 if ( $self->_refresh );
80 0 0       0 $qs->{search_type} = $self->search_type if $self->search_type;
81 0         0 return $qs;
82             }
83              
84             sub _build_query {
85 0     0   0 my $self = shift;
86 0   0     0 my $q = $self->query || { match_all => {} };
87 0 0       0 if ( my $f = $self->filter ) {
88 0         0 $q = { filtered => { query => $q, filter => $f } };
89             }
90             return {
91             query => $q,
92             _source => $self->source,
93             $self->size ? ( size => $self->size ) : (),
94             $self->from ? ( from => $self->from ) : (),
95             $self->fields ? ( fields => $self->fields ) : (),
96             $self->sort ? ( sort => $self->sort ) : (),
97 0 0       0 $self->mixin ? ( %{ $self->mixin } ) : (),
  0 0       0  
    0          
    0          
    0          
98             };
99             }
100              
101             sub put {
102 0     0 1 0 my ( $self, $args, $qs ) = @_;
103 0         0 my $doc = $self->new_document($args);
104 0         0 $doc->put( $self->_build_qs($qs) );
105 0         0 return $doc;
106             }
107              
108             sub new_document {
109 1     1 1 3 my ( $self, $args ) = @_;
110 1         28 return $self->type->name->new( %$args, index => $self->index );
111             }
112              
113             sub inflate_result {
114 0     0 0   my ( $self, $res ) = @_;
115 0           my ( $type, $index ) = ( $res->{_type}, $res->{_index} );
116 0 0         $index = $index ? $self->model->index($index) : $self->index;
117 0 0         $type = $type ? $index->get_type($type) : $self->type;
118 0           my $doc = $type->inflate_result( $index, $res );
119 0 0         unless ( $res->{_source} ) {
120 0           $doc->_loaded_attributes( { map { $_ => 1 } @{ $self->fields } } );
  0            
  0            
121             }
122 0           return $doc;
123             }
124              
125             sub get {
126 0     0 1   my ( $self, $args, $qs ) = @_;
127 0           $qs = $self->_build_qs($qs);
128 0           my ($id);
129 0           my ( $index, $type ) = ( $self->index->name, $self->type->short_name );
130              
131 0 0         if ( !ref $args ) {
    0          
132 0           $id = $args;
133             }
134             elsif ( my $pk = $self->type->get_id_attribute ) {
135 0           my $found = 0;
136             my @fields
137 0           = map { $self->type->find_attribute_by_name($_) } @{ $pk->id };
  0            
  0            
138 0           map { $found++ } grep { exists $args->{ $_->name } } @fields;
  0            
  0            
139 0 0         die "All id fields need to be supplied to get: @fields"
140             unless ( @fields == $found );
141             $id = ElasticSearchX::Model::Util::digest(
142             map {
143 0           $_->has_deflator
144             ? $_->deflate( $self, $args->{ $_->name } )
145 0 0         : $args->{ $_->name }
146             } @fields
147             );
148             }
149              
150             my $res = $self->es->get(
151             index => $index,
152             type => $type,
153             id => $id,
154             $self->fields ? ( fields => $self->fields ) : (),
155             ignore => [404],
156 0 0         %{ $qs || {} },
  0 0          
157             );
158 0 0         return undef unless ($res);
159 0 0         return $self->inflate ? $self->inflate_result($res) : $res;
160             }
161              
162             sub all {
163 0     0 1   my ( $self, $qs ) = @_;
164 0           $qs = $self->_build_qs($qs);
165 0           my ( $index, $type ) = ( $self->index->name, $self->type->short_name );
166             my $res = $self->es->search(
167             {
168             index => $index,
169             type => $type,
170             body => $self->_build_query,
171             version => 1,
172 0 0         %{ $qs || {} },
  0            
173             }
174             );
175 0 0         return $res unless ( $self->inflate );
176 0 0         return () unless ( $res->{hits}->{total} );
177 0           return map { $self->inflate_result($_) } @{ $res->{hits}->{hits} };
  0            
  0            
178             }
179              
180             sub first {
181 0     0 1   my ( $self, $qs ) = @_;
182 0           $qs = $self->_build_qs($qs);
183 0           my @data = $self->size(1)->all($qs);
184 0 0         return undef unless (@data);
185 0 0         return $data[0] if ( $self->inflate );
186 0           return $data[0]->{hits}->{hits}->[0];
187             }
188              
189             sub count {
190 0     0 1   my ( $self, $qs ) = @_;
191 0           $qs = $self->_build_qs($qs);
192 0           my ( $index, $type ) = ( $self->index->name, $self->type->short_name );
193 0           my $query = $self->_build_query;
194 0           delete $query->{_source};
195 0           my $res = $self->es->count(
196             {
197             index => $index,
198             type => $type,
199             body => $query,
200             %$qs,
201             }
202             );
203 0           return $res->{count};
204             }
205              
206             sub delete {
207 0     0 1   my ( $self, $qs ) = @_;
208 0           $qs = $self->_build_qs($qs);
209 0           my $query = $self->_build_query;
210 0           delete $query->{_source};
211              
212 0           my %idx_type = (
213             index => $self->index->name,
214             type => $self->type->short_name
215             );
216              
217 0           my $sc = $self->es->scroll_helper(
218             search_type => 'scan',
219             body => $self->_build_query,
220             size => 500,
221             %idx_type,
222             %$qs,
223             );
224              
225 0           my @ids;
226 0           while ( my @d = $sc->next(500) ) {
227 0           push @ids => map { $_->{_id} } @d;
  0            
228             }
229              
230 0           my $bulk = $self->es->bulk_helper(%idx_type);
231 0           $bulk->delete_ids(@ids);
232 0           return $bulk->flush;
233             }
234              
235             sub scroll {
236 0     0 1   my ( $self, $scroll, $qs ) = @_;
237             return ElasticSearchX::Model::Scroll->new(
238             set => $self,
239             scroll => $scroll || '1m',
240 0 0 0       qs => $self->_build_qs( { version => 1, %{ $qs || {} } } ),
  0            
241             );
242             }
243              
244             __PACKAGE__->meta->make_immutable;
245              
246             __END__
247              
248             =pod
249              
250             =encoding UTF-8
251              
252             =head1 NAME
253              
254             ElasticSearchX::Model::Document::Set - Represents a query used for fetching a set of results
255              
256             =head1 VERSION
257              
258             version 2.0.1
259              
260             =head1 SYNOPSIS
261              
262             my $type = $model->index('default')->type('tweet');
263             my $all = $type->all;
264              
265             my $result = $type->filter( { term => { message => 'hello' } } )->first;
266            
267             my $tweet
268             = $type->get( { user => 'mo', post_date => DateTime->now->iso8601 } );
269              
270              
271             package MyModel::Tweet::Set;
272            
273             use Moose;
274             extends 'ElasticSearchX::Model::Document::Set';
275            
276             sub hello {
277             my $self = shift;
278             return $self->filter({
279             term => { message => 'hello' }
280             });
281             }
282            
283             __PACKAGE__->meta->make_immutable;
284            
285             my $result = $type->hello->first;
286              
287             =head1 DESCRIPTION
288              
289             Whenever a type is accessed by calling L<ElasticSearchX::Model::Index/type>
290             you will receive an instance of this class. The instance can then be used
291             to build new objects (L</new_document>), put new documents in the index
292             (L</put>), do search and so on.
293              
294             =head1 SUBCLASSING
295              
296             If you define a C<::Set> class on top of your document class, this class
297             will be used as set class. This allows you to put most of your business
298             logic in this class.
299              
300             =head1 ATTRIBUTES
301              
302             All attributes have the L<MooseX::Attribute::ChainedClone> trait applied.
303             That means that you can chain calls to these attributes and that a cloned
304             instance is returned whenever you set an attribute. This pattern is inspired
305             by L<DBIx::Class::ResultSet/search>.
306              
307             my $type = $model->index('default')->type('tweet');
308             my @documents = $type->fields(['user'])->all;
309             # $type->fields has not been touched, instead a cloned instance of $type
310             # has been created with "fields" set to ['user']
311              
312             $type = $type->fields(['user']);
313             # this will set $type to a cloned instance of $type with fields
314             # set to ['user']
315             @documents = $type->all;
316             # same result as above
317              
318             =head2 filter
319              
320             Adds a filter to the query. If no L</query> is given, it will automatically
321             build a C<filtered> query, which performs far better.
322              
323             =head2 query
324              
325             =head2 size
326              
327             =head2 from
328              
329             =head2 fields
330              
331             =head2 sort
332              
333             =head2 search_type
334              
335             These attributes are passed directly to the ElasticSearch search request.
336              
337             =head2 mixin
338              
339             The previously mentioned attributes don't cover all of
340             ElasticSearch's options for searching. You can set the
341             L</mixin> attribute to a HashRef which is then merged with
342             the attributes.
343              
344             =head2 inflate
345              
346             Inflate the returned results to the appropriate document
347             object. Defaults to C<1>. You can either use C<< $type->inflate(0) >>
348             to disable this behaviour for extra speed, or you can
349             use the L</raw> convenience method.
350              
351             =head2 index
352              
353             =head2 type
354              
355             =head1 METHODS
356              
357             =head2 all
358              
359             =head2 all( { %qs } )
360              
361             Returns all results as a list, limited by L</size> and L</from>.
362              
363             =head2 scroll
364              
365             =head2 scroll( $scroll, { %qs } )
366              
367             my $iterator = $twitter->type('tweet')->scroll;
368             while ( my $tweet = $iterator->next ) {
369             # do something
370             }
371              
372             Large results should be scrolled thorugh using this iterator.
373             It will return an instance of L<ElasticSearchX::Model::Scroll>.
374             The C<$scroll> parameter is a time value parameter (for example: C<5m>),
375             indicating for how long the nodes that participate in the search will
376             maintain relevant resources in order to continue and support it.
377             C<$scroll> defaults to C<1m>.
378              
379             Scrolling is executed by pulling in L</size> number of documents.
380              
381             =head2 first
382              
383             =head2 first( { %qs } )
384              
385             Returns the first result only. It automatically sets
386             L</size> to C<1> to speed up the retrieval. However,
387             it doesn't touch L</from>. In order to get the second
388             result, you would do:
389              
390             my $second = $type->from(2)->first;
391              
392             =head2 count
393              
394             Returns the number of results.
395              
396             =head2 delete
397              
398             =head2 delete( { %qs } )
399              
400             Delete all documents that match the query. Issues a call to
401             L<ElasticSearch/delete_by_query()>.
402              
403             =head2 get
404              
405             =head2 get( { %qs } )
406              
407             $type->get('fd_ZGWupT2KOxw3w9Q7VSA');
408            
409             $type->get({
410             user => 'mo',
411             post_date => $dt->iso8601,
412             });
413              
414             Get a document by its id from ElasticSearch. You can either
415             pass the id as a string or you can pass a HashRef of
416             the values that make up the id.
417              
418             =head2 put
419              
420             =head2 put( { %qs } )
421              
422             my $doc = $type->put({
423             message => 'hello',
424             });
425              
426             This methods builds a new document using L</new_document> and
427             pushes it to the index. It returns the created document. If
428             no id was supplied, the id will be fetched from ElasticSearch
429             and set on the object in the C<_id> attribute.
430              
431             =head2 new_document
432              
433             my $doc = $type->new_document({
434             message => 'hello',
435             });
436              
437             Builds a new document but doesn't commit it just yet. You
438             can manually commit the new document by calling
439             L<ElasticSearchX::Model::Document/put> on the document
440             object.
441              
442             =head2 raw
443              
444             Don't inflate returned results. This is a convenience
445             method around L</inflate>.
446              
447             =head2 refresh
448              
449             This will add the C<refresh> query parameter to all requests.
450              
451             $users->refresh->put( { nickname => 'mo' } );
452              
453             =head1 AUTHOR
454              
455             Moritz Onken
456              
457             =head1 COPYRIGHT AND LICENSE
458              
459             This software is Copyright (c) 2019 by Moritz Onken.
460              
461             This is free software, licensed under:
462              
463             The (three-clause) BSD License
464              
465             =cut