diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml index 5e9a2151..3c51ffdc 100644 --- a/.github/workflows/links.yml +++ b/.github/workflows/links.yml @@ -13,13 +13,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: lychee Link Checker id: lychee - uses: lycheeverse/lychee-action@v1.0.8 + uses: lycheeverse/lychee-action@v1 with: args: --accept=200,403,429 "**/*.html" "**/*.md" "**/*.txt" "**/*.json" --exclude "https://github.com/\[your*" --exclude "https://localhost:9200" --exclude-mail env: GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - name: Fail if there were link errors - run: exit ${{ steps.lychee.outputs.exit_code }} \ No newline at end of file + run: exit ${{ steps.lychee.outputs.exit_code }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dcbf559..0d1ba7d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Added the `RefreshSearchAnalyzers` endpoint ([[#152](https://github.com/opensearch-project/opensearch-php/issues/152)) - Added support for `format` parameter to specify the sql response format ([#161](https://github.com/opensearch-project/opensearch-php/pull/161)) - Added ml commons model, model group and connector APIs ([#170](https://github.com/opensearch-project/opensearch-php/pull/170)) +- [Feature] Send raw json request ([#171](https://github.com/opensearch-project/opensearch-php/pull/177)) ### Changed diff --git a/guides/raw-request.md b/guides/raw-request.md new file mode 100644 index 00000000..5424f8cf --- /dev/null +++ b/guides/raw-request.md @@ -0,0 +1,50 @@ +# Raw JSON Requests + +Opensearch client implements many high-level APIs out of the box. However, there are times when you need to send a raw +JSON request to the server. This can be done using the `request()` method of the client. + +The `request()` method expects the following parameters: + +| Parameter | Description | +|---------------|------------------------------------------------------------------------------| +| `$method` | The HTTP method to use for the request, `GET`, `POST`, `PUT`, `DELETE`, etc. | +| `$uri` | The URI to send the request to, e.g. `/_search`. | +| `$attributes` | An array of request options. `body`, `params` & `options` | + +> ![NOTE] +> The `request()` is a wrapper around the `\OpenSearch\Transport::performRequest()` method, this explains why [the +> parameters](https://github.com/opensearch-project/opensearch-php/blob/bf9b9815e9636196295432baa9c0368bc2efadd8/src/OpenSearch/Transport.php#L99) +> are intentionally similar. + +Most of the time, you will only need to provide the `body` attribute. The `body` attribute is the JSON payload to send +to the server. The `params` attribute is used to set query parameters, and the `options` attribute is used to set +additional request options. + +To send a raw JSON request you need to map the documentation to the `request()` method parameters. For example, [this +request](https://opensearch.org/docs/2.12/search-plugins/keyword-search/#example) that query searches for the +words `long live king` in the `shakespeare` index: + +``` +GET shakespeare/_search +{ + "query": { + "match": { + "text_entry": "long live king" + } + } +} +``` + +Can be translated to the following `request()` method call: + +```php +$response = $client->request('GET', '/shakespeare/_search', [ + 'body' => [ + 'query' => [ + 'match' => [ + 'text_entry' => 'long live king' + ] + ] + ] +]); +``` diff --git a/src/OpenSearch/Client.php b/src/OpenSearch/Client.php index abb88934..91215886 100644 --- a/src/OpenSearch/Client.php +++ b/src/OpenSearch/Client.php @@ -22,31 +22,26 @@ namespace OpenSearch; use OpenSearch\Common\Exceptions\BadMethodCallException; -use OpenSearch\Common\Exceptions\InvalidArgumentException; use OpenSearch\Common\Exceptions\NoNodesAvailableException; -use OpenSearch\Common\Exceptions\BadRequest400Exception; -use OpenSearch\Common\Exceptions\Missing404Exception; -use OpenSearch\Common\Exceptions\TransportException; use OpenSearch\Endpoints\AbstractEndpoint; -use OpenSearch\Namespaces\AbstractNamespace; -use OpenSearch\Namespaces\MachineLearningNamespace; -use OpenSearch\Namespaces\NamespaceBuilderInterface; +use OpenSearch\Namespaces\AsyncSearchNamespace; use OpenSearch\Namespaces\BooleanRequestWrapper; use OpenSearch\Namespaces\CatNamespace; use OpenSearch\Namespaces\ClusterNamespace; use OpenSearch\Namespaces\DanglingIndicesNamespace; +use OpenSearch\Namespaces\DataFrameTransformDeprecatedNamespace; use OpenSearch\Namespaces\IndicesNamespace; use OpenSearch\Namespaces\IngestNamespace; +use OpenSearch\Namespaces\MachineLearningNamespace; +use OpenSearch\Namespaces\MonitoringNamespace; +use OpenSearch\Namespaces\NamespaceBuilderInterface; use OpenSearch\Namespaces\NodesNamespace; +use OpenSearch\Namespaces\SearchableSnapshotsNamespace; use OpenSearch\Namespaces\SecurityNamespace; use OpenSearch\Namespaces\SnapshotNamespace; use OpenSearch\Namespaces\SqlNamespace; -use OpenSearch\Namespaces\TasksNamespace; -use OpenSearch\Namespaces\AsyncSearchNamespace; -use OpenSearch\Namespaces\DataFrameTransformDeprecatedNamespace; -use OpenSearch\Namespaces\MonitoringNamespace; -use OpenSearch\Namespaces\SearchableSnapshotsNamespace; use OpenSearch\Namespaces\SslNamespace; +use OpenSearch\Namespaces\TasksNamespace; /** * Class Client @@ -1357,7 +1352,7 @@ public function sql(): SqlNamespace public function ml(): MachineLearningNamespace { - return $this->ml; + return $this->ml; } /** @@ -1391,6 +1386,22 @@ public function extractArgument(array &$params, string $arg) } } + /** + * Sends a raw request to the cluster + * @return callable|array + * @throws NoNodesAvailableException + */ + public function request(string $method, string $uri, array $attributes = []) + { + $params = $attributes['params'] ?? []; + $body = $attributes['body'] ?? null; + $options = $attributes['options'] ?? []; + + $promise = $this->transport->performRequest($method, $uri, $params, $body, $options); + + return $this->transport->resultOrFuture($promise, $options); + } + /** * @return callable|array */ diff --git a/tests/ClientIntegrationTest.php b/tests/ClientIntegrationTest.php index 466c885e..f3b480b5 100644 --- a/tests/ClientIntegrationTest.php +++ b/tests/ClientIntegrationTest.php @@ -24,7 +24,6 @@ use OpenSearch\Client; use OpenSearch\ClientBuilder; use OpenSearch\Common\Exceptions\BadRequest400Exception; -use OpenSearch\Common\Exceptions\OpenSearchException; use OpenSearch\Common\Exceptions\Missing404Exception; use OpenSearch\Tests\ClientBuilder\ArrayLogger; use Psr\Log\LogLevel; @@ -33,7 +32,7 @@ * Class ClientTest * * @subpackage Tests - * @group Integration + * @group Integration */ class ClientIntegrationTest extends \PHPUnit\Framework\TestCase { @@ -88,7 +87,7 @@ public function testLogRequestFailHasWarning() try { $result = $client->get([ 'index' => 'foo', - 'id' => 'bar' + 'id' => 'bar', ]); } catch (Missing404Exception $e) { $this->assertNotEmpty($this->getLevelOutput(LogLevel::WARNING, $this->logger->output)); @@ -103,8 +102,8 @@ public function testIndexCannotBeEmptyStringForDelete() $client->delete( [ - 'index' => '', - 'id' => 'test' + 'index' => '', + 'id' => 'test', ] ); } @@ -117,8 +116,8 @@ public function testIdCannotBeEmptyStringForDelete() $client->delete( [ - 'index' => 'test', - 'id' => '' + 'index' => 'test', + 'id' => '', ] ); } @@ -131,8 +130,8 @@ public function testIndexCannotBeArrayOfEmptyStringsForDelete() $client->delete( [ - 'index' => ['', '', ''], - 'id' => 'test' + 'index' => ['', '', ''], + 'id' => 'test', ] ); } @@ -145,12 +144,42 @@ public function testIndexCannotBeArrayOfNullsForDelete() $client->delete( [ - 'index' => [null, null, null], - 'id' => 'test' + 'index' => [null, null, null], + 'id' => 'test', ] ); } + /** @test */ + public function sendRawRequest(): void + { + $client = $this->getClient(); + + $response = $client->request('GET', '/'); + + $this->assertIsArray($response); + $expectedKeys = ['name', 'cluster_name', 'cluster_uuid', 'version', 'tagline']; + foreach ($expectedKeys as $key) { + $this->assertArrayHasKey($key, $response); + } + } + + /** @test */ + public function insertDocumentUsingRawRequest(): void + { + $client = $this->getClient(); + $randomIndex = 'test_index_' .time(); + + $response = $client->request('POST', "/$randomIndex/_doc", ['body' => ['field' => 'value']]); + + $this->assertIsArray($response); + $this->assertArrayHasKey('_index', $response); + $this->assertSame($randomIndex, $response['_index']); + $this->assertArrayHasKey('_id', $response); + $this->assertArrayHasKey('result', $response); + $this->assertSame('created', $response['result']); + } + private function getLevelOutput(string $level, array $output): string { foreach ($output as $out) { @@ -158,6 +187,7 @@ private function getLevelOutput(string $level, array $output): string return $out; } } + return ''; } } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index bc1d092c..4a405c10 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -21,13 +21,13 @@ namespace OpenSearch\Tests; +use GuzzleHttp\Ring\Client\MockHandler; +use GuzzleHttp\Ring\Future\FutureArray; +use Mockery as m; use OpenSearch; use OpenSearch\Client; use OpenSearch\ClientBuilder; use OpenSearch\Common\Exceptions\MaxRetriesException; -use GuzzleHttp\Ring\Client\MockHandler; -use GuzzleHttp\Ring\Future\FutureArray; -use Mockery as m; /** * Class ClientTest @@ -409,4 +409,55 @@ public function testExtractArgumentIterable() $this->assertCount(0, $params); $this->assertInstanceOf(\IteratorIterator::class, $argument); } + + /** @test */ + public function sendRawRequest(): void + { + $callable = function () {}; + $transport = $this->createMock(OpenSearch\Transport::class); + $client = new OpenSearch\Client($transport, $callable, []); + + $transport->expects($this->once())->method('performRequest')->with('GET', '/', [], null, []); + + $client->request('GET', '/'); + } + + /** @test */ + public function sendRawRequestWithBody(): void + { + $callable = function () {}; + $transport = $this->createMock(OpenSearch\Transport::class); + $client = new OpenSearch\Client($transport, $callable, []); + $body = ['query' => ['match' => ['text_entry' => 'long live king']]]; + + $transport->expects($this->once())->method('performRequest')->with('GET', '/shakespeare/_search', [], $body, []); + + $client->request('GET', '/shakespeare/_search', compact('body')); + } + + /** @test */ + public function sendRawRequestWithParams(): void + { + $callable = function () {}; + $transport = $this->createMock(OpenSearch\Transport::class); + $client = new OpenSearch\Client($transport, $callable, []); + $params = ['foo' => 'bar']; + + $transport->expects($this->once())->method('performRequest')->with('GET', '/_search', $params, null, []); + + $client->request('GET', '/_search', compact('params')); + } + + /** @test */ + public function sendRawRequestWithOptions(): void + { + $callable = function () {}; + $transport = $this->createMock(OpenSearch\Transport::class); + $client = new OpenSearch\Client($transport, $callable, []); + $options = ['client' => ['future' => 'lazy']]; + + $transport->expects($this->once())->method('performRequest')->with('GET', '/', [], null, $options); + + $client->request('GET', '/', compact('options')); + } }