<?php
namespace FindStr;
use MeiliSearch\Client;
use MeiliSearch\Endpoints\Indexes;
/**
* Indexer class
* @package FindStr
*
* Singleton class
*/
class Indexer {
/**
@var Indexer|null
*/
private static ?Indexer $instance = null;
/**
* The MeiliSearch server url
*
* @var string $meilisearch_server
*/
private string $meilisearch_server;
/**
* The MeiliSearch index
*
* @var string $meilisearch_index
*/
private string $meilisearch_index;
/**
* @var false|mixed|object
*/
private $settings;
/**
* The MeiliSearch Api "master key"
*
* @var string $meilisearch_private_key
*/
private string $meilisearch_private_key;
/**
* The post types to index
* @var array
*/
private array $post_types_to_index;
/**
* The number of posts to index at once.
*
* @var int
*/
private int $post_to_index_batch_size = 121;
/**
* The filterable attributes.
*
* @var array $filterable_attributes
*/
private array $filterable_attributes = array();
/**
* The sortable attributes.
*
* @var array $sortable_attributes
*/
private array $sortable_attributes = array();
/**
* IDs to delete
* @var array
*/
private array $ids_to_delete;
/**
* Post status to index
* @var array
*/
private array $post_status_to_index;
/**
* Initialize the class and set its properties.
*/
private function __construct() {
//get all settings
$server_settings = ( new ServerSettings() )->get();
$settings = ( new Settings() )->get();
$this->settings = $settings;
$this->meilisearch_index = $server_settings->indexUID ?? false;
$this->meilisearch_server = $server_settings->serverUrl ?? false;
$this->meilisearch_private_key = $server_settings->privateKey ?? false;
/**
* This filter is used to set the number of posts to index at once.
*
* @hook findstr_index_batch_size
*
* @param {int} $batch_size
*
* @return {int} $batch_size
*/
$this->post_to_index_batch_size = apply_filters( 'findstr_index_batch_size', $this->post_to_index_batch_size );
$this->post_types_to_index = $settings->postTypesToIndex ?? array();
add_action( 'findstr_index_documents', array( $this, 'batch_index' ), 10, 2 );
add_action( 'findstr_delete_documents', array( $this, 'batch_delete' ) );
add_action( 'save_post', array( $this, 'save_post' ), 888, 2 );
add_action( 'delete_post', array( $this, 'delete' ), 888, 2 );
add_action( 'shutdown', array( $this, 'indexer_shutdown' ) );
add_action( 'findstr_after_update', array( $this, 'update_index_attributes' ) );
return $this;
}
/**
* @return Indexer
*/
public static function get_instance(): Indexer {
if ( ! isset( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
private function get_index(): Indexes {
$http_client = new \GuzzleHttp\Client(
array(
'timeout' => 10,
'verify' => false,
)
);
$client = new Client( $this->meilisearch_server, $this->meilisearch_private_key, $http_client );
return $client->index( $this->meilisearch_index );
}
/**
* Start indexing process
*
* This function will launch indexing processes and delete the obsoletes posts from index.
*
* @return array|bool
*/
public function start_indexer() {
new Log( 'Start indexer...', 'info', array() );
if ( ! $this->is_indexer_done() ) {
new Log( 'Indexer already running, skip', 'info', array() );
return false;
}
//reset last indexed ids
update_option( 'findstr_current_indexed_ids', array(), false );
update_option( 'findstr_current_index_key', current_time( 'mysql' ) ); //key is used to deleted documents from index
$this->set_indexing_status( true );
//early set sortable and filterable attributes
$this->update_index_attributes();
$posts_ids_to_index = $this->get_post_ids_to_index();
if ( empty( $posts_ids_to_index ) ) {
new Log( 'Nothing to index, bye !', 'info', array() );
$this->set_indexing_status( false );
return false;
}
$all_documents_ids = array_merge( array(), ...array_values( $posts_ids_to_index ) );
/**
* Delete documents
*/
$posts_ids_to_delete = $this->get_post_ids_to_delete( $all_documents_ids );
if ( ! empty( $posts_ids_to_delete ) ) {
new Log( sprintf( 'Found %d documents to delete...', count( $posts_ids_to_delete ) ), 'info' );
$batches = array_chunk( $posts_ids_to_delete, $this->post_to_index_batch_size );
foreach ( $batches as $batch ) {
new Log( sprintf( 'Enqueue a batch of %d documents to delete action', count( $batch ) ), 'debug', array() );
as_enqueue_async_action( 'findstr_delete_documents', array( $batch ), 'findstr_delete_batch' );
}
}
/**
* Index documents
*/
foreach ( $posts_ids_to_index as $language => $ids ) {
new Log( sprintf( 'There is %d documents to index for language %s', count( $ids ), $language ), 'info', array() );
$batches = array_chunk( $ids, $this->post_to_index_batch_size );
foreach ( $batches as $batch_ids ) {
new Log( sprintf( 'Enqueue a batch of %d documents to index action', count( $batch_ids ) ), 'debug', array() );
as_enqueue_async_action( 'findstr_index_documents', array( $batch_ids, $language ), 'findstr_index_batch' );
}
}
/**
* Update last indexed and deleted ids
*/
update_option( 'findstr_last_indexed_ids', $all_documents_ids, false );
update_option( 'findstr_last_deleted_ids', $posts_ids_to_delete, false );
$status = $this->status();
$status['pending_documents'] = count( $all_documents_ids );
$this->write_status_to_file( $status );
//remove action shutdown, to avoid multiple status writing in the same request
remove_action( 'shutdown', array( $this, 'indexer_shutdown' ) );
return $status;
}
/**
* Attempt to index an array of documents
*
* @param array $ids
*
* @return void
*/
public function index( array $ids ) {
/**
* This constant is meant to be used by front-end, if anyone wants to know if findstr is indexing.
*/
if ( ! defined( 'DOING_FINDSTR_INDEX' ) ) {
define( 'DOING_FINDSTR_INDEX', true );
}
$documents = array();
new Log( 'Index documents', 'info', array( $ids, get_locale() ) );
$start_time = microtime( true );
$document_key = get_option( 'findstr_current_index_key', 'false' );
foreach ( $ids as $post_id ) {
if ( true !== apply_filters( 'findstr_document_should_be_indexed', true, $post_id ) ) {
new Log( 'Document should be deleted', 'info', array( $post_id ) );
$this->delete( $post_id );
continue;
}
try {
$document = new Document( $post_id );
$document_to_index = $document->to_index;
$document_to_index['findstr_key'] = $document_key;
$documents[] = $document_to_index;
} catch ( \Exception $e ) {
new Log(
'Document error',
'error',
array(
'message' => $e->getMessage(),
'post ID' => $post_id,
)
);
continue;
}
}
$end_time = microtime( true );
$execution_time = $end_time - $start_time;
new Log( sprintf( 'Documents rendered in %s seconds', $execution_time ), 'debug', array() );
try {
$this->get_index()->addDocuments( $documents );
$end_time = microtime( true );
$execution_time = $end_time - $start_time;
new Log( sprintf( 'Sent %d documents to indexer after %s seconds', count( $documents ), $execution_time ), 'debug', array() );
} catch ( \Exception $e ) {
//todo: do something with non-indexed documents ?
new Log(
'Index error',
'error',
array(
'message' => $e->getMessage(),
'ids' => $ids,
)
);
}
//update last indexed ids
$indexed_ids = get_option( 'findstr_current_indexed_ids', array() );
update_option( 'findstr_current_indexed_ids', array_merge( $indexed_ids, $ids ), false );
}
public function get_post_types_to_index(): array {
$this->post_types_to_index = ( new Settings() )->get_post_types_to_index();
return $this->post_types_to_index;
}
public function get_post_status_to_index(): array {
/**
* This filter is used to set the post status to index.
*
* @hook findstr_index_post_status
*
* @param {array} $post_status_to_index, default is array( 'publish' )
*
* @return {array} $post_status_to_index, use WP_Post Status
*/
$this->post_status_to_index = apply_filters( 'findstr_index_post_status', array( 'publish' ) );
return $this->post_status_to_index;
}
public function get_ids_to_index_query( $args = array() ): array {
/**
* This query is used to get all posts to index.
*
* @hook findstr_index_args
*
* @param {array} $query
*
* @return {array} WP_Query arguments
*/
$default_query = apply_filters(
'findstr_index_args',
array(
'posts_per_page' => - 1,
'post_status' => $this->get_post_status_to_index(),
'post_type' => $this->get_post_types_to_index(),
'suppress_filters' => true,
'fields' => 'ids',
'orderby' => 'ID',
'cache_results' => false,
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
)
);
return Helpers::merge_args( $args, $default_query );
}
/**
* Get posts to index, by language
*
*
* @return array
*/
public function get_post_ids_to_index(): array {
$query = $this->get_ids_to_index_query();
$query = new \WP_Query( $query );
/**
* Use this filter to modify the ids of posts to index.
*
*
* @hook findstr_index_ids
*
* @param {array} $ids of posts to index
*
* @return {array} $ids of posts to index
*
*/
$post_ids = apply_filters( 'findstr_index_ids', $query->posts );
$result = array();
foreach ( $post_ids as $post_id ) {
$language_code = Helpers::get_language_code_by_post_id( $post_id );
if ( is_array( $language_code ) ) {
$language_code = current( $language_code );
}
$result[ $language_code ][] = $post_id;
}
/**
* Filter the post ids to index, sorted by languages.
*
*
* @hook findstr_index_ids_sorted_by_languages
*
* @param {array} languages and their posts ids
*
* @return {array} languages and their posts ids
*
*/
return apply_filters( 'findstr_index_ids_sorted_by_languages', $result );
}
public function get_post_ids_to_index_count(): int {
$query = $this->get_ids_to_index_query(
array(
'posts_per_page' => 1,
'no_found_rows' => false,
'cache_results' => true,
)
);
$query = new \WP_Query( $query );
return (int) apply_filters( 'findstr_indexable_ids_count', $query->found_posts, $query );
}
/**
* Save post actions
*
* @param $post_id
* @param $post
* @return void
*/
public function save_post( $post_id, $post ) {
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
new Log( 'Autosave or revision, skip indexing', 'debug', array( 'ID', $post_id ) );
return;
}
//check if post type is in the list of post types to index
if ( ! in_array( $post->post_type, $this->get_post_types_to_index(), true ) ) {
new Log( 'Post type is not in the list of post types to index', 'debug', array( 'ID', $post_id ) );
return;
}
/**
* This filter is used to disable indexing on save action.
* Useful in case of import or automated posts creation.
*
* @hook findstr_disable_indexing_on_save
*
* @param {bool} $disable_indexing_on_save
* @param {int} $post_id
* @param {WP_Post} $post
*
* @return {bool} $disable_indexing_on_save
*/
$disable_indexing_on_save = apply_filters( 'findstr_disable_indexing_on_save', false, $post_id, $post );
if ( true === $disable_indexing_on_save ) {
new Log( 'Indexing is disabled on save action', 'info' );
return;
}
/**
* This filter is used to determine if a document should be indexed.
*
* @hook findstr_document_should_be_indexed
*
* @param {bool} $should_index
* @param {int} $post_id
*
* @return {bool} $should_index
*/
$should_index = apply_filters( 'findstr_document_should_be_indexed', true, $post_id );
if ( true !== $should_index ) {
new Log( 'Document should not be indexed', 'info', array( 'ID', $post_id ) );
return;
}
$language = Helpers::get_language_code_by_post_id( $post_id );
if ( is_array( $language ) ) {
$language = current( $language );
}
//check if action is already scheduled
if ( as_get_scheduled_actions(
array(
'hook' => 'findstr_index_documents',
'args' => array(
array( $post_id ),
$language,
),
'group' => 'findstr_index_batch',
'status' => \ActionScheduler_Store::STATUS_PENDING,
)
) ) {
new Log( 'Document is already scheduled for indexing', 'info', array( 'ID', $post_id ) );
return;
}
//check if post status is in the list of post status to index
if ( true === $should_index && in_array( $post->post_status, $this->get_post_status_to_index(), true ) ) {
new Log( sprintf( 'Save post %d in lang %s', $post_id, $language ), 'debug' );
as_schedule_single_action( time() + 30, 'findstr_index_documents', array( array( $post_id ), $language ), 'findstr_index_on_save' );
} else {
new Log( sprintf( 'Remove post %d from index', $post_id ), 'debug' );
$this->delete( $post_id );
}
}
public function write_status_to_file( $status = null ): void {
new Log( 'Writing status file', 'debug', array( 'status' => $status ) );
if ( empty( $status ) ) {
$status = $this->status();
}
$path = Helpers::get_index_status_path();
Helpers::file_put_content( $path, wp_json_encode( $status ) );
}
/**
* Attempt to index a single document
*
* @param $id
* @return void
*/
public function delete( $id, $post = null ) {
try {
$this->get_index()->deleteDocument( $id );
new Log( sprintf( 'Delete document %s', $id ), 'debug', array( 'post' => $post ) );
} catch ( \Exception $e ) {
new Log( 'Delete error', 'error', array( 'message' => $e->getMessage() ) );
}
}
/**
* Attempt to delete a batch of documents
*
* @param array $ids
* @return void
*/
public function batch_delete( array $ids = array() ) {
new Log( sprintf( 'Deleting %d documents.', count( $ids ) ), 'debug', array( 'ids' => $ids ) );
try {
$this->get_index()->deleteDocuments( $ids );
} catch ( \Exception $e ) {
new Log( 'Batch delete error', 'error', array( 'message' => $e->getMessage() ) );
}
}
/**
* Set indexing status
*
* @param bool $indexing
*
* @return void
*/
private function set_indexing_status( bool $indexing ) {
global $is_indexing;
$is_indexing = $indexing;
new Log( 'Set indexing status to ' . ( $indexing ? 'true' : 'false' ), 'info', array() );
global $wpdb;
$wpdb->query(
$wpdb->prepare(
"INSERT into $wpdb->options ( option_name, option_value ) value ( %s, %s ) ON DUPLICATE KEY UPDATE option_value = %s",
'findstr_is_indexing',
$indexing,
$indexing
)
);
}
/**
* Check if server is indexing
* @return bool
*/
public function is_indexing(): bool {
global $is_indexing;
if ( isset( $is_indexing ) ) {
new Log( 'Get is_indexing from global', 'debug', array( 'is_indexing' => (bool) $is_indexing ) );
return (bool) $is_indexing;
}
global $wpdb;
$is_indexing = $wpdb->get_var( "SELECT option_value FROM $wpdb->options WHERE option_name = 'findstr_is_indexing'" );
return (bool) $is_indexing;
}
/**
* Determine witch documents should be deleted.
*
* This is done by comparing the last indexed ids with the new ones.
*
* @param $posts_ids_to_index
*
* @return array
*/
public function get_post_ids_to_delete( $posts_ids_to_index ): array {
$last_indexed_ids = get_option( 'findstr_last_indexed_ids' );
if ( empty( $last_indexed_ids ) ) {
return array();
}
$this->ids_to_delete = array_values( array_diff( $last_indexed_ids, $posts_ids_to_index ) );
return $this->ids_to_delete;
}
public function status(): array {
$stats = array(
'numberOfDocuments' => 0,
'isIndexing' => false,
);
try {
$index = $this->get_index();
$stats = $index->stats();
$documents = $index->getDocuments();
$sever_connected = true;
} catch ( \Exception $e ) {
new Log( 'Index not found', 'error', array( 'message' => $e->getMessage() ) );
$sever_connected = false;
}
$findstr_last_indexed_ids = count( get_option( 'findstr_last_indexed_ids', array() ) );
$total_documents_to_index = $findstr_last_indexed_ids;
$documents_in_queue = $this->count_remaining_posts();
$total_documents_to_delete = count( get_option( 'findstr_last_deleted_ids', array() ) );
$total_documents_indexed = $total_documents_to_index - $documents_in_queue;
$percentage = round( $total_documents_to_index ? ( ( $total_documents_indexed ) / $total_documents_to_index ) * 100 : 100, 2 );
$percentage = min( $percentage, 100 );
return array(
'total_documents_to_index' => $total_documents_to_index,
'findstr_last_indexed_ids' => $findstr_last_indexed_ids,
'total_documents_to_delete' => $total_documents_to_delete,
'total_documents_indexed' => isset( $documents ) ? $documents->getTotal() : 0,
'is_indexing' => $this->is_indexing(),
'server_is_indexing' => ! empty( $stats['isIndexing'] ),
'server_is_connected' => $sever_connected,
'pending_documents' => max( $documents_in_queue, 0 ),
'documents_in_queue' => max( $documents_in_queue, 0 ),
'progression' => max( $percentage, 0 ),
'last_status_update' => time(),
'indexing_start_time' => get_option( 'findstr_current_index_key', false ),
);
}
/**
* Index documents in batch.
*
* This is because Meilisearch can handle a lot of queries !
*
* @param array $ids
* @param string $lang
*
* @return void
*/
public function batch_index( array $ids = array(), string $lang = '' ) {
$start_time = microtime( true );
$this->set_indexing_status( true );
if ( empty( $lang ) ) {
_doing_it_wrong( __FUNCTION__, 'The language parameter is required', '0.5.0' );
new Log( 'Batch index error', 'error', array( 'message' => 'The language parameter is required in batch_index function' ) );
}
if ( empty( $ids ) ) {
new Log(
'Batch index error',
'error',
array(
'message' => 'The ids parameter is empty',
)
);
return;
}
//check if $ids is an array of integers
$array_of_ids = array_filter( $ids, 'is_numeric' ) === $ids;
if ( ! $array_of_ids ) {
new Log(
'Batch index error',
'error',
array(
'message' => 'The ids parameter must be an array of integers',
)
);
return;
}
new Log( sprintf( 'Batch index of %s documents received, language %s', count( $ids ), $lang ), 'debug', array() );
/**
* Use this filter to index documents through http call.
* This is useful when you have some translations in constant and you want to index them in the right language.
*
* @hook findstr_indexer_use_network
*
* @returns {bool} $use_network default to false
*/
$use_network = apply_filters( 'findstr_indexer_use_network', false );
if ( false !== $use_network ) {
//post ids to index through http call, to ensure that we use the right language
$path = get_rest_url( null, 'findstr/v1/index?findstr-language=' . $lang . '&lang=' . $lang );
new Log( sprintf( 'Call api %s', $path ), 'debug', array() );
//create a hash and set it in transient
$nonce = 'findstr_' . wp_hash( time() . '_findstr_action' );
set_transient( $nonce, $nonce, 10 * MINUTE_IN_SECONDS );
$call = wp_remote_post(
$path,
array(
'body' => array(
'ids' => $ids,
'nonce' => $nonce,
),
'nonblocking' => true,
'sslverify' => false,
'timeout' => FINDSTR_BATCH_CALL_TIMEOUT,
)
);
if ( is_wp_error( $call ) ) {
new Log( 'Batch index error', 'error', array( 'message' => $call->get_error_message() ) );
}
} else {
//switch to the right language
new Log( sprintf( 'Switch to language %s', $lang ), 'debug', array() );
Helpers::switch_language( $lang );
$this->index( $ids );
$this->write_status_to_file();
}
$end_time = microtime( true );
$execution_time = round( $end_time - $start_time, 2 );
new Log( sprintf( 'Batch index done in %s seconds', $execution_time ), 'debug', array() );
}
/**
* Get sortable attributes.
* This is used to determine which attributes can be used to sort the results.
*
* @return array
*/
public function get_sortable_attributes(): array {
$indexable_fields = (array) ( new SettingsIndexableFields() )->get();
//get all sortable fields based on indexable fields properties
$sortable_attributes = array_filter(
$indexable_fields,
function ( $field ) {
return isset( $field->sortable ) && $field->sortable;
}
);
//keep key only
$sortable_attributes = array_keys( $sortable_attributes );
foreach ( $sortable_attributes as $sortable_attribute ) {
$sortable_attribute = explode( '/', $sortable_attribute );
$this->sortable_attributes[] = end( $sortable_attribute );
}
return array_values(
array_unique(
/**
* Use this filter to modify the sortable attributes.
*
* @hook findstr_sortable_attributes
*
* @param {array} $sortable_attributes
*
* @return {array} $sortable_attributes
*/
apply_filters(
'findstr_sortable_attributes',
array_merge(
array_unique( $this->sortable_attributes ),
array(
'post_title',
'post_date',
'menu_order',
'sticky',
)
)
)
)
);
}
/**
* Get filterable attributes.
* This is used to determine which attributes can be used to filter the results.
*
* @return array
*/
public function get_filterable_attributes(): array {
$indexable_fields = (array) ( new SettingsIndexableFields() )->get();
$filterable_attributes = array_filter(
$indexable_fields,
function ( $field ) {
return isset( $field->filterable ) && $field->filterable;
}
);
$filterable_attributes = array_keys( $filterable_attributes );
foreach ( $filterable_attributes as $filterable_attribute ) {
$filterable_attribute = explode( '/', $filterable_attribute );
$this->filterable_attributes[] = end( $filterable_attribute );
/**
* Use this filter to modify each filterable attribute before adding it.
*
* @hook findstr_each_filterable_attribute
*
* @param string $filterable_attribute The filterable attribute being processed.
*
* @return string The modified filterable attribute.
*/
$this->filterable_attributes = apply_filters( 'findstr_each_filterable_attribute', $this->filterable_attributes, $filterable_attribute );
}
return array_values(
array_unique(
/**
* Use this filter to modify the filterable attributes.
*
* @hook findstr_filterable_attributes
*
* @param {array} $filterable_attributes
*
* @return {array} $filterable_attributes
*/
apply_filters(
'findstr_filterable_attributes',
array_merge(
array_unique( $this->filterable_attributes ),
array(
'language',
'post_type',
'post_author',
'findstr_key',
)
)
)
)
);
}
public function get_ranking_rules() {
/**
* Use this filter to modify the Meilisearch ranking rules.
* @see https://www.meilisearch.com/docs/learn/core_concepts/relevancy#ranking-rules
*
* @hook findstr_ranking_rules
*
* @param {array} $ranking_rules
*
* @return {array} $ranking_rules
*/
return apply_filters(
'findstr_ranking_rules',
array(
'words',
'typo',
'proximity',
'attribute',
'weight:desc',
'sort',
'exactness',
)
);
}
/**
* This function checks if the indexing process is done.
*
* @return bool
*/
public function is_indexer_done(): bool {
if ( ! as_get_scheduled_actions(
array(
'hook' => 'findstr_index_documents',
'group' => 'findstr_index_batch',
'status' => \ActionScheduler_Store::STATUS_PENDING,
)
) ) {
return true;
}
return false;
}
public function count_remaining_posts(): int {
$tasks = as_get_scheduled_actions(
array(
'hook' => 'findstr_index_documents',
'group' => 'findstr_index_batch',
'per_page' => 1000,
'status' => array( \ActionScheduler_Store::STATUS_PENDING, \ActionScheduler_Store::STATUS_RUNNING ),
)
);
$count = 0;
foreach ( $tasks as $task ) {
//set type of $task to ActionScheduler_Action
$args = $task->get_args();
$count += (int) count( $args[0] );
}
return $count;
}
/**
* Shutdown function
*
* @return void
*/
public function indexer_shutdown() {
//check if we are indexing
if ( empty( $this->is_indexing() ) ) {
return;
}
//check when the indexing process was started
$indexing_start_time = get_option( 'findstr_current_index_key' );
if ( false === $indexing_start_time ) {
return;
}
//if indexing was started less than 30 seconds ago, we are not indexing
if ( ( strtotime( current_time( 'mysql' ) ) - strtotime( $indexing_start_time ) ) < 30 ) {
new Log( 'Indexing was started less than 30s ago... (shutdown)', 'info', array() );
return;
}
$status = $this->status();
$this->write_status_to_file( $status );
//attempt to finish the indexing process
if ( $this->is_indexer_done() && $status['is_indexing'] && (int) $status['pending_documents'] <= 0 ) {
new Log( 'Indexing is done, but work is not finished yet...', 'info', array() );
$this->after_index_actions();
}
/**
* check is indexing is stuck
*/
$timeout = apply_filters( 'findstr_indexing_timeout', 30 * MINUTE_IN_SECONDS );
$index_started_at = $status['indexing_start_time'];
if ( false === $index_started_at ) {
return;
}
$index_started_at_timestamp = strtotime( $index_started_at );
if ( $index_started_at_timestamp && ( strtotime( current_time( 'mysql' ) ) - $index_started_at_timestamp ) > $timeout ) {
new Log( 'Indexing got stuck after ' . $timeout . ' seconds. Abort...', 'error', array() );
$this->set_indexing_status( false );
}
}
public function stop_indexing(): void {
new Log( 'Stop indexing...', 'info', array() );
$status = $this->status();
$status['is_indexing'] = false;
$status['pending_documents'] = 0;
$status['progression'] = 100;
$this->write_status_to_file( $status );
//remove all pending actions
as_unschedule_all_actions( 'findstr_index_documents' );
$this->set_indexing_status( false );
}
/**
* function to clean index from deleted documents
*/
public function clean_deleted_posts() {
new Log( 'Clean index', 'info', array() );
$index = $this->get_index();
$post_type_to_index = implode( ', ', $this->get_post_types_to_index() );
$document_key = get_option( 'findstr_current_index_key' );
$remaining_id = $index->search(
'',
array(
'attributesToRetrieve' => array( 'ID' ),
'filter' => "findstr_key EXISTS AND findstr_key != '$document_key' AND post_type IN [$post_type_to_index]",
)
);
$ids = array_map(
function ( $item ) {
return $item['ID'];
},
$remaining_id->getHits()
);
if ( ! empty( $ids ) ) {
$this->batch_delete( $ids );
}
}
/**
* Actions to run at the end of the indexing process
*
* @return void
*/
public function after_index_actions() {
new Log( 'Fire post index actions...', 'debug', array() );
$index = $this->get_index();
/**
* This action is fired after the indexing process is done.
*
* @hook findstr_after_index
*
* @param {array} $args with key index, the meilisearch index
*
*/
do_action( 'findstr_after_index', array( 'index' => $index ) );
//Sortable attributes
new Log( 'Set sortable attributes...', 'debug', $this->get_sortable_attributes() );
$index->updateSortableAttributes( $this->get_sortable_attributes() );
//Filterable attributes
new Log( 'Set filterable attributes...', 'debug', $this->get_filterable_attributes() );
$index->updateFilterableAttributes( $this->get_filterable_attributes() );
//set ranking rules
new Log( 'Set ranking rules...', 'debug', $this->get_ranking_rules() );
$index->updateRankingRules( $this->get_ranking_rules() );
//cleanup transients
$this->cleanup_transients();
//update status
$this->set_indexing_status( false );
$this->write_status_to_file();
//check how log it took to index
$indexing_start_time = get_option( 'findstr_current_index_key' );
$indexing_start_time = strtotime( $indexing_start_time );
$indexing_end_time = strtotime( current_time( 'mysql' ) );
$indexing_duration = $indexing_end_time - $indexing_start_time;
new Log(
sprintf(
'Indexing took %d m %d s',
(int) floor( $indexing_duration / 60 ) % 60,
(int) floor( $indexing_duration % 60 )
),
'info',
array()
);
new Log( '... indexing if finish, bye !', 'info', array() );
}
/**
* This function is called after the plugin is updated.
*
* @return void
*/
public function update_index_attributes(): void {
new Log( 'Update index attributes', 'info', array() );
try {
$index = $this->get_index();
//Sortable attributes
new Log( 'Set sortable attributes.', 'debug', $this->get_sortable_attributes() );
$index->updateSortableAttributes( $this->get_sortable_attributes() );
//Filterable attributes
new Log( 'Set filterable attributes.', 'debug', $this->get_filterable_attributes() );
$index->updateFilterableAttributes( $this->get_filterable_attributes() );
//set ranking rules
new Log( 'Set ranking rules.', 'debug', $this->get_ranking_rules() );
$index->updateRankingRules( $this->get_ranking_rules() );
} catch ( \Exception $e ) {
new Log( 'Update index attributes error', 'error', array( 'message' => $e->getMessage() ) );
}
new Log( 'Index attributes updated', 'info', array() );
}
/**
* Cleanup transients from indexing process
*
* @return void
*/
private function cleanup_transients(): void {
//find all transients from database
//then delete them with WordPress native functions
global $wpdb;
$transients = $wpdb->get_results( "SELECT option_value FROM $wpdb->options WHERE option_name LIKE '_transient_findstr_%' AND option_value LIKE 'findstr_%'" );
foreach ( $transients as $transient ) {
delete_transient( $transient->option_value );
}
}
/**
* Clear index
*
* @return void
*/
public function clear_index() {
new Log( 'Clear index', 'info', array() );
update_option( 'findstr_current_indexed_ids', array(), false );
update_option( 'findstr_last_deleted_ids', array(), false );
update_option( 'findstr_last_indexed_ids', array(), false );
$this->get_index()->deleteAllDocuments();
}
}