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

Bring the Users feature to ElasticPress Labs #50

Merged
merged 4 commits into from
Feb 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ jobs:
sudo sysctl -w fs.file-max=262144
sudo sysctl -w vm.max_map_count=262144

- name: Setup Elasticsearch
uses: getong/elasticsearch-action@v1.2
with:
elasticsearch version: '7.5.0'

- name: Set standard 10up cache directories
run: |
composer config -g cache-dir "${{ env.COMPOSER_CACHE }}"
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@
"phpunit/phpunit": "^9",
"10up/wp_mock": "dev-trunk",
"10up/phpcs-composer": "dev-master",
"10up/elasticpress": "*",
"10up/elasticpress": "dev-develop",
"yoast/phpunit-polyfills": "^1.0"
},
"scripts": {
"lint": "phpcs .",
"lint-fix": "phpcbf .",
"test": "phpunit",
"setup-local-tests": "bash bin/install-wp-tests.sh epl_wp_test root password mysql trunk true"
"setup-local-tests": "bash bin/install-wp-tests.sh epl_wp_test root password 127.0.0.1 latest true"
},
"config": {
"allow-plugins": {
Expand Down
18 changes: 10 additions & 8 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

95 changes: 95 additions & 0 deletions includes/classes/Feature/Users.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php
/**
* Users feature
*
* @since 2.1.0
* @package ElasticPressLabs
*/

namespace ElasticPressLabs\Feature;

use ElasticPress\Feature;
use ElasticPress\Indexables;
use ElasticPress\FeatureRequirementsStatus;

/**
* Users feature class
*/
class Users extends Feature {
/**
* Initialize feature setting it's config
*/
public function __construct() {
$this->slug = 'users';

$this->title = esc_html__( 'Users', 'elasticpress' );

$this->summary = __( 'Improve user search relevancy and query performance.', 'elasticpress' );

$this->docs_url = __( 'https://elasticpress.zendesk.com/hc/en-us/articles/360050447492-Configuring-ElasticPress-via-the-Plugin-Dashboard#users', 'elasticpress' );

$this->requires_install_reindex = true;

Indexables::factory()->register( new \ElasticPressLabs\Indexable\User\User(), false );

parent::__construct();
}

/**
* Hook search functionality
*/
public function setup() {
Indexables::factory()->activate( 'user' );

add_action( 'init', [ $this, 'search_setup' ] );
}

/**
* Setup feature on each page load
*/
public function search_setup() {
add_filter( 'ep_elasticpress_enabled', [ $this, 'integrate_search_queries' ], 10, 2 );
}

/**
* Output feature box long text
*/
public function output_feature_box_long() {
?>
<p><?php esc_html_e( 'This feature will empower your website to overcome traditional WordPress user search and query limitations that can present themselves at scale.', 'elasticpress' ); ?></p>
<p><?php esc_html_e( 'Be aware that storing user data may bound you to certain legal obligations depending on your local government regulations.', 'elasticpress' ); ?></p>
<?php
}

/**
* Enable integration on search queries
*
* @param bool $enabled Whether EP is enabled
* @param WP_User_Query $query Current query object.
* @return bool
*/
public function integrate_search_queries( $enabled, $query ) {
if ( ! is_a( $query, 'WP_User_Query' ) ) {
return $enabled;
}

if ( isset( $query->query_vars['ep_integrate'] ) && ! filter_var( $query->query_vars['ep_integrate'], FILTER_VALIDATE_BOOLEAN ) ) {
$enabled = false;
} elseif ( ! empty( $query->query_vars['search'] ) ) {
$enabled = true;
}

return $enabled;
}

/**
* Determine feature reqs status
*
* @return FeatureRequirementsStatus
*/
public function requirements_status() {
$status = new FeatureRequirementsStatus( 1 );

return $status;
}
}
207 changes: 207 additions & 0 deletions includes/classes/Indexable/User/QueryIntegration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
<?php
/**
* Integrate with WP_User_Query
*
* @since 2.1.0
* @package ElasticPressLabs
*/

namespace ElasticPressLabs\Indexable\User;

use ElasticPress\Indexables;
use \WP_User_Query;

if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}

/**
* Query integration class
*/
class QueryIntegration {

/**
* Checks to see if we should be integrating and if so, sets up the appropriate actions and filters.
*
* @param string $indexable_slug Indexable slug. Optional.
*/
public function __construct( $indexable_slug = 'user' ) {
// Ensure that we are currently allowing ElasticPress to override the normal WP_Query
// Indexable->is_full_reindexing() is not available at this point yet, so using the IndexHelper version of it.
if ( \ElasticPress\IndexHelper::factory()->is_full_reindexing( $indexable_slug ) ) {
return;
}

add_filter( 'users_pre_query', [ $this, 'maybe_filter_query' ], 10, 2 );

// Add header
add_action( 'pre_get_users', array( $this, 'action_pre_get_users' ), 5 );
}

/**
* If WP_User_Query meets certain conditions, query results from ES
*
* @param array $results Users array.
* @param WP_User_Query $query Current query.
* @return array
*/
public function maybe_filter_query( $results, WP_User_Query $query ) {
$user_indexable = Indexables::factory()->get( 'user' );

/**
* Filter to skip user query integration
*
* @hook ep_skip_user_query_integration
* @param {bool} $skip True meanas skip query
* @param {WP_User_Query} $query User query
* @since 2.1.0
* @return {boolean} New value
*/
if ( ! $user_indexable->elasticpress_enabled( $query ) || apply_filters( 'ep_skip_user_query_integration', false, $query ) ) {
return $results;
}

/**
* Filter cached user query users
*
* @hook ep_wp_query_search_cached_users
* @param {array} $users Array of users
* @param {WP_User_Query} $query User query
* @since 2.1.0
* @return {array} New users
*/
$new_users = apply_filters( 'ep_wp_query_search_cached_users', null, $query );

if ( null === $new_users ) {
$formatted_args = $user_indexable->format_args( $query->query_vars, $query );

$ep_query = $user_indexable->query_es( $formatted_args, $query->query_vars, null, $query );

if ( false === $ep_query ) {
return $results;
}

/**
* WP_User_Query does not let us set this property:
*
* $query->elasticsearch_success = true;
*/
$query->query_vars['elasticsearch_success'] = true;

$fields = $query->get( 'fields' );
$new_users = [];

if ( in_array( $fields, [ 'all', 'all_with_meta' ], true ) ) {
foreach ( $ep_query['documents'] as $document ) {
$new_users[] = $document['ID'];
}
} elseif ( is_array( $fields ) ) {
// WP_User_Query returns a stdClass.
foreach ( $ep_query['documents'] as $document ) {

$user = new \stdClass();
$user->elasticsearch = true; // Super useful for debugging.

foreach ( $fields as $field ) {
if ( 'id' === $field ) {
$field = 'ID';
}
$user->$field = $document[ $field ];
}

$new_users[] = $user;
}
} elseif ( is_string( $fields ) && ! empty( $fields ) ) {
foreach ( $ep_query['documents'] as $document ) {
$new_users[] = $document[ $fields ];
}
} else {
$new_users = $this->format_hits_as_users( $ep_query['documents'] );
}
}

$query->total_users = is_array( $ep_query['found_documents'] ) ? $ep_query['found_documents']['value'] : $ep_query['found_documents']; // 7.0+ have this as an array rather than int;

return $new_users;
}

/**
* Format the ES hits/results as WP_User objects.
*
* @param array $users The users that should be formatted.
* @return array
*/
protected function format_hits_as_users( $users ) {
$new_users = [];

foreach ( $users as $user_array ) {
$user = new \stdClass();

/**
* Filter arguments inserted into user object after search
*
* @hook ep_search_user_return_args
* @param {array} $args Array of arguments
* @since 2.1.0
* @return {array} New arguments
*/
$user_return_args = apply_filters(
'ep_search_user_return_args',
[
'ID',
'user_login',
'user_nicename',
'user_email',
'user_url',
'user_registered',
'user_status',
'display_name',
'spam',
'deleted',
'terms',
'meta',
]
);

foreach ( $user_return_args as $key ) {
if ( isset( $user_array[ $key ] ) ) {
$user->$key = $user_array[ $key ];
}
}

$user->elasticsearch = true; // Super useful for debugging.

$new_users[] = $user;
}

return $new_users;
}

/**
* Disables cache_results, adds header.
*
* @param WP_User_Query $query User query
*/
public function action_pre_get_users( $query ) {
/**
* Filter to skip user query integration
*
* @hook ep_skip_user_query_integration
* @param {bool} $skip True meanas skip query
* @param {WP_User_Query} $query User query
* @since 2.1.0
* @return {boolean} New value
*/
if ( ! Indexables::factory()->get( 'user' )->elasticpress_enabled( $query ) || apply_filters( 'ep_skip_user_query_integration', false, $query ) ) {
return;
}

if ( ! headers_sent() ) {
/**
* Manually setting a header as $wp_query isn't yet initialized
* when we call: add_filter('wp_headers', 'filter_wp_headers');
*/
header( 'X-ElasticPress-Search: true' );
}
}
}
Loading