Permalink
Browse files

Added Elastic::Model::SearchBuilder to handle easy serialization of $…

…docs or $uids into search params
  • Loading branch information...
1 parent 179116e commit 79e9aa3daf37aec1c71c2e7d862d2f6aaffe0d61 @clintongormley committed Aug 2, 2012
@@ -0,0 +1,245 @@
+package Elastic::Model::SearchBuilder;
+
+use strict;
+use warnings;
+use parent 'ElasticSearch::SearchBuilder';
+use Carp;
+
+#===================================
+sub _top_ElasticDocREF {
+#===================================
+ my ( $self, $type, $doc ) = @_;
+ $self->_uid_to_terms( $type, '', $doc->uid );
+}
+
+#===================================
+sub _top_ElasticUIDREF {
+#===================================
+ my ( $self, $type, $uid ) = @_;
+ $self->_uid_to_terms( $type, '', $uid );
+}
+
+#===================================
+sub _hashpair_ElasticDocREF {
+#===================================
+ my ( $self, $type, $k, $v ) = @_;
+ $self->_uid_to_terms( $type, $k, $v->uid );
+}
+
+#===================================
+sub _hashpair_ElasticUIDREF {
+#===================================
+ my ( $self, $type, $k, $v ) = @_;
+ $self->_uid_to_terms( $type, $k, $v );
+}
+
+#===================================
+sub _filter_field_terms {
+#===================================
+ my ( $self, $k, $op, $val ) = @_;
+
+ return $self->_SWITCH_refkind(
+ "Filter field operator -$op",
+ $val,
+ { ElasticDocREF =>
+ sub { $self->_uid_to_terms( 'filter', $k, $val->uid ) },
+ ElasticUIDREF =>
+ sub { $self->_uid_to_terms( 'filter', $k, $val ) },
+ FALLBACK => sub {
+ $self->SUPER::_filter_field_terms( $k, $op, $val );
+ },
+ }
+ );
+}
+
+#===================================
+sub _query_field_text {
+#===================================
+ my $self = shift;
+ my ( $k, $op, $val ) = @_;
+
+ return $self->_SWITCH_refkind(
+ "Query field operator -$op",
+ $val,
+ { ElasticDocREF =>
+ sub { $self->_uid_to_terms( 'query', $k, $val->uid ) },
+ ElasticUIDREF =>
+ sub { $self->_uid_to_terms( 'query', $k, $val ) },
+ FALLBACK => sub {
+ $self->SUPER::_query_field_text( $k, $op, $val );
+ },
+ }
+ );
+}
+
+#===================================
+sub _uid_to_terms {
+#===================================
+ my ( $self, $type, $k, $uid ) = @_;
+ $k = length $k ? $k . '.' : '';
+ my @clauses;
+ for (qw(index type id)) {
+ my $val = $uid->$_ or croak "UID missing ($_)";
+ push @clauses, { term => { "${k}uid.$_" => $val } };
+ }
+ return $type eq 'query'
+ ? { bool => { must => \@clauses } }
+ : { and => \@clauses }
+
+}
+
+#===================================
+sub _refkind {
+#===================================
+ my ( $self, $data ) = @_;
+
+ return 'UNDEF' unless defined $data;
+
+ my $ref
+ = !Scalar::Util::blessed($data) ? ref $data
+ : !$data->can('does') ? ''
+ : $data->does('Elastic::Model::Role::Doc') ? 'ElasticDoc'
+ : $data->isa('Elastic::Model::UID') ? 'ElasticUID'
+ : '';
+
+ return 'SCALAR' unless $ref;
+
+ my $n_steps = 1;
+ while ( $ref eq 'REF' ) {
+ $data = $$data;
+ $ref
+ = !Scalar::Util::blessed($data) ? ref $data
+ : !$data->can('does') ? ''
+ : $data->does('Elastic::Model::Role::Doc') ? 'ElasticDoc'
+ : $data->isa('Elastic::Model::UID') ? 'ElasticUID'
+ : '';
+ $n_steps++ if $ref;
+ }
+
+ return ( $ref || 'SCALAR' ) . ( 'REF' x $n_steps );
+}
+
+1;
+
+# ABSTRACT: An Elastic::Model specific subclass of L<ElasticSearch::SearchBuilder>
+
+=head1 DESCRIPTION
+
+L<Elastic::Model::SearchBuilder> is a sub-class of L<ElasticSearch::SearchBuilder>
+to add automatic handling of L<Elastic::Doc> and L<Elastic::Model::UID>
+values.
+
+This document just explains the functionality that
+L<Elastic::Model::SearchBuilder> adds.
+
+B<For the full SearchBuilder docs, see L<ElasticSearch::SearchBuilder>>.
+
+=head1 THE PROBLEM
+
+Consider this class (where C<MyApp::User> is also an
+L<Elastic::Doc> class):
+
+ package MyApp::Comment;
+
+ use Elastic::Doc;
+
+ has 'user' => (
+ is => 'ro',
+ isa => 'MyApp::User,
+ );
+
+ has 'text' => (
+ is => 'ro',
+ isa => 'Str',
+ );
+
+We can create a comment as follows:
+
+ $domain->create(
+ comment => {
+ text => 'I like Elastic::Model',
+ user => $user,
+ }
+ );
+
+The C<comment> object would be stored in ElasticSearch as something like this:
+
+ {
+ text => "I like Elastic::Model",
+ user => {
+ uid => {
+ index => 'myapp',
+ type => 'user',
+ id => 'abcdefg',
+ },
+ .... any other user fields....
+ }
+ }
+
+In order to search for any comments by user C<$user>, you would need to do this:
+
+ $view->type('comment')
+ ->filterb(
+ 'user.uid.index' => $user->uid->index,
+ 'user.uid.type' => $user->uid->type,
+ 'user.uid.id' => $user->uid->id,
+ )
+ ->search;
+
+=head1 THE SOLUTION
+
+With L<Elastic::Model::SearchBuilder>, you can do it as follows:
+
+ $view->type('comment')
+ ->filterb( user => $user )
+ ->search;
+
+Or with the C<UID>:
+
+ $view->type('comment')
+ ->filterb( user => $user->uid )
+ ->search;
+
+
+=head1 FURTHER EXAMPLES
+
+=head2 Query or Filter
+
+This works for both queries and filters, eg:
+
+ $view->queryb ( user => $user )->search;
+ $view->filterb( user => $user )->search;
+
+=head2 Doc or UID
+
+You can use either the doc/object itself, or an L<Elastic::Model::UID> object:
+
+ $uid = $user->uid;
+ $view->queryb ( user => $uid )->search;
+ $view->filterb( user => $uid )->search;
+
+=head2 Negating queries:
+
+ $view->queryb ( user => { '!=' => $user })->search;
+ $view->filterb( user => { '!=' => $user })->search;
+
+=head2 "IN" queries
+
+ $view->queryb ( user => \@users )->search;
+ $view->filterb( user => \@users )->search;
+
+=head2 "NOT IN" queries
+
+ $view->queryb ( user => { '!=' => \@users })->search;
+ $view->filterb( user => { '!=' => \@users })->search;
+
+=head2 Docs that contain the C<$user> in any field
+
+ $view->queryb ( $user )->search;
+ $view->filterb( $user )->search;
+
+=head2 Docs where C<status> is C<active> and any field contains C<$user>:
+
+ $view->queryb ( '' => $user, status => 'active' )->search
+ $view->filterb( '' => $user, status => 'active' )->search
+
View
@@ -7,7 +7,7 @@ use Elastic::Model::Types qw(
IndexNames ArrayRefOfStr SortArgs
HighlightArgs Consistency Replication);
use MooseX::Types::Moose qw(Str Int HashRef ArrayRef Bool Num Object);
-
+use Elastic::Model::SearchBuilder();
use namespace::autoclean;
#===================================
@@ -215,7 +215,7 @@ has 'search_builder' => (
);
#===================================
-sub _build_search_builder { shift->model->es->builder }
+sub _build_search_builder { Elastic::Model::SearchBuilder->new }
#===================================
#===================================
@@ -624,7 +624,7 @@ one or more types.
Specify the query to run in the native
L<ElasticSearch query DSL|http://www.elasticsearch.org/guide/reference/query-dsl/>
or use C<queryb()> to specify your query with the more Perlish
-L<ElasticSearch::SearchBuilder> query syntax.
+L<Elastic::Model::SearchBuilder> query syntax.
By default, the query will
L<match all docs|http://www.elasticsearch.org/guide/reference/query-dsl/match-all-query.html>.
@@ -643,7 +643,7 @@ L<match all docs|http://www.elasticsearch.org/guide/reference/query-dsl/match-al
You can specify a filter to apply to the query results using either
the native ElasticSearch query DSL or, use C<filterb()> to specify your
-filter with the more Perlish L<ElasticSearch::SearchBuilder> DSL.
+filter with the more Perlish L<Elastic::Model::SearchBuilder> DSL.
If a filter is specified, it will be combined with the L</query>
as a L<filtered query|http://www.elasticsearch.org/guide/reference/query-dsl/filtered-query.html>,
or (if no query is specified) as a
@@ -669,7 +669,7 @@ results would then be limited to just those docs where C<tag == perl>.
You can specify a post_filter using either the native ElasticSearch query DSL or,
use C<post_filterb()> to specify it with the more Perlish
-L<ElasticSearch::SearchBuilder> DSL.
+L<Elastic::Model::SearchBuilder> DSL.
=head2 sort
@@ -896,7 +896,7 @@ can later be retrieved using L<ElasticSearch/index_stats()>.
$builder = $view->search_builder;
If you would like to use a different search builder than the default
-L<ElasticSearch::SearchBuilder> for L</"queryb">, L</"filterb"> or
+L<Elastic::Model::SearchBuilder> for L</"queryb">, L</"filterb"> or
L</post_filterb>, then you can set a value for L</search_builder>.
=head1 DELETE ATTRIBUTES
@@ -0,0 +1,37 @@
+#!perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Differences;
+use Test::Exception;
+
+BEGIN {
+ use_ok('Elastic::Model::SearchBuilder') || print "Bail out!";
+}
+
+diag "";
+diag(
+ "Testing Elastic::Model::SearchBuilder $Elastic::Model::SearchBuilder::VERSION, Perl $], $^X"
+);
+
+my $a;
+ok $a = Elastic::Model::SearchBuilder->new, 'Instantiate';
+ok $a = $a->new, 'Instantiate from ref';
+
+is( scalar $a->filter(), undef, 'Empty ()' );
+is( scalar $a->filter(undef), undef, '(undef)' );
+is( scalar $a->filter( [] ), undef, 'Empty []' );
+is( scalar $a->filter( {} ), undef, 'Empty {}' );
+is( scalar $a->filter( [ [], {} ] ), undef, 'Empty [[]{}]' );
+is( scalar $a->filter( { [], {} } ), undef, 'Empty {[]{}}' );
+is( scalar $a->filter( { -ids => [] } ), undef, 'IDS=>[]' );
+eq_or_diff $a->filter( \{ term => { foo => 1 } } ),
+ { filter => { term => { foo => 1 } } },
+ '\\{}';
+
+throws_ok { $a->filter( 1, 2 ) } qr/Too many params/, '1,2';
+throws_ok { $a->filter( [undef] ) } qr/UNDEF in arrayref/, '[undef]';
+
+done_testing;
Oops, something went wrong.

0 comments on commit 79e9aa3

Please sign in to comment.