Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Post Meta and Generic Term Aggregations #56

Merged
merged 8 commits into from
Oct 17, 2023
5 changes: 4 additions & 1 deletion .phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
<description>PHP_CodeSniffer standard for Elasticsearch Extensions.</description>

<!-- Include Alley Rules -->
<rule ref="Alley-Interactive"/>
<rule ref="Alley-Interactive">
<!-- Turn off spacing after param type because it does some undesirable things with phpstan annotations. -->
<exclude name="Squiz.Commenting.FunctionComment.SpacingAfterParamType" />
</rule>

<!--
Pass some flags to PHPCS:
Expand Down
160 changes: 148 additions & 12 deletions lib/adapters/class-adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
use Elasticsearch_Extensions\Aggregations\CAP_Author;
use Elasticsearch_Extensions\Aggregations\Custom_Date_Range;
use Elasticsearch_Extensions\Aggregations\Post_Date;
use Elasticsearch_Extensions\Aggregations\Post_Meta;
use Elasticsearch_Extensions\Aggregations\Post_Type;
use Elasticsearch_Extensions\Aggregations\Relative_Date;
use Elasticsearch_Extensions\Aggregations\Taxonomy;
use Elasticsearch_Extensions\Aggregations\Term;
use Elasticsearch_Extensions\DSL;
use Elasticsearch_Extensions\Interfaces\Hookable;

