diff --git a/README.md b/README.md index e8e38e0..646a6f5 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,9 @@ Born out of frustration about the dependency hell of the available client packages. I didn't need a library with all the features, as a result this package was born. It provides the bare minimum to index, delete and query documents. +This library is compatible with elasticsearch 6.x and 7.x and has no dependencies on the official elasticsearch client +packages. + All issues should go to the [issue tracker from github](https://github.com/CodeDuck42/elasticsearch/issues). ## Features @@ -21,84 +24,40 @@ All issues should go to the [issue tracker from github](https://github.com/CodeD - Send multiple adding and delete actions as a bulk action - Run a query on an elasticsearch index -## TODO - -- Complete documentation -- Actions should return the response from elasticsearch, especially for bulk actions -- Investigating options for authentication besides username and password in the server url (necessary?) - -## Compatibility - -- PHP 7.4 / PHP 8.0 -- Elasticsearch 6.x + 7.x - ## Usage ~~~php -use CodeDuck\Elasticsearch\Actions\Bulk; -use CodeDuck\Elasticsearch\Actions\Delete; -use CodeDuck\Elasticsearch\Actions\Index; -use CodeDuck\Elasticsearch\Actions\Query; use CodeDuck\Elasticsearch\Client; -use CodeDuck\Elasticsearch\ValueObjects\Document; -use CodeDuck\Elasticsearch\ValueObjects\Identifier; +use CodeDuck\Elasticsearch\SimpleClient; use Symfony\Component\HttpClient\HttpClient; -$id1 = new Identifier('my-index', 'ID-123', '_doc'); -$id2 = new Identifier('my-index', 'ID-234', '_doc'); -$id3 = new Identifier('my-index', 'ID-341', '_doc'); - -$document1 = new Document($id1, ['name' => 'foo', 'foo' => 12345]); -$document2 = new Document($id2, ['name' => 'bar', 'foo' => 12345]); -$document3 = new Document($id3, ['name' => 'foobar', 'foo' => 12345]); - -$client = new Client(HttpClient::create(), 'http://127.0.0.1:9200'); - -// index one document -$client->execute(new Index($document1)); - -// bulk index -$client->execute(new Bulk( - new Index($document1), - new Index($document2), - new Index($document3), -)); - -// or -$documents = [ - new Index($document1), - - new Index($document2), - new Index($document3), -]; - -$client->execute(new Bulk(...$documents)); - -// do a search -$result = $client->execute( - new Query(['query' => ['term' => ['name' => 'foobar']]], 'my-index') +$client = new SimpleClient( + new Client(HttpClient::create(), 'http://127.0.0.1:9200'), + 'my-index', '_doc' ); -echo sprintf( - 'It took %f ms to query %d documents, the highest score was %f' . PHP_EOL, - $result->getTook(), - $result->getCount(), - $result->getMaxScore() -); +$client->begin(); +$client->add('ID-123', ['name' => 'foo', 'foo' => 12345]); +$client->add('ID-234', ['name' => 'bar', 'foo' => 12345]); +$client->commit(); + +$result = $client->query(['query' => ['term' => ['name' => 'bar']]]); foreach ($result->getDocuments() as $document) { - echo sprintf( - 'Score: %f, Json: %s' . PHP_EOL, - $document->getScore(), - json_encode($document->getSource(), JSON_THROW_ON_ERROR) - ); + echo json_encode($document->getSource(), JSON_THROW_ON_ERROR) . PHP_EOL; } -// bulk delete -$client->execute(new Bulk( - new Delete($id1), - new Delete($id2), - new Delete($id3), -)); +$client->begin(); +$client->delete('ID-123'); +$client->delete('ID-234'); +$client->commit(); ~~~ + +More detailed examples can be found here: [full client](docs/example-full-client.md), [simple client](docs/example-simple-client.md). + +## TODO + +- Complete documentation +- Actions should return the response from elasticsearch, especially for bulk actions +- Investigating options for authentication besides username and password in the server url (necessary?) diff --git a/docs/example-full-client.md b/docs/example-full-client.md new file mode 100644 index 0000000..5f85654 --- /dev/null +++ b/docs/example-full-client.md @@ -0,0 +1,69 @@ +# Example for using the full client + +~~~php +use CodeDuck\Elasticsearch\Actions\Bulk; +use CodeDuck\Elasticsearch\Actions\Delete; +use CodeDuck\Elasticsearch\Actions\Index; +use CodeDuck\Elasticsearch\Actions\Query; +use CodeDuck\Elasticsearch\Client; +use CodeDuck\Elasticsearch\ValueObjects\Document; +use CodeDuck\Elasticsearch\ValueObjects\Identifier; +use Symfony\Component\HttpClient\HttpClient; + +$id1 = new Identifier('my-index', 'ID-123', '_doc'); +$id2 = new Identifier('my-index', 'ID-234', '_doc'); +$id3 = new Identifier('my-index', 'ID-341', '_doc'); + +$document1 = new Document($id1, ['name' => 'foo', 'foo' => 12345]); +$document2 = new Document($id2, ['name' => 'bar', 'foo' => 12345]); +$document3 = new Document($id3, ['name' => 'foobar', 'foo' => 12345]); + +$client = new Client(HttpClient::create(), 'http://127.0.0.1:9200'); + +// index one document +$client->execute(new Index($document1)); + +// bulk index +$client->execute(new Bulk( + new Index($document1), + new Index($document2), + new Index($document3), +)); + +// alternative for bulk index +$documents = [ + new Index($document1), + new Index($document2), + new Index($document3), +]; + +$client->execute(new Bulk(...$documents)); + +// doing a search +$result = $client->execute( + new Query(['query' => ['term' => ['name' => 'foobar']]], 'my-index') +); + +echo sprintf( + 'It took %f ms to query %d documents, the highest score was %f' . PHP_EOL, + $result->getTook(), + $result->getCount(), + $result->getMaxScore() +); + +foreach ($result->getDocuments() as $document) { + echo sprintf( + 'Score: %f, Json: %s' . PHP_EOL, + $document->getScore(), + json_encode($document->getSource(), JSON_THROW_ON_ERROR) + ); +} + +// bulk delete +$client->execute(new Bulk( + new Delete($id1), + new Delete($id2), + new Delete($id3), +)); + +~~~ diff --git a/docs/example-simple-client.md b/docs/example-simple-client.md new file mode 100644 index 0000000..b4537da --- /dev/null +++ b/docs/example-simple-client.md @@ -0,0 +1,54 @@ +# Example for using the simple client + +The simple client differs from the full client in the following features: + +- The index information is given on client creation +- Bulk actions are wrapped with a begin/commit/rollBack construct like in sql + +~~~php +use CodeDuck\Elasticsearch\Client; +use CodeDuck\Elasticsearch\SimpleClient; +use Symfony\Component\HttpClient\HttpClient; + +$id1 = 'ID-123'; +$id2 = 'ID-234'; +$id3 = 'ID-341'; + +$document1 = ['name' => 'foo', 'foo' => 12345]; +$document2 = ['name' => 'bar', 'foo' => 12345]; +$document3 = ['name' => 'foobar', 'foo' => 12345]; + +$client = new SimpleClient( + new Client(HttpClient::create(), 'http://127.0.0.1:9200'), + 'my-index', '_doc' +); + +// index one document +$client->add($id1, $document1); + +// bulk actions +$client->begin(); +$client->delete($id1); +$client->add($id2, $document2); +$client->add($id3, $document3); +$client->commit(); + +// do a search +$result = $client->query(['query' => ['term' => ['name' => 'foobar']]]); + +echo sprintf( + 'It took %f ms to query %d documents, the highest score was %f' . PHP_EOL, + $result->getTook(), + $result->getCount(), + $result->getMaxScore() +); + +foreach ($result->getDocuments() as $document) { + echo sprintf( + 'Score: %f, Json: %s' . PHP_EOL, + $document->getScore(), + json_encode($document->getSource(), JSON_THROW_ON_ERROR) + ); +} + +~~~ diff --git a/docs/excpetions.md b/docs/exceptions.md similarity index 100% rename from docs/excpetions.md rename to docs/exceptions.md diff --git a/src/Contracts/SimpleClientInterface.php b/src/Contracts/SimpleClientInterface.php new file mode 100644 index 0000000..bec1b31 --- /dev/null +++ b/src/Contracts/SimpleClientInterface.php @@ -0,0 +1,40 @@ +client = $client; + $this->index = $index; + $this->type = $type; + } + + public function add(string $id, array $data): void + { + $this->execute(new Index(new Document($this->createIdentifier($id), $data))); + } + + public function begin(): void + { + $this->isBulkActive = true; + } + + public function commit(): void + { + if (count($this->bulkActions) > 0) { + $this->client->execute(new Bulk(...$this->bulkActions)); + } + + $this->bulkActions = []; + $this->isBulkActive = false; + } + + public function delete(string $id): void + { + $this->execute(new Delete($this->createIdentifier($id))); + } + + public function query(array $query): QueryResult + { + return $this->execute(new Query($query, $this->index)); + } + + public function rollBack(): void + { + $this->bulkActions = []; + $this->isBulkActive = false; + } + + private function createIdentifier(string $id): Identifier + { + return new Identifier($this->index, $id, $this->type); + } + + /** + * @psalm-return ($action is QueryActionInterface ? QueryResult : null) + */ + private function execute(ActionInterface $action): ?QueryResult + { + if ($action instanceof QueryActionInterface) { + return $this->client->execute($action); + } + + if ($this->isBulkActive && $action instanceof BulkActionInterface) { + $this->bulkActions[] = $action; + + return null; + } + + return $this->client->execute($action); + } +} diff --git a/tests/integration/ClientIntegrationTest.php b/tests/integration/ClientIntegrationTest.php index d407264..0fb06db 100644 --- a/tests/integration/ClientIntegrationTest.php +++ b/tests/integration/ClientIntegrationTest.php @@ -17,12 +17,13 @@ class ClientIntegrationTest extends TestCase { + private Client $sut; + public function testBulkDeleteWithNoneExistingDocument(): void { $identifier = new Identifier('test-index', 'TBDWNED'); - $client = new Client(HttpClient::create(), 'http://localhost:9200'); - $client->execute(new Bulk(new Delete($identifier))); + $this->sut->execute(new Bulk(new Delete($identifier))); self::assertTrue(true); } @@ -31,10 +32,9 @@ public function testDeleteWithExistingDocument(): void { $identifier = new Identifier('test-index', 'TDWED'); - $client = new Client(HttpClient::create(), 'http://localhost:9200'); - $client->execute(new Index(new Document($identifier, ['name' => 'example']))); + $this->sut->execute(new Index(new Document($identifier, ['name' => 'example']))); sleep(5); // wait for index - $client->execute(new Delete($identifier)); + $this->sut->execute(new Delete($identifier)); self::assertTrue(true); } @@ -43,22 +43,19 @@ public function testDeleteWithNoneExistingDocument(): void { $this->expectException(TransportException::class); - $client = new Client(HttpClient::create(), 'http://localhost:9200'); - $client->execute(new Delete(new Identifier('test-index', 'TDWNED'))); + $this->sut->execute(new Delete(new Identifier('test-index', 'TDWNED'))); } public function testIndex(): void { - $client = new Client(HttpClient::create(), 'http://localhost:9200'); - $client->execute(new Index(new Document(new Identifier('test-index', '11111'), ['name' => 'example']))); + $this->sut->execute(new Index(new Document(new Identifier('test-index', '11111'), ['name' => 'example']))); self::assertTrue(true); } public function testQuery(): void { - $client = new Client(HttpClient::create(), 'http://localhost:9200'); - $client->execute( + $this->sut->execute( new Bulk( new Index(new Document(new Identifier('test-index', '11111'), ['name' => 'example'])), new Index(new Document(new Identifier('test-index', '22222'), ['name' => 'banana'])), @@ -68,10 +65,15 @@ public function testQuery(): void sleep(5); // wait for index $action = new Query(['query' => ['term' => ['name' => 'banana']]], 'test-index'); - $result = $client->execute($action); + $result = $this->sut->execute($action); self::assertInstanceOf(QueryResult::class, $result); self::assertEquals(1, $result->getCount()); self::assertEquals(['name' => 'banana'], $result->getDocuments()[0]->getSource()); } + + protected function setUp(): void + { + $this->sut = new Client(HttpClient::create(), 'http://localhost:9200'); + } } diff --git a/tests/integration/SimpleClientIntegrationTest.php b/tests/integration/SimpleClientIntegrationTest.php new file mode 100644 index 0000000..bc5cd4c --- /dev/null +++ b/tests/integration/SimpleClientIntegrationTest.php @@ -0,0 +1,66 @@ +sut->begin(); + $this->sut->delete('TBDWNED'); + $this->sut->commit(); + + self::assertTrue(true); + } + + public function testDeleteWithExistingDocument(): void + { + $this->sut->add('TDWED', ['name' => 'example']); + sleep(5); // wait for index + $this->sut->delete('TDWED'); + + self::assertTrue(true); + } + + public function testDeleteWithNoneExistingDocument(): void + { + $this->expectException(TransportException::class); + + $this->sut->delete('TDWNED'); + } + + public function testIndex(): void + { + $this->sut->add('11111', ['name' => 'example']); + + self::assertTrue(true); + } + + public function testQuery(): void + { + $this->sut->begin(); + $this->sut->add('11111', ['name' => 'example']); + $this->sut->add('22222', ['name' => 'banana']); + $this->sut->commit(); + + sleep(5); // wait for index + + $result = $this->sut->query(['query' => ['term' => ['name' => 'banana']]]); + + self::assertEquals(1, $result->getCount()); + self::assertEquals(['name' => 'banana'], $result->getDocuments()[0]->getSource()); + } + + protected function setUp(): void + { + $this->sut = new SimpleClient(new Client(HttpClient::create(), 'http://localhost:9200'), 'test-index'); + } +} diff --git a/tests/unit/SimpleClientTest.php b/tests/unit/SimpleClientTest.php new file mode 100644 index 0000000..34993a1 --- /dev/null +++ b/tests/unit/SimpleClientTest.php @@ -0,0 +1,156 @@ +clientMock + ->expects(self::once()) + ->method('execute') + ->with(self::isInstanceOf(Index::class)); + + $this->sut->add('1234', ['foo' => 'bar']); + } + + public function testCommittingAgainShouldNotBeProcessed(): void + { + $this->clientMock + ->expects(self::once()) + ->method('execute'); + + $this->sut->begin(); + $this->sut->add('1234', ['foo' => 'bar']); + $this->sut->commit(); + $this->sut->commit(); + } + + public function testCommittingBulkActions(): void + { + $this->clientMock + ->expects(self::once()) + ->method('execute') + ->with(self::isInstanceOf(Bulk::class)); + + $this->sut->begin(); + $this->sut->add('1234', ['foo' => 'bar']); + $this->sut->delete('1234'); + $this->sut->commit(); + } + + public function testCommittingShouldResetTheBulkStatus(): void + { + $this->clientMock + ->expects(self::exactly(2)) + ->method('execute') + ->withConsecutive( + [self::isInstanceOf(Bulk::class)], + [self::isInstanceOf(Delete::class)] + ); + + $this->sut->begin(); + $this->sut->delete('1234'); + $this->sut->commit(); + + $this->sut->delete('1234'); + } + + public function testCommittingShouldResetTheBulkStatusEmptyBulkActionsShouldNotBeProcessed(): void + { + $this->clientMock + ->expects(self::never()) + ->method('execute'); + + $this->sut->begin(); + $this->sut->commit(); + } + + public function testDeletingDocuments(): void + { + $this->clientMock + ->expects(self::once()) + ->method('execute') + ->with(self::isInstanceOf(Delete::class)); + + $this->sut->delete('1234'); + } + + public function testQuery(): void + { + $this->clientMock + ->expects(self::once()) + ->method('execute') + ->with(self::isInstanceOf(Query::class)) + ->willReturn(new QueryResult([], 123, 0.9)); + + $this->sut->query([]); + } + + public function testQueryDuringOpenBulkShouldBeDirectlyProcessed(): void + { + $this->clientMock + ->expects(self::once()) + ->method('execute') + ->with(self::isInstanceOf(Query::class)) + ->willReturn(new QueryResult([], 123, 0.9)); + + $this->sut->begin(); + $this->sut->query([]); + $this->sut->commit(); + } + + public function testRollBack(): void + { + $this->clientMock + ->expects(self::never()) + ->method('execute') + ->with(self::isInstanceOf(Bulk::class)); + + $this->sut->begin(); + $this->sut->add('1234', ['foo' => 'bar']); + $this->sut->rollBack(); + + // nothing should happen here + $this->sut->begin(); + $this->sut->commit(); + } + + public function testRollBackShouldResetTheBulkStatus(): void + { + $this->clientMock + ->expects(self::once()) + ->method('execute') + ->with(self::isInstanceOf(Delete::class)); + + $this->sut->begin(); + $this->sut->add('1234', ['foo' => 'bar']); + $this->sut->rollBack(); + + $this->sut->delete('1234'); + } + + protected function setUp(): void + { + $this->clientMock = $this->createMock(ClientInterface::class); + $this->sut = new SimpleClient($this->clientMock, 'test-index', 'document'); + } +}