Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Added Elastic::Model::Results::Cached

Closes #4
  • Loading branch information...
commit 3973ea4156ef6461423511788f3fb19b37d7ce74 1 parent d3e3786
@clintongormley authored
View
4 lib/Elastic/Model.pm
@@ -318,6 +318,10 @@ C<results> C<-------------> L<Elastic::Model::Results>
=item *
+C<cached_results> C<------> L<Elastic::Model::Results::Cached>
+
+=item *
+
C<scrolled_results> C<----> L<Elastic::Model::Results::Scrolled>
=item *
View
1  lib/Elastic/Model/Meta/Class/Model.pm
@@ -59,6 +59,7 @@ has 'classes' => (
view => 'Elastic::Model::View',
scope => 'Elastic::Model::Scope',
results => 'Elastic::Model::Results',
+ cached_results => 'Elastic::Model::Results::Cached',
scrolled_results => 'Elastic::Model::Results::Scrolled',
result => 'Elastic::Model::Result',
bulk => 'Elastic::Model::Bulk'
View
3  lib/Elastic/Model/Result.pm
@@ -190,7 +190,8 @@ __END__
=head1 DESCRIPTION
L<Elastic::Model::Result> wraps the individual result returned from
-L<Elastic::Model::Results> or L<Elastic::Model::Results::Scrolled>.
+L<Elastic::Model::Results>, L<Elastic::Model::Results::Cached>
+or L<Elastic::Model::Results::Scrolled>.
=head1 ATTRIBUTES
View
128 lib/Elastic/Model/Results/Cached.pm
@@ -0,0 +1,128 @@
+package Elastic::Model::Results::Cached;
+
+use Carp;
+use Moose;
+with 'Elastic::Model::Role::Results';
+use MooseX::Types::Moose qw(Str Num Object HashRef);
+
+use namespace::autoclean;
+
+#===================================
+has 'took' => (
+#===================================
+ isa => Num,
+ is => 'ro',
+ writer => '_set_took',
+);
+
+#===================================
+has 'cache' => (
+#===================================
+ is => 'ro',
+ isa => Object,
+ required => 1,
+);
+
+#===================================
+has 'cache_opts' => (
+#===================================
+ is => 'ro',
+ isa => HashRef,
+);
+
+#===================================
+has 'cache_key' => (
+#===================================
+ is => 'ro',
+ isa => Str,
+ builder => '_build_cache_key',
+ lazy => 1,
+
+);
+
+no Moose;
+
+our $json;
+#===================================
+sub _build_cache_key {
+#===================================
+ my $self = shift;
+ require JSON;
+ $json ||= JSON->new->canonical->utf8;
+ return $json->encode( $self->search );
+}
+
+#===================================
+sub BUILD {
+#===================================
+ my $self = shift;
+
+ my $cache = $self->cache;
+ my $cache_opts = $self->cache_opts || {};
+ my $cache_key = $self->cache_key;
+
+ my $cached;
+ my $result = $cache->get( $cache_key, %$cache_opts )
+ unless delete $cache_opts->{force_set};
+
+ if ( defined $result ) {
+ $cached++;
+ }
+ else {
+ $result = $self->model->search( $self->search );
+ }
+
+ my $hits = $result->{hits};
+ $self->_set_total( $hits->{total} );
+ $self->_set_elements( $hits->{hits} );
+ $self->_set_max_score( $hits->{max_score} || 0 );
+
+ $self->_set_took( $result->{took} || 0 );
+ $self->_set_facets( $result->{facets} || {} );
+
+ $cache->set( $cache_key, $result, $cache_opts )
+ unless $cached;
+}
+
+1;
+
+__END__
+
+# ABSTRACT: A cacheable iterator over bounded/finite search results
+
+=head1 SYNOPSIS
+
+ $cache = CHI->new(...);
+ $view = $model->view
+ ->cache( $cache )
+ ->cache_opts( expires_in => '30 sec' );
+
+ $results = $view->cached_search;
+ $results = $view->cached_search( expires_in => '2 sec', force_set => 1 );
+
+
+=head1 DESCRIPTION
+
+An L<Elastic::Model::Results::Cached> object is returned when you call
+L<Elastic::Model::View/cached_search()>, and behaves exactly the same
+as L<Elastic::Model::Results> except that it will try to retrieve the
+results from the cache, before hitting Elasticsearch.
+
+=head1 ADDITIONAL ATTRIBUTES
+
+=head2 cache
+
+The L<CHI>-compatible cache object from L<Elastic::Model::View/cache>.
+
+=head2 cache_opts
+
+The combination of the default L<Elastic::Model::View/cache_opts> plus
+any additional options passed in to L<Elastic::Model::View/cached_search()>.
+These options are passed to
+L<CHI's get() or set()|'https://metacpan.org/module/CHI#Getting-and-setting>
+methods.
+
+=head2 cache_key
+
+The cache_key is a canonical JSON string representation of the full
+L<Elastic::Model::Role::Results/search> parameter.
View
8 lib/Elastic/Model/Role/Model.pm
@@ -15,8 +15,8 @@ use List::MoreUtils qw(uniq);
use namespace::autoclean;
my @wrapped_classes = qw(
- domain namespace store view scope
- results scrolled_results result bulk
+ domain namespace store view scope
+ results cached_results scrolled_results result bulk
);
for my $class (@wrapped_classes) {
@@ -1021,6 +1021,10 @@ C<results_class> C<-------------> L<Elastic::Model::Results>
=item *
+C<cached_results_class> C<------> L<Elastic::Model::Results::Cached>
+
+=item *
+
C<scrolled_results_class> C<----> L<Elastic::Model::Results::Scrolled>
=item *
View
4 lib/Elastic/Model/Role/Results.pm
@@ -275,8 +275,8 @@ __END__
L<Elastic::Model::Role::Results> adds a number of methods and attributes
to those provided by L<Elastic::Model::Role::Iterator> to better handle
-result sets from ElasticSearch. It is used by L<Elastic::Model::Results>
-and by L<Elastic::Model::Results::Scrolled>.
+result sets from ElasticSearch. It is used by L<Elastic::Model::Results>,
+L<Elastic::Model::Results::Cached> and by L<Elastic::Model::Results::Scrolled>.
See those modules for more complete documentation. This module just
documents the attributes and methods added in L<Elastic::Model::Role::Results>
View
103 lib/Elastic/Model/View.pm
@@ -232,6 +232,20 @@ has 'search_builder' => (
);
#===================================
+has 'cache' => (
+#===================================
+ is => 'rw',
+ isa => Object,
+);
+
+#===================================
+has 'cache_opts' => (
+#===================================
+ is => 'rw',
+ isa => HashRef,
+);
+
+#===================================
sub _build_search_builder { Elastic::Model::SearchBuilder->new }
#===================================
@@ -265,9 +279,9 @@ sub post_filterb {
around [
#===================================
- 'from', 'size', 'timeout', 'track_scores',
- 'search_builder', 'preference', 'min_score', 'explain',
- 'consistency', 'replication'
+ 'from', 'size', 'timeout', 'track_scores',
+ 'search_builder', 'preference', 'min_score', 'explain',
+ 'consistency', 'replication', 'cache'
#===================================
] => sub { _clone_args( \&_scalar_args, @_ ) };
@@ -281,7 +295,7 @@ around [
around [
#===================================
'facets', 'index_boosts', 'script_fields', 'highlighting',
- 'query', 'filter', 'post_filter'
+ 'query', 'filter', 'post_filter', 'cache_opts'
#===================================
] => sub { _clone_args( \&_hash_args, @_ ) };
@@ -375,6 +389,23 @@ sub search {
}
#===================================
+sub cached_search {
+#===================================
+ my $self = shift;
+ my $cache = $self->cache
+ or return $self->search;
+
+ my %cache_opts
+ = ( %{ $self->cache_opts || {} }, @_ == 1 ? %{ $_[0] } : @_ );
+
+ $self->model->cached_results_class->new(
+ search => $self->_build_search,
+ cache => $cache,
+ cache_opts => \%cache_opts
+ )->as_results;
+}
+
+#===================================
sub scroll { shift->_scroll(@_)->as_results }
#===================================
@@ -518,6 +549,14 @@ Efficiently retrieve all posts, unsorted:
do_something_with($result);
);
+Cached results:
+
+ $cache = CHI->new(....);
+ $view = $view->cache( $cache )->cache_opts( expires_in => '2 min');
+
+ $results = $view->queryb( 'perl' )->cached_search();
+ $results = $view->queryb( 'perl' )->cached_search( expires => '30 sec');
+
=head1 DESCRIPTION
L<Elastic::Model::View> is used to query your docs in ElasticSearch.
@@ -562,6 +601,41 @@ with at most L</size> results.
This is useful for returning finite results, ie where you know how many
results you want. For instance: I<"give me the 10 best results">.
+=head2 cached_search()
+
+B<NOTE: Think carefully before you cache data outside of Elasticsearch.
+Elasticsearch already has smart filter caches, which are updated as your data
+changes. Most of the time, you will be better off using those directly,
+instead of an external cache.>
+
+ $results = $view->cache( $cache )->cached_search( %opts );
+
+If a L</cache> attribute has been specified for the current view, then
+L</cached_search()> tries to retrieve the search results from the L</cache>.
+If it fails, then a L</search()> is executed, and the results are stored in
+the L</cache>. An L<Elastic::Model::Results::Cached> object is returned.
+
+Any C<%opts> that are passed in override any default L</cache_opts>, and are
+passed to L<CHI's get() or set()|'https://metacpan.org/module/CHI#Getting-and-setting>
+methods.
+
+ $view = $view->cache_opts( expires_in => '30 sec' );
+
+ $results = $view->cached_search; # 30 seconds
+ $results = $view->cached_search( expires_in => '2 min' ); # 2 minutes
+
+Given the near-real-time nature of Elasticsearch, you sometimes want to
+invalidate a cached result in the near future. For instance, if you have
+cached a list of comments on a blog post, but then you add a new comment,
+you want to invalidate the cached comments list. However, the new
+comment will only become visible to search sometime within the next second, so
+invalidating the cache immediately may or may not be useful.
+
+Use the special argument C<force_set> to bypass the cache C<get()> and to force
+the cached version to be updated, along with a new expiry time:
+
+ $results = $view->cached_search( force_set => 1, expires_in => '2 sec');
+
=head2 scroll()
$scroll_timeout = '1m';
@@ -925,6 +999,27 @@ By default, If you sort on a field other than C<_score>, ElasticSearch
does not return the calculated relevance score for each doc. If
L</track_scores> is true, these scores will be returned regardless.
+=head1 CACHING ATTRIBUTES
+
+Bounded searches (those returned by calling L</search()>) can be stored
+in a L<CHI>-compatible cache.
+
+=head2 cache
+
+ $cache = CHI->new(...);
+ $new_view = $view->cache( $cache );
+
+Stores an instance of a L<CHI>-compatible cache, to be used with
+L</cached_search()>.
+
+=head2 cache_opts
+
+ $new_view = $view->cache_opts( expires_in => '20 sec', ...);
+
+Stores the default options that should be passed to
+L<CHI's get() or set()|'https://metacpan.org/module/CHI#Getting-and-setting>.
+These can be overridden by passing options to L</cached_search()>.
+
=head1 DEBUGGING ATTRIBUTES
=head2 explain
View
91 t/40_view/05_cached_results.t
@@ -0,0 +1,91 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use Test::More 0.96;
+use Test::Exception;
+use Test::Deep;
+
+use lib 't/lib';
+
+our ( $es, $store );
+do 'es.pl';
+
+use_ok 'MyApp' || print 'Bail out';
+
+use Elastic::Model::Role::Store();
+my $store_search = 0;
+{
+
+ package Elastic::Model::Role::Store;
+ use Moose::Role;
+ around 'search' => sub {
+ my $orig = shift;
+ my $self = shift;
+ $store_search++;
+ $self->$orig(@_);
+
+ };
+
+ package main;
+}
+
+my $model = new_ok( 'MyApp', [ es => $es ], 'Model' );
+ok my $ns = $model->namespace('myapp'), 'Got ns';
+
+create_users($model);
+
+isa_ok my $domain = $model->domain('myapp'), 'Elastic::Model::Domain',
+ 'Domain';
+isa_ok my $view = $domain->view, 'Elastic::Model::View', 'View';
+
+isa_ok my $results = $view->cached_search, 'Elastic::Model::Results';
+
+SKIP: {
+ $store_search = 0;
+ skip "CHI not available for testing", 26
+ unless eval { require CHI };
+ isa_ok my $cache = CHI->new( driver => 'Memory', global => 1 ),
+ 'CHI::Driver';
+ ok $view = $view->cache($cache), 'Set cache';
+ ok $view = $view->cache_opts( expire_in => '30 sec' ), 'Set cache opts';
+
+ isa_ok $results = $view->cached_search, 'Elastic::Model::Results::Cached';
+ is $results->total, 196, 'Total is OK';
+ is $store_search, 1, 'From index';
+
+ isa_ok $results = $view->cached_search, 'Elastic::Model::Results::Cached';
+ is $results->total, 196, 'Total is OK';
+ is $store_search, 1, 'From cache';
+
+ ok $model->view->domain('myapp2')->delete, 'Delete users in myapp2';
+ is $domain->view->search->total, '65', 'Total now 65';
+ is $store_search, 2, 'From index';
+
+ isa_ok $results = $view->cached_search, 'Elastic::Model::Results::Cached';
+ is $results->total, 196, 'Total is cached';
+ is $store_search, 2, 'From cache';
+
+ isa_ok $results
+ = $view->cached_search( force_set => 1, expires_in => '2 sec' ),
+ 'Elastic::Model::Results::Cached';
+ is $results->total, 65, 'Total is refreshed';
+ is $store_search, 3, 'From index';
+
+ ok $domain->view->delete, 'Deleted all';
+
+ isa_ok $results = $view->cached_search, 'Elastic::Model::Results::Cached';
+ is $results->total, 65, 'Total is cached';
+ is $store_search, 3, 'From cache';
+
+ sleep 2;
+
+ isa_ok $results = $view->cached_search, 'Elastic::Model::Results::Cached';
+ is $results->total, 0, 'Total is refreshed';
+ is $store_search, 4, 'From cache';
+
+}
+
+done_testing;
+
+__END__
View
0  t/40_view/05_result.t → t/40_view/06_result.t
File renamed without changes
View
0  t/40_view/06_routing.t → t/40_view/07_routing.t
File renamed without changes
Please sign in to comment.
Something went wrong with that request. Please try again.