| @@ -41,13 +41,8 @@ | ||
| 41 | 41 | * |
| 42 | 42 | * @return bool True on success, false on failure. |
| 43 | 43 | */ |
| 44 | 44 | public function generate_and_store( $product_id, $title, $content ) { |
| 45 | - $ai_data = AIFns::activated_ai_data(); | |
| 46 | - if ( empty( $ai_data['api_key'] ) ) { | |
| 47 | - return false; | |
| 48 | - } | |
| 49 | - | |
| 50 | 45 | $text = $title . ' ' . wp_strip_all_tags( $content ); |
| 51 | 46 | $ai_service = AIFns::initializeAIService(); |
| 52 | 47 | $embedding = $ai_service->generateEmbedding( $text ); |
| 53 | 48 | if ( empty( $embedding ) || ! is_array( $embedding ) ) { |
| @@ -76,8 +71,9 @@ | ||
| 76 | 71 | return []; |
| 77 | 72 | } |
| 78 | 73 | // Log query embedding. |
| 79 | 74 | $rows = AIDB::get_all(); |
| 75 | + | |
| 80 | 76 | if ( empty( $rows ) ) { |
| 81 | 77 | return []; |
| 82 | 78 | } |
| 83 | 79 | $results = $this->find_similar( $query_embedding, $rows, $limit ); |
| @@ -97,8 +93,9 @@ | ||
| 97 | 93 | * @return array List of matched items with product ID, title, and similarity score. |
| 98 | 94 | */ |
| 99 | 95 | public function find_similar( array $embedding, array $rows, int $limit = 5 ): array { |
| 100 | 96 | $minimum_match = AIFns::get_embedding_minimum_accuracy(); |
| 97 | + $query_norm = sqrt( array_sum( array_map( static fn( $x ) => $x * $x, $embedding ) ) ); | |
| 101 | 98 | $matches = []; |
| 102 | 99 | foreach ( $rows as $row ) { |
| 103 | 100 | if ( empty( $row['embedding'] ) ) { |
| 104 | 101 | continue; |
| @@ -107,9 +104,9 @@ | ||
| 107 | 104 | if ( ! is_array( $vector ) ) { |
| 108 | 105 | continue; |
| 109 | 106 | } |
| 110 | 107 | // Avoid redundant norm computation if similarity calc includes it. |
| 111 | - $score = $this->calculate_similarity_score( $embedding, $vector ); | |
| 108 | + $score = $this->calculate_similarity_score( $embedding, $vector, $query_norm ); | |
| 112 | 109 | if ( $score >= $minimum_match ) { |
| 113 | 110 | $matches[] = [ |
| 114 | 111 | 'post_id' => isset( $row['product_id'] ) ? (int) $row['product_id'] : 0, |
| 115 | 112 | 'post_title' => isset( $row['title'] ) ? sanitize_text_field( $row['title'] ) : '', |
| @@ -133,20 +130,22 @@ | ||
| 133 | 130 | * query norm and computes the dot product and target norm in one pass. |
| 134 | 131 | * |
| 135 | 132 | * @param array $query_vec The query embedding vector. |
| 136 | 133 | * @param array $target_vec The stored embedding vector. |
| 134 | + * @param float $query_norm Precomputed L2 norm of the query vector. | |
| 137 | 135 | * |
| 138 | 136 | * @return float Cosine similarity score (0.0–1.0). |
| 139 | 137 | */ |
| 140 | - protected function calculate_similarity_score( array $query_vec, array $target_vec ): float { | |
| 138 | + protected function calculate_similarity_score( array $query_vec, array $target_vec, float $query_norm ): float { | |
| 141 | 139 | $dot = 0.0; |
| 142 | - $normA = 0.0; | |
| 143 | 140 | $normB = 0.0; |
| 144 | - $count = min( count( $query_vec ), count( $target_vec ) ); | |
| 145 | - for ( $i = 0; $i < $count; $i++ ) { | |
| 141 | + $len = min( count( $query_vec ), count( $target_vec ) ); | |
| 142 | + for ( $i = 0; $i < $len; $i++ ) { | |
| 146 | 143 | $dot += $query_vec[ $i ] * $target_vec[ $i ]; |
| 147 | - $normA += $query_vec[ $i ] ** 2; | |
| 148 | 144 | $normB += $target_vec[ $i ] ** 2; |
| 149 | 145 | } |
| 150 | - return $dot / ( sqrt( $normA ) * sqrt( $normB ) ); | |
| 146 | + if ( $query_norm <= 0.0 || $normB <= 0.0 ) { | |
| 147 | + return 0.0; | |
| 148 | + } | |
| 149 | + return $dot / ( $query_norm * sqrt( $normB ) ); | |
| 151 | 150 | } |
| 152 | 151 | } |