Changeset 3412691
- Timestamp:
- 12/05/2025 10:52:36 PM (4 months ago)
- Location:
- ai-search
- Files:
-
- 25 added
- 5 edited
-
tags/1.9.1 (added)
-
tags/1.9.1/admin (added)
-
tags/1.9.1/admin/class-admin-manager.php (added)
-
tags/1.9.1/admin/class-settings-pages.php (added)
-
tags/1.9.1/admin/class-setup-wizard.php (added)
-
tags/1.9.1/admin/views (added)
-
tags/1.9.1/admin/views/settings-cache.php (added)
-
tags/1.9.1/admin/views/settings-custom-fields.php (added)
-
tags/1.9.1/admin/views/settings-embeddings.php (added)
-
tags/1.9.1/admin/views/settings-general.php (added)
-
tags/1.9.1/admin/views/settings-woocommerce.php (added)
-
tags/1.9.1/admin/views/wizard (added)
-
tags/1.9.1/admin/views/wizard/completion.php (added)
-
tags/1.9.1/admin/views/wizard/step-final.php (added)
-
tags/1.9.1/admin/views/wizard/step-provider.php (added)
-
tags/1.9.1/admin/views/wizard/step-welcome.php (added)
-
tags/1.9.1/ai-search.php (added)
-
tags/1.9.1/assets (added)
-
tags/1.9.1/assets/icon.svg (added)
-
tags/1.9.1/includes (added)
-
tags/1.9.1/includes/class-ai-search-service.php (added)
-
tags/1.9.1/readme.txt (added)
-
trunk/admin/class-settings-pages.php (modified) (1 diff)
-
trunk/admin/views/settings-cache.php (added)
-
trunk/admin/views/settings-custom-fields.php (added)
-
trunk/admin/views/settings-general.php (modified) (1 diff)
-
trunk/admin/views/settings-woocommerce.php (added)
-
trunk/ai-search.php (modified) (8 diffs)
-
trunk/includes/class-ai-search-service.php (modified) (3 diffs)
-
trunk/readme.txt (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
ai-search/trunk/admin/class-settings-pages.php
r3412677 r3412691 304 304 public function get_custom_fields_for_post_type( $post_type ) { 305 305 global $wpdb; 306 306 307 $fields = []; 308 309 // Get regular custom fields (non-underscore prefixed) 307 310 $query = $wpdb->prepare( " 308 311 SELECT DISTINCT pm.meta_key 309 312 FROM {$wpdb->postmeta} pm 310 313 INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID 311 WHERE p.post_type = %s 312 AND pm.meta_key NOT LIKE '\_%' 314 WHERE p.post_type = %s 315 AND pm.meta_key NOT LIKE '\_%' 313 316 AND pm.meta_key != 'ai_search_embedding' 314 317 AND pm.meta_value != '' 315 318 ORDER BY pm.meta_key 316 319 ", $post_type ); 317 318 $results = $wpdb->get_col( $query ); 319 return $results ? $results : []; 320 321 $regular_fields = $wpdb->get_col( $query ); 322 if ( $regular_fields ) { 323 $fields = array_merge( $fields, $regular_fields ); 324 } 325 326 // If ACF is active, get ACF field groups for this post type 327 if ( function_exists( 'acf_get_field_groups' ) ) { 328 $field_groups = acf_get_field_groups( [ 'post_type' => $post_type ] ); 329 330 foreach ( $field_groups as $field_group ) { 331 $acf_fields = acf_get_fields( $field_group['key'] ); 332 333 if ( $acf_fields ) { 334 foreach ( $acf_fields as $field ) { 335 // Only include fields that can be converted to text 336 $searchable_types = [ 337 'text', 'textarea', 'number', 'email', 'url', 'password', 338 'wysiwyg', 'select', 'checkbox', 'radio', 'button_group', 339 'true_false', 'post_object', 'relationship', 'taxonomy', 340 'user', 'date_picker', 'date_time_picker', 'time_picker', 341 'color_picker' 342 ]; 343 344 if ( in_array( $field['type'], $searchable_types ) ) { 345 $fields[] = $field['name']; 346 } 347 } 348 } 349 } 350 } 351 352 // Remove duplicates and sort 353 $fields = array_unique( $fields ); 354 sort( $fields ); 355 356 return $fields; 320 357 } 321 358 -
ai-search/trunk/admin/views/settings-general.php
r3412677 r3412691 122 122 <br/><br/> 123 123 124 <label for="similarity_threshold">Similarity Threshold (0. 5-1):</label><br>125 <input type="range" id="similarity_threshold" name="similarity_threshold" min="0. 5" max="1" step="0.001" value="<?php echo esc_attr( $similarity_threshold ); ?>" oninput="this.nextElementSibling.value = Number(this.value).toFixed(3); updateThresholdDemo(this.value)">124 <label for="similarity_threshold">Similarity Threshold (0.2-1):</label><br> 125 <input type="range" id="similarity_threshold" name="similarity_threshold" min="0.2" max="1" step="0.001" value="<?php echo esc_attr( $similarity_threshold ); ?>" oninput="this.nextElementSibling.value = Number(this.value).toFixed(3); updateThresholdDemo(this.value)"> 126 126 <output><?php echo number_format( $similarity_threshold, 3 ); ?></output> 127 <p><em>Defines how similar a post must be to appear in search results. Higher values (closer to 1.0) mean stricter, more relevant results. Values below 0.5 often return too many irrelevant matches. Default: 0.500.</em></p>127 <p><em>Defines how similar a post must be to appear in search results. Higher values (closer to 1.0) mean stricter, more relevant results. Lower values (0.2-0.4) return more results but may include less relevant matches. Recommended: 0.3-0.5.</em></p> 128 128 129 129 <!-- Interactive threshold demonstration --> -
ai-search/trunk/ai-search.php
r3412677 r3412691 3 3 * Plugin Name: AI Search 4 4 * Description: Replaces the default search with an intelligent search system. 5 * Version: 1.9. 05 * Version: 1.9.1 6 6 * Author: samuelsilvapt 7 7 * License: GPL2 … … 138 138 139 139 /** 140 * Get custom fields content for a post. 141 * 142 * @param int $post_id Post ID. 143 * @param string $post_type Post type. 144 * 145 * @return string Custom fields content. 146 */ 147 private function get_custom_fields_content( $post_id, $post_type ) { 148 $content = ''; 149 150 // Get selected custom fields for this post type 151 $selected_fields = get_option( 'ai_search_custom_fields_' . $post_type, [] ); 152 153 if ( empty( $selected_fields ) || ! is_array( $selected_fields ) ) { 154 return $content; 155 } 156 157 // Get custom field values 158 foreach ( $selected_fields as $field_key ) { 159 $field_value = get_post_meta( $post_id, $field_key, true ); 160 161 if ( ! empty( $field_value ) ) { 162 // Get field label (support ACF if available) 163 $field_label = $field_key; 164 165 // Try to get ACF field label 166 if ( function_exists( 'get_field_object' ) ) { 167 $field_object = get_field_object( $field_key, $post_id ); 168 if ( $field_object && isset( $field_object['label'] ) ) { 169 $field_label = $field_object['label']; 170 } 171 } 172 173 // Handle different field value types 174 if ( is_array( $field_value ) ) { 175 // Handle ACF relationship, post object, or other array fields 176 $processed_values = []; 177 foreach ( $field_value as $item ) { 178 if ( is_object( $item ) && isset( $item->post_title ) ) { 179 // ACF Post Object or Relationship field 180 $processed_values[] = $item->post_title; 181 } elseif ( is_string( $item ) ) { 182 $processed_values[] = $item; 183 } elseif ( is_array( $item ) && isset( $item['label'] ) ) { 184 // ACF select/checkbox with labels 185 $processed_values[] = $item['label']; 186 } elseif ( is_array( $item ) && isset( $item['title'] ) ) { 187 // ACF image/file field 188 $processed_values[] = $item['title']; 189 } 190 } 191 if ( ! empty( $processed_values ) ) { 192 $field_value = implode( ' ', $processed_values ); 193 } else { 194 continue; // Skip if we couldn't extract meaningful data 195 } 196 } elseif ( is_object( $field_value ) ) { 197 // Handle ACF User field or other object types 198 if ( isset( $field_value->display_name ) ) { 199 $field_value = $field_value->display_name; 200 } elseif ( isset( $field_value->post_title ) ) { 201 $field_value = $field_value->post_title; 202 } else { 203 continue; // Skip objects we can't process 204 } 205 } 206 207 // Sanitize and add to content with field label 208 if ( is_string( $field_value ) ) { 209 $content .= ' ' . sanitize_text_field( $field_label ) . ': ' . sanitize_text_field( $field_value ); 210 } 211 } 212 } 213 214 // Handle WooCommerce products if applicable 215 if ( $post_type === 'product' && class_exists( 'WooCommerce' ) ) { 216 $woo_fields = get_option( 'ai_search_woocommerce_fields', [] ); 217 218 if ( ! empty( $woo_fields ) && is_array( $woo_fields ) ) { 219 $product = wc_get_product( $post_id ); 220 221 if ( $product ) { 222 foreach ( $woo_fields as $woo_field ) { 223 switch ( $woo_field ) { 224 case 'product_description': 225 $desc = $product->get_description(); 226 if ( ! empty( $desc ) ) { 227 $content .= ' product_description: ' . sanitize_textarea_field( $desc ); 228 } 229 break; 230 case 'product_short_description': 231 $short_desc = $product->get_short_description(); 232 if ( ! empty( $short_desc ) ) { 233 $content .= ' product_short_description: ' . sanitize_textarea_field( $short_desc ); 234 } 235 break; 236 case 'product_sku': 237 $sku = $product->get_sku(); 238 if ( ! empty( $sku ) ) { 239 $content .= ' product_sku: ' . sanitize_text_field( $sku ); 240 } 241 break; 242 case 'product_categories': 243 $categories = wp_get_post_terms( $post_id, 'product_cat', [ 'fields' => 'names' ] ); 244 if ( ! is_wp_error( $categories ) && ! empty( $categories ) ) { 245 $content .= ' product_categories: ' . implode( ' ', array_map( 'sanitize_text_field', $categories ) ); 246 } 247 break; 248 case 'product_tags': 249 $tags = wp_get_post_terms( $post_id, 'product_tag', [ 'fields' => 'names' ] ); 250 if ( ! is_wp_error( $tags ) && ! empty( $tags ) ) { 251 $content .= ' product_tags: ' . implode( ' ', array_map( 'sanitize_text_field', $tags ) ); 252 } 253 break; 254 } 255 } 256 257 // Handle product attributes 258 $attributes = $product->get_attributes(); 259 foreach ( $attributes as $attribute ) { 260 $attribute_key = 'attribute_' . $attribute->get_name(); 261 if ( in_array( $attribute_key, $woo_fields ) ) { 262 $values = $attribute->get_options(); 263 if ( ! empty( $values ) ) { 264 $attribute_label = wc_attribute_label( $attribute->get_name() ); 265 $attribute_values = []; 266 267 if ( is_array( $values ) ) { 268 foreach ( $values as $value ) { 269 $term = get_term( $value ); 270 if ( $term && ! is_wp_error( $term ) ) { 271 $attribute_values[] = sanitize_text_field( $term->name ); 272 } 273 } 274 } else { 275 $attribute_values[] = sanitize_text_field( $values ); 276 } 277 278 if ( ! empty( $attribute_values ) ) { 279 $content .= ' ' . $attribute_label . ': ' . implode( ' ', $attribute_values ); 280 } 281 } 282 } 283 } 284 } 285 } 286 } 287 288 return trim( $content ); 289 } 290 291 /** 140 292 * Get embedding from OpenAI. 141 293 * … … 146 298 private function get_embedding( $content ) { 147 299 $content = sanitize_textarea_field( $content ); 300 148 301 // Check for Cached Embedding 149 302 $cached_embedding = get_transient( 'ai_search_embedding_' . md5( $content ) ); … … 158 311 return false; // No token available, service not registered 159 312 } 160 313 161 314 $embedding = $this->service_client->get_embedding( $content ); 162 315 if ( $embedding && ! empty( $embedding['embedding']) ) { … … 206 359 207 360 $post_type = $query->get( 'post_type' ); 208 361 209 362 $search_query = get_search_query(); 210 363 $search_query = sanitize_text_field( $search_query ); 211 364 $query_embedding = $this->get_embedding( $search_query ); 212 365 213 366 if ( ! $query_embedding ) { 214 367 return $posts; 215 368 } 216 369 217 370 $similarities = []; 218 371 … … 224 377 ]); 225 378 226 379 227 380 foreach ( $all_posts as $post ) { 228 381 … … 246 399 return $this->enhanced_fallback_search( $search_query, $post_type, $posts ); 247 400 } 248 401 249 402 arsort( $similarities ); 250 403 $ordered_ids = array_keys( $similarities ); … … 257 410 'posts_per_page' => -1, 258 411 ]); 259 412 260 413 return $posts; 261 414 } -
ai-search/trunk/includes/class-ai-search-service.php
r3385425 r3412691 71 71 } 72 72 73 $body = json_decode( wp_remote_retrieve_body( $response ), true ); 73 $status_code = wp_remote_retrieve_response_code( $response ); 74 $response_body = wp_remote_retrieve_body( $response ); 75 $body = json_decode( $response_body, true ); 74 76 75 if ( isset( $body['embeddings'][0]['embedding'], $body['embeddings'][0]['id'] ) ) { 76 $result = json_decode( $body['embeddings'][0]['embedding'] ); 77 set_transient( $cache_key, $result, MONTH_IN_SECONDS ); 78 return $result; 77 if ( $status_code !== 200 ) { 78 return false; 79 } 80 81 if ( isset( $body['embeddings'][0]['embedding'] ) ) { 82 $embedding = $body['embeddings'][0]['embedding']; 83 84 // Check if embedding is a string that needs decoding or already an array 85 if ( is_string( $embedding ) ) { 86 $result = json_decode( $embedding, true ); 87 } else { 88 $result = $embedding; 89 } 90 91 if ( ! is_array( $result ) || empty( $result ) ) { 92 return false; 93 } 94 95 // Prepare the return value with both embedding and id/index 96 $return_value = [ 97 'embedding' => $result, 98 'id' => isset( $body['embeddings'][0]['id'] ) ? $body['embeddings'][0]['id'] : ( isset( $body['embeddings'][0]['index'] ) ? $body['embeddings'][0]['index'] : 0 ) 99 ]; 100 101 set_transient( $cache_key, $return_value, MONTH_IN_SECONDS ); 102 return $return_value; 79 103 } 80 104 … … 106 130 107 131 if ( is_wp_error( $response ) ) { 108 // Log the error for debugging109 error_log( 'AI Search Token Validation Error: ' . $response->get_error_message() );110 132 return false; 111 133 } … … 113 135 $status_code = wp_remote_retrieve_response_code( $response ); 114 136 $body = json_decode( wp_remote_retrieve_body( $response ), true ); 115 116 // Log the response for debugging 117 error_log( 'AI Search Token Validation Response: ' . wp_remote_retrieve_body( $response ) ); 118 137 119 138 return $body; 120 139 } -
ai-search/trunk/readme.txt
r3412677 r3412691 3 3 Tags: search, AI, OpenAI, WordPress 4 4 Tested up to: 6.8 5 Stable tag: 1.9. 05 Stable tag: 1.9.1 6 6 Requires PHP: 8.0 7 7 License: GPLv2
Note: See TracChangeset
for help on using the changeset viewer.