get_results( $wpdb->prepare( "SELECT item_id, score FROM `{$table}` WHERE subject_id = %d ORDER BY reco_rank ASC LIMIT %d", $subject_id, max( 1, (int) $limit ) ), ARRAY_A ); $exclude = ( isset( $args['exclude'] ) && is_array( $args['exclude'] ) ) ? array_flip( array_map( 'intval', $args['exclude'] ) ) : array(); $out = array(); foreach ( (array) $rows as $r ) { $iid = (int) $r['item_id']; if ( isset( $exclude[ $iid ] ) ) { continue; } $out[] = array( 'item_id' => $iid, 'score' => (float) $r['score'] ); } return $out; } public static function lookalikes( $subject_id, $limit = 10 ) { $subject_id = (int) $subject_id; if ( $subject_id <= 0 || ! class_exists( 'AIEO_Behavioural_Vector' ) || ! class_exists( 'AI_eShop_Chat_Database_Manager' ) ) { return array(); } $vec = AIEO_Behavioural_Vector::get_vector( $subject_id ); if ( empty( $vec ) ) { return array(); } global $wpdb; $emb = $wpdb->prefix . 'aieo_chat_embeddings'; $subj = $wpdb->prefix . 'aieo_subjects'; $model = (string) get_option( 'aieo_chat_embedding_model', 'voyage-2' ); $db = AI_eShop_Chat_Database_Manager::get_instance(); $cap = (int) apply_filters( 'aieo_behavioural_pool_cap', self::LOOKALIKE_CAP ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT e.post_id, e.embedding FROM `{$emb}` e LEFT JOIN `{$subj}` s ON s.subject_id = e.post_id WHERE e.post_type = 'user' AND e.engine_type = %s AND e.post_id <> %d AND e.embedding <> '' AND COALESCE(s.merged_into, 0) = 0 ORDER BY e.updated_at DESC LIMIT %d", $model, $subject_id, $cap ), ARRAY_A ); $dim = count( $vec ); $scores = array(); foreach ( (array) $rows as $r ) { $ov = $db->decompress_embedding( $r['embedding'] ); if ( ! is_array( $ov ) || count( $ov ) !== $dim ) { continue; } $sc = self::cosine( $vec, $ov ); if ( $sc > 0 ) { $scores[ (int) $r['post_id'] ] = $sc; } } arsort( $scores ); $out = array(); foreach ( $scores as $sid => $score ) { $out[] = array( 'subject_id' => (int) $sid, 'score' => round( (float) $score, 5 ) ); if ( count( $out ) >= max( 1, (int) $limit ) ) { break; } } return $out; } public static function instigators( $limit = 100, $window_days = 14 ) { global $wpdb; if ( ! function_exists( 'aieo_interaction_summary_table' ) ) { return array(); } $is = aieo_interaction_summary_table(); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT subject_id, item_id, weight AS intent, last_at FROM `{$is}` WHERE item_type = 'product' AND conversions = 0 AND ( carts > 0 OR wishlists > 0 OR weight >= 4 ) AND last_at >= (UTC_TIMESTAMP() - INTERVAL %d DAY) ORDER BY weight DESC LIMIT %d", (int) $window_days, (int) $limit ), ARRAY_A ); $out = array(); foreach ( (array) $rows as $r ) { $out[] = array( 'subject_id' => (int) $r['subject_id'], 'item_id' => (int) $r['item_id'], 'intent' => (float) $r['intent'], 'last_at' => (string) $r['last_at'], ); } return $out; } public static function digest_items( $default, $ctx ) { $user_id = (int) ( $ctx['user_id'] ?? 0 ); $subject_id = (int) ( $ctx['subject_id'] ?? 0 ); $count = max( 1, (int) ( $ctx['count'] ?? 6 ) ); if ( $subject_id <= 0 && $user_id > 0 && class_exists( 'AIEO_Subject_Resolver' ) ) { $subject_id = (int) AIEO_Subject_Resolver::subject_for_user( $user_id ); } if ( $subject_id <= 0 ) { return $default; } $recs = self::recommend( $subject_id, $count * 3 ); if ( empty( $recs ) ) { return $default; } $items = array(); foreach ( $recs as $rec ) { if ( count( $items ) >= $count ) { break; } $pid = (int) $rec['item_id']; $card = self::product_card( $pid ); if ( $card ) { $items[] = $card; } } if ( count( $items ) < $count && is_array( $default ) ) { $have = array(); foreach ( $items as $it ) { $have[ $it['url'] ] = true; } foreach ( $default as $d ) { if ( count( $items ) >= $count ) { break; } if ( empty( $have[ $d['url'] ] ) ) { $items[] = $d; $have[ $d['url'] ] = true; } } } return ! empty( $items ) ? $items : $default; } private static function product_card( $pid ) { if ( function_exists( 'wc_get_product' ) ) { $p = wc_get_product( $pid ); if ( ! $p || ! is_object( $p ) ) { return null; } if ( 'publish' !== $p->get_status() || ! $p->is_visible() || ! $p->is_in_stock() ) { return null; } $img = $p->get_image_id(); return array( 'title' => (string) $p->get_name(), 'url' => (string) get_permalink( $pid ), 'image' => $img ? (string) wp_get_attachment_image_url( $img, 'medium' ) : '', 'meta' => class_exists( 'AIEO_Catalogue_Mode' ) && AIEO_Catalogue_Mode::prices_off( $p ) ? '' : (string) wp_strip_all_tags( wc_price( $p->get_price() ) ), ); } if ( 'publish' !== get_post_status( $pid ) ) { return null; } return array( 'title' => (string) get_the_title( $pid ), 'url' => (string) get_permalink( $pid ), 'image' => (string) get_the_post_thumbnail_url( $pid, 'medium' ), 'meta' => '', ); } private static function cosine( array $a, array $b ) { $dot = 0.0; $na = 0.0; $nb = 0.0; $n = count( $a ); for ( $i = 0; $i < $n; $i++ ) { $x = (float) $a[ $i ]; $y = (float) $b[ $i ]; $dot += $x * $y; $na += $x * $x; $nb += $y * $y; } if ( $na <= 0.0 || $nb <= 0.0 ) { return 0.0; } return $dot / ( sqrt( $na ) * sqrt( $nb ) ); } }