connection_manager = AIVectorSearch_Connection_Manager::instance(); $this->analytics = AIVectorSearch_Analytics::instance(); $this->init_hooks(); } private function init_hooks() { if (!$this->is_search_enabled()) { return; } // Regular WordPress search interception add_action('pre_get_posts', [$this, 'intercept_product_search'], 9999); // Woodmart AJAX search integration $this->init_woodmart_integration(); // Track clicks on search results add_action('template_redirect', [$this, 'maybe_track_click']); } /** * Initialize Woodmart AJAX search integration */ private function init_woodmart_integration() { // Only initialize if Woodmart integration is enabled if (get_option('aivesese_enable_woodmart_integration', '0') !== '1') { return; } // Register AJAX handlers for both our custom endpoint and original Woodmart endpoint add_action('wp_ajax_aivs_woodmart_search', [$this, 'handle_woodmart_ajax_search']); add_action('wp_ajax_nopriv_aivs_woodmart_search', [$this, 'handle_woodmart_ajax_search']); // Keep support for original Woodmart endpoint for backward compatibility add_action('wp_ajax_woodmart_ajax_search', [$this, 'handle_woodmart_ajax_search']); add_action('wp_ajax_nopriv_woodmart_ajax_search', [$this, 'handle_woodmart_ajax_search']); // Remove original Woodmart handlers add_action('wp_loaded', [$this, 'override_woodmart_ajax'], 99); // Fallback method using pre_get_posts add_action('pre_get_posts', [$this, 'intercept_woodmart_query'], 1); // Enqueue nonce for frontend add_action('wp_enqueue_scripts', [$this, 'enqueue_search_nonce']); } /** * Override Woodmart's original AJAX search handlers */ public function override_woodmart_ajax() { // Remove original Woodmart handlers if they exist remove_action('wp_ajax_woodmart_ajax_search', 'woodmart_ajax_search'); remove_action('wp_ajax_nopriv_woodmart_ajax_search', 'woodmart_ajax_search'); } /** * Enqueue search nonce for frontend */ public function enqueue_search_nonce() { wp_localize_script('jquery', 'aivs_search_data', [ 'nonce' => wp_create_nonce('aivesese_search_nonce'), 'ajax_url' => admin_url('admin-ajax.php') ]); } /** * Handle Woodmart AJAX search with AI results and analytics */ public function handle_woodmart_ajax_search() { // Verify nonce for our custom endpoint $action = isset($_REQUEST['action']) ? sanitize_key($_REQUEST['action']) : ''; if ($action === 'aivs_woodmart_search') { if (!wp_verify_nonce($_REQUEST['nonce'] ?? '', 'aivesese_search_nonce')) { wp_send_json_error(['message' => 'Security check failed']); return; } } // For backward compatibility with original Woodmart endpoint, we're more lenient // but still sanitize all inputs if (!$this->is_search_enabled()) { wp_send_json(['suggestions' => []]); return; } $query = sanitize_text_field(wp_unslash($_REQUEST['query'] ?? '')); $number = intval($_REQUEST['number'] ?? $_REQUEST['limit'] ?? 20); if (strlen($query) < 3) { if ($action === 'aivs_woodmart_search') { wp_send_json_success([]); } else { wp_send_json(['suggestions' => []]); } return; } // Get AI search results $product_ids = $this->search_products($query, $number); $product_ids = $this->normalize_product_ids($product_ids); // Track analytics $this->track_search_analytics($query, $product_ids); // Build suggestions for Woodmart $suggestions = []; $results = []; if (!empty($product_ids)) { foreach ($product_ids as $product_id) { $product = wc_get_product($product_id); if (!$product || $product->get_status() !== 'publish') { continue; } $image_id = get_post_thumbnail_id($product_id); $image_url = $image_id ? wp_get_attachment_image_src($image_id, 'woocommerce_gallery_thumbnail')[0] : ''; $suggestions[] = [ 'value' => $product->get_name(), 'permalink' => add_query_arg([ 'from_search' => '1', 'search_term' => urlencode($query) ], get_permalink($product_id)), 'price' => $product->get_price_html(), 'thumbnail' => $product->get_image(), 'sku' => $product->get_sku() ? 'SKU: ' . $product->get_sku() : '', 'group' => 'product' ]; $results[] = [ 'id' => $product_id, 'name' => $product->get_name(), 'url' => add_query_arg([ 'from_search' => '1', 'search_term' => urlencode($query) ], get_permalink($product_id)), 'price' => $product->get_price_html(), 'image' => $image_url, 'sku' => $product->get_sku() ]; } } if ($action === 'aivs_woodmart_search') { wp_send_json_success($results); } else { wp_send_json(['suggestions' => $suggestions]); } exit; } /** * Intercept Woodmart query as fallback */ public function intercept_woodmart_query($query) { // Only for AJAX requests if (!wp_doing_ajax() || is_admin()) { return; } // Only for Woodmart search $action = isset($_REQUEST['action']) ? sanitize_key($_REQUEST['action']) : ''; if ($action !== 'woodmart_ajax_search') { return; } $search_term = sanitize_text_field(wp_unslash($_REQUEST['query'] ?? '')); if (strlen($search_term) < 3) { return; } // Get our AI search results $product_ids = $this->search_products($search_term, 20); $product_ids = $this->normalize_product_ids($product_ids); if (!empty($product_ids)) { // Override the query to use our results $query->set('post__in', $product_ids); $query->set('orderby', 'post__in'); $query->set('post_type', 'product'); $query->set('posts_per_page', 20); // Clear default search parameters $query->set('s', ''); $query->set('meta_query', []); $query->set('tax_query', []); } } /** * Regular WordPress search interception */ public function intercept_product_search(WP_Query $query) { if (is_admin() || ! $query->is_main_query()) { return; } $search_term = ''; $is_ajax_search = false; // Regular WordPress search if ($query->is_search()) { if (!$query->get('post_type')) { $query->set('post_type', ['product']); } $search_term = $query->get('s'); } // Generic AJAX search detection (not Woodmart specific) if (wp_doing_ajax() && !isset($_REQUEST['action'])) { $search_term = sanitize_text_field(wp_unslash($_REQUEST['s'] ?? $_REQUEST['query'] ?? '')); if ($search_term) { $is_ajax_search = true; $query->set('post_type', ['product']); } } if (!$search_term || strlen($search_term) < 3) { return; } $product_ids = $this->search_products($search_term); $product_ids = $this->normalize_product_ids($product_ids); $this->track_search_analytics($search_term, $product_ids); if (empty($product_ids)) { if ($is_ajax_search) { // $query->set('post__in', [0]); // Force no results return; } return; } // Clear the search term to prevent WordPress from doing its own search $query->set('s', ''); $query->set('post__in', $product_ids); $query->set('orderby', 'post__in'); $query->set('posts_per_page', $is_ajax_search ? 10 : 20); } /** * Main search method */ public function search_products(string $term, int $limit = 0): array { if (!$this->is_search_enabled()) { return []; } // Use configured limit if none provided if ($limit === 0) { $limit = aivesese_get_search_results_limit(); } $use_semantic = $this->should_use_semantic_search($term); if ($use_semantic) { $ids = $this->search_semantic($term, $limit); if (count($ids) < 3) { $ids = array_unique(array_merge( $ids, $this->search_fulltext($term, $limit) )); } // If still no results, try SKU search as fallback if (empty($ids)) { $ids = $this->search_sku($term, $limit); } return $ids; } // For non-semantic search $ids = $this->search_fulltext($term, $limit); // If no results from full-text search, try SKU search as fallback if (empty($ids)) { $ids = $this->search_sku($term, $limit); } // Add fuzzy search as final fallback for non-semantic search too if (empty($ids)) { $ids = $this->search_fuzzy($term, $limit); } return $ids; } /** * Track search analytics */ private function track_search_analytics(string $term, array $product_ids) { // Determine search type used $search_type = 'fts'; // default if ($this->should_use_semantic_search($term)) { $search_type = 'semantic'; } // If no results from main search, check if SKU search would work if (empty($product_ids)) { $sku_results = $this->search_sku($term, 5); if (!empty($sku_results)) { $search_type = 'sku'; $product_ids = $sku_results; // Update results for tracking } } // Track the search if (is_object($this->analytics)) { $this->analytics->track_search($term, $search_type, $product_ids); } } /** * Track clicks on search results */ public function maybe_track_click() { // Check if this is a click from search results if (isset($_GET['from_search']) && isset($_GET['search_term']) && is_singular('product')) { $search_term = sanitize_text_field(wp_unslash($_GET['search_term'] ?? '')); $product_id = get_the_ID(); // Track the click in analytics if (is_object($this->analytics)) { $this->analytics->track_click($search_term, $product_id); } } } /** * Search using SKU - uses connection manager */ private function search_sku(string $term, int $limit): array { return $this->connection_manager->search_products_sku($term, $limit); } /** * Search using semantic search - uses connection manager */ private function search_semantic(string $term, int $limit): array { if ($this->connection_manager->is_api_mode()) { // API mode handles embedding generation internally return $this->connection_manager->search_products_semantic($term, [], $limit); } else { // Self-hosted mode needs to generate embedding first $embedding = $this->connection_manager->generate_embedding($term); if (!$embedding) { return []; } $results = $this->connection_manager->search_products_semantic($term, $embedding, $limit); return $results; } } /** * Search using full-text search - uses connection manager */ private function search_fulltext(string $term, int $limit): array { return $this->connection_manager->search_products_fts($term, $limit); } /** * Fuzzy search fallback */ private function search_fuzzy(string $term, int $limit): array { return $this->connection_manager->search_products_fuzzy($term, $limit); } /** * Determine if semantic search should be used */ private function should_use_semantic_search(string $term): bool { return get_option('aivesese_semantic_toggle') === '1' && strlen($term) >= 3 && $this->connection_manager->is_semantic_search_available(); } private function normalize_product_ids(array $ids): array { $out = []; foreach ($ids as $id) { $p = wc_get_product($id); if (!$p) continue; // Map variations to parent if ($p->is_type('variation')) { $parent = $p->get_parent_id(); if (!$parent) continue; $id = $parent; $p = wc_get_product($id); if (!$p) continue; } // Keep only published products if ($p->get_status() !== 'publish') continue; // Preserve order while making unique if (!isset($out[$id])) $out[$id] = true; } return array_map('intval', array_keys($out)); } /** * Get search suggestions for autocomplete */ public function get_search_suggestions(string $term, int $limit = 5): array { if (strlen($term) < 2) { return []; } // Get popular search terms from analytics that start with the term $suggestions = $this->analytics->get_popular_terms(10, 30); $matching_suggestions = []; foreach ($suggestions as $suggestion) { if (stripos($suggestion->search_term, $term) === 0) { $matching_suggestions[] = $suggestion->search_term; if (count($matching_suggestions) >= $limit) { break; } } } return $matching_suggestions; } /** * Get trending searches */ public function get_trending_searches(int $limit = 5): array { return $this->analytics->get_popular_terms($limit, 7); // Last 7 days } /** * Preview search results (for admin/testing) */ public function preview_search_results(string $term, int $limit = 10): array { $product_ids = $this->search_products($term, $limit); $results = []; foreach ($product_ids as $product_id) { $product = wc_get_product($product_id); if ($product) { $results[] = [ 'id' => $product_id, 'name' => $product->get_name(), 'price' => $product->get_price_html(), 'url' => get_permalink($product_id), 'image' => get_the_post_thumbnail_url($product_id, 'thumbnail') ]; } } return $results; } /** * Public search API for external use */ public function public_search(string $term, array $args = []): array { if (!$this->is_search_enabled()) { return []; } $defaults = [ 'limit' => aivesese_get_search_results_limit(), 'track' => true, 'include_data' => false ]; $args = wp_parse_args($args, $defaults); $product_ids = $this->search_products($term, $args['limit']); // Track if enabled if ($args['track']) { $this->track_search_analytics($term, $product_ids); } // Return just IDs or include product data if (!$args['include_data']) { return $product_ids; } return $this->preview_search_results($term, $args['limit']); } /** * Debug method for testing */ public function debug_search(string $term): array { if (!current_user_can('manage_options')) { return []; } $results = [ 'term' => $term, 'fts_results' => $this->search_fulltext($term, 10), 'sku_results' => $this->search_sku($term, 10), 'final_results' => $this->search_products($term, 10), 'semantic_available' => $this->should_use_semantic_search($term), 'woodmart_integration' => get_option('aivesese_enable_woodmart_integration', '0') === '1', 'analytics_enabled' => true ]; if ($this->should_use_semantic_search($term)) { $results['semantic_results'] = $this->search_semantic($term, 10); } // Add analytics data $results['recent_searches'] = $this->analytics->get_popular_terms(5, 7); $results['zero_results'] = $this->analytics->get_zero_result_searches(3, 7); return $results; } /** * Determine if search features should be active */ private function is_search_enabled(): bool { return get_option('aivesese_enable_search', '1') === '1'; } }