url = rtrim( (string) $url, '/' ); $this->key = (string) $key; $this->collection = preg_replace( '/[^a-zA-Z0-9_\-]/', '', (string) $collection ); } /** * Store ID. * * @return string */ public function get_id() { return 'qdrant'; } /** * Display name. * * @return string */ public function get_label() { return 'Qdrant'; } /** * Whether the store is configured. * * @return bool */ public function is_configured() { return '' !== $this->url && '' !== $this->key && '' !== $this->collection; } /** * Authentication headers. * * @return array */ protected function headers() { return array( 'api-key' => $this->key ); } /** * Collection URL. * * @return string */ protected function collection_url() { return $this->url . '/collections/' . rawurlencode( $this->collection ); } /** * Converts a string ID into a 63-bit integer. * * @param string $id Original ID. * @return int */ protected function numeric_id( $id ) { $hex = substr( hash( 'sha256', (string) $id ), 0, 15 ); return (int) hexdec( $hex ); } /** * 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', __( 'Qdrant is not configured.', 'aihouse' ) ); } if ( empty( $records ) ) { return true; } $points = array(); foreach ( $records as $record ) { $metadata = isset( $record['metadata'] ) && is_array( $record['metadata'] ) ? $record['metadata'] : array(); $metadata['content'] = isset( $record['content'] ) ? (string) $record['content'] : ''; $metadata['source_id'] = isset( $metadata['source_id'] ) ? (int) $metadata['source_id'] : 0; $points[] = array( 'id' => $this->numeric_id( $record['id'] ), 'vector' => isset( $record['values'] ) ? $record['values'] : array(), 'payload' => $metadata, ); } $response = MYBOT_HTTP::post_json( $this->collection_url() . '/points?wait=true', array( 'points' => $points ), $this->headers(), 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', __( 'Qdrant is not configured.', 'aihouse' ) ); } $body = array( 'vector' => $vector, 'limit' => max( 1, (int) $top_k ), 'with_payload' => true, 'score_threshold' => (float) $min_score, ); $response = MYBOT_HTTP::post_json( $this->collection_url() . '/points/search', $body, $this->headers(), 15 ); if ( is_wp_error( $response ) ) { return $response; } $rows = isset( $response['result'] ) && is_array( $response['result'] ) ? $response['result'] : array(); $results = array(); foreach ( $rows as $row ) { $payload = isset( $row['payload'] ) && is_array( $row['payload'] ) ? $row['payload'] : array(); $content = isset( $payload['content'] ) ? (string) $payload['content'] : ''; unset( $payload['content'] ); $results[] = array( 'id' => isset( $row['id'] ) ? (string) $row['id'] : '', 'score' => isset( $row['score'] ) ? (float) $row['score'] : 0.0, 'content' => $content, 'metadata' => $payload, ); } return $results; } /** * Deletes by a list of IDs. * * @param array $ids List of IDs. * @return true|WP_Error */ public function delete( $ids ) { $ids = array_values( array_filter( array_map( 'strval', (array) $ids ), 'strlen' ) ); if ( empty( $ids ) ) { return true; } $numeric = array(); foreach ( $ids as $id ) { $numeric[] = $this->numeric_id( $id ); } return $this->delete_request( array( 'points' => $numeric ) ); } /** * 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 ) { return $this->delete_request( array( 'filter' => array( 'must' => array( array( 'key' => 'source_id', 'match' => array( 'value' => (int) absint( $source_id ) ), ), ), ), ) ); } /** * Sends a delete request. * * @param array $body Request body. * @return true|WP_Error */ protected function delete_request( $body ) { if ( ! $this->is_configured() ) { return new WP_Error( 'mybot_vector_unconfigured', __( 'Qdrant is not configured.', 'aihouse' ) ); } $response = MYBOT_HTTP::post_json( $this->collection_url() . '/points/delete?wait=true', $body, $this->headers(), 15 ); return is_wp_error( $response ) ? $response : true; } }