settings = get_option( 'aica_settings', array() ); // Hook into product save/update add_action( 'woocommerce_update_product', array( $this, 'index_single_product' ), 10, 1 ); add_action( 'woocommerce_new_product', array( $this, 'index_single_product' ), 10, 1 ); // Hook into product delete add_action( 'before_delete_post', array( $this, 'remove_product_from_index' ), 10, 1 ); // Register AJAX handlers add_action( 'wp_ajax_aica_index_products', array( $this, 'ajax_index_products' ) ); add_action( 'wp_ajax_aica_get_indexing_progress', array( $this, 'ajax_get_progress' ) ); add_action( 'wp_ajax_aica_test_ajax', array( $this, 'test_ajax' ) ); // Register cron job add_action( 'aica_reindex_products', array( $this, 'cron_reindex_products' ) ); } /** * Test AJAX handler * * @return void */ public function test_ajax() { wp_send_json_success( array( 'message' => 'AJAX is working!', 'wp_version' => get_bloginfo( 'version' ), 'wc_active' => class_exists( 'WooCommerce' ), 'product_count' => wp_count_posts( 'product' )->publish ?? 0, 'tables_exist' => AICA_Installer::tables_exist(), ) ); } /** * Index all products * * @param bool $force Force reindex even if already indexed * @return array Result with success status and stats */ public function index_all_products( $force = false ) { global $wpdb; // Check if WooCommerce is active if ( !function_exists( 'wc_get_products' ) ) { return array( 'success' => false, 'message' => __( 'WooCommerce is not active', 'aica-smart-ai-commerce-assistant' ), 'stats' => array(), ); } $stats = array( 'total' => 0, 'indexed' => 0, 'skipped' => 0, 'errors' => 0, 'with_embeddings' => 0, ); // Get all products $args = array( 'post_type' => 'product', 'post_status' => 'publish', 'posts_per_page' => -1, 'fields' => 'ids', ); $product_ids = get_posts( $args ); $stats['total'] = count( $product_ids ); if ( empty( $product_ids ) ) { return array( 'success' => false, 'message' => __( 'No products found to index', 'aica-smart-ai-commerce-assistant' ), 'stats' => $stats, ); } error_log( 'AICA: Found ' . count( $product_ids ) . ' products to index' ); // Clear existing index if forcing reindex if ( $force ) { $deleted = $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}aica_indexed_products" ); error_log( 'AICA: Truncated index table' ); } // Reduce batch size for better compatibility $batch_size = 20; // Smaller batches $batches = array_chunk( $product_ids, $batch_size ); $enable_embeddings = isset( $this->settings['enable_embeddings'] ) && $this->settings['enable_embeddings'] === 'yes'; error_log( 'AICA: Processing ' . count( $batches ) . ' batches of ' . $batch_size . ' products each' ); foreach ( $batches as $batch_index => $batch ) { foreach ( $batch as $product_id ) { try { $result = $this->index_single_product( $product_id, $enable_embeddings ); if ( $result['success'] ) { ++$stats['indexed']; if ( $result['has_embedding'] ) { ++$stats['with_embeddings']; } } elseif ( $result['skipped'] ) { ++$stats['skipped']; } else { ++$stats['errors']; error_log( 'AICA: Failed to index product ' . $product_id . ': ' . ( $result['error'] ?? 'unknown' ) ); } } catch ( Exception $e ) { ++$stats['errors']; error_log( 'AICA: Exception indexing product ' . $product_id . ': ' . $e->getMessage() ); } } // Update progress $progress = round( ( ( $batch_index + 1 ) / count( $batches ) ) * 100 ); set_transient( 'aica_indexing_progress', $progress, HOUR_IN_SECONDS ); // Prevent timeout // @set_time_limit( 60 ); // Removed per WordPress.org guidelines // Clear any output if ( ob_get_level() > 0 ) { ob_flush(); flush(); } } // Clear progress transient delete_transient( 'aica_indexing_progress' ); // Update last indexed timestamp update_option( 'aica_last_indexed', current_time( 'mysql' ) ); error_log( 'AICA: Indexing completed - ' . print_r( $stats, true ) ); return array( 'success' => true, 'message' => sprintf( /* translators: 1: indexed count, 2: embeddings count, 3: skipped count, 4: error count */ __( 'Successfully indexed %1$d products (%2$d with embeddings, %3$d skipped, %4$d errors)', 'aica-smart-ai-commerce-assistant' ), $stats['indexed'], $stats['with_embeddings'], $stats['skipped'], $stats['errors'] ), 'stats' => $stats, ); } /** * Index single product * * @param int $product_id Product ID * @param bool $generate_embedding Generate embedding * @return array Result */ public function index_single_product( $product_id, $generate_embedding = false ) { global $wpdb; $product = wc_get_product( $product_id ); if ( !$product ) { return array( 'success' => false, 'skipped' => false, 'error' => 'Product not found', ); } // Skip drafts and out of stock if configured if ( $product->get_status() !== 'publish' ) { return array( 'success' => false, 'skipped' => true, ); } // Build searchable content $content = $this->build_searchable_content( $product ); // Prepare metadata $metadata = array( 'type' => $product->get_type(), 'sku' => $product->get_sku(), 'attributes' => $this->get_product_attributes( $product ), 'tags' => wp_get_post_terms( $product_id, 'product_tag', array( 'fields' => 'names' ) ), ); // Get categories $categories = wp_get_post_terms( $product_id, 'product_cat', array( 'fields' => 'ids' ) ); $categories_str = !empty( $categories ) ? implode( ',', $categories ) : ''; // Generate embedding if enabled $embedding = null; if ( $generate_embedding && !empty( $this->settings['openai_api_key'] ) ) { $ai_engine = AICA_AI_Engine::get_instance(); $embedding_vector = $ai_engine->generate_embedding( $content ); if ( $embedding_vector ) { $embedding = wp_json_encode( $embedding_vector ); } } // Check if product already indexed (short cache for batch processes) $existing = wp_cache_get( 'aica_indexed_status_' . $product_id, 'aica_product_indexer' ); if ( false === $existing ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $existing = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}aica_indexed_products WHERE product_id = %d", $product_id ) ); wp_cache_set( 'aica_indexed_status_' . $product_id, $existing, 'aica_product_indexer', 60 ); } $data = array( 'product_id' => $product_id, 'content' => $content, 'metadata' => wp_json_encode( $metadata ), 'price' => $product->get_price(), 'stock_quantity' => $product->get_stock_quantity(), 'categories' => $categories_str, 'embedding' => $embedding, 'last_updated' => current_time( 'mysql' ), ); $format = array( '%d', '%s', '%s', '%f', '%d', '%s', '%s', '%s' ); if ( $existing ) { // Update existing // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $result = $wpdb->update( $wpdb->prefix . 'aica_indexed_products', $data, array( 'product_id' => $product_id ), $format, array( '%d' ) ); } else { // Insert new // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $result = $wpdb->insert( $wpdb->prefix . 'aica_indexed_products', $data, $format ); } if ( $result === false ) { return array( 'success' => false, 'skipped' => false, 'error' => $wpdb->last_error, ); } wp_cache_delete( 'aica_indexed_status_' . $product_id, 'aica_product_indexer' ); wp_cache_delete( 'aica_indexer_stats', 'aica_product_indexer' ); return array( 'success' => true, 'has_embedding' => !empty( $embedding ), ); } /** * Build searchable content from product * * @param WC_Product $product Product object * @return string Searchable content */ private function build_searchable_content( $product ) { $parts = array(); // Product name (most important) $parts[] = $product->get_name(); $parts[] = $product->get_name(); // Duplicate for weight // SKU if ( $product->get_sku() ) { $parts[] = $product->get_sku(); } // Short description $short_desc = $product->get_short_description(); if ( $short_desc ) { $parts[] = wp_strip_all_tags( $short_desc ); } // Full description (truncated) $full_desc = $product->get_description(); if ( $full_desc ) { $parts[] = wp_trim_words( wp_strip_all_tags( $full_desc ), 100 ); } // Categories $categories = wp_get_post_terms( $product->get_id(), 'product_cat', array( 'fields' => 'names' ) ); if ( !empty( $categories ) ) { $parts[] = implode( ' ', $categories ); } // Tags $tags = wp_get_post_terms( $product->get_id(), 'product_tag', array( 'fields' => 'names' ) ); if ( !empty( $tags ) ) { $parts[] = implode( ' ', $tags ); } // Attributes $attributes = $this->get_product_attributes( $product ); if ( !empty( $attributes ) ) { foreach ( $attributes as $attr_name => $attr_value ) { $parts[] = $attr_name . ': ' . $attr_value; } } // Price range (for search like "under 50") $price = $product->get_price(); if ( $price ) { if ( $price < 25 ) { $parts[] = 'budget affordable cheap inexpensive'; } elseif ( $price < 100 ) { $parts[] = 'mid-range reasonable'; } else { $parts[] = 'premium high-end luxury'; } } return implode( ' ', array_filter( $parts ) ); } /** * Get product attributes * * @param WC_Product $product Product object * @return array Attributes */ private function get_product_attributes( $product ) { $attributes = array(); foreach ( $product->get_attributes() as $attribute ) { if ( $attribute->is_taxonomy() ) { $terms = wp_get_post_terms( $product->get_id(), $attribute->get_name(), array( 'fields' => 'names' ) ); if ( !empty( $terms ) ) { $attributes[ $attribute->get_name() ] = implode( ', ', $terms ); } } else { $attributes[ $attribute->get_name() ] = implode( ', ', $attribute->get_options() ); } } return $attributes; } /** * Remove product from index * * @param int $post_id Post ID * @return void */ public function remove_product_from_index( $post_id ) { if ( get_post_type( $post_id ) !== 'product' ) { return; } global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->delete( $wpdb->prefix . 'aica_indexed_products', array( 'product_id' => $post_id ), array( '%d' ) ); wp_cache_delete( 'aica_indexed_status_' . $post_id, 'aica_product_indexer' ); wp_cache_delete( 'aica_indexer_stats', 'aica_product_indexer' ); } /** * AJAX handler for indexing * * @return void */ public function ajax_index_products() { // Log that we reached this point error_log( 'AICA: ajax_index_products called' ); // Verify nonce if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'aica_indexer' ) ) { error_log( 'AICA: Nonce verification failed' ); wp_send_json_error( array( 'message' => __( 'Security check failed', 'aica-smart-ai-commerce-assistant' ), ) ); return; } // Check permissions if ( !current_user_can( 'manage_options' ) ) { error_log( 'AICA: Permission denied' ); wp_send_json_error( array( 'message' => __( 'Permission denied', 'aica-smart-ai-commerce-assistant' ), ) ); return; } try { // Start indexing error_log( 'AICA: Starting product indexing...' ); $result = $this->index_all_products( true ); error_log( 'AICA: Indexing result: ' . print_r( $result, true ) ); if ( $result['success'] ) { wp_send_json_success( array( 'message' => $result['message'], 'stats' => $result['stats'], ) ); } else { wp_send_json_error( array( 'message' => $result['message'], ) ); } } catch ( Exception $e ) { error_log( 'AICA: Indexing exception: ' . $e->getMessage() ); wp_send_json_error( array( 'message' => $e->getMessage(), ) ); } } /** * AJAX handler for progress * * @return void */ public function ajax_get_progress() { check_ajax_referer( 'aica_indexer', 'nonce' ); if ( !current_user_can( 'manage_options' ) ) { wp_send_json_error(); } $progress = get_transient( 'aica_indexing_progress' ); wp_send_json_success( array( 'progress' => $progress ? intval( $progress ) : 0, ) ); } /** * Cron job for reindexing * * @return void */ public function cron_reindex_products() { // Only reindex products modified in last 24 hours $args = array( 'post_type' => 'product', 'post_status' => 'publish', 'posts_per_page' => -1, 'fields' => 'ids', 'date_query' => array( array( 'column' => 'post_modified', 'after' => '24 hours ago', ), ), ); $product_ids = get_posts( $args ); if ( empty( $product_ids ) ) { return; } $enable_embeddings = isset( $this->settings['enable_embeddings'] ) && $this->settings['enable_embeddings'] === 'yes'; foreach ( $product_ids as $product_id ) { $this->index_single_product( $product_id, $enable_embeddings ); } error_log( 'AICA: Cron reindexed ' . count( $product_ids ) . ' products' ); } /** * Get indexing statistics * * @return array Stats */ public function get_stats() { global $wpdb; $stats = wp_cache_get( 'aica_indexer_stats', 'aica_product_indexer' ); if ( false === $stats ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $total_indexed = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}aica_indexed_products" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $with_embeddings = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}aica_indexed_products WHERE embedding IS NOT NULL" ); $stats = array( 'total_indexed' => $total_indexed, 'with_embeddings' => $with_embeddings, ); wp_cache_set( 'aica_indexer_stats', $stats, 'aica_product_indexer', HOUR_IN_SECONDS ); } $total_indexed = $stats['total_indexed']; $with_embeddings = $stats['with_embeddings']; $total_products = wp_count_posts( 'product' )->publish; $last_indexed = get_option( 'aica_last_indexed', null ); return array( 'total_products' => $total_products, 'total_indexed' => $total_indexed, 'with_embeddings' => $with_embeddings, 'last_indexed' => $last_indexed, 'coverage' => $total_products > 0 ? round( ( $total_indexed / $total_products ) * 100, 2 ) : 0, ); } }