url = rtrim( (string) $url, '/' ); $this->key = (string) $key; /** * Change the name of the table that stores vectors. * * @param string $table Table name. */ $this->table = (string) apply_filters( 'mybot_supabase_table', $this->table ); } /** * Store ID. * * @return string */ public function get_id() { return 'supabase'; } /** * Display name. * * @return string */ public function get_label() { return 'Supabase (pgvector)'; } /** * Whether the store is configured. * * @return bool */ public function is_configured() { return '' !== $this->url && '' !== $this->key; } /** * Authentication headers. * * @param bool $prefer_merge Whether to send the merge-duplicates header. * @return array */ protected function headers( $prefer_merge = false ) { $headers = array( 'apikey' => $this->key, 'Authorization' => 'Bearer ' . $this->key, ); if ( $prefer_merge ) { $headers['Prefer'] = 'resolution=merge-duplicates,return=minimal'; } return $headers; } /** * Writes vectors. * * @param array $records List of records. * @return true|WP_Error */ public function upsert( $records ) { if ( ! $this->is_configured() ) { return new WP_Error( 'mybot_vector_unconfigured', __( 'Supabase is not configured.', 'aihouse' ) ); } if ( empty( $records ) ) { return true; } $rows = array(); foreach ( $records as $record ) { $rows[] = array( 'id' => (string) $record['id'], 'content' => isset( $record['content'] ) ? (string) $record['content'] : '', 'metadata' => isset( $record['metadata'] ) ? $record['metadata'] : array(), 'embedding' => isset( $record['values'] ) ? $record['values'] : array(), ); } $response = MYBOT_HTTP::post_json( $this->url . '/rest/v1/' . rawurlencode( $this->table ), $rows, $this->headers( true ), 20, 1 ); return is_wp_error( $response ) ? $response : true; } /** * Queries vectors. * * @param array $vector Query vector. * @param int $top_k Number of results. * @param float $min_score Score threshold. * @return array|WP_Error */ public function query( $vector, $top_k = 4, $min_score = 0.0 ) { if ( ! $this->is_configured() ) { return new WP_Error( 'mybot_vector_unconfigured', __( 'Supabase is not configured.', 'aihouse' ) ); } $response = MYBOT_HTTP::post_json( $this->url . '/rest/v1/rpc/match_mybot_documents', array( 'query_embedding' => $vector, 'match_count' => max( 1, (int) $top_k ), 'min_score' => (float) $min_score, ), $this->headers(), 15 ); if ( is_wp_error( $response ) ) { return $response; } // PostgREST returns either a bare array or a payload wrapped in 'data', depending on the configuration. $rows = isset( $response['data'] ) && is_array( $response['data'] ) ? $response['data'] : $response; $results = array(); foreach ( (array) $rows as $row ) { if ( ! is_array( $row ) ) { continue; } $results[] = array( 'id' => isset( $row['id'] ) ? (string) $row['id'] : '', 'score' => isset( $row['score'] ) ? (float) $row['score'] : 0.0, 'content' => isset( $row['content'] ) ? (string) $row['content'] : '', 'metadata' => isset( $row['metadata'] ) ? $row['metadata'] : array(), ); } return $results; } /** * Deletes by a list of IDs. * * @param array $ids List of IDs. * @return true|WP_Error */ public function delete( $ids ) { if ( ! $this->is_configured() ) { return new WP_Error( 'mybot_vector_unconfigured', __( 'Supabase is not configured.', 'aihouse' ) ); } $ids = array_values( array_filter( array_map( 'strval', (array) $ids ), 'strlen' ) ); if ( empty( $ids ) ) { return true; } $query = '?id=in.(' . implode( ',', array_map( 'rawurlencode', $ids ) ) . ')'; $response = wp_remote_request( $this->url . '/rest/v1/' . rawurlencode( $this->table ) . $query, array( 'method' => 'DELETE', 'timeout' => 15, 'headers' => $this->headers(), ) ); if ( is_wp_error( $response ) ) { return $response; } $code = (int) wp_remote_retrieve_response_code( $response ); return ( $code >= 200 && $code < 300 ) ? true /* translators: %d: the HTTP status code Supabase returned */ : new WP_Error( 'mybot_vector_delete', sprintf( __( 'Supabase returned status %d.', 'aihouse' ), $code ) ); } /** * Deletes every vector that belongs to a post. * * @param int $source_id Post ID. * @return true|WP_Error */ public function delete_by_source( $source_id ) { if ( ! $this->is_configured() ) { return new WP_Error( 'mybot_vector_unconfigured', __( 'Supabase is not configured.', 'aihouse' ) ); } $query = '?metadata->>source_id=eq.' . rawurlencode( (string) absint( $source_id ) ); $response = wp_remote_request( $this->url . '/rest/v1/' . rawurlencode( $this->table ) . $query, array( 'method' => 'DELETE', 'timeout' => 15, 'headers' => $this->headers(), ) ); if ( is_wp_error( $response ) ) { return $response; } $code = (int) wp_remote_retrieve_response_code( $response ); return ( $code >= 200 && $code < 300 ) ? true /* translators: %d: the HTTP status code Supabase returned */ : new WP_Error( 'mybot_vector_delete', sprintf( __( 'Supabase returned status %d.', 'aihouse' ), $code ) ); } }