PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.2.6
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.2.6
3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / modules / search.php
ai-engine / classes / modules Last commit date
advisor.php 7 months ago chatbot.php 7 months ago discussions.php 8 months ago files.php 7 months ago forms-manager.php 10 months ago gdpr.php 11 months ago search.php 11 months ago security.php 11 months ago tasks-examples.php 9 months ago tasks.php 7 months ago wand.php 7 months ago
search.php
929 lines
1 <?php
2
3 class Meow_MWAI_Modules_Search {
4 private $core = null;
5 private $namespace = 'mwai-ui/v1';
6
7 public function __construct( $core ) {
8 $this->core = $core;
9 add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
10 add_action( 'pre_get_posts', [ $this, 'pre_get_posts' ] );
11 add_filter( 'the_posts', [ $this, 'filter_embeddings_search_results' ], 10, 2 );
12 add_filter( 'get_search_query', [ $this, 'customize_search_display' ] );
13
14 // Initialize search frontend settings if they don't exist
15 $this->init_frontend_settings();
16 }
17
18 private function init_frontend_settings() {
19 // Ensure frontend search settings exist with defaults
20 if ( $this->core->get_option( 'search_frontend_method' ) === null ) {
21 $this->core->update_option( 'search_frontend_method', 'wordpress' );
22 }
23 if ( $this->core->get_option( 'search_frontend_env_id' ) === null ) {
24 $this->core->update_option( 'search_frontend_env_id', null );
25 }
26 if ( $this->core->get_option( 'search_website_context' ) === null ) {
27 $this->core->update_option( 'search_website_context', 'This is a website with useful information and content.' );
28 }
29 }
30
31 public function rest_api_init() {
32 register_rest_route( $this->namespace, '/search', [
33 'methods' => 'POST',
34 'callback' => [ $this, 'rest_search' ],
35 'permission_callback' => '__return_true'
36 ] );
37 }
38
39 public function pre_get_posts( $query ) {
40 if ( is_admin() || !$query->is_main_query() || !$query->is_search() ) {
41 return;
42 }
43 $search = $query->get( 's' );
44 if ( empty( $search ) ) {
45 return;
46 }
47
48 // Get the frontend search method setting
49 $frontend_method = $this->core->get_option( 'search_frontend_method', 'wordpress' );
50
51 // If WordPress method is selected, do nothing (use default WordPress search)
52 if ( $frontend_method === 'wordpress' ) {
53 return;
54 }
55
56 // For Keywords method, use the full progressive search like admin
57 if ( $frontend_method === 'keywords' ) {
58 // Store the original search for later use
59 $query->set( 'mwai_original_search', $search );
60 // Set a unique search term that won't match anything - we'll handle this in the_posts filter
61 $query->set( 's', 'MWAI_KEYWORDS_SEARCH_' . md5( $search ) );
62 return;
63 }
64
65 // For Embeddings method, we need to handle this differently
66 // Since we can't easily replace WordPress search with embeddings in pre_get_posts,
67 // we'll modify the query to return no results and handle embeddings in template_redirect
68 if ( $frontend_method === 'embeddings' ) {
69 // Store the original search for later use
70 $query->set( 'mwai_original_search', $search );
71 // Set a unique search term that won't match anything
72 $query->set( 's', 'MWAI_EMBEDDINGS_SEARCH_' . md5( $search ) );
73 }
74 }
75
76 public function filter_embeddings_search_results( $posts, $query ) {
77 // Only handle main search queries with our special markers
78 if ( !$query->is_main_query() || !$query->is_search() || is_admin() ) {
79 return $posts;
80 }
81
82 $search_term = $query->get( 's' );
83 $original_search = $query->get( 'mwai_original_search' );
84
85 // Check if this is our embeddings or keywords search
86 if ( empty( $original_search ) ||
87 ( strpos( $search_term, 'MWAI_EMBEDDINGS_SEARCH_' ) !== 0 &&
88 strpos( $search_term, 'MWAI_KEYWORDS_SEARCH_' ) !== 0 ) ) {
89 return $posts;
90 }
91
92 // Handle Keywords search
93 if ( strpos( $search_term, 'MWAI_KEYWORDS_SEARCH_' ) === 0 ) {
94 return $this->handle_frontend_keywords_search( $original_search, $query );
95 }
96
97 // Handle Embeddings search (existing logic below)
98 if ( strpos( $search_term, 'MWAI_EMBEDDINGS_SEARCH_' ) !== 0 ) {
99 return $posts;
100 }
101
102 // Get the frontend search method to double-check
103 $frontend_method = $this->core->get_option( 'search_frontend_method', 'wordpress' );
104 if ( $frontend_method !== 'embeddings' ) {
105 return $posts;
106 }
107
108 // Get the embeddings environment ID
109 $env_id = $this->core->get_option( 'search_frontend_env_id', null );
110 if ( empty( $env_id ) ) {
111 return $posts; // No environment selected, return empty
112 }
113
114 try {
115 // Perform embeddings search
116 $embedding_result = $this->search_with_embeddings( $original_search, $env_id );
117
118 if ( isset( $embedding_result['error'] ) || empty( $embedding_result['post_ids'] ) ) {
119 return []; // Return empty array if search failed or no results
120 }
121
122 // Get the post IDs from embeddings results
123 $post_ids = array_map( function ( $result ) {
124 return $result['id'];
125 }, $embedding_result['post_ids'] );
126
127 if ( empty( $post_ids ) ) {
128 return [];
129 }
130
131 // Get the actual post objects
132 $embeddings_posts = get_posts( [
133 'post__in' => $post_ids,
134 'orderby' => 'post__in',
135 'posts_per_page' => count( $post_ids ),
136 'post_type' => 'post',
137 'post_status' => 'publish'
138 ] );
139
140 // Update the query's found_posts count
141 $query->found_posts = count( $embeddings_posts );
142 $query->max_num_pages = 1;
143
144 return $embeddings_posts;
145
146 }
147 catch ( Exception $e ) {
148 error_log( 'AI Engine Search: Frontend embeddings search failed - ' . $e->getMessage() );
149 return [];
150 }
151 }
152
153 private function handle_frontend_keywords_search( $original_search, $query ) {
154 // Get website context for keywords search
155 $website_context = $this->core->get_option( 'search_website_context', '' );
156
157 try {
158 // Use the same search logic as the admin REST API
159 $search_queries = $this->generate_keyword_tiers( $original_search, $website_context );
160 $keyword_result = $this->search_with_keywords( $search_queries );
161
162 if ( !empty( $keyword_result['results'] ) ) {
163 // Extract post IDs from results
164 $post_ids = array_map( function ( $result ) {
165 return $result['id'];
166 }, $keyword_result['results'] );
167
168 // Get the actual post objects
169 $posts = get_posts( [
170 'post__in' => $post_ids,
171 'orderby' => 'post__in',
172 'posts_per_page' => count( $post_ids ),
173 'post_type' => 'post',
174 'post_status' => 'publish'
175 ] );
176
177 // Update the query's found_posts count
178 $query->found_posts = count( $posts );
179 $query->max_num_pages = 1;
180
181 return $posts;
182 }
183
184 // If no results with keywords, fallback to original search
185 $fallback_posts = get_posts( [
186 's' => $original_search,
187 'posts_per_page' => 20,
188 'post_type' => 'post',
189 'post_status' => 'publish'
190 ] );
191
192 $query->found_posts = count( $fallback_posts );
193 $query->max_num_pages = 1;
194
195 return $fallback_posts;
196
197 }
198 catch ( Exception $e ) {
199 error_log( 'AI Engine Search: Frontend keywords search failed - ' . $e->getMessage() );
200
201 // Fallback to original search
202 $fallback_posts = get_posts( [
203 's' => $original_search,
204 'posts_per_page' => 20,
205 'post_type' => 'post',
206 'post_status' => 'publish'
207 ] );
208
209 return $fallback_posts;
210 }
211 }
212
213 private function get_site_context() {
214 // Get all categories
215 $categories = get_categories( [ 'hide_empty' => false ] );
216 $category_names = array_map( function ( $cat ) {
217 return $cat->name;
218 }, $categories );
219
220 // Get all tags
221 $tags = get_tags( [ 'hide_empty' => false ] );
222 $tag_names = array_map( function ( $tag ) {
223 return $tag->name;
224 }, $tags );
225
226 return [
227 'categories' => $category_names,
228 'tags' => $tag_names
229 ];
230 }
231
232 private function generate_keyword_tiers( $text, $website_context = '' ) {
233 $context = $this->get_site_context();
234
235 $message = "Generate 40 progressive search queries for: \"$text\"\n\n";
236
237 if ( !empty( $website_context ) ) {
238 $message .= 'Website Context: ' . $website_context . "\n\n";
239 }
240
241 if ( !empty( $context['categories'] ) ) {
242 $message .= 'Site Categories: ' . implode( ', ', array_slice( $context['categories'], 0, 20 ) ) . "\n";
243 }
244 if ( !empty( $context['tags'] ) ) {
245 $message .= 'Site Tags: ' . implode( ', ', array_slice( $context['tags'], 0, 20 ) ) . "\n";
246 }
247
248 $message .= "\nCreate 40 search queries optimized for WordPress search:\n";
249 $message .= "- WordPress searches for EXACT WORDS in post content, not concepts\n";
250 $message .= "- Use SIMPLE, COMMON words that authors actually write in posts\n";
251 $message .= "- Avoid complex phrases, technical terms, or descriptive adjectives\n";
252 $message .= "- Focus on core nouns, verbs, and basic adjectives\n";
253 $message .= "- Consider the website context and categories\n";
254 $message .= "- HIGH SCORES (100-70): 3 keywords that are really good matches\n";
255 $message .= "- MEDIUM SCORES (69-31): 2 keywords for broader matches\n";
256 $message .= "- LOW SCORES (30 and below): 1 keyword for broadest possible matches\n";
257 $message .= "- Each line must be unique\n";
258 $message .= "- Format exactly as: SCORE: keyword1 keyword2 keyword3\n";
259 $message .= "- Do NOT add any other text, bullets, or formatting\n\n";
260
261 $message .= "Example for 'funny adventure game' on a gaming website:\n";
262 $message .= "100: funny adventure game\n";
263 $message .= "97: adventure game comedy\n";
264 $message .= "94: comedy adventure game\n";
265 $message .= "91: funny game adventure\n";
266 $message .= "88: adventure comedy game\n";
267 $message .= "85: game adventure funny\n";
268 $message .= "82: humor adventure game\n";
269 $message .= "79: adventure game humor\n";
270 $message .= "76: funny adventure quest\n";
271 $message .= "73: comedy game adventure\n";
272 $message .= "70: adventure game fun\n";
273 $message .= "67: adventure game\n";
274 $message .= "64: funny game\n";
275 $message .= "61: comedy game\n";
276 $message .= "58: adventure comedy\n";
277 $message .= "55: game humor\n";
278 $message .= "52: funny adventure\n";
279 $message .= "49: adventure quest\n";
280 $message .= "46: game comedy\n";
281 $message .= "43: adventure story\n";
282 $message .= "40: game fun\n";
283 $message .= "37: funny story\n";
284 $message .= "34: comedy quest\n";
285 $message .= "31: adventure play\n";
286 $message .= "30: adventure\n";
287 $message .= "29: game\n";
288 $message .= "28: funny\n";
289 $message .= "27: comedy\n";
290 $message .= "26: quest\n";
291 $message .= "25: humor\n";
292 $message .= "24: story\n";
293 $message .= "23: play\n";
294 $message .= "22: fun\n";
295 $message .= "21: character\n";
296 $message .= "20: world\n";
297 $message .= "19: level\n";
298 $message .= "18: puzzle\n";
299 $message .= "17: island\n";
300 $message .= "16: pirate\n";
301 $message .= "15: monkey\n\n";
302
303 $message .= "IMPORTANT: Generate EXACTLY 40 queries following this format. Start at 100 and decrease by 2-3 points each time.\n";
304 $message .= "Now generate 40 queries for \"$text\":\n";
305
306 $query = new Meow_MWAI_Query_Text( $message );
307 $query->set_max_tokens( 2000 ); // Use max_tokens instead of max_results
308
309 try {
310 $reply = $this->core->run_query( $query );
311 if ( !empty( $reply->result ) ) {
312 $parsed = $this->parse_search_queries( $reply->result );
313 if ( $parsed !== null ) {
314 return $parsed;
315 }
316 }
317 }
318 catch ( Exception $e ) {
319 error_log( 'AI Engine Search: Failed to generate search queries - ' . $e->getMessage() );
320 }
321
322 // Fallback
323 return $this->fallback_keyword_tiers( $text );
324 }
325
326 private function parse_search_queries( $ai_response ) {
327 $searches = [];
328
329 $lines = explode( "\n", $ai_response );
330 foreach ( $lines as $line ) {
331 $line = trim( $line );
332 if ( empty( $line ) ) {
333 continue;
334 }
335
336 // Parse lines like "100: keyword1 keyword2 keyword3"
337 // Also handle lines that might start with a dash or bullet
338 $line = preg_replace( '/^[-•*]\s*/', '', $line );
339
340 if ( preg_match( '/^(\d+)\s*:\s*(.+)$/', $line, $matches ) ) {
341 $score = intval( $matches[1] );
342 $keywords = trim( $matches[2] );
343
344 if ( $score >= 0 && $score <= 100 && !empty( $keywords ) ) {
345 $searches[] = [
346 'score' => $score,
347 'keywords' => $keywords
348 ];
349 }
350 }
351 }
352
353 // If we got good results, return them
354 if ( count( $searches ) >= 5 ) {
355 // Sort by score descending
356 usort( $searches, function ( $a, $b ) {
357 return $b['score'] <=> $a['score'];
358 } );
359 return $searches;
360 }
361
362 // Otherwise use fallback
363 return null;
364 }
365
366 private function fallback_keyword_tiers( $text ) {
367 // Extract meaningful words
368 $words = str_word_count( strtolower( $text ), 1 );
369
370 // Remove stop words
371 $stop_words = [ 'i', 'me', 'my', 'we', 'our', 'you', 'your', 'he', 'she', 'it', 'they',
372 'want', 'need', 'like', 'love', 'to', 'a', 'an', 'the', 'with', 'is',
373 'for', 'of', 'and', 'or', 'but', 'in', 'on', 'at', 'which', 'that' ];
374
375 $meaningful = array_diff( $words, $stop_words );
376 $meaningful = array_values( $meaningful );
377
378 // Synonyms for common terms - focused on actual search intent
379 $synonyms = [
380 'game' => ['games', 'gaming', 'play', 'gameplay'],
381 'space' => ['galaxy', 'universe', 'cosmic', 'stellar', 'sci-fi'],
382 'funny' => ['humor', 'comedy', 'fun', 'humorous'],
383 'adventure' => ['quest', 'journey', 'exploration', 'story'],
384 'huge' => ['large', 'big', 'massive', 'vast'],
385 'world' => ['universe', 'realm', 'map', 'environment']
386 ];
387
388 // Create 40 search queries directly for better fallback
389 $search_queries = [];
390 $score = 100;
391
392 // If we have no meaningful words, use the original text
393 if ( empty( $meaningful ) ) {
394 $meaningful = $words;
395 if ( empty( $meaningful ) ) {
396 // Last resort: split the original text
397 $meaningful = explode( ' ', $text );
398 }
399 }
400
401 // First batch: exact words from query (100-85)
402 if ( count( $meaningful ) >= 1 ) {
403 // 4-5 keywords
404 for ( $i = 0; $i < 5 && $score >= 85; $i++ ) {
405 $keywords = [];
406 shuffle( $meaningful );
407 $num_keywords = min( 4 + rand( 0, 1 ), count( $meaningful ) );
408 $keywords = array_slice( $meaningful, 0, max( 1, $num_keywords ) );
409 if ( count( $keywords ) >= 1 ) {
410 $search_queries[] = [
411 'score' => $score,
412 'keywords' => implode( ' ', $keywords )
413 ];
414 $score -= 3;
415 }
416 }
417 }
418
419 // Second batch: mix exact with synonyms (84-60)
420 $all_related = $meaningful;
421 foreach ( $meaningful as $word ) {
422 if ( isset( $synonyms[$word] ) ) {
423 $all_related = array_merge( $all_related, array_slice( $synonyms[$word], 0, 2 ) );
424 }
425 }
426 $all_related = array_unique( $all_related );
427
428 for ( $i = 0; $i < 15 && $score >= 60; $i++ ) {
429 shuffle( $all_related );
430 $num_keywords = 3 + rand( 0, 1 );
431 $keywords = array_slice( $all_related, 0, min( $num_keywords, count( $all_related ) ) );
432 if ( count( $keywords ) >= 2 ) {
433 $search_queries[] = [
434 'score' => $score,
435 'keywords' => implode( ' ', $keywords )
436 ];
437 $score -= 2;
438 }
439 }
440
441 // Third batch: fewer keywords (59-30)
442 for ( $i = 0; $i < 20 && $score >= 30; $i++ ) {
443 shuffle( $all_related );
444 $keywords = array_slice( $all_related, 0, 2 );
445 if ( count( $keywords ) >= 2 ) {
446 $search_queries[] = [
447 'score' => $score,
448 'keywords' => implode( ' ', $keywords )
449 ];
450 $score -= 2;
451 }
452 }
453
454 // If we couldn't generate any searches, create at least one with the original text
455 if ( empty( $search_queries ) ) {
456 $search_queries[] = [
457 'score' => 100,
458 'keywords' => $text
459 ];
460 }
461
462 // Return search queries in same format as AI would generate
463 return array_slice( $search_queries, 0, 40 );
464 }
465
466 private function create_search_combinations( $searches, $max_searches = 40 ) {
467 // If we have AI-generated searches, use them directly
468 if ( is_array( $searches ) && !empty( $searches ) && isset( $searches[0] ) && isset( $searches[0]['keywords'] ) ) {
469 $combinations = [];
470 foreach ( $searches as $search ) {
471 // Skip empty keywords
472 if ( !empty( trim( $search['keywords'] ) ) ) {
473 $combinations[] = [
474 'keywords' => $search['keywords'],
475 'score' => $search['score'],
476 'strategy' => 'ai_generated'
477 ];
478 }
479 if ( count( $combinations ) >= $max_searches ) {
480 break;
481 }
482 }
483 // If we have some combinations, return them
484 if ( !empty( $combinations ) ) {
485 return $combinations;
486 }
487 }
488
489 // Otherwise, this is the fallback format, generate combinations
490 $tiers = $searches;
491 $combinations = [];
492 $exact = $tiers['exact'] ?? [];
493 $contextual = $tiers['contextual'] ?? [];
494 $general = $tiers['general'] ?? [];
495
496 // Simple fallback algorithm
497 $search_count = 0;
498
499 // Mix of different combinations - start at 100
500 $strategies = [
501 [ 'exact' => 4, 'score' => 100 ],
502 [ 'exact' => 3, 'score' => 90 ],
503 [ 'exact' => 2, 'contextual' => 2, 'score' => 80 ],
504 [ 'exact' => 2, 'score' => 70 ],
505 [ 'exact' => 1, 'contextual' => 2, 'score' => 60 ],
506 [ 'contextual' => 2, 'score' => 50 ],
507 [ 'exact' => 1, 'general' => 1, 'score' => 40 ],
508 [ 'general' => 2, 'score' => 35 ]
509 ];
510
511 foreach ( $strategies as $strategy ) {
512 for ( $i = 0; $i < 5 && $search_count < $max_searches; $i++ ) {
513 $keywords = [];
514
515 if ( isset( $strategy['exact'] ) && count( $exact ) >= $strategy['exact'] ) {
516 shuffle( $exact );
517 $keywords = array_merge( $keywords, array_slice( $exact, 0, $strategy['exact'] ) );
518 }
519
520 if ( isset( $strategy['contextual'] ) && count( $contextual ) >= $strategy['contextual'] ) {
521 shuffle( $contextual );
522 $keywords = array_merge( $keywords, array_slice( $contextual, 0, $strategy['contextual'] ) );
523 }
524
525 if ( isset( $strategy['general'] ) && count( $general ) >= $strategy['general'] ) {
526 shuffle( $general );
527 $keywords = array_merge( $keywords, array_slice( $general, 0, $strategy['general'] ) );
528 }
529
530 if ( count( $keywords ) >= 2 ) {
531 $keywords_str = implode( ' ', $keywords );
532 if ( !empty( trim( $keywords_str ) ) ) {
533 $combinations[] = [
534 'keywords' => $keywords_str,
535 'score' => $strategy['score'] + rand( -5, 5 ),
536 'strategy' => 'fallback'
537 ];
538 $search_count++;
539 }
540 }
541 }
542 }
543
544 // If we have no combinations, create at least one from whatever we have
545 if ( empty( $combinations ) ) {
546 // Try to create a basic search from exact keywords
547 if ( !empty( $exact ) ) {
548 $combinations[] = [
549 'keywords' => implode( ' ', array_slice( $exact, 0, 3 ) ),
550 'score' => 50,
551 'strategy' => 'fallback_emergency'
552 ];
553 }
554 // Or from the original text
555 else if ( is_string( $searches ) && !empty( trim( $searches ) ) ) {
556 $combinations[] = [
557 'keywords' => $searches, // This will be the original text in worst case
558 'score' => 30,
559 'strategy' => 'fallback_original'
560 ];
561 }
562 }
563
564 return array_slice( $combinations, 0, $max_searches );
565 }
566
567 private function get_combinations( $array, $length ) {
568 if ( $length == 1 ) {
569 return array_map( function ( $el ) { return [ $el ]; }, $array );
570 }
571
572 $combinations = [];
573 $array_length = count( $array );
574
575 for ( $i = 0; $i < $array_length - $length + 1; $i++ ) {
576 $head = array_slice( $array, $i, 1 );
577 $tail_combinations = $this->get_combinations( array_slice( $array, $i + 1 ), $length - 1 );
578 foreach ( $tail_combinations as $tail ) {
579 $combinations[] = array_merge( $head, $tail );
580 }
581 }
582
583 return $combinations;
584 }
585
586 private function search_with_keywords( $search_queries ) {
587 $all_results = [];
588 $searches_performed = 0;
589 $max_searches = 40;
590 $min_results_needed = 3;
591
592 // Create search combinations with scores
593 $search_combinations = $this->create_search_combinations( $search_queries, $max_searches );
594
595 $debug_searches = [];
596
597 foreach ( $search_combinations as $combination ) {
598 $searches_performed++;
599 $keywords = $combination['keywords'];
600 $score = $combination['score'];
601 $strategy = $combination['strategy'];
602
603 // Record what we're searching
604 $debug_searches[] = [
605 'attempt' => $searches_performed,
606 'keywords' => $keywords,
607 'score' => $score,
608 'strategy' => $strategy,
609 'found' => 0
610 ];
611
612 // Perform the search
613 $posts = get_posts( [
614 's' => $keywords,
615 'posts_per_page' => 10,
616 'post_type' => 'post',
617 'post_status' => 'publish',
618 'fields' => 'ids'
619 ] );
620
621 if ( !empty( $posts ) ) {
622 // Update found count
623 $debug_searches[count( $debug_searches ) - 1]['found'] = count( $posts );
624
625 // Add to results with score
626 foreach ( $posts as $post_id ) {
627 if ( !isset( $all_results[$post_id] ) ) {
628 $all_results[$post_id] = [
629 'id' => $post_id,
630 'best_score' => $score,
631 'found_with' => []
632 ];
633 }
634 else {
635 // Keep the best (highest) score
636 if ( $score > $all_results[$post_id]['best_score'] ) {
637 $all_results[$post_id]['best_score'] = $score;
638 }
639 }
640 $all_results[$post_id]['found_with'][] = [
641 'keywords' => $keywords,
642 'score' => $score
643 ];
644 }
645
646 // Stop if we have enough unique results
647 if ( count( $all_results ) >= $min_results_needed ) {
648 break;
649 }
650 }
651
652 // Stop if we've done too many searches
653 if ( $searches_performed >= $max_searches ) {
654 break;
655 }
656 }
657
658 // Sort results by score (highest first)
659 uasort( $all_results, function ( $a, $b ) {
660 return $b['best_score'] <=> $a['best_score'];
661 } );
662
663 // Get full post data for results
664 $final_results = [];
665 $post_ids = array_keys( $all_results );
666
667 if ( !empty( $post_ids ) ) {
668 // Get posts but maintain our score order
669 $posts_data = [];
670 $posts = get_posts( [
671 'post__in' => $post_ids,
672 'posts_per_page' => 20,
673 'post_type' => 'post',
674 'post_status' => 'publish'
675 ] );
676
677 // Create a map for easy access
678 foreach ( $posts as $post ) {
679 $posts_data[$post->ID] = $post;
680 }
681
682 // Build results in score order
683 foreach ( $post_ids as $post_id ) {
684 if ( isset( $posts_data[$post_id] ) ) {
685 $post = $posts_data[$post_id];
686 $result_data = $all_results[$post_id];
687
688 // Get the keywords that found this post with the best score
689 $best_keywords = '';
690 foreach ( $result_data['found_with'] as $found ) {
691 if ( $found['score'] == $result_data['best_score'] ) {
692 $best_keywords = $found['keywords'];
693 break;
694 }
695 }
696
697 $final_results[] = [
698 'id' => $post->ID,
699 'title' => get_the_title( $post ),
700 'excerpt' => wp_trim_words( $post->post_content, 30 ),
701 'score' => $result_data['best_score'] / 100, // Convert 0-100 to 0-1 for frontend consistency
702 'found_with' => $best_keywords
703 ];
704 }
705 }
706 }
707
708 return [
709 'results' => $final_results,
710 'debug' => [
711 'total_searches' => $searches_performed,
712 'keyword_tiers' => is_array( $search_queries ) && isset( $search_queries['exact'] ) ? $search_queries : null,
713 'searches' => $debug_searches
714 ]
715 ];
716 }
717
718 private function search_with_embeddings( $search_text, $env_id = null ) {
719 if ( !class_exists( 'MeowPro_MWAI_Embeddings' ) ) {
720 return [ 'error' => 'Embeddings module not available' ];
721 }
722
723 // Validate environment exists
724 if ( !$env_id ) {
725 return [ 'error' => 'No embeddings environment selected' ];
726 }
727
728 $env = $this->core->get_embeddings_env( $env_id );
729 if ( !$env ) {
730 return [ 'error' => 'Invalid embeddings environment selected. Please select a valid environment.' ];
731 }
732
733 try {
734 // Get the embeddings instance
735 $embeddings = new MeowPro_MWAI_Embeddings( $this->core );
736
737 // Use the query_vectors method which handles everything internally
738 // Parameters: offset, limit, filters, sort
739 $filters = [
740 'envId' => $env_id,
741 'search' => $search_text
742 ];
743 $result = $embeddings->query_vectors( 0, 20, $filters );
744
745 $vectors = isset( $result['rows'] ) ? $result['rows'] : [];
746
747 if ( empty( $vectors ) ) {
748 return [
749 'post_ids' => [],
750 'debug' => [
751 'total_vectors' => 0,
752 'message' => 'No matching vectors found'
753 ]
754 ];
755 }
756
757 // Extract post IDs from results
758 $post_ids = [];
759 $debug_info = [];
760
761 foreach ( $vectors as $vector ) {
762 $debug_info[] = [
763 'refId' => $vector['refId'] ?? 'unknown',
764 'score' => $vector['score'] ?? 0,
765 'type' => $vector['type'] ?? 'unknown',
766 'title' => $vector['title'] ?? 'unknown'
767 ];
768
769 // Check if this is a post embedding
770 if ( !empty( $vector['type'] ) && $vector['type'] === 'postId' && !empty( $vector['refId'] ) ) {
771 $score = isset( $vector['score'] ) ? (float) $vector['score'] : 0;
772 $post_ids[] = [
773 'id' => (int) $vector['refId'],
774 'score' => $score
775 ];
776 }
777 }
778
779 // Sort by score descending
780 usort( $post_ids, function ( $a, $b ) {
781 return $b['score'] <=> $a['score'];
782 } );
783
784 return [
785 'post_ids' => $post_ids,
786 'debug' => [
787 'total_vectors' => count( $vectors ),
788 'filtered_posts' => count( $post_ids ),
789 'sample_vectors' => array_slice( $debug_info, 0, 5 )
790 ]
791 ];
792 }
793 catch ( Exception $e ) {
794 error_log( 'AI Engine Search: Embeddings search failed - ' . $e->getMessage() );
795 return [ 'error' => 'Embeddings search failed: ' . $e->getMessage() ];
796 }
797 }
798
799 public function rest_search( $request ) {
800 $params = $request->get_json_params();
801 $search = isset( $params['search'] ) ? sanitize_text_field( $params['search'] ) : '';
802 $method = isset( $params['method'] ) ? $params['method'] : 'wordpress';
803 $env_id = isset( $params['envId'] ) ? $params['envId'] : null;
804 $website_context = isset( $params['websiteContext'] ) ? sanitize_text_field( $params['websiteContext'] ) : '';
805
806 if ( empty( $search ) ) {
807 return new WP_REST_Response( [ 'success' => false, 'message' => 'Empty search' ], 400 );
808 }
809
810 $results = [];
811 $debug_info = [];
812
813 if ( $method === 'wordpress' ) {
814 // Use standard WordPress search
815 $posts = get_posts( [
816 's' => $search,
817 'posts_per_page' => 20,
818 'post_type' => 'post',
819 'post_status' => 'publish'
820 ] );
821
822 foreach ( $posts as $post ) {
823 $results[] = [
824 'id' => $post->ID,
825 'title' => get_the_title( $post ),
826 'excerpt' => wp_trim_words( $post->post_content, 30 )
827 ];
828 }
829
830 $debug_info = [
831 'method' => 'Standard WordPress search',
832 'query' => $search,
833 'found' => count( $posts )
834 ];
835 }
836 elseif ( $method === 'embeddings' ) {
837 // Search using embeddings
838 $embedding_result = $this->search_with_embeddings( $search, $env_id );
839
840 if ( isset( $embedding_result['error'] ) ) {
841 return new WP_REST_Response( [
842 'success' => false,
843 'message' => $embedding_result['error'],
844 'debug' => $embedding_result['debug'] ?? null
845 ], 200 );
846 }
847
848 $debug_info = $embedding_result['debug'] ?? [];
849
850 if ( !empty( $embedding_result['post_ids'] ) ) {
851 $post_ids = array_map( function ( $result ) {
852 return $result['id'];
853 }, $embedding_result['post_ids'] );
854
855 $posts = get_posts( [
856 'post__in' => $post_ids,
857 'orderby' => 'post__in',
858 'posts_per_page' => count( $post_ids ),
859 'post_type' => 'post',
860 'post_status' => 'publish'
861 ] );
862
863 // Map posts with their scores
864 $score_map = [];
865 foreach ( $embedding_result['post_ids'] as $result ) {
866 $score_map[$result['id']] = $result['score'];
867 }
868
869 foreach ( $posts as $post ) {
870 $results[] = [
871 'id' => $post->ID,
872 'title' => get_the_title( $post ),
873 'excerpt' => wp_trim_words( $post->post_content, 30 ),
874 'score' => $score_map[$post->ID] ?? 0
875 ];
876 }
877 }
878 }
879 else {
880 // Search using AI keywords with smart algorithm
881 $search_queries = $this->generate_keyword_tiers( $search, $website_context );
882 $keyword_result = $this->search_with_keywords( $search_queries );
883
884 $results = $keyword_result['results'];
885 $debug_info = $keyword_result['debug'];
886 }
887
888 $response = [
889 'success' => true,
890 'results' => $results,
891 'method' => $method,
892 'debug' => $debug_info
893 ];
894
895 return new WP_REST_Response( $response, 200 );
896 }
897
898 public function customize_search_display( $search_query ) {
899 // Only customize on frontend search pages
900 if ( is_admin() || !is_search() ) {
901 return $search_query;
902 }
903
904 // Get the frontend search method setting
905 $frontend_method = $this->core->get_option( 'search_frontend_method', 'wordpress' );
906
907 // If using standard WordPress search, no customization needed
908 if ( $frontend_method === 'wordpress' ) {
909 return $search_query;
910 }
911
912 // Check if this was an AI-powered search by looking for our special markers
913 global $wp_query;
914 $current_search = $wp_query->get( 's' );
915 $original_search = $wp_query->get( 'mwai_original_search' );
916
917 // Check if current search is one of our AI search markers
918 $is_keywords_search = strpos( $search_query, 'MWAI_KEYWORDS_SEARCH_' ) === 0;
919 $is_embeddings_search = strpos( $search_query, 'MWAI_EMBEDDINGS_SEARCH_' ) === 0;
920
921 // If we have an original search stored and this is an AI search, just return the original search
922 if ( !empty( $original_search ) && ( $is_keywords_search || $is_embeddings_search ) ) {
923 return $original_search;
924 }
925
926 return $search_query;
927 }
928 }
929