>, source:string, code:string, message:string} */ public function search( string $text, string $token, int $top_k = 30, string $language = 'en' ): array { $lexical = $this->lexical( $text, $token, $top_k, $language ); if ( $lexical['ok'] || ! $this->lexical_disabled( $lexical ) ) { return $lexical; } return $this->semantic( $text, $token, $top_k, $language ); } /** * Lexical search. * * @param string $text What the visitor typed. * @param string $token The visitor's bearer. * @param int $top_k How many hits to ask for. * @param string $language Language id for the query. * * @return array{ok:bool, hits:array>, source:string, code:string, message:string} */ public function lexical( string $text, string $token, int $top_k = 30, string $language = 'en' ): array { return $this->query( '/knowledge/query-lexical', $token, array( 'text' => $text, 'topK' => min( $top_k, self::MAX_TOP_K ), 'tags' => null, 'languageId' => $language, 'includeSnippets' => true, ), 'lexical' ); } /** * Semantic search — what Ask retrieves with, and the lexical fallback. * * @param string $text What the visitor typed. * @param string $token The visitor's bearer. * @param int $top_k How many hits to ask for. * @param string $language Language id for the query. * * @return array{ok:bool, hits:array>, source:string, code:string, message:string} */ public function semantic( string $text, string $token, int $top_k = 30, string $language = 'en' ): array { return $this->query( '/knowledge/query', $token, array( 'text' => $text, // 07-D5: the platform's rewritten description is not what the // visitor searched against, and not what they should read. 'includeDescription' => false, 'topK' => min( $top_k, self::MAX_TOP_K ), 'tags' => null, 'languageId' => $language, ), 'semantic' ); } /** * One call, with the hits pulled out of whichever shape came back. * * @param string $path Endpoint after the workspace. * @param string $token The visitor's bearer. * @param array $body Request body. * @param string $source Which engine this is. * * @return array{ok:bool, hits:array>, source:string, code:string, message:string} */ private function query( string $path, string $token, array $body, string $source ): array { $result = ( new Client( $this->connection ) )->request( 'POST', $path, array( 'headers' => array( 'Authorization' => 'Bearer ' . $token ), 'body' => $body, 'timeout' => self::TIMEOUT, ) ); if ( ! $result['ok'] ) { return array( 'ok' => false, 'hits' => array(), 'source' => $source, 'code' => $result['code'], 'message' => $result['message'], ); } return array( 'ok' => true, 'hits' => self::hits( $result['data'] ), 'source' => $source, 'code' => '', 'message' => '', ); } /** * The hits, whichever key the answer wrapped them in. * * A bare list, `{results: […]}` and `{knowledges: […]}` have all been seen * on this contract. Reading all three is cheaper than a page that goes * blank the day one of them changes. * * @param mixed $data The decoded body. * * @return array> */ private static function hits( $data ): array { if ( ! is_array( $data ) ) { return array(); } foreach ( array( 'results', 'knowledges', 'hits', 'items' ) as $key ) { if ( isset( $data[ $key ] ) && is_array( $data[ $key ] ) ) { $data = $data[ $key ]; break; } } if ( ! array_is_list( $data ) ) { return array(); } return array_values( array_filter( $data, 'is_array' ) ); } /** * Whether a failure is the platform saying lexical is off here. * * @param array{ok:bool, code:string, message:string} $result A failed call. */ private function lexical_disabled( array $result ): bool { return str_contains( $result['code'] . ' ' . $result['message'], 'LEXICAL_SEARCH_DISABLED' ); } }