Expand Down Expand Up @@ -108,7 +110,21 @@ private function add_aggregation( Aggregation $aggregation ): void {
/**
* Adds a new Co-Authors Plus author aggregation to the list of active aggregations.
*
* @param array $args Optional. Additional arguments to pass to the aggregation.
* @param array{label?: string, order?: 'ASC'|'DESC', orderby?: 'count'|'display_name'|'first_name'|'key'|'label'|'last_name', query_var?: string, relation?: 'AND'|'OR', term_field?: string} $args {
* Optional. Arguments to pass to the adapter's aggregation configuration.
*
* @type string $label Optional. The human-readable name for this aggregation. Defaults to 'Author'.
* @type string $order Optional. How to sort by the `orderby` field. Valid options are 'ASC', 'DESC'.
* Defaults to 'DESC'.
* @type string $orderby Optional. The field to order results by. Valid options are 'count', 'display_name',
* 'first_name', 'key', 'label', 'last_name'. Defaults to 'count'.
* @type string $query_var Optional. The query var to use in the URL. Accepts any URL-safe string. Defaults to
* 'taxonomy_author'.
* @type string $relation Optional. The logical relationship between each selected author when there is more
* than one. Valid options are 'AND', 'OR'. Defaults to 'AND'.
* @type string $term_field Optional. The term field to use in the DSL for this aggregation. Defaults to the
* value of the taxonomy name for the 'author' taxonomy, as looked up in the DSL map.
* }
*/
public function add_cap_author_aggregation( array $args = [] ): void {
$this->add_aggregation( new CAP_Author( $this->dsl, $args ) );
Expand All @@ -117,7 +133,14 @@ public function add_cap_author_aggregation( array $args = [] ): void {
/**
* Adds a new custom date range aggregation to the list of active aggregations.
*
* @param array $args Optional. Additional arguments to pass to the aggregation.
* @param array{label?: string, query_var?: string} $args {
* Optional. Arguments to pass to the adapter's aggregation configuration.
*
* @type string $label Optional. The human-readable name for this aggregation. Defaults to 'Custom Date
* Range'.
* @type string $query_var Optional. The query var to use in the URL. Accepts any URL-safe string. Defaults to
* 'custom_date_range'.
* }
*/
public function add_custom_date_range_aggregation( array $args = [] ): void {
$this->add_aggregation( new Custom_Date_Range( $this->dsl, $args ) );
Expand All @@ -126,16 +149,69 @@ public function add_custom_date_range_aggregation( array $args = [] ): void {
/**
* Adds a new post date aggregation to the list of active aggregations.
*
* @param array $args Optional. Additional arguments to pass to the aggregation.
* @param array{interval?: 'year'|'quarter'|'month'|'week'|'day'|'hour'|'minute', label?: string, order?: 'ASC'|'DESC', orderby?: 'count'|'key'|'label', query_var?: string} $args {
* Optional. Arguments to pass to the adapter's aggregation configuration.
*
* @type string $interval Optional. The unit of time to aggregate results by. Valid options are 'year',
* 'quarter', 'month', 'week', 'day', 'hour', 'minute'. Defaults to 'year'.
* @type string $label Optional. The human-readable name for this aggregation. Defaults to 'Date'.
* @type string $order Optional. How to sort by the `orderby` field. Valid options are 'ASC', 'DESC'.
* Defaults to 'DESC'.
* @type string $orderby Optional. The field to order results by. Valid options are 'count', 'key', 'label'.
* Defaults to 'count'.
* @type string $query_var Optional. The query var to use in the URL. Accepts any URL-safe string. Defaults to
* 'post_date'.
* }
*/
public function add_post_date_aggregation( array $args = [] ): void {
$this->add_aggregation( new Post_Date( $this->dsl, $args ) );
}

/**
* Adds a new post meta aggregation to the list of active aggregations.
*
* @param string $meta_key The meta key to aggregate on.
* @param array{data_type?: string, label?: string, order?: 'ASC'|'DESC', orderby?: 'count'|'key'|'label', query_var?: string, relation?: 'AND'|'OR', term_field?: string} $args {
* Optional. Arguments to pass to the adapter's aggregation configuration.
*
* @type string $data_type Optional. The data type of the meta key, if the meta key is indexed using multiple
* data types (e.g., 'long'). Defaults to empty and uses the "raw" postmeta value.
* @type string $label Optional. The human-readable name for this aggregation. Defaults to a halfhearted
* attempt at turning the meta key into a title case string.
* @type string $order Optional. How to sort by the `orderby` field. Valid options are 'ASC', 'DESC'.
* Defaults to 'DESC'.
* @type string $orderby Optional. The field to order results by. Valid options are 'count', 'key', 'label'.
* Defaults to 'count'.
* @type string $query_var Optional. The query var to use in the URL. Accepts any URL-safe string. Defaults to
* 'post_meta_%s' where %s is the meta key.
* @type string $relation Optional. The logical relationship between each selected meta value when there is
* more than one. Valid options are 'AND', 'OR'. Defaults to 'AND'.
* @type string $term_field Optional. The term field to use in the DSL for this aggregation. Defaults to the
* value of the post meta key, as looked up in the DSL map.
* }
*/
public function add_post_meta_aggregation( string $meta_key, array $args = [] ): void {
$this->add_aggregation( new Post_Meta( $this->dsl, wp_parse_args( $args, [ 'meta_key' => $meta_key ] ) ) );
}

/**
* Adds a new post type aggregation to the list of active aggregations.
*
* @param array $args Optional. Additional arguments to pass to the aggregation.
* @param array{label?: string, order?: 'ASC'|'DESC', orderby?: 'count'|'key'|'label', query_var?: string, relation?: 'AND'|'OR', term_field?: string} $args {
* Optional. Arguments to pass to the adapter's aggregation configuration.
*
* @type string $label Optional. The human-readable name for this aggregation. Defaults to 'Content Type'.
* @type string $order Optional. How to sort by the `orderby` field. Valid options are 'ASC', 'DESC'.
* Defaults to 'DESC'.
* @type string $orderby Optional. The field to order results by. Valid options are 'count', 'key', 'label'.
* Defaults to 'count'.
* @type string $query_var Optional. The query var to use in the URL. Accepts any URL-safe string. Defaults to
* 'post_type'.
* @type string $relation Optional. The logical relationship between each selected author when there is more
* than one. Valid options are 'AND', 'OR'. Defaults to 'AND'.
* @type string $term_field Optional. The term field to use in the DSL for this aggregation. Defaults to the
* 'post_type' field, as looked up in the DSL map.
* }
*/
public function add_post_type_aggregation( array $args = [] ): void {
$this->add_aggregation( new Post_Type( $this->dsl, $args ) );
Expand All @@ -144,7 +220,19 @@ public function add_post_type_aggregation( array $args = [] ): void {
/**
* Adds a new relative date aggregation to the list of active aggregations.
*
* @param array $args Optional. Additional arguments to pass to the aggregation.
* @param array{intervals?: int[], label?: string, order?: 'ASC'|'DESC', orderby?: 'count'|'key'|'label', query_var?: string} $args {
* Optional. Arguments to pass to the adapter's aggregation configuration.
*
* @type int[] $intervals Optional. The number of days prior to the current date to include in each bucket.
* Accepts an array of integers. Defaults to `[7, 30, 90]`.
* @type string $label Optional. The human-readable name for this aggregation. Defaults to 'Relative Date'.
* @type string $order Optional. How to sort by the `orderby` field. Valid options are 'ASC', 'DESC'.
* Defaults to 'DESC'.
* @type string $orderby Optional. The field to order results by. Valid options are 'count', 'key', 'label'.
* Defaults to 'count'.
* @type string $query_var Optional. The query var to use in the URL. Accepts any URL-safe string. Defaults to
* 'relative_date'.
* }
*/
public function add_relative_date_aggregation( array $args = [] ): void {
$this->add_aggregation( new Relative_Date( $this->dsl, $args ) );
Expand All @@ -153,13 +241,61 @@ public function add_relative_date_aggregation( array $args = [] ): void {
/**
* Adds a new taxonomy aggregation to the list of active aggregations.
*
* @param string $taxonomy The taxonomy slug to add (e.g., category, post_tag).
* @param array $args Optional. Additional arguments to pass to the aggregation.
* @param string $taxonomy The taxonomy slug for which to enable an aggregation.
* @param array{label?: string, order?: 'ASC'|'DESC', orderby?: 'count'|'key'|'label', query_var?: string, relation?: 'AND'|'OR', term_field?: string} $args {
* Optional. Arguments to pass to the adapter's aggregation configuration.
*
* @type string $label Optional. The human-readable name for this aggregation. Defaults to the singular
* name of the taxonomy (e.g., 'Category').
* @type string $order Optional. How to sort by the `orderby` field. Valid options are 'ASC', 'DESC'.
* Defaults to 'DESC'.
* @type string $orderby Optional. The field to order results by. Valid options are 'count', 'key', 'label'.
* Defaults to 'count'.
* @type string $query_var Optional. The query var to use in the URL. Accepts any URL-safe string. Defaults to
* 'taxonomy_%s' where %s is the taxonomy slug.
* @type string $relation Optional. The logical relationship between each term when there is more than one.
* Valid options are 'AND', 'OR'. Defaults to 'AND'.
* @type string $term_field Optional. The term field to use in the DSL for this aggregation. Defaults to the
* taxonomy's slug field, as looked up in the DSL map.
* }
*/
public function add_taxonomy_aggregation( string $taxonomy, array $args = [] ): void {
$this->add_aggregation( new Taxonomy( $this->dsl, wp_parse_args( $args, [ 'taxonomy' => $taxonomy ] ) ) );
}

/**
* Adds a new generic term aggregation to the list of active aggregations.
*
* @param string $label The human-readable label for this aggregation.
* @param string $term_field The term field to aggregate on.
* @param string $query_var The query var to use for this aggregation for filters on the front-end.
* @param array{label?: string, order?: 'ASC'|'DESC', orderby?: 'count'|'key'|'label', relation?: 'AND'|'OR'} $args {
* Optional. Arguments to pass to the adapter's aggregation configuration.
*
* @type string $order Optional. How to sort by the `orderby` field. Valid options are 'ASC', 'DESC'.
* Defaults to 'DESC'.
* @type string $orderby Optional. The field to order results by. Valid options are 'count', 'key', 'label'.
* Defaults to 'count'.
* @type string $relation Optional. The logical relationship between each term when there is more than one.
* Valid options are 'AND', 'OR'. Defaults to 'AND'.
* }
*/
public function add_term_aggregation( string $label, string $term_field, string $query_var, array $args = [] ): void {
$this->add_aggregation(
new Term(
$this->dsl,
wp_parse_args(
$args,
[
'label' => $label,
'query_var' => $query_var,
'term_field' => $this->dsl->map_field( $term_field ),
]
)
)
);
}

/**
* Get an aggregation by its label.
*
Expand Down Expand Up @@ -228,7 +364,7 @@ public function get_enable_search_suggestions(): bool {
* between each plugin's Elasticsearch implementation, and use the result
* of this function when initializing the DSL class in the constructor.
*
* @return array The field map.
* @return array<string, string> The field map.
*/
abstract protected function get_field_map(): array;

Expand Down Expand Up @@ -274,9 +410,9 @@ protected function get_restricted_search_suggestions_post_types(): array {
* depending on the context (e.g., main search vs. custom search
* interfaces).
*
* @param array $post_types The default list of post type slugs from the adapter.
* @param string[] $post_types The default list of post type slugs from the adapter.
*
* @return array An array of searchable post type slugs.
* @return string[] An array of searchable post type slugs.
*/
protected function get_searchable_post_types( array $post_types ): array {
/**
Expand Down Expand Up @@ -304,7 +440,7 @@ public function is_show_search_suggestions_in_rest_enabled(): bool {
* Parses aggregations from an aggregations object in an Elasticsearch
* response into the loaded aggregations.
*
* @param array $aggregations Aggregations from the Elasticsearch response.
* @param array<string, mixed> $aggregations Aggregations from the Elasticsearch response.
*/
protected function parse_aggregations( array $aggregations ): void {
foreach ( $aggregations as $aggregation_key => $aggregation ) {
Expand All @@ -318,7 +454,7 @@ protected function parse_aggregations( array $aggregations ): void {
* Suggest posts that match the given search term.
*
* @param string $search Search string.
* @param array $args {
* @param array{subtypes?: string[], page?: int, per_page?: int, include?: int[], exclude?: int[]} $args {
* Optional. An array of arguments.
*
* @type string[] $subtypes Limit suggestions to this subset of all post
Expand Down
2 changes: 1 addition & 1 deletion lib/adapters/class-generic.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Generic extends Adapter {
/**
* Gets the field map for this adapter.
*
* @return array The field map.
* @return array<string, string> The field map.
*/
public function get_field_map(): array {
return [];
Expand Down
43 changes: 43 additions & 0 deletions lib/aggregations/class-post-meta.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php
/**
* Elasticsearch Extensions: Post_Meta Aggregation Class
*
* @package Elasticsearch_Extensions
*/

namespace Elasticsearch_Extensions\Aggregations;

use Elasticsearch_Extensions\DSL;

/**
* Post_Meta aggregation class. Responsible for building the DSL and requests
* for aggregations as well as holding the result of the aggregation after a
* response was received.
*/
class Post_Meta extends Term {

/**
* The postmeta key this aggregation is associated with.
*
* @var string
*/
protected string $meta_key;

/**
* Configure the Post_Meta aggregation.
*
* @param DSL $dsl The DSL object, initialized with the map from the adapter.
* @param array $args Optional. Additional arguments to pass to the aggregation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's important to expand the phpdoc here to document all the arguments the aggregation accepts (comment applies throughout)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, will do

*/
public function __construct( DSL $dsl, array $args ) {
if ( ! empty( $args['meta_key'] ) ) {
$this->label = ucwords( str_replace( [ '-', '_' ], ' ', $args['meta_key'] ) );
$this->meta_key = $args['meta_key'];
$this->query_var = 'post_meta_' . $args['meta_key'];
$this->term_field = $dsl->map_meta_field( $args['meta_key'], $args['data_type'] ?? '' );
unset( $args['meta_key'] );
}

parent::__construct( $dsl, $args );
}
}
Comment on lines +1 to +43
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Post_Meta class extends the Term class and adds a meta_key property. It overrides the constructor to accept a meta_key argument and uses it to set the label, meta_key, query_var, and term_field properties. The meta_key argument is then removed from the $args array before calling the parent constructor. This class seems to be well-structured and follows the principles of object-oriented programming. However, there is no error handling or validation for the meta_key argument. If it's not provided or is an empty string, the class properties will not be properly initialized, which could lead to unexpected behavior or errors later on. Consider adding a check for this and throwing an exception if it's not valid.

	public function __construct( DSL $dsl, array $args ) {
+		if ( empty( $args['meta_key'] ) ) {
+			throw new \InvalidArgumentException( 'The meta_key argument is required and cannot be empty.' );
+		}
		if ( ! empty( $args['meta_key'] ) ) {
			$this->label      = ucwords( str_replace( [ '-', '_' ], ' ', $args['meta_key'] ) );
			$this->meta_key   = $args['meta_key'];
			$this->query_var  = 'post_meta_' . $args['meta_key'];
			$this->term_field = $dsl->map_meta_field( $args['meta_key'], $args['data_type'] ?? '' );
			unset( $args['meta_key'] );
		}

		parent::__construct( $dsl, $args );
	}
}

33 changes: 4 additions & 29 deletions lib/aggregations/class-post-type.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* for aggregations as well as holding the result of the aggregation after a
* response was received.
*/
class Post_Type extends Aggregation {
class Post_Type extends Term {

/**
* Configure the Post Type aggregation.
Expand All @@ -23,23 +23,12 @@ class Post_Type extends Aggregation {
* @param array $args Optional. Additional arguments to pass to the aggregation.
*/
public function __construct( DSL $dsl, array $args ) {
$this->label = __( 'Content Type', 'elasticsearch-extensions' );
$this->query_var = 'post_type';
$this->label = __( 'Content Type', 'elasticsearch-extensions' );
$this->query_var = 'post_type';
$this->term_field = $dsl->map_field( 'post_type' );
parent::__construct( $dsl, $args );
}

/**
* Gets an array of DSL representing each filter for this aggregation that
* should be applied in the query in order to match the requested values.
*
* @return array Array of DSL fragments to apply.
*/
public function filter(): array {
return ! empty( $this->query_values )
? [ $this->dsl->terms( 'post_type', $this->query_values ) ]
: [];
}

/**
* Given a raw array of Elasticsearch aggregation buckets, parses it into
* Bucket objects and saves them in this object.
Expand Down Expand Up @@ -67,18 +56,4 @@ public function parse_buckets( array $buckets ): void {
}
$this->set_buckets( $bucket_objects );
}

/**
* Get DSL for the aggregation to add to the Elasticsearch request object.
* Instructs Elasticsearch to return buckets for this aggregation in the
* response.
*
* @return array DSL fragment.
*/
public function request(): array {
return $this->dsl->aggregate_terms(
$this->query_var,
$this->dsl->map_field( 'post_type' )
);
}
}
Loading
Loading