├── Resources ├── doc │ ├── overwriting_bundle.md │ ├── configuration.md │ ├── index.md │ ├── scan.md │ ├── meta_fields.md │ ├── find_functions.md │ ├── crud.md │ └── results_parsing.md ├── public │ └── images │ │ ├── blue_picto_less.gif │ │ └── blue_picto_more.gif └── config │ └── services.yml ├── Tests ├── app │ ├── fixture │ │ ├── data │ │ │ ├── command_import_10.json.gz │ │ │ ├── command_import_11.json.gz │ │ │ ├── command_import_9.json.gz │ │ │ ├── command_import_9.json │ │ │ ├── command_import_10.json │ │ │ ├── command_import_11.json │ │ │ └── command_import_20.json │ │ └── TestBundle │ │ │ ├── TestBundle.php │ │ │ ├── Document │ │ │ ├── SubcategoryObject.php │ │ │ ├── LongDescriptionTrait.php │ │ │ ├── CategoryObject.php │ │ │ └── User.php │ │ │ └── Entity │ │ │ ├── CategoryObject.php │ │ │ └── Product.php │ ├── config │ │ └── config_test.yml │ └── AppKernel.php ├── WebTestCase.php ├── Unit │ ├── Result │ │ ├── DummyIterator.php │ │ ├── DocumentIteratorTest.php │ │ ├── RawIteratorTest.php │ │ ├── AbstractResultsIteratorTest.php │ │ └── ConverterTest.php │ ├── Event │ │ ├── CommitEventTest.php │ │ ├── BulkEventTest.php │ │ └── PrePersistEventTest.php │ ├── Annotation │ │ └── PropertyTest.php │ ├── Profiler │ │ └── ElasticsearchProfilerTest.php │ ├── Mapping │ │ ├── DocumentParserTest.php │ │ ├── MetadataCollectorTest.php │ │ └── DocumentFinderTest.php │ ├── Collection │ │ └── CollectionTest.php │ ├── EventListener │ │ └── TerminateListenerTest.php │ ├── Service │ │ ├── ManagerFactoryTest.php │ │ ├── RepositoryTest.php │ │ ├── GenerateServiceTest.php │ │ └── Json │ │ │ └── JsonWriterTest.php │ └── ElasticsearchBundleTest.php └── Functional │ ├── Annotation │ ├── PropertyTest.php │ └── DocumentTest.php │ ├── Command │ ├── GenerateDocumentCommandTest.php │ ├── DropIndexCommandTest.php │ ├── CacheClearCommandTest.php │ └── IndexImportCommandTest.php │ ├── Result │ ├── DocumentWithNullObjectFieldTest.php │ ├── GetDocumentSortTest.php │ ├── DocumentWithMultipleFieldsTest.php │ ├── AggregationIteratorFindTest.php │ ├── ArrayIteratorTest.php │ ├── HashMapObjectIteratorTest.php │ └── DocumentIteratorTest.php │ ├── DependencyInjection │ └── ElasticsearchExtensionTest.php │ └── Mapping │ ├── DocumentFinderTest.php │ └── MetadataCollectorTest.php ├── .gitignore ├── .github └── ISSUE_TEMPLATE.md ├── Exception ├── DocumentParserException.php ├── MissingDocumentAnnotationException.php └── BulkWithErrorsException.php ├── Annotation ├── Object.php ├── MetaField.php ├── Id.php ├── Version.php ├── Document.php ├── Routing.php ├── Nested.php ├── ParentDocument.php ├── ObjectType.php ├── HashMap.php ├── Property.php └── Embedded.php ├── Collection └── Collection.php ├── Result ├── RawIterator.php ├── DocumentIterator.php ├── ArrayIterator.php ├── ObjectIterator.php └── Aggregation │ └── AggregationValue.php ├── Mapping ├── DumperInterface.php └── Caser.php ├── DependencyInjection ├── Compiler │ ├── AnnotationReaderPass.php │ ├── ManagerPass.php │ ├── MappingPass.php │ └── RepositoryPass.php └── ONGRElasticsearchExtension.php ├── Profiler ├── CollectTrait.php └── Handler │ └── CollectionHandler.php ├── Event ├── PrePersistEvent.php ├── BaseEvent.php ├── PostCreateManagerEvent.php ├── Events.php ├── CommitEvent.php ├── PreCreateManagerEvent.php └── BulkEvent.php ├── LICENSE ├── README.md ├── Service ├── IndexSuffixFinder.php ├── GenerateService.php ├── ImportService.php ├── Json │ └── JsonWriter.php └── ExportService.php ├── ONGRElasticsearchBundle.php ├── phpunit.xml.dist ├── Command ├── CacheClearCommand.php ├── IndexDropCommand.php ├── AbstractManagerAwareCommand.php ├── IndexImportCommand.php └── IndexExportCommand.php ├── EventListener └── TerminateListener.php └── composer.json /Resources/doc/overwriting_bundle.md: -------------------------------------------------------------------------------- 1 | # Overwriting bundle parts 2 | 3 | Documentation in progress... 4 | -------------------------------------------------------------------------------- /Resources/public/images/blue_picto_less.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/handcraftedinthealps/ElasticsearchBundle/HEAD/Resources/public/images/blue_picto_less.gif -------------------------------------------------------------------------------- /Resources/public/images/blue_picto_more.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/handcraftedinthealps/ElasticsearchBundle/HEAD/Resources/public/images/blue_picto_more.gif -------------------------------------------------------------------------------- /Tests/app/fixture/data/command_import_10.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/handcraftedinthealps/ElasticsearchBundle/HEAD/Tests/app/fixture/data/command_import_10.json.gz -------------------------------------------------------------------------------- /Tests/app/fixture/data/command_import_11.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/handcraftedinthealps/ElasticsearchBundle/HEAD/Tests/app/fixture/data/command_import_11.json.gz -------------------------------------------------------------------------------- /Tests/app/fixture/data/command_import_9.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/handcraftedinthealps/ElasticsearchBundle/HEAD/Tests/app/fixture/data/command_import_9.json.gz -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /Tests/app/cache/ 3 | /Tests/app/logs/ 4 | /Tests/app/build/ 5 | /phpunit.xml 6 | /composer.lock 7 | /var/cache/ 8 | .phpunit.result.cache 9 | /.idea/ 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /Tests/WebTestCase.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle; 13 | 14 | use Symfony\Component\HttpKernel\Bundle\Bundle; 15 | 16 | /** 17 | * AcmeTestBundle for testing. 18 | */ 19 | class TestBundle extends Bundle 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/DocumentParserException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Exception; 13 | 14 | /** 15 | * This is the exception which should be thrown when document has invalid or incomplete annotations. 16 | */ 17 | class DocumentParserException extends \Exception 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Annotation/Object.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Annotation; 13 | 14 | if (version_compare(PHP_VERSION, '7.2.0') < 0) { 15 | class_alias('ONGR\ElasticsearchBundle\Annotation\ObjectType', 'ONGR\ElasticsearchBundle\Annotation\Object', false); 16 | class_exists('ONGR\ElasticsearchBundle\Annotation\ObjectType'); 17 | } 18 | -------------------------------------------------------------------------------- /Exception/MissingDocumentAnnotationException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Exception; 13 | 14 | /** 15 | * This is the exception which should be thrown when class does not have @ONGR\ElasticsearchBundle\Annotation\Document 16 | * annotation. 17 | */ 18 | class MissingDocumentAnnotationException extends DocumentParserException 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /Collection/Collection.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Collection; 13 | 14 | use Doctrine\Common\Collections\ArrayCollection; 15 | 16 | /** 17 | * This class is a holder for collection of objects. 18 | * 19 | * @deprecated Use Doctrine\Common\Collections\ArrayCollection instead. 20 | */ 21 | class Collection extends ArrayCollection 22 | { 23 | } 24 | -------------------------------------------------------------------------------- /Result/RawIterator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Result; 13 | 14 | /** 15 | * Raw documents iterator. 16 | */ 17 | class RawIterator extends AbstractResultsIterator 18 | { 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | protected function convertDocument(array $document) 23 | { 24 | return $document; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/app/fixture/TestBundle/Document/SubcategoryObject.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\Document; 13 | 14 | use ONGR\ElasticsearchBundle\Annotation as ES; 15 | 16 | /** 17 | * Subcategory object for testing. 18 | * 19 | * @ES\ObjectType 20 | */ 21 | class SubcategoryObject extends CategoryObject 22 | { 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Tests/app/fixture/TestBundle/Entity/CategoryObject.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\Entity; 13 | 14 | use ONGR\ElasticsearchBundle\Annotation as ES; 15 | 16 | /** 17 | * @ES\ObjectType 18 | */ 19 | class CategoryObject 20 | { 21 | /** 22 | * @var string 23 | * @ES\Property(type="keyword") 24 | */ 25 | public $title; 26 | } 27 | -------------------------------------------------------------------------------- /Tests/Unit/Result/DummyIterator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Unit\Result; 13 | 14 | use ONGR\ElasticsearchBundle\Result\AbstractResultsIterator; 15 | 16 | class DummyIterator extends AbstractResultsIterator 17 | { 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | protected function convertDocument(array $document) 22 | { 23 | return $document; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/app/fixture/data/command_import_9.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"count":9}, 3 | {"_type":"product","_id":"doc1","_source":{"title":"Document 1"}}, 4 | {"_type":"product","_id":"doc2","_source":{"title":"Document 2"}}, 5 | {"_type":"product","_id":"doc3","_source":{"title":"Document 3"}}, 6 | {"_type":"product","_id":"doc4","_source":{"title":"Document 4"}}, 7 | {"_type":"product","_id":"doc5","_source":{"title":"Document 5"}}, 8 | {"_type":"product","_id":"doc6","_source":{"title":"Document 6"}}, 9 | {"_type":"product","_id":"doc7","_source":{"title":"Document 7"}}, 10 | {"_type":"product","_id":"doc8","_source":{"title":"Document 8"}}, 11 | {"_type":"product","_id":"doc9","_source":{"title":"Document 9"}} 12 | ] 13 | -------------------------------------------------------------------------------- /Mapping/DumperInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Mapping; 13 | 14 | /** 15 | * DumperInterface is the interface implemented by elasticsearch document annotations. 16 | */ 17 | interface DumperInterface 18 | { 19 | /** 20 | * Dumps properties into array. 21 | * 22 | * @param array $exclude Properties array to exclude from dump. 23 | * 24 | * @return array 25 | */ 26 | public function dump(array $exclude = []); 27 | } 28 | -------------------------------------------------------------------------------- /Annotation/MetaField.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Annotation; 13 | 14 | /** 15 | * All meta-field annotations must implement this interface. 16 | */ 17 | interface MetaField 18 | { 19 | /** 20 | * Returns meta-field name. 21 | * 22 | * @return string 23 | */ 24 | public function getName(); 25 | 26 | /** 27 | * Returns meta-field settings. 28 | * 29 | * @return array 30 | */ 31 | public function getSettings(); 32 | } 33 | -------------------------------------------------------------------------------- /Tests/Unit/Event/CommitEventTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Unit\Event; 13 | 14 | use ONGR\ElasticsearchBundle\Event\CommitEvent; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | class CommitEventTest extends TestCase 18 | { 19 | public function testGetters() 20 | { 21 | $event = new CommitEvent('flush', []); 22 | 23 | $this->assertEquals('flush', $event->getCommitMode()); 24 | $this->assertEquals([], $event->getBulkParams()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/app/fixture/data/command_import_10.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"count":10}, 3 | {"_type":"product","_id":"doc1","_source":{"title":"Document 1"}}, 4 | {"_type":"product","_id":"doc2","_source":{"title":"Document 2"}}, 5 | {"_type":"product","_id":"doc3","_source":{"title":"Document 3"}}, 6 | {"_type":"product","_id":"doc4","_source":{"title":"Document 4"}}, 7 | {"_type":"product","_id":"doc5","_source":{"title":"Document 5"}}, 8 | {"_type":"product","_id":"doc6","_source":{"title":"Document 6"}}, 9 | {"_type":"product","_id":"doc7","_source":{"title":"Document 7"}}, 10 | {"_type":"product","_id":"doc8","_source":{"title":"Document 8"}}, 11 | {"_type":"product","_id":"doc9","_source":{"title":"Document 9"}}, 12 | {"_type":"product","_id":"doc10","_source":{"title":"Document 10"}} 13 | ] 14 | -------------------------------------------------------------------------------- /Tests/Unit/Event/BulkEventTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Unit\Event; 13 | 14 | use ONGR\ElasticsearchBundle\Event\BulkEvent; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | class BulkEventTest extends TestCase 18 | { 19 | public function testGetters() 20 | { 21 | $event = new BulkEvent('index', 'test_type', []); 22 | 23 | $this->assertEquals('test_type', $event->getType()); 24 | $this->assertEquals('test_type', $event->getType()); 25 | $this->assertEquals([], $event->getQuery()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/Unit/Event/PrePersistEventTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Unit\Event; 13 | 14 | use ONGR\ElasticsearchBundle\Event\PrePersistEvent; 15 | use ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\Document\Product; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | class PrePersistEventTest extends TestCase 19 | { 20 | public function testGetters() 21 | { 22 | $entity = new Product(); 23 | $event = new PrePersistEvent($entity); 24 | 25 | $this->assertInstanceOf(Product::class, $event->getDocument()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Annotation/Id.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Annotation; 13 | 14 | /** 15 | * Annotation to associate document property with _id meta-field. 16 | * 17 | * @Annotation 18 | * @Target("PROPERTY") 19 | */ 20 | final class Id implements MetaField 21 | { 22 | const NAME = '_id'; 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function getName() 28 | { 29 | return self::NAME; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function getSettings() 36 | { 37 | return []; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Annotation/Version.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Annotation; 13 | 14 | /** 15 | * Associates document property with _version meta-field. 16 | * 17 | * @Annotation 18 | * @Target("PROPERTY") 19 | */ 20 | final class Version implements MetaField 21 | { 22 | const NAME = '_version'; 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function getName() 28 | { 29 | return self::NAME; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function getSettings() 36 | { 37 | return []; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/app/fixture/data/command_import_11.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"count":11}, 3 | {"_type":"product","_id":"doc1","_source":{"title":"Document 1"}}, 4 | {"_type":"product","_id":"doc2","_source":{"title":"Document 2"}}, 5 | {"_type":"product","_id":"doc3","_source":{"title":"Document 3"}}, 6 | {"_type":"product","_id":"doc4","_source":{"title":"Document 4"}}, 7 | {"_type":"product","_id":"doc5","_source":{"title":"Document 5"}}, 8 | {"_type":"product","_id":"doc6","_source":{"title":"Document 6"}}, 9 | {"_type":"product","_id":"doc7","_source":{"title":"Document 7"}}, 10 | {"_type":"product","_id":"doc8","_source":{"title":"Document 8"}}, 11 | {"_type":"product","_id":"doc9","_source":{"title":"Document 9"}}, 12 | {"_type":"product","_id":"doc10","_source":{"title":"Document 10"}}, 13 | {"_type":"product","_id":"doc11","_source":{"title":"Document 11"}} 14 | ] -------------------------------------------------------------------------------- /DependencyInjection/Compiler/AnnotationReaderPass.php: -------------------------------------------------------------------------------- 1 | hasDefinition('es.annotations.reader')) { 18 | return; 19 | } 20 | 21 | $definition = new Definition(AnnotationReader::class); 22 | $definition->addMethodCall('addGlobalIgnoredName', ['required']); 23 | $container->setDefinition('es.annotations.reader', $definition); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Exception/BulkWithErrorsException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Exception; 13 | 14 | class BulkWithErrorsException extends \Exception 15 | { 16 | /** 17 | * @var array 18 | */ 19 | protected $response; 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function __construct($message = '', $code = 0, \Exception $previous = null, $response = []) 25 | { 26 | parent::__construct($message, $code, $previous); 27 | $this->response = $response; 28 | } 29 | 30 | /** 31 | * @return array 32 | */ 33 | public function getResponse() 34 | { 35 | return $this->response; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Annotation/Document.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Annotation; 13 | 14 | use ONGR\ElasticsearchBundle\Mapping\DumperInterface; 15 | 16 | /** 17 | * Annotation to mark a class as an Elasticsearch document. 18 | * 19 | * @Annotation 20 | * @Target("CLASS") 21 | */ 22 | final class Document implements DumperInterface 23 | { 24 | /** 25 | * @var string 26 | */ 27 | public $type; 28 | 29 | /** 30 | * @var array 31 | */ 32 | public $options = []; 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function dump(array $exclude = []) 38 | { 39 | return array_diff_key( 40 | $this->options, 41 | $exclude 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/Unit/Result/DocumentIteratorTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Unit\Result; 13 | 14 | use ONGR\ElasticsearchBundle\Result\DocumentIterator; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | class DocumentIteratorTest extends TestCase 18 | { 19 | /** 20 | * Test for getAggregation() in case requested aggregation is not set. 21 | */ 22 | public function testGetAggregationNull() 23 | { 24 | $manager = $this->getMockBuilder('ONGR\ElasticsearchBundle\Service\Manager') 25 | ->disableOriginalConstructor() 26 | ->getMock(); 27 | 28 | $iterator = new DocumentIterator([], $manager); 29 | 30 | $this->assertNull($iterator->getAggregation('foo')); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Profiler/CollectTrait.php: -------------------------------------------------------------------------------- 1 | doCollect($request, $response, $exception); 20 | } 21 | } 22 | } else { 23 | /** 24 | * @internal 25 | */ 26 | trait CollectTrait 27 | { 28 | public function collect(Request $request, Response $response, \Throwable $exception = null) 29 | { 30 | return $this->doCollect($request, $response, $exception); 31 | } 32 | } 33 | } 34 | // @codingStandardsIgnoreEnd 35 | -------------------------------------------------------------------------------- /Event/PrePersistEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Event; 13 | 14 | class PrePersistEvent extends BaseEvent 15 | { 16 | /** 17 | * @var object 18 | */ 19 | private $document; 20 | 21 | /** 22 | * PrePersistEvent constructor. 23 | * @param $document 24 | */ 25 | public function __construct($document) 26 | { 27 | $this->document = $document; 28 | } 29 | 30 | /** 31 | * @return object 32 | */ 33 | public function getDocument() 34 | { 35 | return $this->document; 36 | } 37 | 38 | /** 39 | * @param object $document 40 | */ 41 | public function setDocument($document) 42 | { 43 | $this->document = $document; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Tests/Unit/Annotation/PropertyTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Unit\Annotation; 13 | 14 | use ONGR\ElasticsearchBundle\Annotation\Property; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | class PropertyTest extends TestCase 18 | { 19 | /** 20 | * Tests if values are filtered correctly. 21 | */ 22 | public function testFilter() 23 | { 24 | $type = new Property(); 25 | 26 | $type->name = 'id'; 27 | $type->index = 'no_index'; 28 | $type->type = 'string'; 29 | 30 | $this->assertEquals( 31 | [ 32 | 'type' => 'string', 33 | ], 34 | $type->dump(), 35 | 'Properties should be filtered' 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Annotation/Routing.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Annotation; 13 | 14 | /** 15 | * Annotation used to enable ROUTING and associate document property with _routing meta-field. 16 | * 17 | * @Annotation 18 | * @Target("PROPERTY") 19 | */ 20 | final class Routing implements MetaField 21 | { 22 | const NAME = '_routing'; 23 | 24 | /** 25 | * @var bool 26 | */ 27 | public $required = false; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function getName() 33 | { 34 | return self::NAME; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function getSettings() 41 | { 42 | return [ 43 | 'required' => $this->required 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/app/fixture/TestBundle/Document/LongDescriptionTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\Document; 13 | 14 | use ONGR\ElasticsearchBundle\Annotation as ES; 15 | 16 | trait LongDescriptionTrait 17 | { 18 | /** 19 | * @ES\Property(type="text", name="long_description") 20 | * @var string 21 | */ 22 | private $long_description; 23 | 24 | /** 25 | * @param string $long_description 26 | */ 27 | public function setLongDescription($long_description) 28 | { 29 | $this->long_description = $long_description; 30 | return $this; 31 | } 32 | 33 | /** 34 | * @return string 35 | */ 36 | public function getLongDescription() 37 | { 38 | return $this->long_description; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Annotation/Nested.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Annotation; 13 | 14 | /** 15 | * Annotation to mark a class as a nested type during the parsing process. 16 | * 17 | * @Annotation 18 | * @Target("CLASS") 19 | * 20 | * @deprecated Object is reserved word in PHP 7.2 This class due Object class will be changed to NestedType as well. 21 | */ 22 | final class Nested 23 | { 24 | const NAME = 'nested'; 25 | 26 | /** 27 | * In this field you can define options. 28 | * 29 | * @var array 30 | */ 31 | public $options = []; 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function dump(array $exclude = []) 37 | { 38 | return array_diff_key( 39 | $this->options, 40 | $exclude 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Event/BaseEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | // @codingStandardsIgnoreStart 13 | 14 | namespace ONGR\ElasticsearchBundle\Event; 15 | 16 | use Symfony\Component\EventDispatcher\Event; 17 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 18 | use Symfony\Contracts\EventDispatcher\Event as ContractsEvent; 19 | use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as ContractsEventDispatcherInterface; 20 | 21 | // Clean up when sf 3.4 support is removed 22 | if (is_subclass_of(EventDispatcherInterface::class, ContractsEventDispatcherInterface::class)) { 23 | /** 24 | * @internal 25 | */ 26 | abstract class BaseEvent extends ContractsEvent 27 | { 28 | } 29 | } else { 30 | /** 31 | * @internal 32 | */ 33 | abstract class BaseEvent extends Event 34 | { 35 | } 36 | } 37 | // @codingStandardsIgnoreEnd 38 | -------------------------------------------------------------------------------- /Event/PostCreateManagerEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Event; 13 | 14 | use ONGR\ElasticsearchBundle\Service\Manager; 15 | 16 | class PostCreateManagerEvent extends BaseEvent 17 | { 18 | /** 19 | * @var Manager 20 | */ 21 | private $manager; 22 | 23 | /** 24 | * PostCreateManagerEvent constructor. 25 | * 26 | * @param Manager $manager 27 | */ 28 | public function __construct(Manager $manager) 29 | { 30 | $this->manager = $manager; 31 | } 32 | 33 | /** 34 | * @return Manager 35 | */ 36 | public function getManager() 37 | { 38 | return $this->manager; 39 | } 40 | 41 | /** 42 | * @param Manager $manager 43 | */ 44 | public function setManager($manager) 45 | { 46 | $this->manager = $manager; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/Functional/Annotation/PropertyTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Functional\Annotation; 13 | 14 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 15 | 16 | class PropertyTest extends AbstractElasticsearchTestCase 17 | { 18 | /** 19 | * Test if field names are correctly generated from property names. 20 | */ 21 | public function testIfNamesFormedCorrectly() 22 | { 23 | $mappings = $this->getManager()->getMetadataCollector()->getMapping('TestBundle:User'); 24 | 25 | $expected = [ 26 | 'first_name' => [ 27 | 'type' => 'text', 28 | ], 29 | 'last_name' => [ 30 | 'type' => 'text', 31 | ], 32 | ]; 33 | 34 | $this->assertEquals($expected, $mappings['properties']); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2016 NFQ Technologies UAB 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Annotation/ParentDocument.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Annotation; 13 | 14 | /** 15 | * Annotation used to enable parent-child relationship. 16 | * 17 | * @Annotation 18 | * @Target("PROPERTY") 19 | */ 20 | final class ParentDocument implements MetaField 21 | { 22 | const NAME = '_parent'; 23 | 24 | /** 25 | * Parent document class name. 26 | * 27 | * @var string 28 | * 29 | * @Doctrine\Common\Annotations\Annotation\Required 30 | */ 31 | public $class; 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function getName() 37 | { 38 | return self::NAME; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function getSettings() 45 | { 46 | return [ 47 | 'type' => null, // Actual value will be generated from $class property 48 | ]; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Annotation/ObjectType.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Annotation; 13 | 14 | /** 15 | * Annotation to mark a class as an object during the parsing process. 16 | * 17 | * `Object` name as class name is forbidden in PHP 7 but we never create this 18 | * class as object and only use it for annotation definition. 19 | * 20 | * @Annotation 21 | * @Target("CLASS") 22 | * 23 | * @deprecated Object is reserved word in PHP 7.2, it will be changed to ObjectType class 24 | */ 25 | final class ObjectType 26 | { 27 | const NAME = 'object'; 28 | 29 | /** 30 | * In this field you can define options. 31 | * 32 | * @var array 33 | */ 34 | public $options = []; 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function dump(array $exclude = []) 40 | { 41 | return array_diff_key( 42 | $this->options, 43 | $exclude 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Profiler/Handler/CollectionHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Profiler\Handler; 13 | 14 | use Monolog\Handler\AbstractProcessingHandler; 15 | use Monolog\LogRecord; 16 | 17 | /** 18 | * Handler that saves all records to him self. 19 | */ 20 | class CollectionHandler extends AbstractProcessingHandler 21 | { 22 | /** 23 | * @var array[]|LogRecord[] 24 | */ 25 | private $records = []; 26 | 27 | /** 28 | * @param array|LogRecord $record 29 | */ 30 | protected function write($record): void 31 | { 32 | $this->records[] = $record; 33 | } 34 | 35 | /** 36 | * Returns recorded data. 37 | * 38 | * @return array[]|LogRecord[] 39 | */ 40 | public function getRecords() 41 | { 42 | return $this->records; 43 | } 44 | 45 | /** 46 | * Clears recorded data. 47 | */ 48 | public function clearRecords() 49 | { 50 | $this->records = []; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/app/config/config_test.yml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: "SUPER-TOP-SECRET" 3 | test: ~ 4 | 5 | ongr_elasticsearch: 6 | analysis: 7 | filter: 8 | incremental_filter: 9 | type: edge_ngram 10 | min_gram: 1 11 | max_gram: 20 12 | analyzer: 13 | incrementalAnalyzer: #-> analyzer name 14 | type: custom 15 | tokenizer: standard 16 | filter: 17 | - lowercase 18 | - incremental_filter 19 | managers: 20 | custom_dir: 21 | index: 22 | hosts: 23 | - 127.0.0.1:9200 24 | index_name: ongr-custom-document-dir-test 25 | mappings: 26 | TestBundle: 27 | document_dir: Entity 28 | default: 29 | index: 30 | hosts: 31 | - 127.0.0.1:9200 32 | index_name: ongr-esb-test 33 | settings: 34 | refresh_interval: -1 35 | number_of_replicas: 0 36 | number_of_shards: 5 37 | mappings: 38 | - TestBundle 39 | -------------------------------------------------------------------------------- /Tests/Unit/Profiler/ElasticsearchProfilerTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Unit\Profiler; 13 | 14 | use ONGR\ElasticsearchBundle\Profiler\ElasticsearchProfiler; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | class ElasticsearchProfilerTest extends TestCase 18 | { 19 | /** 20 | * Tests if correct name is being returned. 21 | */ 22 | public function testGetName() 23 | { 24 | $collector = new ElasticsearchProfiler(); 25 | $this->assertEquals('ongr.profiler', $collector->getName()); 26 | } 27 | 28 | /** 29 | * Tests getManagers method. 30 | */ 31 | public function testGetManagers() 32 | { 33 | $collector = new ElasticsearchProfiler(); 34 | $collector->setManagers([ 'default' => [], 'acme' => [] ]); 35 | 36 | $result = $collector->getManagers(); 37 | $this->assertEquals( 38 | [ 'default' => 'es.manager', 'acme' => 'es.manager.acme' ], 39 | $result 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Tests/Functional/Annotation/DocumentTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Functional\Annotation; 13 | 14 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 15 | 16 | class DocumentTest extends AbstractElasticsearchTestCase 17 | { 18 | /** 19 | * Test document mapping. 20 | */ 21 | public function testDocumentMapping() 22 | { 23 | $manager = $this->getManager(); 24 | $repo = $manager->getRepository('TestBundle:Product'); 25 | 26 | $type = $repo->getType(); 27 | $mappings = $manager->getClient()->indices()->getMapping(['index' => $manager->getIndexName()]); 28 | 29 | $this->assertArrayHasKey($type, $mappings[$manager->getIndexName()]['mappings']); 30 | 31 | $managerMappings = $manager->getMetadataCollector()->getMapping('TestBundle:Product'); 32 | 33 | $this->assertEquals( 34 | sort($managerMappings['properties']), 35 | sort($mappings[$manager->getIndexName()]['mappings'][$type]) 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/app/AppKernel.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | use Symfony\Component\Config\Loader\LoaderInterface; 13 | use Symfony\Component\HttpKernel\Kernel; 14 | 15 | /** 16 | * AppKernel class. 17 | */ 18 | class AppKernel extends Kernel 19 | { 20 | /** 21 | * Register bundles. 22 | * 23 | * @return array 24 | */ 25 | #[\ReturnTypeWillChange] public function registerBundles(): iterable /* 26 | public function registerBundles() /* PHP<8 */ 27 | { // @codingStandardsIgnoreLine 28 | return [ 29 | new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), 30 | new ONGR\ElasticsearchBundle\ONGRElasticsearchBundle(), 31 | new ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\TestBundle(), 32 | ]; 33 | } 34 | 35 | /** 36 | * Register container configuration. 37 | * 38 | * @param LoaderInterface $loader 39 | */ 40 | public function registerContainerConfiguration(LoaderInterface $loader) 41 | { 42 | $loader->load(__DIR__ . '/config/config_' . $this->getEnvironment() . '.yml'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ElasticsearchBundle 2 | 3 | This is a fork of the [ongr/elasticsearch-bundle](https://github.com/ongr-io/elasticsearchbundle). 4 | With some basic changes to support wider range of Symfony Versions. 5 | 6 | | Version | Supported Elasticsearch Version | Supported Symfony Version | Supported Doctrine Annotations Version | 7 | |-------------|---------------------------------|--------------------------------------|----------------------------------------| 8 | | `5.5` | `^7.0, ^6.0, ^5.0` | `^6.0, ^5.0, ^4.0, ^3.4` | `^1.13, ^2.0` | 9 | | `5.0 - 5.4` | `^7.0, ^6.0, ^5.0` | `^7.0, ^6.0, ^5.0, ^4.0, ^3.4, ^2.8` | `^1.0` | 10 | | `1.x` | `^1.0, ^2.0` | `^3.0, ^2.7` | `^1.0` | 11 | 12 | ## Documentation 13 | 14 | [The online documentation of the bundle is here](Resources/doc/index.md) 15 | 16 | ## Try it! 17 | 18 | ### Installation 19 | 20 | Install library with [composer](https://getcomposer.org): 21 | 22 | ```bash 23 | composer require handcraftedinthealps/elasticsearch-bundle 24 | ``` 25 | 26 | ### Usage 27 | 28 | Its used by the [sulu/article-bundle](https://github.com/sulu/SuluArticleBundle/) to support newer a wide range of symfony and elasticsearch versions. 29 | -------------------------------------------------------------------------------- /Tests/app/fixture/TestBundle/Document/CategoryObject.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\Document; 13 | 14 | use ONGR\ElasticsearchBundle\Annotation as ES; 15 | 16 | /** 17 | * Category object for testing. 18 | * 19 | * @ES\ObjectType 20 | */ 21 | class CategoryObject 22 | { 23 | /** 24 | * @var string 25 | * @ES\Property(type="keyword") 26 | */ 27 | private $title; 28 | 29 | /** 30 | * Public property to test converter if it can handle private and public properties. 31 | * 32 | * @var string 33 | * @ES\Property(type="text", options={"analyzer":"keyword"}) 34 | */ 35 | public $description; 36 | 37 | /* 38 | * Test the use of traits 39 | */ 40 | use LongDescriptionTrait; 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function getTitle() 46 | { 47 | return $this->title; 48 | } 49 | 50 | /** 51 | * @param string $title 52 | */ 53 | public function setTitle($title) 54 | { 55 | $this->title = $title; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Resources/doc/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration tree 2 | 3 | Here's an example of full configuration with all possible options including default values: 4 | 5 | ```yml 6 | ongr_elasticsearch: 7 | analysis: 8 | analyzer: 9 | pathAnalyzer: 10 | type: custom 11 | tokenizer: pathTokenizer 12 | tokenizer: 13 | pathTokenizer: 14 | type : path_hierarchy 15 | buffer_size: 2024 16 | skip: 0 17 | delimiter: / 18 | filter: 19 | incremental_filter: 20 | type: edge_ngram 21 | min_gram: 1 22 | max_gram: 20 23 | managers: 24 | default: 25 | index: 26 | hosts: 27 | - 127.0.0.1:9200 28 | index_name: ongr-default 29 | settings: 30 | refresh_interval: -1 31 | number_of_replicas: 0 32 | number_of_shards: 1 33 | logger: true #default %kernel.debug% 34 | mappings: 35 | - AcmeBarBundle #Scans all bundle documents 36 | custom: 37 | index: 38 | hosts: 39 | - 10.0.0.1:9200 #default 127.0.0.1:9200 40 | index_name: ongr-custom 41 | mappings: 42 | AcmeBundle: 43 | document_dir: Document 44 | ``` -------------------------------------------------------------------------------- /Event/Events.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Event; 13 | 14 | /** 15 | * Contains all events thrown in the ONGRElasticsearchBundle 16 | */ 17 | final class Events 18 | { 19 | /** 20 | * The PRE_PERSIST event occurs before convert to Array 21 | */ 22 | const PRE_PERSIST = 'es.pre_persist'; 23 | 24 | /** 25 | * The BULK event occurs before during the processing of bulk method 26 | */ 27 | const BULK = 'es.bulk'; 28 | 29 | /** 30 | * The PRE_COMMIT event occurs before committing queries to ES 31 | */ 32 | const PRE_COMMIT = 'es.pre_commit'; 33 | 34 | /** 35 | * The POST_COMMIT event occurs after committing queries to ES 36 | */ 37 | const POST_COMMIT = 'es.post_commit'; 38 | 39 | /** 40 | * The PRE_MANAGER_CREATE event occurs before manager is created, right after client is initiated. 41 | * You can modify anything in the core elasticsearch-php client by this event. 42 | */ 43 | const PRE_MANAGER_CREATE = 'es.pre_manager_create'; 44 | 45 | /** 46 | * The POST_MANAGER_CREATE event occurs after manager is created. 47 | */ 48 | const POST_MANAGER_CREATE = 'es.post_manager_create'; 49 | } 50 | -------------------------------------------------------------------------------- /Tests/app/fixture/data/command_import_20.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"count":20}, 3 | {"_type":"product","_id":"doc1","_source":{"title":"Document 1"}}, 4 | {"_type":"product","_id":"doc2","_source":{"title":"Document 2"}}, 5 | {"_type":"product","_id":"doc3","_source":{"title":"Document 3"}}, 6 | {"_type":"product","_id":"doc4","_source":{"title":"Document 4"}}, 7 | {"_type":"product","_id":"doc5","_source":{"title":"Document 5"}}, 8 | {"_type":"product","_id":"doc6","_source":{"title":"Document 6"}}, 9 | {"_type":"product","_id":"doc7","_source":{"title":"Document 7"}}, 10 | {"_type":"product","_id":"doc8","_source":{"title":"Document 8"}}, 11 | {"_type":"product","_id":"doc9","_source":{"title":"Document 9"}}, 12 | {"_type":"product","_id":"doc10","_source":{"title":"Document 10"}}, 13 | {"_type":"product","_id":"doc11","_source":{"title":"Document 11"}}, 14 | {"_type":"product","_id":"doc12","_source":{"title":"Document 12"}}, 15 | {"_type":"product","_id":"doc13","_source":{"title":"Document 13"}}, 16 | {"_type":"product","_id":"doc14","_source":{"title":"Document 14"}}, 17 | {"_type":"product","_id":"doc15","_source":{"title":"Document 15"}}, 18 | {"_type":"product","_id":"doc16","_source":{"title":"Document 16"}}, 19 | {"_type":"product","_id":"doc17","_source":{"title":"Document 17"}}, 20 | {"_type":"product","_id":"doc18","_source":{"title":"Document 18"}}, 21 | {"_type":"product","_id":"doc19","_source":{"title":"Document 19"}}, 22 | {"_type":"product","_id":"doc20","_source":{"title":"Document 20"}} 23 | ] -------------------------------------------------------------------------------- /Service/IndexSuffixFinder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Service; 13 | 14 | /** 15 | * Constructs index name with date's suffix. 16 | */ 17 | class IndexSuffixFinder 18 | { 19 | /** 20 | * Constructs index name with date suffix. Sets name in the connection. 21 | * 22 | * E.g. 2022.03.22-5 (if 4 indexes exists already for given date) 23 | * 24 | * @param Manager $manager Connection to act upon. 25 | * @param null|\DateTime $time Date for which the suffix will be based on. 26 | * Current date if null. 27 | * 28 | * @return string 29 | */ 30 | public function setNextFreeIndex(Manager $manager, \DateTime $time = null) 31 | { 32 | if ($time === null) { 33 | $time = new \DateTime(); 34 | } 35 | 36 | $date = $time->format('Y.m.d'); 37 | $indexName = $manager->getIndexName(); 38 | 39 | $nameBase = $indexName . '-' . $date; 40 | $name = $nameBase; 41 | $i = 0; 42 | $manager->setIndexName($name); 43 | 44 | while ($manager->indexExists()) { 45 | $i++; 46 | $name = "{$nameBase}-{$i}"; 47 | $manager->setIndexName($name); 48 | } 49 | 50 | return $name; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ONGRElasticsearchBundle.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle; 13 | 14 | use ONGR\ElasticsearchBundle\DependencyInjection\Compiler\AnnotationReaderPass; 15 | use ONGR\ElasticsearchBundle\DependencyInjection\Compiler\ManagerPass; 16 | use ONGR\ElasticsearchBundle\DependencyInjection\Compiler\MappingPass; 17 | use ONGR\ElasticsearchBundle\DependencyInjection\Compiler\RepositoryPass; 18 | use Symfony\Component\DependencyInjection\Compiler\PassConfig; 19 | use Symfony\Component\DependencyInjection\ContainerBuilder; 20 | use Symfony\Component\HttpKernel\Bundle\Bundle; 21 | 22 | /** 23 | * ONGR Elasticsearch bundle system file required by kernel. 24 | */ 25 | class ONGRElasticsearchBundle extends Bundle 26 | { 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function build(ContainerBuilder $container) 31 | { 32 | parent::build($container); 33 | 34 | $container->addCompilerPass(new MappingPass()); 35 | $container->addCompilerPass(new ManagerPass()); 36 | $container->addCompilerPass(new AnnotationReaderPass()); 37 | // The `RepositoryPass` need to be behind the Symfony `DecoratorServicePass` 38 | // to allow decorating the annotation reader 39 | $container->addCompilerPass(new RepositoryPass(), PassConfig::TYPE_OPTIMIZE, -10); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Resources/doc/index.md: -------------------------------------------------------------------------------- 1 | ## Elasticsearch Bundle 2 | 3 | Welcome to the ElasticsearchBundle, the modern solution to work with [Elasticsearch database](https://www.elastic.co/products/elasticsearch) in the [Symfony](https://github.com/symfony/symfony-standard) applications. We created this bundle with love :heart: and we think you will love it too. 4 | 5 | > Bundle set up guide you can find in the `README.md` file. 6 | 7 | #### Usage 8 | * [Mapping explained](mapping.md) 9 | * [Using Meta-Fields](meta_fields.md) 10 | * [Configuration](configuration.md) 11 | * [Console commands](commands.md) 12 | * [How to do a simple CRUD actions](crud.md) 13 | * [Quick find functions](find_functions.md) 14 | * [How to search the index](search.md) 15 | * [Scan through the index](scan.md) 16 | * [Parsing the results](results_parsing.md) 17 | 18 | #### Troubleshooting 19 | * [How to upgrade from the older versions?](upgrade.md) 20 | * [How to overwrite some parts of the bundle?](overwriting_bundle.md) 21 | 22 | #### Some news for upcoming versions 23 | 24 | We already have a new milestone set for v1.1.0 version (see active milestones [here](https://github.com/ongr-io/ElasticsearchBundle/milestones)). For the new milestone we are planning to support new endpoints: 25 | * Highlight 26 | * Autocomplete 27 | * Suggestions 28 | 29 | We are hoping that those features will help our community to create better projects. **If you think that we are missing something very important which would help you, please do not hesitate to create an issue and ask about it.** We are very open and looking forward to see a projects running on this awesome bundle :smirk:. 30 | -------------------------------------------------------------------------------- /Tests/Unit/Mapping/DocumentParserTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Unit\Mapping; 13 | 14 | use ONGR\ElasticsearchBundle\Mapping\DocumentParser; 15 | use ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\Document\LongDescriptionTrait; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | class DocumentParserTest extends TestCase 19 | { 20 | /* 21 | * @var \Doctrine\Common\Annotations\Reader 22 | */ 23 | private $reader; 24 | 25 | /* 26 | * @var \ONGR\ElasticsearchBundle\Mapping\DocumentFinder 27 | */ 28 | private $finder; 29 | 30 | protected function setUp(): void 31 | { 32 | $this->reader = $this->getMockBuilder('Doctrine\Common\Annotations\Reader') 33 | ->disableOriginalConstructor() 34 | ->getMock(); 35 | 36 | $this->finder = $this->getMockBuilder('ONGR\ElasticsearchBundle\Mapping\DocumentFinder') 37 | ->disableOriginalConstructor() 38 | ->getMock(); 39 | } 40 | 41 | public function testReturnFalseOnTrait() 42 | { 43 | $traitReflection = new \ReflectionClass( 44 | '\\ONGR\\ElasticsearchBundle\\Tests\\app\\fixture\\TestBundle\\Document\\LongDescriptionTrait' 45 | ); 46 | 47 | $parser = new DocumentParser($this->reader, $this->finder); 48 | $this->assertFalse($parser->parse($traitReflection)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | ./Tests/Unit/ 16 | 17 | 18 | ./Tests/Functional/ 19 | 20 | 21 | ./Tests/ 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ./ 34 | 35 | ./Tests 36 | ./vendor 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Event/CommitEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Event; 13 | 14 | class CommitEvent extends BaseEvent 15 | { 16 | /** 17 | * @var string 18 | */ 19 | private $commitMode; 20 | 21 | /** 22 | * @var array 23 | */ 24 | private $bulkParams; 25 | 26 | /** 27 | * @param string $commitMode 28 | * @param array|null $bulkParams BulkQueries or BulkResponse, depending on event 29 | */ 30 | public function __construct($commitMode, $bulkParams = []) 31 | { 32 | $this->commitMode = $commitMode; 33 | $this->bulkParams = $bulkParams; 34 | } 35 | 36 | /** 37 | * Returns commit mode 38 | * 39 | * @return string 40 | */ 41 | public function getCommitMode() 42 | { 43 | return $this->commitMode; 44 | } 45 | 46 | /** 47 | * @param string $commitMode 48 | */ 49 | public function setCommitMode($commitMode) 50 | { 51 | $this->commitMode = $commitMode; 52 | } 53 | 54 | /** 55 | * Returns params 56 | * 57 | * @return array 58 | */ 59 | public function getBulkParams() 60 | { 61 | return $this->bulkParams; 62 | } 63 | 64 | /** 65 | * @param array $bulkParams 66 | */ 67 | public function setBulkParams($bulkParams) 68 | { 69 | $this->bulkParams = $bulkParams; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Result/DocumentIterator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Result; 13 | 14 | use ONGR\ElasticsearchBundle\Result\Aggregation\AggregationValue; 15 | 16 | /** 17 | * Class DocumentIterator. 18 | */ 19 | class DocumentIterator extends AbstractResultsIterator 20 | { 21 | /** 22 | * Returns aggregations. 23 | * 24 | * @return array 25 | */ 26 | public function getAggregations() 27 | { 28 | $aggregations = []; 29 | 30 | foreach (parent::getAggregations() as $key => $aggregation) { 31 | $aggregations[$key] = $this->getAggregation($key); 32 | } 33 | 34 | return $aggregations; 35 | } 36 | 37 | /** 38 | * Get a specific aggregation by name. It fetches from the top level only. 39 | * 40 | * @param string $name 41 | * 42 | * @return AggregationValue|null 43 | */ 44 | public function getAggregation($name) 45 | { 46 | $aggregations = parent::getAggregations(); 47 | if (!array_key_exists($name, $aggregations)) { 48 | return null; 49 | } 50 | 51 | return new AggregationValue($aggregations[$name]); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | protected function convertDocument(array $document) 58 | { 59 | return $this->getConverter()->convertToDocument($document, $this->getManager()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Event/PreCreateManagerEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Event; 13 | 14 | use Elasticsearch\ClientBuilder; 15 | 16 | class PreCreateManagerEvent extends BaseEvent 17 | { 18 | /** 19 | * @var ClientBuilder 20 | */ 21 | private $client; 22 | 23 | /** 24 | * @var array 25 | */ 26 | private $indexSettings; 27 | 28 | /** 29 | * CreateManagerEvent constructor. 30 | * 31 | * @param ClientBuilder $client 32 | * @param $indexSettings array 33 | */ 34 | public function __construct(ClientBuilder $client, &$indexSettings) 35 | { 36 | $this->client = $client; 37 | $this->indexSettings = $indexSettings; 38 | } 39 | 40 | /** 41 | * @return ClientBuilder 42 | */ 43 | public function getClient() 44 | { 45 | return $this->client; 46 | } 47 | 48 | /** 49 | * @param ClientBuilder $client 50 | */ 51 | public function setClient(ClientBuilder $client) 52 | { 53 | $this->client = $client; 54 | } 55 | 56 | /** 57 | * @return array 58 | */ 59 | public function getIndexSettings() 60 | { 61 | return $this->indexSettings; 62 | } 63 | 64 | /** 65 | * @param array $indexSettings 66 | */ 67 | public function setIndexSettings($indexSettings) 68 | { 69 | $this->indexSettings = $indexSettings; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Tests/Functional/Command/GenerateDocumentCommandTest.php: -------------------------------------------------------------------------------- 1 | expectException(\InvalidArgumentException::class); 18 | 19 | $app = new Application(); 20 | $app->add($this->getCommand()); 21 | 22 | $command = $app->find('ongr:es:document:generate'); 23 | 24 | $tester = new CommandTester($command); 25 | $tester->execute(['command' => $command->getName()], ['interactive' => false]); 26 | $tester->execute( 27 | ['command' => $command->getName(), '--no-interaction' => true], 28 | ['interactive' => false] 29 | ); 30 | } 31 | 32 | /** 33 | * @return DocumentGenerateCommand 34 | */ 35 | private function getCommand() 36 | { 37 | $container = self::createClient()->getContainer(); 38 | 39 | return new DocumentGenerateCommand( 40 | $container->getParameter('kernel.bundles'), 41 | $container->get('es.generate'), 42 | $container->get('es.metadata_collector'), 43 | $container->get('es.annotations.cached_reader'), 44 | ['es.manager.default' => $container->get('es.manager.default')] 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Result/ArrayIterator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Result; 13 | 14 | /** 15 | * Class DocumentIterator. 16 | */ 17 | class ArrayIterator extends AbstractResultsIterator implements \ArrayAccess 18 | { 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | #[\ReturnTypeWillChange] 23 | public function offsetExists($offset) 24 | { 25 | return $this->documentExists($offset); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | #[\ReturnTypeWillChange] 32 | public function offsetGet($offset) 33 | { 34 | return $this->getDocument($offset); 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | #[\ReturnTypeWillChange] 41 | public function offsetSet($offset, $value) 42 | { 43 | $this->documents[$offset] = $value; 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | #[\ReturnTypeWillChange] 50 | public function offsetUnset($offset) 51 | { 52 | unset($this->documents[$offset]); 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | protected function convertDocument(array $document) 59 | { 60 | if (array_key_exists('_source', $document)) { 61 | return $document['_source']; 62 | } elseif (array_key_exists('fields', $document)) { 63 | return array_map('reset', $document['fields']); 64 | } 65 | 66 | return $document; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tests/Functional/Result/DocumentWithNullObjectFieldTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace ONGR\ElasticsearchBundle\Tests\Functional\Result; 12 | 13 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 14 | use ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\Document\Product; 15 | 16 | class DocumentNullObjectFieldTest extends AbstractElasticsearchTestCase 17 | { 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | protected function getDataArray() 22 | { 23 | return [ 24 | 'default' => [ 25 | 'product' => [ 26 | [ 27 | '_id' => 'foo', 28 | 'title' => 'Bar Product', 29 | 'location' => null, 30 | 'released' => null, 31 | ], 32 | ], 33 | ], 34 | ]; 35 | } 36 | 37 | /** 38 | * Test if fetched object field is actually NULL. 39 | */ 40 | public function testResultWithNullObjectField() 41 | { 42 | /** @var Product $document */ 43 | $document = $this->getManager()->find('TestBundle:Product', 'foo'); 44 | 45 | $this->assertInstanceOf( 46 | 'ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\Document\Product', 47 | $document 48 | ); 49 | 50 | $this->assertNull($document->getLocation()); 51 | $this->assertNull($document->getReleased()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /DependencyInjection/Compiler/ManagerPass.php: -------------------------------------------------------------------------------- 1 | findTaggedServiceIds(self::TAG_NAME); 16 | foreach ($taggedServices as $key => $ids) { 17 | $managers[$key] = $container->getDefinition($key); 18 | } 19 | 20 | $this->addManagers($container, 'es.command.cache_clear', $managers); 21 | $this->addManagers($container, 'es.command.document_generate', $managers); 22 | $this->addManagers($container, 'es.command.index_create', $managers); 23 | $this->addManagers($container, 'es.command.index_export', $managers); 24 | $this->addManagers($container, 'es.command.index_import', $managers); 25 | $this->addManagers($container, 'es.command.index_drop', $managers); 26 | } 27 | 28 | private function addManagers(ContainerBuilder $container, $definitionName, array $managers) 29 | { 30 | $definition = $container->getDefinition($definitionName); 31 | 32 | if (method_exists($definition, 'setArgument')) { 33 | $definition->setArgument('$managers', $managers); 34 | 35 | return; 36 | } 37 | 38 | $arguments = $definition->getArguments(); 39 | $arguments[] = $managers; 40 | 41 | $definition->setArguments($arguments); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Command/CacheClearCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Command; 13 | 14 | use Symfony\Component\Console\Input\InputInterface; 15 | use Symfony\Component\Console\Output\OutputInterface; 16 | use Symfony\Component\Console\Style\SymfonyStyle; 17 | 18 | /** 19 | * Symfony command for clearing elasticsearch cache. 20 | */ 21 | class CacheClearCommand extends AbstractManagerAwareCommand 22 | { 23 | public static $defaultName = 'ongr:es:cache:clear'; 24 | 25 | public function __construct(array $managers = []) 26 | { 27 | parent::__construct($managers, self::$defaultName); 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | protected function configure(): void 34 | { 35 | parent::configure(); 36 | 37 | $this 38 | ->setName(static::$defaultName) 39 | ->setDescription('Clears elasticsearch client cache.'); 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | protected function execute(InputInterface $input, OutputInterface $output): int 46 | { 47 | $io = new SymfonyStyle($input, $output); 48 | $this 49 | ->getManager($input->getOption('manager')) 50 | ->clearCache(); 51 | $io->success( 52 | sprintf( 53 | 'Elasticsearch index cache has been cleared for manager named `%s`', 54 | $input->getOption('manager') 55 | ) 56 | ); 57 | 58 | return 0; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Annotation/HashMap.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Annotation; 13 | 14 | use Doctrine\Common\Annotations\Annotation\Enum; 15 | 16 | /** 17 | * Annotation for property which points to inner object. 18 | * 19 | * @Annotation 20 | * @Target("PROPERTY") 21 | */ 22 | final class HashMap 23 | { 24 | const NAME = 'hash_map'; 25 | 26 | /** 27 | * Name of the type field. Defaults to normalized property name. 28 | * 29 | * @var string 30 | */ 31 | public $name; 32 | 33 | /** 34 | * Property type for nested structure values. 35 | * 36 | * @var mixed 37 | * @Enum({ 38 | * "text", "keyword", 39 | * "long", "integer", "short", "byte", "double", "float", 40 | * "date", 41 | * "boolean", 42 | * "binary", 43 | * "geo_point", "geo_shape", 44 | * "ip", "completion", "token_count", "murmur3", "attachments", "percolator" 45 | * }) 46 | */ 47 | public $type; 48 | 49 | /** 50 | * In this field you can define options. 51 | * 52 | * @var array 53 | */ 54 | public $options = []; 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function dump(array $exclude = []) 60 | { 61 | return array_diff_key( 62 | array_merge( 63 | [ 64 | 'type' => $this->type 65 | ], 66 | $this->options 67 | ), 68 | $exclude 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Result/ObjectIterator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Result; 13 | 14 | use Doctrine\Common\Collections\AbstractLazyCollection; 15 | use Doctrine\Common\Collections\ArrayCollection; 16 | 17 | /** 18 | * ObjectIterator class. 19 | */ 20 | class ObjectIterator extends AbstractLazyCollection 21 | { 22 | /** 23 | * @var Converter 24 | */ 25 | private $converter; 26 | 27 | /** 28 | * @var array Aliases information. 29 | */ 30 | private $alias; 31 | 32 | /** 33 | * Converts raw document data to objects when requested. 34 | * 35 | * @param Converter $converter 36 | * @param array $objects 37 | * @param array $alias 38 | */ 39 | public function __construct($converter, $objects, $alias) 40 | { 41 | $this->converter = $converter; 42 | $this->alias = $alias; 43 | $this->collection = new ArrayCollection($objects); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | protected function convertDocument(array $document) 50 | { 51 | return $this->converter->assignArrayToObject( 52 | $document, 53 | new $this->alias['namespace'](), 54 | $this->alias['aliases'] 55 | ); 56 | } 57 | 58 | /** 59 | * @inheritDoc 60 | */ 61 | protected function doInitialize() 62 | { 63 | $this->collection = $this->collection->map(function ($rawObject) { 64 | return $this->convertDocument($rawObject); 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Annotation/Property.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Annotation; 13 | 14 | use Doctrine\Common\Annotations\Annotation\Enum; 15 | 16 | /** 17 | * Annotation used to check mapping type during the parsing process. 18 | * 19 | * @Annotation 20 | * @Target("PROPERTY") 21 | */ 22 | final class Property 23 | { 24 | /** 25 | * Field type. 26 | * 27 | * @var string 28 | * 29 | * @Doctrine\Common\Annotations\Annotation\Required 30 | * @Enum({ 31 | * "text", "keyword", 32 | * "long", "integer", "short", "byte", "double", "float", 33 | * "date", 34 | * "boolean", 35 | * "binary", 36 | * "geo_point", "geo_shape", 37 | * "ip", "completion", "token_count", "murmur3", "attachments", "percolator" 38 | * }) 39 | */ 40 | public $type; 41 | 42 | /** 43 | * Name of the type field. Defaults to normalized property name. 44 | * 45 | * @var string 46 | */ 47 | public $name; 48 | 49 | /** 50 | * In this field you can define options (like analyzers and etc) for specific field types. 51 | * 52 | * @var array 53 | */ 54 | public $options = []; 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function dump(array $exclude = []) 60 | { 61 | return array_diff_key( 62 | array_merge( 63 | [ 64 | 'type' => $this->type 65 | ], 66 | $this->options 67 | ), 68 | $exclude 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Tests/Functional/DependencyInjection/ElasticsearchExtensionTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Functional\DependencyInjection; 13 | 14 | use ONGR\ElasticsearchBundle\Tests\WebTestCase; 15 | 16 | class ElasticsearchExtensionTest extends WebTestCase 17 | { 18 | /** 19 | * @return array 20 | */ 21 | public function getTestContainerData() 22 | { 23 | return [ 24 | [ 25 | 'es.manager', 26 | 'ONGR\ElasticsearchBundle\Service\Manager', 27 | ], 28 | [ 29 | 'es.manager.default', 30 | 'ONGR\ElasticsearchBundle\Service\Manager', 31 | ], 32 | [ 33 | 'es.manager.default.product', 34 | 'ONGR\ElasticsearchBundle\Service\Repository', 35 | ], 36 | [ 37 | 'es.metadata_collector', 38 | 'ONGR\ElasticsearchBundle\Mapping\MetadataCollector', 39 | ], 40 | ]; 41 | } 42 | 43 | /** 44 | * Tests if container has all services. 45 | * 46 | * @param string $id 47 | * @param string $instance 48 | * 49 | * @dataProvider getTestContainerData 50 | */ 51 | public function testContainer($id, $instance) 52 | { 53 | $container = static::createClient()->getContainer(); 54 | 55 | $this->assertTrue($container->has($id), 'Container should have set id.'); 56 | $this->assertInstanceOf($instance, $container->get($id), 'Container has wrong instance set to id.'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/Unit/Collection/CollectionTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Unit\Collection; 13 | 14 | use Doctrine\Common\Collections\ArrayCollection; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | class CollectionTest extends TestCase 18 | { 19 | /** 20 | * @var array 21 | */ 22 | private $data = [ 23 | 'foo' => 'Bob 1', 24 | 'bar' => 'Bob 2', 25 | ]; 26 | 27 | /** 28 | * Tests \Countable implementation. 29 | */ 30 | public function testCountable() 31 | { 32 | $this->assertCount(count($this->data), new ArrayCollection($this->data)); 33 | } 34 | 35 | /** 36 | * Tests \Iterator implementation. 37 | */ 38 | public function testIterator() 39 | { 40 | $this->assertEquals($this->data, iterator_to_array(new ArrayCollection($this->data))); 41 | } 42 | 43 | /** 44 | * Tests \ArrayAccess implementation. 45 | */ 46 | public function testArrayAccess() 47 | { 48 | $collection = new ArrayCollection($this->data); 49 | 50 | $this->assertArrayHasKey('foo', $collection); 51 | $this->assertEquals($this->data['foo'], $collection['foo']); 52 | 53 | $newData = $this->data; 54 | $newData['baz'] = 'Bob 3'; 55 | $newData[] = 'Bob 4'; 56 | $collection['baz'] = 'Bob 3'; 57 | $collection[] = 'Bob 4'; 58 | 59 | $this->assertEquals($newData, iterator_to_array($collection)); 60 | 61 | unset($collection['baz']); 62 | $this->assertArrayNotHasKey('baz', $collection); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /EventListener/TerminateListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\EventListener; 13 | 14 | use ONGR\ElasticsearchBundle\Service\Manager; 15 | use Symfony\Component\DependencyInjection\Container; 16 | 17 | class TerminateListener 18 | { 19 | /** 20 | * @var Container 21 | */ 22 | private $container; 23 | 24 | /** 25 | * @var array 26 | */ 27 | private $managers; 28 | 29 | /** 30 | * Constructor 31 | * 32 | * @param Container $container 33 | * @param array $managers 34 | */ 35 | public function __construct(Container $container, array $managers) 36 | { 37 | $this->container = $container; 38 | $this->managers = $managers; 39 | } 40 | 41 | /** 42 | * Forces commit to elasticsearch on kernel terminate 43 | */ 44 | public function onKernelTerminate() 45 | { 46 | foreach ($this->managers as $key => $value) { 47 | if ($value['force_commit']) { 48 | try { 49 | $managerName = sprintf('es.manager.%s', $key); 50 | 51 | // Ignore managers who have not been initialized. 52 | if (!$this->container->initialized($managerName)) { 53 | continue; 54 | } 55 | 56 | /** @var Manager $manager */ 57 | $manager = $this->container->get($managerName); 58 | } catch (\Exception $e) { 59 | continue; 60 | } 61 | $manager->commit(); 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Tests/Functional/Command/DropIndexCommandTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Functional\Command; 13 | 14 | use ONGR\ElasticsearchBundle\Command\IndexDropCommand; 15 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 16 | use Symfony\Component\Console\Application; 17 | use Symfony\Component\Console\Tester\CommandTester; 18 | 19 | class DropIndexCommandTest extends AbstractElasticsearchTestCase 20 | { 21 | /** 22 | * Tests dropping index. Configuration from tests yaml. 23 | */ 24 | public function testExecute() 25 | { 26 | $manager = $this->getManager(); 27 | 28 | $command = new IndexDropCommand(['es.manager.default' => $this->getManager()]); 29 | 30 | $app = new Application(); 31 | $app->add($command); 32 | 33 | // Does not drop index. 34 | $command = $app->find('ongr:es:index:drop'); 35 | $commandTester = new CommandTester($command); 36 | $commandTester->execute( 37 | [ 38 | 'command' => $command->getName(), 39 | ] 40 | ); 41 | $this->assertTrue( 42 | $manager 43 | ->indexExists(), 44 | 'Index should still exist.' 45 | ); 46 | 47 | // Does drop index. 48 | $commandTester->execute( 49 | [ 50 | 'command' => $command->getName(), 51 | '--force' => true, 52 | ] 53 | ); 54 | 55 | $this->assertFalse( 56 | $manager 57 | ->indexExists(), 58 | 'Index should be dropped.' 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/Functional/Mapping/DocumentFinderTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Functional\Mapping; 13 | 14 | use ONGR\ElasticsearchBundle\Mapping\DocumentFinder; 15 | use ONGR\ElasticsearchBundle\Tests\WebTestCase; 16 | use Symfony\Component\DependencyInjection\ContainerInterface; 17 | use Symfony\Component\DependencyInjection\Container; 18 | 19 | class DocumentFinderTest extends WebTestCase 20 | { 21 | /** 22 | * Tests if document paths are returned for fixture bundle. 23 | */ 24 | public function testGetBundleDocumentClasses() 25 | { 26 | $finder = new DocumentFinder($this->getClientContainer()->getParameter('kernel.bundles')); 27 | $this->assertGreaterThan(0, count($finder->getBundleDocumentClasses('TestBundle'))); 28 | $this->assertEquals(0, count($finder->getBundleDocumentClasses('FrameworkBundle'))); 29 | } 30 | 31 | /** 32 | * Tests if exception is thrown for unregistered bundle. 33 | */ 34 | public function testGetBundleClassException() 35 | { 36 | $this->expectException(\LogicException::class); 37 | $this->expectExceptionMessage('Bundle \'NotExistingBundle\' does not exist.'); 38 | 39 | $finder = new DocumentFinder($this->getClientContainer()->getParameter('kernel.bundles')); 40 | $finder->getBundleClass('NotExistingBundle'); 41 | } 42 | 43 | /** 44 | * Returns service container. 45 | * 46 | * @return ContainerInterface 47 | */ 48 | protected static function getClientContainer(): ContainerInterface 49 | { 50 | return static::createClient()->getContainer(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/app/fixture/TestBundle/Document/User.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\Document; 13 | 14 | use ONGR\ElasticsearchBundle\Annotation as ES; 15 | 16 | /** 17 | * User document for testing. Type name is different than class name. 18 | * 19 | * @ES\Document(type="users") 20 | */ 21 | class User 22 | { 23 | /** 24 | * @var string 25 | * 26 | * @ES\Id() 27 | */ 28 | private $id; 29 | 30 | /** 31 | * @var string 32 | * @ES\Property(type="text") 33 | */ 34 | private $firstName; 35 | 36 | /** 37 | * @var string 38 | * @ES\Property(type="text", name="last_name") 39 | */ 40 | private $lastName; 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function getId() 46 | { 47 | return $this->id; 48 | } 49 | 50 | /** 51 | * @param string $id 52 | */ 53 | public function setId($id) 54 | { 55 | $this->id = $id; 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public function getFirstName() 62 | { 63 | return $this->firstName; 64 | } 65 | 66 | /** 67 | * @param string $firstName 68 | */ 69 | public function setFirstName($firstName) 70 | { 71 | $this->firstName = $firstName; 72 | } 73 | 74 | /** 75 | * @return string 76 | */ 77 | public function getLastName() 78 | { 79 | return $this->lastName; 80 | } 81 | 82 | /** 83 | * @param string $lastName 84 | */ 85 | public function setLastName($lastName) 86 | { 87 | $this->lastName = $lastName; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Event/BulkEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Event; 13 | 14 | class BulkEvent extends BaseEvent 15 | { 16 | /** 17 | * @var string 18 | */ 19 | private $operation; 20 | 21 | /** 22 | * @var string|array 23 | */ 24 | private $type; 25 | 26 | /** 27 | * @var array 28 | */ 29 | private $query; 30 | 31 | /** 32 | * @param string $operation 33 | * @param string|array $type 34 | * @param array $query 35 | */ 36 | public function __construct($operation, $type, array $query) 37 | { 38 | $this->type = $type; 39 | $this->query = $query; 40 | $this->operation = $operation; 41 | } 42 | 43 | /** 44 | * @return array|string 45 | */ 46 | public function getType() 47 | { 48 | return $this->type; 49 | } 50 | 51 | /** 52 | * @param array|string $type 53 | */ 54 | public function setType($type) 55 | { 56 | $this->type = $type; 57 | } 58 | 59 | /** 60 | * @return array 61 | */ 62 | public function getQuery() 63 | { 64 | return $this->query; 65 | } 66 | 67 | /** 68 | * @param array $query 69 | */ 70 | public function setQuery($query) 71 | { 72 | $this->query = $query; 73 | } 74 | 75 | /** 76 | * @return array 77 | */ 78 | public function getOperation() 79 | { 80 | return $this->operation; 81 | } 82 | 83 | /** 84 | * @param string $operation 85 | */ 86 | public function setOperation($operation) 87 | { 88 | $this->operation = $operation; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Tests/Unit/EventListener/TerminateListenerTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Unit\EventListener; 13 | 14 | use ONGR\ElasticsearchBundle\EventListener\TerminateListener; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | /** 18 | * Tests TerminateListener class 19 | */ 20 | class TerminateListenerTest extends TestCase 21 | { 22 | /** 23 | * Tests kernel terminate event 24 | */ 25 | public function testKernelTerminate() 26 | { 27 | $manager = $this->getMockBuilder('ONGR\ElasticsearchBundle\Service\Manager') 28 | ->disableOriginalConstructor() 29 | ->getMock(); 30 | 31 | $manager->expects($this->once()) 32 | ->method('commit'); 33 | 34 | $container = $this->getMockBuilder('Symfony\Component\DependencyInjection\Container') 35 | ->disableOriginalConstructor() 36 | ->getMock(); 37 | 38 | $container->expects($this->any()) 39 | ->method('initialized') 40 | ->with('es.manager.test_available') 41 | ->willReturn(true); 42 | 43 | $container->expects($this->any()) 44 | ->method('get') 45 | ->with('es.manager.test_available') 46 | ->willReturn($manager); 47 | 48 | $listener = new TerminateListener( 49 | $container, 50 | [ 51 | 'test_available' => [ 52 | 'force_commit' => true, 53 | ], 54 | 'test_unavailable' => [ 55 | 'force_commit' => true, 56 | ], 57 | ] 58 | ); 59 | 60 | $listener->onKernelTerminate(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Resources/doc/scan.md: -------------------------------------------------------------------------------- 1 | # Scan through the index 2 | 3 | If the index is huge and in a single request there is not enough to get all records index scan might help. 4 | 5 | > More info about index scanning in [elastic official docs](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html#scroll-scan) 6 | 7 | You can scan index with any structured search and do a continuous scroll. To execute that kind of search you only need to append a scroll time amount to a `Search` object. 8 | 9 | > Scan & Scroll doesn't work for `Repository::findArray()` method. 10 | 11 | Here's an example with scrolling: 12 | 13 | ```php 14 | 15 | $repo = $this->get('es.manager.default.city'); 16 | $search = $repo->createSearch(); 17 | 18 | $search->setScroll('10m'); // Scroll time 19 | 20 | $termQuery = new TermQuery('country', 'Lithuania'); 21 | $search->addQuery($termQuery); 22 | 23 | $results = $repo->findDocuments($search); 24 | 25 | foreach ($results as $document) { 26 | 27 | //.... 28 | 29 | } 30 | 31 | ``` 32 | 33 | Usually result amount will be 10 (if no size set), but if the result type is any iterator, it will do a next request when the results set limit is reached and will continue results scrolling in foreach cycle. You dont have to worry about any scroll ids and how to perform second request, everything is handled automatically. 34 | 35 | If you are using `findRaw()` method, then bundle won't request next iteration and you should do it by yourself. Here's an example how to do it: 36 | 37 | 38 | ```php 39 | 40 | $repo = $this->get('es.manager.default.city'); 41 | $search = $repo->createSearch(); 42 | 43 | $search->setScroll('10m'); // Scroll time 44 | 45 | $termQuery = new TermQuery('country', 'Lithuania'); 46 | $search->addQuery($termQuery); 47 | 48 | $results = $repo->findRaw($search); 49 | 50 | foreach ($results as $raw) { 51 | 52 | // Do something with RAW results 53 | 54 | } 55 | 56 | $nextIteration = $repo->getManager()->scroll($results['_scroll_id'], '10m'); 57 | 58 | ``` 59 | -------------------------------------------------------------------------------- /Tests/app/fixture/TestBundle/Entity/Product.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\Entity; 13 | 14 | use Doctrine\Common\Collections\ArrayCollection; 15 | use ONGR\ElasticsearchBundle\Annotation as ES; 16 | 17 | /** 18 | * Product document for testing. 19 | * 20 | * @ES\Document() 21 | */ 22 | class Product 23 | { 24 | /** 25 | * @var string 26 | * 27 | * @ES\Id() 28 | */ 29 | public $id; 30 | 31 | /** 32 | * @var string 33 | * @ES\Property(type="keyword", name="title") 34 | */ 35 | public $title; 36 | 37 | /** 38 | * @var CategoryObject[] 39 | * 40 | * @ES\Embedded(class="TestBundle:CategoryObject", multiple=true) 41 | */ 42 | public $categories; 43 | 44 | public function __construct() 45 | { 46 | $this->categories = new ArrayCollection(); 47 | } 48 | 49 | /** 50 | * @return string 51 | */ 52 | public function getId() 53 | { 54 | return $this->id; 55 | } 56 | 57 | /** 58 | * @param string $id 59 | */ 60 | public function setId($id) 61 | { 62 | $this->id = $id; 63 | } 64 | 65 | /** 66 | * @return string 67 | */ 68 | public function getTitle() 69 | { 70 | return $this->title; 71 | } 72 | 73 | /** 74 | * @param string $title 75 | */ 76 | public function setTitle($title) 77 | { 78 | $this->title = $title; 79 | } 80 | 81 | /** 82 | * @return CategoryObject[] 83 | */ 84 | public function getCategories() 85 | { 86 | return $this->categories; 87 | } 88 | 89 | /** 90 | * @param CategoryObject[] $categories 91 | */ 92 | public function setCategories($categories) 93 | { 94 | $this->categories = $categories; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Command/IndexDropCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Command; 13 | 14 | use Symfony\Component\Console\Input\InputInterface; 15 | use Symfony\Component\Console\Input\InputOption; 16 | use Symfony\Component\Console\Output\OutputInterface; 17 | use Symfony\Component\Console\Style\SymfonyStyle; 18 | 19 | /** 20 | * Command for dropping Elasticsearch index. 21 | */ 22 | class IndexDropCommand extends AbstractManagerAwareCommand 23 | { 24 | public static $defaultName = 'ongr:es:index:drop'; 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | protected function configure(): void 30 | { 31 | parent::configure(); 32 | 33 | $this 34 | ->setName(static::$defaultName) 35 | ->setDescription('Drops elasticsearch index.') 36 | ->addOption( 37 | 'force', 38 | 'f', 39 | InputOption::VALUE_NONE, 40 | 'Set this parameter to execute this command' 41 | ); 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | protected function execute(InputInterface $input, OutputInterface $output): int 48 | { 49 | $io = new SymfonyStyle($input, $output); 50 | if ($input->getOption('force')) { 51 | $this->getManager($input->getOption('manager'))->dropIndex(); 52 | 53 | $io->text( 54 | sprintf( 55 | 'Dropped index for the `%s` manager', 56 | $input->getOption('manager') 57 | ) 58 | ); 59 | } else { 60 | $io->error('ATTENTION:'); 61 | $io->text('This action should not be used in the production environment.'); 62 | $io->error('Option --force is mandatory to drop type(s).'); 63 | } 64 | 65 | return 0; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Tests/Unit/Result/RawIteratorTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Unit\Result; 13 | 14 | use ONGR\ElasticsearchBundle\Result\RawIterator; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | class RawIteratorTest extends TestCase 18 | { 19 | /** 20 | * Test for getAggregations(). 21 | */ 22 | public function testGetAggregations() 23 | { 24 | $rawData = [ 25 | 'aggregations' => [ 26 | 'avg_grade' => [ 27 | 'value' => 75, 28 | ], 29 | ], 30 | ]; 31 | 32 | $manager = $this->getMockBuilder('ONGR\ElasticsearchBundle\Service\Manager') 33 | ->disableOriginalConstructor() 34 | ->getMock(); 35 | 36 | $iterator = new RawIterator($rawData, $manager); 37 | 38 | $this->assertEquals($rawData['aggregations'], $iterator->getAggregations()); 39 | } 40 | 41 | /** 42 | * Tests iterator. 43 | */ 44 | public function testIterator() 45 | { 46 | $rawData = [ 47 | 'hits' => [ 48 | 'total' => 1, 49 | 'hits' => [ 50 | [ 51 | '_index' => 'test', 52 | '_type' => 'product', 53 | '_id' => 'foo', 54 | '_score' => 1, 55 | '_source' => [ 56 | 'title' => 'Product Foo', 57 | ], 58 | ], 59 | ], 60 | ], 61 | ]; 62 | 63 | $manager = $this->getMockBuilder('ONGR\ElasticsearchBundle\Service\Manager') 64 | ->disableOriginalConstructor() 65 | ->getMock(); 66 | 67 | $iterator = new RawIterator($rawData, $manager); 68 | 69 | $this->assertEquals($rawData['hits']['hits'][0], $iterator->current()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Service/GenerateService.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Service; 13 | 14 | use ONGR\ElasticsearchBundle\Generator\DocumentGenerator; 15 | use Symfony\Component\Filesystem\Filesystem; 16 | use Symfony\Component\HttpKernel\Bundle\BundleInterface; 17 | 18 | /** 19 | * Generate Service 20 | */ 21 | class GenerateService 22 | { 23 | /** 24 | * @var DocumentGenerator 25 | */ 26 | private $generator; 27 | 28 | /** 29 | * @var Filesystem 30 | */ 31 | private $filesystem; 32 | 33 | /** 34 | * Constructor 35 | * 36 | * @param DocumentGenerator $generator 37 | * @param Filesystem $filesystem 38 | */ 39 | public function __construct(DocumentGenerator $generator, Filesystem $filesystem) 40 | { 41 | $this->generator = $generator; 42 | $this->filesystem = $filesystem; 43 | } 44 | 45 | /** 46 | * Generates document class 47 | * 48 | * @param BundleInterface $bundle 49 | * @param string $document 50 | * @param string $annotation 51 | * @param string $type 52 | * @param array $properties 53 | */ 54 | public function generate( 55 | BundleInterface $bundle, 56 | $document, 57 | $annotation, 58 | $type, 59 | array $properties 60 | ) { 61 | $documentPath = $bundle->getPath() . '/Document/' . str_replace('\\', '/', $document) . '.php'; 62 | $class = [ 63 | 'name' => $bundle->getNamespace() . '\\Document\\' . $document, 64 | 'annotation' => $annotation, 65 | 'type' => $type, 66 | 'properties' => $properties, 67 | ]; 68 | 69 | $documentCode = $this->generator->generateDocumentClass($class); 70 | 71 | $this->filesystem->mkdir(dirname($documentPath)); 72 | file_put_contents($documentPath, $documentCode); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Annotation/Embedded.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Annotation; 13 | 14 | use ONGR\ElasticsearchBundle\Mapping\Caser; 15 | 16 | /** 17 | * Annotation for property which points to inner object. 18 | * 19 | * @Annotation 20 | * @Target("PROPERTY") 21 | */ 22 | final class Embedded 23 | { 24 | /** 25 | * Inner object class name. 26 | * 27 | * @var string Object name to map 28 | * 29 | * @Doctrine\Common\Annotations\Annotation\Required 30 | */ 31 | public $class; 32 | 33 | /** 34 | * Name of the type field. Defaults to normalized property name. 35 | * 36 | * @var string 37 | */ 38 | public $name; 39 | 40 | /** 41 | * Defines if related value will store a single object or an array of objects 42 | * 43 | * If this value is set to true, in the result ObjectIterator will be provided, 44 | * otherwise you will get single object. 45 | * 46 | * @var bool Object or ObjectIterator 47 | */ 48 | public $multiple; 49 | 50 | /** 51 | * In this field you can define options. 52 | * 53 | * @var array 54 | */ 55 | public $options; 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function dump(array $exclude = []) 61 | { 62 | $array = array_diff_key( 63 | array_filter( 64 | get_object_vars($this), 65 | function ($value) { 66 | return $value || is_bool($value); 67 | } 68 | ), 69 | array_flip(array_merge(['class', 'name', 'multiple'], $exclude)) 70 | ); 71 | 72 | return array_combine( 73 | array_map( 74 | function ($key) { 75 | return Caser::snake($key); 76 | }, 77 | array_keys($array) 78 | ), 79 | array_values($array) 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Mapping/Caser.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Mapping; 13 | 14 | use Doctrine\Inflector\Inflector; 15 | use Doctrine\Inflector\InflectorFactory; 16 | 17 | /** 18 | * Utility for string case transformations. 19 | */ 20 | class Caser 21 | { 22 | /** 23 | * @var Inflector 24 | */ 25 | private static $inflector; 26 | 27 | /** 28 | * Transforms string to camel case (e.g., resultString). 29 | * 30 | * @param string $string Text to transform. 31 | * 32 | * @return string 33 | */ 34 | public static function camel($string) 35 | { 36 | $inflector = static::getInflector(); 37 | 38 | return $inflector->camelize($string); 39 | } 40 | 41 | /** 42 | * Transforms string to snake case (e.g., result_string). 43 | * 44 | * @param string $string Text to transform. 45 | * 46 | * @return string 47 | */ 48 | public static function snake($string) 49 | { 50 | $string = preg_replace('#([A-Z\d]+)([A-Z][a-z])#', '\1_\2', self::camel($string)); 51 | $string = preg_replace('#([a-z\d])([A-Z])#', '\1_\2', $string); 52 | 53 | return strtolower(strtr($string, '-', '_')); 54 | } 55 | 56 | /** 57 | * @return Inflector 58 | */ 59 | private static function getInflector() 60 | { 61 | if (static::$inflector === null) { 62 | if (\class_exists(InflectorFactory::class)) { 63 | static::$inflector = InflectorFactory::create()->build(); 64 | } else { 65 | @trigger_error( 66 | 'Using the old inflector is deprecated please upgrade the "doctrine/inflector" package.', 67 | E_USER_DEPRECATED 68 | ); 69 | 70 | static::$inflector = new \Doctrine\Common\Inflector\Inflector(); 71 | } 72 | } 73 | 74 | return static::$inflector; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Tests/Unit/Service/ManagerFactoryTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Unit\Service; 13 | 14 | use ONGR\ElasticsearchBundle\Service\ManagerFactory; 15 | use ONGR\ElasticsearchBundle\Service\Manager; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | class ManagerFactoryTest extends TestCase 19 | { 20 | /** 21 | * Tests createManager with logger 22 | */ 23 | public function testCreateManagerWithEnabledLogger() 24 | { 25 | $metadataCollector = $this->getMockBuilder('ONGR\ElasticsearchBundle\Mapping\MetadataCollector') 26 | ->disableOriginalConstructor() 27 | ->getMock(); 28 | $metadataCollector->expects($this->any())->method('getClientMapping')->will($this->returnValue([])); 29 | $converter = $this->getMockBuilder('ONGR\ElasticsearchBundle\Result\Converter') 30 | ->disableOriginalConstructor() 31 | ->getMock(); 32 | 33 | $dispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcher') 34 | ->disableOriginalConstructor() 35 | ->getMock(); 36 | 37 | $logger = $this->createMock('Psr\Log\LoggerInterface'); 38 | $managerFactory = new ManagerFactory( 39 | $metadataCollector, 40 | $converter, 41 | $logger, 42 | $logger 43 | ); 44 | 45 | $managerFactory->setEventDispatcher($dispatcher); 46 | 47 | $manager = $managerFactory->createManager( 48 | 'test', 49 | [ 50 | 'index_name' => 'test', 51 | 'settings' => [], 52 | 'hosts' => [] 53 | ], 54 | [], 55 | [ 56 | 'mappings' => [], 57 | 'logger' => [ 58 | 'enabled' => true 59 | ], 60 | 'commit_mode' => 'flush', 61 | 'bulk_size' => 10 62 | ] 63 | ); 64 | $this->assertTrue($manager instanceof Manager); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tests/Functional/Result/GetDocumentSortTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Functional\Result; 13 | 14 | use ONGR\ElasticsearchDSL\Query\MatchAllQuery; 15 | use ONGR\ElasticsearchDSL\Sort\FieldSort; 16 | use ONGR\ElasticsearchBundle\Service\Repository; 17 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 18 | 19 | class GetDocumentSortTest extends AbstractElasticsearchTestCase 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | protected function getDataArray() 25 | { 26 | return [ 27 | 'default' => [ 28 | 'product' => [ 29 | [ 30 | '_id' => 'doc1', 31 | 'title' => 'Foo Product', 32 | 'price' => 5.00, 33 | ], 34 | [ 35 | '_id' => 'doc2', 36 | 'title' => 'Bar Product', 37 | 'price' => 8.33, 38 | ], 39 | [ 40 | '_id' => 'doc3', 41 | 'title' => 'Lao Product', 42 | 'price' => 1.95, 43 | ], 44 | ], 45 | ], 46 | ]; 47 | } 48 | 49 | /** 50 | * GetDocumentSort test. 51 | */ 52 | public function testGetDocumentSort() 53 | { 54 | /** @var Repository $repo */ 55 | $repo = $this->getManager()->getRepository('TestBundle:Product'); 56 | $match = new MatchAllQuery(); 57 | $sort = new FieldSort('price', 'asc'); 58 | $search = $repo->createSearch()->addQuery($match); 59 | $search->addSort($sort); 60 | $results = $repo->findDocuments($search); 61 | $sort_result = []; 62 | $expected = [1.95, 5, 8.33]; 63 | 64 | foreach ($results as $result) { 65 | $sort_result[] = $results->getDocumentSort(); 66 | } 67 | 68 | $this->assertEquals($sort_result, $expected); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Command/AbstractManagerAwareCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Command; 13 | 14 | use ONGR\ElasticsearchBundle\Service\Manager; 15 | use Symfony\Component\Console\Command\Command; 16 | use Symfony\Component\Console\Input\InputOption; 17 | 18 | /** 19 | * AbstractElasticsearchCommand class. 20 | */ 21 | abstract class AbstractManagerAwareCommand extends Command 22 | { 23 | /** 24 | * @var Manager[] 25 | */ 26 | protected $managers; 27 | 28 | public function __construct(array $managers, $name = null) 29 | { 30 | parent::__construct($name); 31 | $this->managers = $managers; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | protected function configure(): void 38 | { 39 | $this->addOption( 40 | 'manager', 41 | 'm', 42 | InputOption::VALUE_REQUIRED, 43 | 'Manager name', 44 | 'default' 45 | ); 46 | } 47 | 48 | /** 49 | * Returns elasticsearch manager by name from service container. 50 | * 51 | * @param string $name Manager name defined in configuration. 52 | * 53 | * @return Manager 54 | * 55 | * @throws \RuntimeException If manager was not found. 56 | */ 57 | protected function getManager($name) 58 | { 59 | $id = $this->getManagerId($name); 60 | 61 | if (array_key_exists($id, $this->managers)) { 62 | return $this->managers[$id]; 63 | } 64 | 65 | throw new \RuntimeException( 66 | sprintf( 67 | 'Manager named `%s` not found. Available: `%s`.', 68 | $name, 69 | implode('`, `', array_keys($this->managers)) 70 | ) 71 | ); 72 | } 73 | 74 | /** 75 | * Formats manager service id from its name. 76 | * 77 | * @param string $name Manager name. 78 | * 79 | * @return string Service id. 80 | */ 81 | private function getManagerId($name) 82 | { 83 | return sprintf('es.manager.%s', $name); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /DependencyInjection/Compiler/MappingPass.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\DependencyInjection\Compiler; 13 | 14 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | use Symfony\Component\DependencyInjection\Definition; 17 | use Symfony\Component\DependencyInjection\Reference; 18 | use Symfony\Component\HttpKernel\Kernel; 19 | 20 | /** 21 | * Compiles elastic search data. 22 | */ 23 | class MappingPass implements CompilerPassInterface 24 | { 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function process(ContainerBuilder $container) 29 | { 30 | $analysis = $container->getParameter('es.analysis'); 31 | $managers = $container->getParameter('es.managers'); 32 | 33 | foreach ($managers as $managerName => $manager) { 34 | $connection = $manager['index']; 35 | $managerName = strtolower($managerName); 36 | 37 | $managerDefinition = new Definition( 38 | 'ONGR\ElasticsearchBundle\Service\Manager', 39 | [ 40 | $managerName, 41 | $connection, 42 | $analysis, 43 | $manager, 44 | ] 45 | ); 46 | $managerDefinition->setPublic(true); 47 | $managerDefinition->setFactory( 48 | [ 49 | new Reference('es.manager_factory'), 50 | 'createManager', 51 | ] 52 | ); 53 | $managerKey = sprintf('es.manager.%s', $managerName); 54 | $managerDefinition->addTag('es.manager', ['key' => $managerKey]); 55 | $container->setDefinition($managerKey, $managerDefinition); 56 | 57 | // Make es.manager.default as es.manager service. 58 | if ($managerName === 'default') { 59 | $container->setAlias('es.manager', 'es.manager.default'); 60 | 61 | if (Kernel::MAJOR_VERSION >= 4) { 62 | $container->getAlias('es.manager') 63 | ->setPublic(true); 64 | } 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tests/Unit/ElasticsearchBundleTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Unit; 13 | 14 | use ONGR\ElasticsearchBundle\ONGRElasticsearchBundle; 15 | use PHPUnit\Framework\TestCase; 16 | use Symfony\Component\DependencyInjection\Compiler\PassConfig; 17 | use Symfony\Component\DependencyInjection\ContainerBuilder; 18 | use Symfony\Component\Finder\Finder; 19 | use Symfony\Component\Finder\SplFileInfo; 20 | 21 | /** 22 | * Unit test for ONGR\ElasticsearchBundle. 23 | */ 24 | class ElasticsearchBundleTest extends TestCase 25 | { 26 | /** 27 | * @var array List of passes, which should not be added to compiler. 28 | */ 29 | protected $passesBlacklist = []; 30 | 31 | /** 32 | * Check whether all Passes in DependencyInjection/Compiler/ are added to container. 33 | */ 34 | public function testPassesRegistered() 35 | { 36 | $container = new ContainerBuilder(); 37 | $bundle = new ONGRElasticsearchBundle(); 38 | $bundle->build($container); 39 | 40 | /** @var array $loadedPasses Array of class names of loaded passes */ 41 | $loadedPasses = []; 42 | /** @var PassConfig $passConfig */ 43 | $passConfig = $container->getCompiler()->getPassConfig(); 44 | foreach ($passConfig->getPasses() as $pass) { 45 | $classPath = explode('\\', get_class($pass)); 46 | $loadedPasses[] = end($classPath); 47 | } 48 | 49 | $finder = new Finder(); 50 | $finder->files()->in(__DIR__ . '/../../DependencyInjection/Compiler/'); 51 | 52 | /** @var $file SplFileInfo */ 53 | foreach ($finder as $file) { 54 | $passName = str_replace('.php', '', $file->getFilename()); 55 | // Check whether pass is not blacklisted and not added by bundle. 56 | if (!in_array($passName, $this->passesBlacklist)) { 57 | $this->assertContains( 58 | $passName, 59 | $loadedPasses, 60 | sprintf( 61 | "Compiler pass '%s' is not added to container or not blacklisted in test.", 62 | $passName 63 | ) 64 | ); 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Resources/doc/meta_fields.md: -------------------------------------------------------------------------------- 1 | Using Elasticsearch Meta-Fields 2 | === 3 | 4 | This page is about document metadata also known as meta-fields. For detailed 5 | explanation on what it is and how it works read official Elasticsearch 6 | [documentation][1]. Below we explain how to use meta-fields supported 7 | by this bundle. 8 | 9 | Supported Meta-Fields 10 | --- 11 | 12 | ### @Id (_id) 13 | 14 | None of meta-fields is mandatory, but probably you will be using `_id` more than 15 | others. This meta-field works both ways. When you read document from Elasticsearch 16 | you get document's ID in associated property. Anytime you want to change document's ID 17 | or set custom ID for new document, just set value to this property and it will be 18 | stored in Elasticsearch. `_id` meta-field is represented by `@Id` annotation: 19 | 20 | ```php 21 | use ONGR\ElasticsearchBundle\Annotation as ES; 22 | 23 | /** 24 | * @ES\Document() 25 | */ 26 | class Person 27 | { 28 | /** 29 | * @ES\Id() 30 | */ 31 | public $id; 32 | 33 | // ... 34 | } 35 | ``` 36 | 37 | ### @ParentDocument (_parent) 38 | 39 | `_parent` meta-field is used to create a parent-child relationship between two mapping 40 | types. It is represented by `@ParentDocument` annotation. The only one and required 41 | attribute is `class`, where you need to specify class of parent document. 42 | 43 | ```php 44 | /** 45 | * @ES\ParentDocument(class="AppBundle:Family") 46 | */ 47 | public $parent; 48 | ``` 49 | 50 | ### @Routing (_routing) 51 | 52 | Custom routing patterns can be implemented by specifying a custom routing value per document. 53 | The same routing value needs to be provided when getting, deleting, or updating the document. 54 | It is represented by the `@Routing` annotation. Here is an example of such a field: 55 | 56 | ```php 57 | /** 58 | * @ES\Routing() 59 | */ 60 | public $routing; 61 | ``` 62 | 63 | Forgetting the routing value can lead to a document being indexed on more than one shard. 64 | As a safeguard, the _routing field can be configured to make a custom routing value 65 | required for all CRUD operations. This can be implemented by setting the `equired` 66 | attribute to true (`@ES\Routing(required=true)`). 67 | 68 | More information on routing can be found in the [dedicated docs][2] 69 | 70 | [1]: https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-fields.html 71 | [2]: https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-routing-field.html 72 | -------------------------------------------------------------------------------- /Tests/Unit/Mapping/MetadataCollectorTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Unit\Mapping; 13 | 14 | use Doctrine\Common\Cache\CacheProvider; 15 | use ONGR\ElasticsearchBundle\Mapping\DocumentFinder; 16 | use ONGR\ElasticsearchBundle\Mapping\DocumentParser; 17 | use ONGR\ElasticsearchBundle\Mapping\MetadataCollector; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | class MetadataCollectorTest extends TestCase 21 | { 22 | /** 23 | * @var MetadataCollector 24 | */ 25 | private $metadataCollector; 26 | 27 | /** 28 | * @var DocumentFinder 29 | */ 30 | private $docFinder; 31 | 32 | /** 33 | * @var DocumentParser 34 | */ 35 | private $docParser; 36 | 37 | /** 38 | * @var CacheProvider 39 | */ 40 | private $cache; 41 | 42 | /** 43 | * Initialize MetadataCollector. 44 | */ 45 | protected function setUp(): void 46 | { 47 | $this->docFinder = $this->getMockBuilder('ONGR\ElasticsearchBundle\Mapping\DocumentFinder') 48 | ->disableOriginalConstructor() 49 | ->getMock(); 50 | 51 | $this->docParser = $this->getMockBuilder('ONGR\ElasticsearchBundle\Mapping\DocumentParser') 52 | ->disableOriginalConstructor() 53 | ->getMock(); 54 | 55 | $this->cache = $this->getMockBuilder('Doctrine\Common\Cache\FilesystemCache') 56 | ->disableOriginalConstructor() 57 | ->getMock(); 58 | 59 | $this->metadataCollector = new MetadataCollector($this->docFinder, $this->docParser, $this->cache); 60 | } 61 | 62 | /** 63 | * Test bundle mapping parser when requesting non string bundle name. 64 | */ 65 | public function testGetBundleMappingWithNotStringName() 66 | { 67 | $this->expectException(\LogicException::class); 68 | $this->expectExceptionMessage('getBundleMapping() in the Metadata collector expects a string argument only!'); 69 | 70 | $this->metadataCollector->getBundleMapping(1000); 71 | } 72 | 73 | /** 74 | * Test for getClientMapping() in case no mapping exists. 75 | */ 76 | public function testGetClientMappingNull() 77 | { 78 | $this->assertNull($this->metadataCollector->getClientMapping([])); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "handcraftedinthealps/elasticsearch-bundle", 3 | "description": "Elasticsearch bundle for Symfony.", 4 | "type": "symfony-bundle", 5 | "homepage": "http://ongr.io", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "ONGR team", 10 | "homepage": "http://www.ongr.io" 11 | }, 12 | { 13 | "name": "Handcrafted in the Alps Team", 14 | "homepage": "https://github.com/handcraftedinthealps/ElasticsearchBundle/graphs/contributors" 15 | } 16 | ], 17 | "require": { 18 | "php": "^7.2|^8.0", 19 | "symfony/framework-bundle": "^3.4|^4|^5|^6|^7", 20 | "symfony/console": "^3.4|^4|^5|^6|^7", 21 | "symfony/stopwatch": "^3.4|^4|^5|^6|^7", 22 | "symfony/templating": "^3.4|^4|^5|^6|^7", 23 | "symfony/asset": "^3.4|^4|^5|^6|^7", 24 | "doctrine/annotations": "^1.13 || ^2.0", 25 | "doctrine/inflector": "^1.0 || ^2.0", 26 | "doctrine/collections": "^1.4|^2.0", 27 | "monolog/monolog": "^1.10 || ^2.0 || ^3.0", 28 | "handcraftedinthealps/elasticsearch-dsl": "^5.0.7.1|^6.2.0.1|^7.2.0.1", 29 | "symfony/event-dispatcher": "^3.4|^4|^5|^6|^7" 30 | }, 31 | "require-dev": { 32 | "mikey179/vfsstream": "~1.4", 33 | "squizlabs/php_codesniffer": "^2.0|^3.0", 34 | "symfony/browser-kit" : "^3.4|^4|^5|^6|^7", 35 | "symfony/expression-language" : "^3.4|^4|^5|^6|^7", 36 | "symfony/twig-bundle": "^3.4|^4|^5|^6|^7", 37 | "symfony/serializer": "^3.4|^4|^5|^6|^7", 38 | "symfony/yaml": "^3.4|^4|^5|^6|^7", 39 | "symfony/phpunit-bridge": "^5.1|^6|^7", 40 | "symfony/dependency-injection": "^3.4|^4|^5|^6|^7", 41 | "symfony/validator": "^3.4|^4|^5|^6|^7", 42 | "symfony/options-resolver": "^3.4|^4|^5|^6|^7" 43 | }, 44 | "autoload": { 45 | "psr-4": { "ONGR\\ElasticsearchBundle\\": "" }, 46 | "exclude-from-classmap": ["/Tests/", "/Test/"] 47 | }, 48 | "conflict": { 49 | "handcraftedinthealps/elasticsearch-dsl": "6.0 - 6.1.1 || 7.0 - 7.1.1", 50 | "ongr/elasticsearch-dsl": "6.0 - 6.1.1 || 7.0 - 7.1.1" 51 | }, 52 | "replace": { 53 | "ongr/elasticsearch-bundle": "self.version" 54 | }, 55 | "minimum-stability": "dev", 56 | "extra": { 57 | "symfony": { 58 | "recipe-contrib": false, 59 | "allow-contrib": false, 60 | "require": "7.1.*" 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests/Unit/Service/RepositoryTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Unit\Service; 13 | 14 | use ONGR\ElasticsearchBundle\Service\Repository; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | class RepositoryTest extends TestCase 18 | { 19 | /** 20 | * Data provider for testConstructorException(). 21 | * 22 | * @return array 23 | */ 24 | public function getTestConstructorExceptionData() 25 | { 26 | return [ 27 | [ 28 | 12345, 29 | '\InvalidArgumentException', 30 | 'must be a string', 31 | ], 32 | [ 33 | 'Non\Existing\ClassName', 34 | '\InvalidArgumentException', 35 | 'non-existing class', 36 | ], 37 | ]; 38 | } 39 | 40 | /** 41 | * @param $className 42 | * @param $expectedException 43 | * @param $expectedExceptionMessage 44 | * 45 | * @dataProvider getTestConstructorExceptionData() 46 | */ 47 | public function testConstructorException($className, $expectedException, $expectedExceptionMessage) 48 | { 49 | $this->expectException($expectedException, $expectedExceptionMessage); 50 | 51 | new Repository(null, $className); 52 | } 53 | 54 | /** 55 | * Tests class getter 56 | */ 57 | public function testGetRepositoryClass() 58 | { 59 | $collector = $this->getMockBuilder('ONGR\ElasticsearchBundle\Mapping\MetadataCollector') 60 | ->disableOriginalConstructor() 61 | ->getMock(); 62 | $collector->expects($this->any())->method('getDocumentType')->willReturn('product'); 63 | $manager = $this->getMockBuilder('ONGR\ElasticsearchBundle\Service\Manager') 64 | ->disableOriginalConstructor() 65 | ->getMock(); 66 | $manager->expects($this->any())->method('getMetadataCollector')->willReturn($collector); 67 | $repository = new Repository( 68 | $manager, 69 | 'ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\Document\Product' 70 | ); 71 | $this->assertEquals( 72 | 'ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\Document\Product', 73 | $repository->getClassName() 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Tests/Functional/Command/CacheClearCommandTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Functional\Command; 13 | 14 | use ONGR\ElasticsearchBundle\Command\CacheClearCommand; 15 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 16 | use Symfony\Component\Console\Application; 17 | use Symfony\Component\Console\Tester\CommandTester; 18 | 19 | class CacheClearCommandTest extends AbstractElasticsearchTestCase 20 | { 21 | 22 | const COMMAND_NAME = 'ongr:es:cache:clear'; 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | protected function getDataArray() 28 | { 29 | return ['default' => []]; 30 | } 31 | 32 | /** 33 | * Tests if command is being executed. 34 | */ 35 | public function testExecute() 36 | { 37 | $this->getManager(); 38 | 39 | $app = new Application(); 40 | $app->add($this->getCommand()); 41 | $command = $app->find(self::COMMAND_NAME); 42 | $tester = new CommandTester($command); 43 | $tester->execute( 44 | [ 45 | 'command' => $command->getName(), 46 | ] 47 | ); 48 | 49 | $this->assertStringContainsString( 50 | 'Elasticsearch index cache has been cleared for manager named `default`', 51 | $tester->getDisplay() 52 | ); 53 | $this->assertEquals(0, $tester->getStatusCode(), 'Status code should be zero.'); 54 | } 55 | 56 | /** 57 | * Tests if exception is thown when no manager is found. 58 | */ 59 | public function testExecuteException() 60 | { 61 | $this->expectException(\RuntimeException::class); 62 | 63 | $app = new Application(); 64 | $app->add($this->getCommand()); 65 | $command = $app->find('ongr:es:cache:clear'); 66 | $tester = new CommandTester($command); 67 | $tester->execute( 68 | [ 69 | 'command' => $command->getName(), 70 | '--manager' => 'notexistingmanager', 71 | ] 72 | ); 73 | } 74 | 75 | /** 76 | * Returns cache clear command instance. 77 | * 78 | * @return CacheClearCommand 79 | */ 80 | private function getCommand() 81 | { 82 | return new CacheClearCommand(['es.manager.default' => $this->getManager()]); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Tests/Unit/Service/GenerateServiceTest.php: -------------------------------------------------------------------------------- 1 | tmpDir = sys_get_temp_dir() . '/ongr'; 28 | 29 | $this->filesystem = new Filesystem(); 30 | $this->filesystem->remove($this->tmpDir); 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function doTearDown() 37 | { 38 | $this->filesystem->remove($this->tmpDir); 39 | } 40 | 41 | public function testGenerate() 42 | { 43 | $service = new GenerateService(new DocumentGenerator(), $this->filesystem); 44 | 45 | $service->generate( 46 | $this->getBundle(), 47 | 'Foo', 48 | 'Document', 49 | 'foo', 50 | [ 51 | [ 52 | 'field_name' => 'test', 53 | 'annotation' => 'property', 54 | 'visibility' => 'private', 55 | 'property_type' => 'string', 56 | 'property_name' => 'testProperty', 57 | 'property_options' => 'test', 58 | ], 59 | [ 60 | 'field_name' => 'embedded', 61 | 'visibility' => 'protected', 62 | 'annotation' => 'embedded', 63 | 'property_class' => 'TestBundle:Product', 64 | 'property_multiple' => true, 65 | ] 66 | ] 67 | ); 68 | 69 | $this->assertFileExists($this->getBundle()->getPath() . '/Document/Foo.php'); 70 | } 71 | 72 | /** 73 | * @return \PHPUnit_Framework_MockObject_MockObject 74 | */ 75 | private function getBundle() 76 | { 77 | $bundle = $this->createMock('Symfony\Component\HttpKernel\Bundle\BundleInterface'); 78 | $bundle->expects($this->any())->method('getPath')->will($this->returnValue($this->tmpDir)); 79 | $bundle->expects($this->any())->method('getName')->will($this->returnValue('FooBarBundle')); 80 | $bundle->expects($this->any())->method('getNamespace')->will($this->returnValue('Foo\BarBundle')); 81 | 82 | return $bundle; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Service/ImportService.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Service; 13 | 14 | use ONGR\ElasticsearchBundle\Service\Json\JsonReader; 15 | use Symfony\Component\Console\Helper\ProgressBar; 16 | use Symfony\Component\Console\Output\OutputInterface; 17 | 18 | /** 19 | * ImportService class. 20 | */ 21 | class ImportService 22 | { 23 | /** 24 | * Imports Elasticsearch index data. 25 | * 26 | * @param Manager $manager 27 | * @param string $filename 28 | * @param OutputInterface $output 29 | * @param array $options 30 | */ 31 | public function importIndex( 32 | Manager $manager, 33 | $filename, 34 | OutputInterface $output, 35 | $options 36 | ) { 37 | $reader = $this->getReader($manager, $this->getFilePath($filename), $options); 38 | 39 | $progress = new ProgressBar($output, $reader->count()); 40 | $progress->setRedrawFrequency(100); 41 | $progress->start(); 42 | 43 | $bulkSize = $options['bulk-size']; 44 | foreach ($reader as $key => $document) { 45 | $data = $document['_source']; 46 | $data['_id'] = $document['_id']; 47 | 48 | if (array_key_exists('fields', $document)) { 49 | $data = array_merge($document['fields'], $data); 50 | } 51 | 52 | $manager->bulk('index', $document['_type'], $data); 53 | 54 | if (($key + 1) % $bulkSize == 0) { 55 | $manager->commit(); 56 | } 57 | 58 | $progress->advance(); 59 | } 60 | 61 | $manager->commit(); 62 | 63 | $progress->finish(); 64 | $output->writeln(''); 65 | } 66 | 67 | /** 68 | * Returns a real file path. 69 | * 70 | * @param string $filename 71 | * 72 | * @return string 73 | */ 74 | protected function getFilePath($filename) 75 | { 76 | if ($filename[0] == '/' || strstr($filename, ':') !== false) { 77 | return $filename; 78 | } 79 | 80 | return realpath(getcwd() . '/' . $filename); 81 | } 82 | 83 | /** 84 | * Prepares JSON reader. 85 | * 86 | * @param Manager $manager 87 | * @param string $filename 88 | * @param array $options 89 | * 90 | * @return JsonReader 91 | */ 92 | protected function getReader($manager, $filename, $options) 93 | { 94 | return new JsonReader($manager, $filename, $options); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Tests/Functional/Result/DocumentWithMultipleFieldsTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Functional\Result; 13 | 14 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 15 | use ONGR\ElasticsearchDSL\Query\FullText\MatchQuery; 16 | use ONGR\ElasticsearchDSL\Query\TermLevel\TermQuery; 17 | 18 | class DocumentWithMultipleFieldsTest extends AbstractElasticsearchTestCase 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | protected function getDataArray() 24 | { 25 | return [ 26 | 'default' => [ 27 | 'product' => [ 28 | [ 29 | '_id' => 'doc1', 30 | 'title' => 'Bar Product', 31 | 'related_categories' => [ 32 | [ 33 | 'title' => 'Acme', 34 | ], 35 | [ 36 | 'title' => 'Bar', 37 | ], 38 | ], 39 | ], 40 | [ 41 | '_id' => 'doc2', 42 | 'title' => 'Foo Product', 43 | ], 44 | [ 45 | '_id' => 'doc3', 46 | 'title' => 'Bar Production', 47 | ], 48 | ], 49 | ], 50 | ]; 51 | } 52 | 53 | /** 54 | * Test if we can add more objects into document's "multiple objects" field. 55 | */ 56 | public function testMultipleFields() 57 | { 58 | $repo = $this->getManager()->getRepository('TestBundle:Product'); 59 | 60 | // throw new \Exception('Fuck you phpunit'); 61 | 62 | $query = new MatchQuery('title', 'Bar'); 63 | $search = $repo->createSearch(); 64 | $search->addQuery($query); 65 | $result = $repo->findDocuments($search); 66 | $this->assertEquals(2, count($result)); 67 | 68 | $query = new TermQuery('title.raw', 'Bar'); 69 | $search = $repo->createSearch(); 70 | $search->addQuery($query); 71 | $result = $repo->findDocuments($search); 72 | $this->assertEquals(0, count($result)); 73 | 74 | $query = new TermQuery('title.raw', 'Foo Product'); 75 | $search = $repo->createSearch(); 76 | $search->addQuery($query); 77 | $result = $repo->findDocuments($search); 78 | $this->assertEquals(1, count($result)); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Tests/Functional/Mapping/MetadataCollectorTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Functional\Mapping; 13 | 14 | use ONGR\ElasticsearchBundle\Exception\MissingDocumentAnnotationException; 15 | use ONGR\ElasticsearchBundle\Mapping\MetadataCollector; 16 | use ONGR\ElasticsearchBundle\Tests\WebTestCase; 17 | 18 | class MetadataCollectorTest extends WebTestCase 19 | { 20 | /** 21 | * @var MetadataCollector 22 | */ 23 | private $metadataCollector; 24 | 25 | /** 26 | * Initialize MetadataCollector. 27 | */ 28 | protected function setUp(): void 29 | { 30 | $container = $this->createClient()->getContainer(); 31 | $this->metadataCollector = $container->get('es.metadata_collector'); 32 | } 33 | 34 | /** 35 | * Test if function throws exception if ES type names are not unique. 36 | */ 37 | public function testGetBundleMappingWithTwoSameESTypes() 38 | { 39 | $this->expectException(\LogicException::class); 40 | 41 | $this->metadataCollector->getMappings(['TestBundle', 'TestBundle']); 42 | } 43 | 44 | /** 45 | * Test mapping getter when there are no bundles loaded from parser. 46 | */ 47 | public function testGetBundleMappingWithNoBundlesLoaded() 48 | { 49 | $this->expectException(\LogicException::class); 50 | $this->expectExceptionMessage('Bundle \'acme\' does not exist.'); 51 | 52 | $this->metadataCollector->getBundleMapping('acme'); 53 | } 54 | 55 | /** 56 | * Test for getBundleMapping(). Make sure meta fields are excluded from mapping. 57 | */ 58 | public function testGetBundleMapping() 59 | { 60 | $mapping = $this->metadataCollector->getBundleMapping('TestBundle'); 61 | 62 | $properties = $mapping['product']['properties']; 63 | $this->assertArrayNotHasKey('_id', $properties); 64 | // $this->assertArrayNotHasKey('_ttl', $properties); 65 | 66 | $aliases = $mapping['product']['aliases']; 67 | $this->assertArrayHasKey('_id', $aliases); 68 | // $this->assertArrayHasKey('_ttl', $aliases); 69 | $this->assertArrayHasKey('_routing', $aliases); 70 | } 71 | 72 | /** 73 | * Test for getDocumentType() in case invalid class given. 74 | */ 75 | public function testGetDocumentTypeException() 76 | { 77 | $this->expectException(MissingDocumentAnnotationException::class); 78 | $this->expectExceptionMessage('cannot be parsed as document because @Document annotation is missing'); 79 | 80 | $this->metadataCollector->getDocumentType('\StdClass'); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Command/IndexImportCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Command; 13 | 14 | use ONGR\ElasticsearchBundle\Service\ImportService; 15 | use Symfony\Component\Console\Input\InputArgument; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Input\InputOption; 18 | use Symfony\Component\Console\Output\OutputInterface; 19 | use Symfony\Component\Console\Style\SymfonyStyle; 20 | 21 | /** 22 | * IndexImportCommand class. 23 | */ 24 | class IndexImportCommand extends AbstractManagerAwareCommand 25 | { 26 | public static $defaultName = 'ongr:es:index:import'; 27 | 28 | /** 29 | * @var ImportService 30 | */ 31 | protected $importService; 32 | 33 | public function __construct(ImportService $importService, array $managers = []) 34 | { 35 | parent::__construct($managers, self::$defaultName); 36 | 37 | $this->importService = $importService; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | protected function configure(): void 44 | { 45 | parent::configure(); 46 | 47 | $this 48 | ->setName(static::$defaultName) 49 | ->setDescription('Imports data to elasticsearch index.') 50 | ->addArgument( 51 | 'filename', 52 | InputArgument::REQUIRED, 53 | 'Select file to store output' 54 | ) 55 | ->addOption( 56 | 'bulk-size', 57 | 'b', 58 | InputOption::VALUE_REQUIRED, 59 | 'Set bulk size for import', 60 | 1000 61 | ) 62 | ->addOption( 63 | 'gzip', 64 | 'z', 65 | InputOption::VALUE_NONE, 66 | 'Import a gzip file' 67 | ); 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | protected function execute(InputInterface $input, OutputInterface $output): int 74 | { 75 | $io = new SymfonyStyle($input, $output); 76 | $manager = $this->getManager($input->getOption('manager')); 77 | 78 | // Initialize options array 79 | $options = []; 80 | if ($input->getOption('gzip')) { 81 | $options['gzip'] = null; 82 | } 83 | $options['bulk-size'] = $input->getOption('bulk-size'); 84 | 85 | $this->importService->importIndex( 86 | $manager, 87 | $input->getArgument('filename'), 88 | $output, 89 | $options 90 | ); 91 | 92 | $io->success('Data import completed!'); 93 | 94 | return 0; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Resources/doc/find_functions.md: -------------------------------------------------------------------------------- 1 | # Quick find functions 2 | 3 | > If you haven't read about maping and simple usage of the bundle please take a look at first to the [mapping](mapping.md) docs. 4 | 5 | For all examples below we will use `Content` document class from the [CRUD actions](crud.md) chapter. 6 | 7 | ## Find a document by ID 8 | 9 | Find by id will execute [elasticsearch get query](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html). 10 | 11 | ```php 12 | 13 | $repo = $this->get('es.manager.default.content'); 14 | 15 | /** @var $content Content **/ 16 | $content = $repo->find(1); // 5 is the document _uid in the elasticsearch. 17 | 18 | ``` 19 | 20 | > All `find` methods return an object. If you want to get raw result use `execute($search, Result::RESULTS_RAW)`. 21 | 22 | ## Find multiple documents by ID 23 | 24 | If multiple documents need to be found by their IDs, `findByIds()` method can be used. It accepts an array of document IDs 25 | and returns `DocumentIterator` with found documents: 26 | 27 | ```php 28 | 29 | $documents = $repo->findByIds(['26', '8', '11']); 30 | 31 | ``` 32 | 33 | For this functionality the `Repository` uses 34 | [elasticsearch multi get API](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-multi-get.html). 35 | 36 | ## Find by field 37 | 38 | Find by field uses [query_string query](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html) to fetch results by a specified field value. 39 | 40 | > Document matches heavily depend on how mapping is defined. If you are unsure of whether it will work, it is better to use structured search from `Search` object. 41 | 42 | 43 | ```php 44 | 45 | $repo = $this->get('es.manager.default.content'); 46 | 47 | /** @var $content Content **/ 48 | $content = $repo->findBy(['title' => 'Acme']); 49 | 50 | ``` 51 | 52 | The return will be: 53 | 54 | ``` 55 | Array 56 | ( 57 | [0] => Array 58 | ( 59 | [title] => Acme 60 | ) 61 | ) 62 | ``` 63 | 64 | Also with `findBy` you can define the way the results are ordered, limit the amount of retrieved documents and define the offset of the results, eg: 65 | 66 | ```php 67 | 68 | $content = $repo->findBy(['title' => 'Acme'], ['price' => 'asc'], 20, 10); 69 | 70 | ``` 71 | 72 | This will return up to 20 documents with the word 'Acme' in their title, also it will skip the first 10 results and the results will be ordered from the ones with the smallest price to the ones with the highest. 73 | 74 | ## Find one document by field 75 | 76 | Completely the same as `findBy()` function, except it will return the first document. 77 | 78 | ```php 79 | 80 | $repo = $this->get('es.manager.default.content'); 81 | 82 | /** @var $content Content **/ 83 | $content = $repo->findOneBy(['title' => 'Acme']); 84 | 85 | ``` 86 | 87 | The return will be: 88 | 89 | ``` 90 | Array 91 | ( 92 | [title] => Acme 93 | ) 94 | ``` 95 | -------------------------------------------------------------------------------- /Command/IndexExportCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Command; 13 | 14 | use ONGR\ElasticsearchBundle\Service\ExportService; 15 | use Symfony\Component\Console\Input\InputArgument; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Input\InputOption; 18 | use Symfony\Component\Console\Output\OutputInterface; 19 | use Symfony\Component\Console\Style\SymfonyStyle; 20 | 21 | /** 22 | * IndexExportCommand class. 23 | */ 24 | class IndexExportCommand extends AbstractManagerAwareCommand 25 | { 26 | public static $defaultName = 'ongr:es:index:export'; 27 | 28 | /** 29 | * @var ExportService 30 | */ 31 | protected $exportService; 32 | 33 | public function __construct(ExportService $exportService, array $managers = []) 34 | { 35 | parent::__construct($managers, self::$defaultName); 36 | 37 | $this->exportService = $exportService; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | protected function configure(): void 44 | { 45 | parent::configure(); 46 | 47 | $this 48 | ->setName(static::$defaultName) 49 | ->setDescription('Exports data from elasticsearch index.') 50 | ->addArgument( 51 | 'filename', 52 | InputArgument::REQUIRED, 53 | 'Select file to store output' 54 | )->addOption( 55 | 'types', 56 | null, 57 | InputOption::VALUE_REQUIRED + InputOption::VALUE_IS_ARRAY, 58 | 'Export specific types only' 59 | )->addOption( 60 | 'chunk', 61 | null, 62 | InputOption::VALUE_REQUIRED, 63 | 'Chunk size to use in scan api', 64 | 500 65 | )->addOption( 66 | 'split', 67 | null, 68 | InputOption::VALUE_REQUIRED, 69 | 'Split file in a separate parts if line number exceeds provided value', 70 | 300000 71 | ); 72 | } 73 | 74 | /** 75 | * {@inheritdoc} 76 | */ 77 | protected function execute(InputInterface $input, OutputInterface $output): int 78 | { 79 | $io = new SymfonyStyle($input, $output); 80 | $manager = $this->getManager($input->getOption('manager')); 81 | 82 | $this->exportService->exportIndex( 83 | $manager, 84 | $input->getArgument('filename'), 85 | $input->getOption('types'), 86 | $input->getOption('chunk'), 87 | $output, 88 | $input->getOption('split') 89 | ); 90 | 91 | $io->success('Data export completed!'); 92 | 93 | return 0; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Tests/Functional/Result/AggregationIteratorFindTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Functional\Result; 13 | 14 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 15 | use ONGR\ElasticsearchDSL\Aggregation\Bucketing\RangeAggregation; 16 | 17 | class AggregationIteratorFindTest extends AbstractElasticsearchTestCase 18 | { 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | protected function getDataArray() 23 | { 24 | return [ 25 | 'default' => [ 26 | 'product' => [ 27 | [ 28 | '_id' => 1, 29 | 'title' => 'Onion', 30 | 'description' => 'solid', 31 | 'price' => 10.45, 32 | ], 33 | [ 34 | '_id' => 2, 35 | 'title' => 'Tomato', 36 | 'description' => 'weak', 37 | 'price' => 32, 38 | ], 39 | [ 40 | '_id' => 3, 41 | 'title' => 'Pizza', 42 | 'description' => 'weak', 43 | 'price' => 15.1, 44 | ], 45 | ], 46 | ], 47 | ]; 48 | } 49 | 50 | /** 51 | * Aggregation iterator main test. 52 | */ 53 | public function testIteration() 54 | { 55 | $expected = [ 56 | [ 57 | 'key' => '*-20.0', 58 | 'doc_count' => 2, 59 | ], 60 | [ 61 | 'key' => '20.0-*', 62 | 'doc_count' => 1, 63 | ], 64 | ]; 65 | 66 | $repository = $this 67 | ->getManager() 68 | ->getRepository('TestBundle:Product'); 69 | 70 | $rangeAggregation = new RangeAggregation('range', 'price'); 71 | $rangeAggregation->addRange(null, 20); 72 | $rangeAggregation->addRange(20, null); 73 | 74 | $search = $repository 75 | ->createSearch() 76 | ->addAggregation($rangeAggregation); 77 | 78 | $results = $repository->findDocuments($search); 79 | $rangeResult = $results->getAggregation('range'); 80 | 81 | $this->assertInstanceOf('ONGR\ElasticsearchBundle\Result\Aggregation\AggregationValue', $rangeResult); 82 | 83 | foreach ($rangeResult->getBuckets() as $aggKey => $subAgg) { 84 | $this->assertInstanceOf('ONGR\ElasticsearchBundle\Result\Aggregation\AggregationValue', $subAgg); 85 | $this->assertEquals($expected[$aggKey]['key'], $subAgg->getValue('key')); 86 | $this->assertEquals($expected[$aggKey]['doc_count'], $subAgg->getValue('doc_count')); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Tests/Functional/Result/ArrayIteratorTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Functional\Result; 13 | 14 | use ONGR\ElasticsearchDSL\Query\MatchAllQuery; 15 | use ONGR\ElasticsearchBundle\Service\Repository; 16 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 17 | 18 | class ArrayIteratorTest extends AbstractElasticsearchTestCase 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | protected function getDataArray() 24 | { 25 | return [ 26 | 'default' => [ 27 | 'product' => [ 28 | [ 29 | '_id' => 'doc1', 30 | 'title' => 'Foo Product', 31 | 'related_categories' => [ 32 | [ 33 | 'title' => 'Acme', 34 | ], 35 | ], 36 | ], 37 | [ 38 | '_id' => 'doc2', 39 | 'title' => 'Bar Product', 40 | 'related_categories' => [ 41 | [ 42 | 'title' => 'Acme', 43 | ], 44 | [ 45 | 'title' => 'Bar', 46 | ], 47 | ], 48 | ], 49 | ], 50 | ], 51 | ]; 52 | } 53 | 54 | /** 55 | * Iteration test. 56 | */ 57 | public function testIteration() 58 | { 59 | /** @var Repository $repo */ 60 | $repo = $this->getManager()->getRepository('TestBundle:Product'); 61 | $match = new MatchAllQuery(); 62 | $search = $repo->createSearch()->addQuery($match); 63 | $iterator = $repo->findArray($search); 64 | 65 | $this->assertInstanceOf('ONGR\ElasticsearchBundle\Result\ArrayIterator', $iterator); 66 | 67 | $assertResults = $this->getDataArray(); 68 | foreach ($iterator as $key => $document) { 69 | $assertDocument = $assertResults['default']['product'][$key]; 70 | unset($assertDocument['_id']); 71 | $this->assertEquals($assertDocument, $document); 72 | } 73 | } 74 | 75 | /** 76 | * Test array results iteration with fields set. 77 | */ 78 | public function testIterationWhenFieldsAreSet() 79 | { 80 | /** @var Repository $repo */ 81 | $repo = $this->getManager()->getRepository('TestBundle:Product'); 82 | $match = new MatchAllQuery(); 83 | $search = $repo->createSearch()->addQuery($match); 84 | $iterator = $repo->findArray($search); 85 | 86 | $this->assertInstanceOf('ONGR\ElasticsearchBundle\Result\ArrayIterator', $iterator); 87 | 88 | $assertResults = $this->getDataArray(); 89 | foreach ($iterator as $key => $document) { 90 | $this->assertEquals($assertResults['default']['product'][$key]['title'], $document['title']); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /DependencyInjection/Compiler/RepositoryPass.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\DependencyInjection\Compiler; 13 | 14 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | use Symfony\Component\DependencyInjection\Definition; 17 | use Symfony\Component\DependencyInjection\Reference; 18 | use Symfony\Component\HttpKernel\Kernel; 19 | 20 | /** 21 | * Compiles elastic search data. 22 | */ 23 | class RepositoryPass implements CompilerPassInterface 24 | { 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function process(ContainerBuilder $container) 29 | { 30 | $managers = $container->getParameter('es.managers'); 31 | 32 | $removeContainerBuildId = false; 33 | if (!$container->hasParameter('container.build_id')) { 34 | // the 'container.build_id' is required for `es.cache_engine` system cache which normally can not 35 | // be constructor inside a compiler pass. This is a workaround to make it work. 36 | // see also: 37 | // - https://github.com/symfony/symfony/blob/52a92926f7fed15cdff399c6921100a10e0d6f61/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php#L389 38 | // - https://github.com/symfony/symfony/blob/52a92926f7fed15cdff399c6921100a10e0d6f61/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php#L2322 39 | $container->setParameter('container.build_id', hash('crc32', 'Abc123' . time())); 40 | $removeContainerBuildId = true; 41 | } 42 | 43 | $collector = $container->get('es.metadata_collector'); 44 | if ($removeContainerBuildId) { 45 | $container->getParameterBag()->remove('container.build_id'); 46 | } 47 | 48 | foreach ($managers as $managerName => $manager) { 49 | $mappings = $collector->getMappings($manager['mappings']); 50 | 51 | // Building repository services. 52 | foreach ($mappings as $repositoryType => $repositoryDetails) { 53 | $repositoryDefinition = new Definition( 54 | 'ONGR\ElasticsearchBundle\Service\Repository', 55 | [$repositoryDetails['namespace']] 56 | ); 57 | $repositoryDefinition->setPublic(true); 58 | 59 | if (isset($repositoryDetails['directory_name']) && $managerName == 'default') { 60 | $container->get('es.document_finder')->setDocumentDir($repositoryDetails['directory_name']); 61 | } 62 | 63 | $repositoryDefinition->setFactory( 64 | [ 65 | new Reference(sprintf('es.manager.%s', $managerName)), 66 | 'getRepository', 67 | ] 68 | ); 69 | 70 | $repositoryId = sprintf('es.manager.%s.%s', $managerName, $repositoryType); 71 | $container->setDefinition($repositoryId, $repositoryDefinition); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Service/Json/JsonWriter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Service\Json; 13 | 14 | /** 15 | * Serializes records one by one. Outputs given metadata before first record. 16 | * 17 | * Sample output: 18 | *

19 | * [ 20 | * {"count":2}, 21 | * {"_id":"doc1","title":"Document 1"}, 22 | * {"_id":"doc2","title":"Document 2"} 23 | * ] 24 | *

25 | */ 26 | class JsonWriter 27 | { 28 | /** 29 | * @var string 30 | */ 31 | private $filename; 32 | 33 | /** 34 | * @var resource A file system pointer resource. 35 | */ 36 | private $handle; 37 | 38 | /** 39 | * @var array 40 | */ 41 | private $metadata; 42 | 43 | /** 44 | * @var int Current record number. 45 | */ 46 | private $currentPosition = 0; 47 | 48 | /** 49 | * Constructor. 50 | * 51 | * Metadata can contain any fields but only the field "count" 52 | * is recognized and used while writing to file. If written lines count 53 | * reaches "count", writer will automatically finalize file. 54 | * 55 | * @param string $filename A file in which data will be saved. 56 | * @param array $metadata Additional metadata to be stored. 57 | */ 58 | public function __construct($filename, $metadata = []) 59 | { 60 | $this->filename = $filename; 61 | $this->metadata = $metadata; 62 | } 63 | 64 | /** 65 | * Destructor. Closes file handler if open. 66 | */ 67 | public function __destruct() 68 | { 69 | $this->finalize(); 70 | } 71 | 72 | /** 73 | * Performs initialization. 74 | */ 75 | protected function initialize() 76 | { 77 | if ($this->handle !== null) { 78 | return; 79 | } 80 | 81 | $this->handle = fopen($this->filename, 'w'); 82 | fwrite($this->handle, "[\n"); 83 | fwrite($this->handle, json_encode($this->metadata)); 84 | } 85 | 86 | /** 87 | * Performs finalization. 88 | */ 89 | public function finalize() 90 | { 91 | $this->initialize(); 92 | 93 | if (is_resource($this->handle)) { 94 | fwrite($this->handle, "\n]"); 95 | fclose($this->handle); 96 | } 97 | } 98 | 99 | /** 100 | * Writes single document to stream. 101 | * 102 | * @param mixed $document Object to insert into stream. 103 | * 104 | * @throws \OverflowException 105 | */ 106 | public function push($document) 107 | { 108 | $this->initialize(); 109 | $this->currentPosition++; 110 | 111 | if (isset($this->metadata['count']) && $this->currentPosition > $this->metadata['count']) { 112 | throw new \OverflowException( 113 | sprintf('This writer was set up to write %d documents, got more.', $this->metadata['count']) 114 | ); 115 | } 116 | 117 | fwrite($this->handle, ",\n"); 118 | fwrite($this->handle, json_encode($document)); 119 | 120 | if (isset($this->metadata['count']) && $this->currentPosition == $this->metadata['count']) { 121 | $this->finalize(); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Tests/Unit/Mapping/DocumentFinderTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Unit\Mapping; 13 | 14 | use ONGR\ElasticsearchBundle\Mapping\DocumentFinder; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | class DocumentFinderTest extends TestCase 18 | { 19 | /** 20 | * Data provider for testGetNamespace(). 21 | * 22 | * @return array 23 | */ 24 | public function getTestGetNamespaceData() 25 | { 26 | return [ 27 | [ 28 | 'ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\Document\Product', 29 | 'TestBundle:Product' 30 | ], 31 | [ 32 | 'ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\Document\User', 33 | 'TestBundle:User' 34 | ], 35 | ]; 36 | } 37 | 38 | /** 39 | * Data provider for testGetNamespaceWithSubDirInDocumentDirectory(). 40 | * 41 | * @return array 42 | */ 43 | public function getTestGetNamespaceDataWithSubDirInDocumentDir() 44 | { 45 | return [ 46 | [ 47 | 'ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\Document\Store\Product', 48 | 'TestBundle:Product', 49 | 'Document\Store' 50 | ], 51 | ]; 52 | } 53 | 54 | /** 55 | * Tests for getNamespace(). 56 | * 57 | * @param string $expectedNamespace 58 | * @param string $className 59 | * 60 | * @dataProvider getTestGetNamespaceData() 61 | */ 62 | public function testGetNamespace($expectedNamespace, $className) 63 | { 64 | $bundles = [ 65 | 'TestBundle' => 'ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\TestBundle' 66 | ]; 67 | $finder = new DocumentFinder($bundles); 68 | 69 | $this->assertEquals($expectedNamespace, $finder->getNamespace($className)); 70 | } 71 | 72 | /** 73 | * Tests for getNamespace() with a configured document directory. 74 | * 75 | * @param string $expectedNamespace 76 | * @param string $className 77 | * @param string $documentDir 78 | * 79 | * @dataProvider getTestGetNamespaceDataWithSubDirInDocumentDir() 80 | */ 81 | public function testGetNamespaceWithSubDirInDocumentDirectory($expectedNamespace, $className, $documentDir) 82 | { 83 | $bundles = [ 84 | 'TestBundle' => 'ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\TestBundle' 85 | ]; 86 | $finder = new DocumentFinder($bundles); 87 | 88 | $this->assertEquals($expectedNamespace, $finder->getNamespace($className, $documentDir)); 89 | } 90 | 91 | /** 92 | * Test for getBundleDocumentClasses(). 93 | */ 94 | public function testGetBundleDocumentClasses() 95 | { 96 | $bundles = [ 97 | 'TestBundle' => 'ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\TestBundle' 98 | ]; 99 | $finder = new DocumentFinder($bundles); 100 | 101 | $documents = $finder->getBundleDocumentClasses('TestBundle'); 102 | 103 | $this->assertGreaterThan(0, count($documents)); 104 | $this->assertContains('Product', $documents); 105 | $this->assertContains('User', $documents); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Tests/Unit/Service/Json/JsonWriterTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Unit\Service\Json; 13 | 14 | use ONGR\ElasticsearchBundle\Service\Json\JsonWriter; 15 | use org\bovigo\vfs\vfsStream; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | class JsonWriterTest extends TestCase 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public static function setUpBeforeClass(): void 24 | { 25 | parent::setUpBeforeClass(); 26 | 27 | vfsStream::setup('tmp'); 28 | } 29 | 30 | /** 31 | * Data provider for testPush(). 32 | * 33 | * @return array 34 | */ 35 | public function getTestPushData() 36 | { 37 | $cases = []; 38 | 39 | // Case #0 Standard case. 40 | $metadata = ['count' => 2]; 41 | $documents = [ 42 | [ 43 | '_id' => 'doc1', 44 | 'title' => 'Document 1', 45 | ], 46 | [ 47 | '_id' => 'doc2', 48 | 'title' => 'Document 2', 49 | ], 50 | ]; 51 | $expectedOutput = << 'doc1', 70 | 'title' => 'Document 1', 71 | ], 72 | [ 73 | '_id' => 'doc2', 74 | 'title' => 'Document 2', 75 | ], 76 | ]; 77 | $expectedOutput = <<push($document); 118 | } 119 | 120 | $writer->finalize(); 121 | 122 | $this->assertEquals($expectedOutput, file_get_contents($filename)); 123 | } 124 | 125 | /** 126 | * Test for push() in case of too many documents passed. 127 | */ 128 | public function testPushException() 129 | { 130 | $this->expectException(\OverflowException::class); 131 | 132 | $filename = vfsStream::url('tmp/test.json'); 133 | 134 | $writer = new JsonWriter($filename, ['count' => 0]); 135 | $writer->push(null); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /DependencyInjection/ONGRElasticsearchExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\DependencyInjection; 13 | 14 | use Symfony\Component\Config\FileLocator; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | use Symfony\Component\DependencyInjection\ContainerInterface; 17 | use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; 18 | use Symfony\Component\DependencyInjection\Loader; 19 | use Symfony\Component\DependencyInjection\Definition; 20 | use Symfony\Component\HttpKernel\DependencyInjection\Extension; 21 | use Symfony\Component\DependencyInjection\Reference; 22 | use Symfony\Component\HttpKernel\Kernel; 23 | use Symfony\Component\HttpKernel\KernelEvents; 24 | 25 | /** 26 | * This is the class that loads and manages bundle configuration. 27 | */ 28 | class ONGRElasticsearchExtension extends Extension implements PrependExtensionInterface 29 | { 30 | public function prepend(ContainerBuilder $container) 31 | { 32 | if ($container->hasExtension('framework')) { 33 | $container->prependExtensionConfig( 34 | 'framework', 35 | [ 36 | 'cache' => [ 37 | 'pools' => [ 38 | 'es.cache_engine' => [ 39 | 'adapter' => 'cache.system', 40 | ], 41 | ], 42 | ], 43 | ] 44 | ); 45 | } 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function load(array $configs, ContainerBuilder $container) 52 | { 53 | $configuration = new Configuration(); 54 | $config = $this->processConfiguration($configuration, $configs); 55 | 56 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); 57 | $loader->load('services.yml'); 58 | 59 | $config['cache'] = isset($config['cache']) ? 60 | $config['cache'] : !$container->getParameter('kernel.debug'); 61 | $config['profiler'] = isset($config['profiler']) ? 62 | $config['profiler'] : $container->getParameter('kernel.debug'); 63 | 64 | $managers = $config['managers']; 65 | foreach ($managers as &$manager) { 66 | $manager['mappings'] = array_unique($manager['mappings'], SORT_REGULAR); 67 | } 68 | $container->setParameter('es.cache', $config['cache']); 69 | $container->setParameter('es.analysis', $config['analysis']); 70 | $container->setParameter('es.managers', $managers); 71 | $container->setParameter('es.app_root_class', $config['app_root_class'] ?? null); 72 | $definition = new Definition( 73 | 'ONGR\ElasticsearchBundle\Service\ManagerFactory', 74 | [ 75 | new Reference('es.metadata_collector'), 76 | new Reference('es.result_converter'), 77 | $config['profiler'] ? new Reference('es.tracer') : null, 78 | new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE), 79 | ] 80 | ); 81 | $definition->addMethodCall('setEventDispatcher', [new Reference('event_dispatcher')]); 82 | $definition->addMethodCall( 83 | 'setStopwatch', 84 | [ 85 | new Reference('debug.stopwatch', ContainerInterface::NULL_ON_INVALID_REFERENCE) 86 | ] 87 | ); 88 | $definition->setPublic(true); 89 | $container->setDefinition('es.manager_factory', $definition); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/Functional/Result/HashMapObjectIteratorTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Functional\Result; 13 | 14 | use ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\Document\Product; 15 | use ONGR\ElasticsearchDSL\Query\MatchAllQuery; 16 | use ONGR\ElasticsearchBundle\Service\Repository; 17 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 18 | 19 | class HashMapObjectIteratorTest extends AbstractElasticsearchTestCase 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | protected function getDataArray() 25 | { 26 | return [ 27 | 'default' => [ 28 | 'product' => [ 29 | [ 30 | '_id' => '1', 31 | 'title' => 'Foo Product', 32 | 'custom_attributes' => [ 33 | [ 34 | 'one' => 'Acme', 35 | ], 36 | [ 37 | 'two' => 'Bar', 38 | ], 39 | [ 40 | 'three' => 'Foo', 41 | ], 42 | ], 43 | ], 44 | [ 45 | '_id' => '2', 46 | 'title' => 'Bar Product', 47 | 'custom_attributes' => [ 48 | [ 49 | 'title' => 'Acme', 50 | ], 51 | [ 52 | 'title' => 'Bar', 53 | ], 54 | [ 55 | 'title' => 'Foo', 56 | ], 57 | ], 58 | ], 59 | ], 60 | ], 61 | ]; 62 | } 63 | 64 | /** 65 | * Iteration test. 66 | */ 67 | public function testIteration() 68 | { 69 | /** @var Repository $repo */ 70 | $repo = $this->getManager()->getRepository('TestBundle:Product'); 71 | $match = new MatchAllQuery(); 72 | $search = $repo->createSearch()->addQuery($match); 73 | $iterator = $repo->findDocuments($search); 74 | 75 | $this->assertInstanceOf('ONGR\ElasticsearchBundle\Result\DocumentIterator', $iterator); 76 | 77 | /** @var Product $document */ 78 | foreach ($iterator as $document) { 79 | $this->assertInstanceOf( 80 | 'ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\Document\Product', 81 | $document 82 | ); 83 | 84 | $this->assertCount(3, $document->getCustomAttributes()); 85 | 86 | $attributes = $document->getCustomAttributes(); 87 | 88 | switch ($document->getId()) { 89 | case '1': 90 | $this->assertEquals($attributes[0]['one'], 'Acme'); 91 | $this->assertEquals($attributes[1]['two'], 'Bar'); 92 | $this->assertEquals($attributes[2]['three'], 'Foo'); 93 | break; 94 | case '2': 95 | $this->assertEquals($attributes[0]['title'], 'Acme'); 96 | $this->assertEquals($attributes[1]['title'], 'Bar'); 97 | $this->assertEquals($attributes[2]['title'], 'Foo'); 98 | break; 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Tests/Unit/Result/AbstractResultsIteratorTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Unit\Result; 13 | 14 | use PHPUnit\Framework\TestCase; 15 | 16 | class AbstractResultsIteratorTest extends TestCase 17 | { 18 | /** 19 | * Test if scroll is cleared on destructor. 20 | */ 21 | public function testClearScroll() 22 | { 23 | $rawData = [ 24 | '_scroll_id' => 'foo', 25 | ]; 26 | 27 | $manager = $this->getMockBuilder('ONGR\ElasticsearchBundle\Service\Manager') 28 | ->setMethods(['getConfig', 'clearScroll']) 29 | ->disableOriginalConstructor() 30 | ->getMock(); 31 | $manager->expects($this->any())->method('getConfig')->willReturn([]); 32 | $manager->expects($this->once())->method('clearScroll')->with('foo'); 33 | 34 | $scroll = ['_scroll_id' => 'foo', 'duration' => '5m']; 35 | $iterator = new DummyIterator($rawData, $manager, $scroll); 36 | 37 | // Trigger destructor call 38 | unset($iterator); 39 | } 40 | 41 | /** 42 | * Test for getDocumentScore(). 43 | */ 44 | public function testGetDocumentScore() 45 | { 46 | $rawData = [ 47 | 'hits' => [ 48 | 'total' => 3, 49 | 'hits' => [ 50 | [ 51 | '_index' => 'test', 52 | '_type' => 'product', 53 | '_id' => 'foo', 54 | '_score' => 1, 55 | '_source' => [ 56 | 'title' => 'Product Foo', 57 | ], 58 | ], 59 | [ 60 | '_index' => 'test', 61 | '_type' => 'product', 62 | '_id' => 'bar', 63 | '_score' => 2, 64 | '_source' => [ 65 | 'title' => 'Product Bar', 66 | ], 67 | ], 68 | [ 69 | '_index' => 'test', 70 | '_type' => 'product', 71 | '_id' => 'baz', 72 | '_score' => null, 73 | '_source' => [ 74 | 'title' => 'Product Baz', 75 | ], 76 | ], 77 | ], 78 | ], 79 | ]; 80 | 81 | $manager = $this->getMockBuilder('ONGR\ElasticsearchBundle\Service\Manager') 82 | ->disableOriginalConstructor() 83 | ->getMock(); 84 | 85 | $results = new DummyIterator($rawData, $manager); 86 | 87 | $expectedScores = [1, 2, null]; 88 | $actualScores = []; 89 | 90 | foreach ($results as $item) { 91 | $actualScores[] = $results->getDocumentScore(); 92 | } 93 | 94 | $this->assertEquals($expectedScores, $actualScores); 95 | } 96 | 97 | /** 98 | * Test for getDocumentScore() in case called when current iterator value is not valid. 99 | */ 100 | public function testGetScoreException() 101 | { 102 | $this->expectException(\LogicException::class); 103 | $this->expectExceptionMessage('Document score is available only while iterating over results'); 104 | 105 | $manager = $this->getMockBuilder('ONGR\ElasticsearchBundle\Service\Manager') 106 | ->disableOriginalConstructor() 107 | ->getMock(); 108 | 109 | $results = new DummyIterator([], $manager); 110 | $results->getDocumentScore(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Tests/Functional/Result/DocumentIteratorTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Functional\Result; 13 | 14 | use ONGR\ElasticsearchDSL\Query\MatchAllQuery; 15 | use ONGR\ElasticsearchBundle\Service\Repository; 16 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 17 | 18 | class DocumentIteratorTest extends AbstractElasticsearchTestCase 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | protected function getDataArray() 24 | { 25 | return [ 26 | 'default' => [ 27 | 'product' => [ 28 | [ 29 | '_id' => 'doc1', 30 | 'title' => 'Foo Product', 31 | 'related_categories' => [ 32 | [ 33 | 'title' => 'Acme', 34 | ], 35 | ], 36 | ], 37 | [ 38 | '_id' => 'doc2', 39 | 'title' => 'Bar Product', 40 | 'related_categories' => [ 41 | [ 42 | 'title' => 'Acme', 43 | ], 44 | [ 45 | 'title' => 'Bar', 46 | ], 47 | ], 48 | ], 49 | ], 50 | ], 51 | ]; 52 | } 53 | 54 | /** 55 | * Iteration test. 56 | */ 57 | public function testIteration() 58 | { 59 | /** @var Repository $repo */ 60 | $repo = $this->getManager()->getRepository('TestBundle:Product'); 61 | $match = new MatchAllQuery(); 62 | $search = $repo->createSearch()->addQuery($match); 63 | $iterator = $repo->findDocuments($search); 64 | 65 | $this->assertInstanceOf('ONGR\ElasticsearchBundle\Result\DocumentIterator', $iterator); 66 | 67 | foreach ($iterator as $document) { 68 | $categories = $document->getRelatedCategories(); 69 | 70 | $this->assertInstanceOf( 71 | 'ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\Document\Product', 72 | $document 73 | ); 74 | $this->assertInstanceOf('ONGR\ElasticsearchBundle\Result\ObjectIterator', $categories); 75 | 76 | foreach ($categories as $category) { 77 | $this->assertInstanceOf( 78 | 'ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\Document\CategoryObject', 79 | $category 80 | ); 81 | } 82 | } 83 | } 84 | 85 | /** 86 | * Tests if current() returns null when data doesn't exist. 87 | */ 88 | public function testCurrentWithEmptyIterator() 89 | { 90 | $repo = $this->getManager()->getRepository('TestBundle:User'); 91 | $search = $repo 92 | ->createSearch() 93 | ->addQuery(new MatchAllQuery()); 94 | $result = $repo->findDocuments($search); 95 | 96 | $this->assertNull($result->current()); 97 | } 98 | 99 | /** 100 | * Tests AbstractResultsIterator#first method. 101 | */ 102 | public function testIteratorFirst() 103 | { 104 | $repo = $this->getManager()->getRepository('TestBundle:Product'); 105 | $search = $repo 106 | ->createSearch() 107 | ->addQuery(new MatchAllQuery()); 108 | $document = $repo->findDocuments($search)->first(); 109 | 110 | $this->assertEquals('Foo Product', $document->getTitle()); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Result/Aggregation/AggregationValue.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Result\Aggregation; 13 | 14 | /** 15 | * This is the class for plain aggregation result with nested aggregations support. 16 | */ 17 | class AggregationValue implements \ArrayAccess, \IteratorAggregate 18 | { 19 | /** 20 | * @var array 21 | */ 22 | private $rawData; 23 | 24 | /** 25 | * Constructor. 26 | * 27 | * @param array $rawData 28 | */ 29 | public function __construct($rawData) 30 | { 31 | $this->rawData = $rawData; 32 | } 33 | 34 | /** 35 | * Returns aggregation value by name. 36 | * 37 | * @param string $name 38 | * 39 | * @return array 40 | */ 41 | public function getValue($name = 'key') 42 | { 43 | if (!isset($this->rawData[$name])) { 44 | return null; 45 | } 46 | 47 | return $this->rawData[$name]; 48 | } 49 | 50 | /** 51 | * Returns the document count of the aggregation 52 | * 53 | * @return integer 54 | */ 55 | public function getCount() 56 | { 57 | return $this->getValue('doc_count'); 58 | } 59 | 60 | /** 61 | * Returns array of bucket values. 62 | * 63 | * @return AggregationValue[]|null 64 | */ 65 | public function getBuckets() 66 | { 67 | if (!isset($this->rawData['buckets'])) { 68 | return null; 69 | } 70 | 71 | $buckets = []; 72 | 73 | foreach ($this->rawData['buckets'] as $bucket) { 74 | $buckets[] = new self($bucket); 75 | } 76 | 77 | return $buckets; 78 | } 79 | 80 | /** 81 | * Returns sub-aggregation. 82 | * 83 | * @param string $name 84 | * 85 | * @return AggregationValue|null 86 | */ 87 | public function getAggregation($name) 88 | { 89 | if (!isset($this->rawData[$name])) { 90 | return null; 91 | } 92 | 93 | return new self($this->rawData[$name]); 94 | } 95 | 96 | /** 97 | * Applies path method to aggregations. 98 | * 99 | * @param string $path 100 | * 101 | * @return AggregationValue|null 102 | */ 103 | public function find($path) 104 | { 105 | $name = explode('.', $path, 2); 106 | $aggregation = $this->getAggregation($name[0]); 107 | 108 | if ($aggregation === null || !isset($name[1])) { 109 | return $aggregation; 110 | } 111 | 112 | return $aggregation->find($name[1]); 113 | } 114 | 115 | /** 116 | * {@inheritdoc} 117 | */ 118 | #[\ReturnTypeWillChange] 119 | public function offsetExists($offset) 120 | { 121 | return array_key_exists($offset, $this->rawData); 122 | } 123 | 124 | /** 125 | * {@inheritdoc} 126 | */ 127 | #[\ReturnTypeWillChange] 128 | public function offsetGet($offset) 129 | { 130 | if (!isset($this->rawData[$offset])) { 131 | return null; 132 | } 133 | 134 | return $this->rawData[$offset]; 135 | } 136 | 137 | /** 138 | * {@inheritdoc} 139 | */ 140 | #[\ReturnTypeWillChange] 141 | public function offsetSet($offset, $value) 142 | { 143 | throw new \LogicException('Aggregation result can not be changed on runtime.'); 144 | } 145 | 146 | /** 147 | * {@inheritdoc} 148 | */ 149 | #[\ReturnTypeWillChange] 150 | public function offsetUnset($offset) 151 | { 152 | throw new \LogicException('Aggregation result can not be changed on runtime.'); 153 | } 154 | 155 | /** 156 | * {@inheritdoc} 157 | */ 158 | public function getIterator() 159 | { 160 | $buckets = $this->getBuckets(); 161 | 162 | if ($buckets === null) { 163 | throw new \LogicException('Can not iterate over aggregation without buckets!'); 164 | } 165 | 166 | return new \ArrayIterator($this->getBuckets()); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Resources/doc/crud.md: -------------------------------------------------------------------------------- 1 | # CRUD actions 2 | 3 | > To proceed with steps bellow it is necessary to read [mapping](mapping.md) topic and have defined documents in the bundle. 4 | 5 | For all steps below we assume that there is an `AppBundle` with the `Content` document. 6 | 7 | ```php 8 | // src/AppBundle/Document/Content.php 9 | 10 | namespace AppBundle\Document; 11 | 12 | use ONGR\ElasticsearchBundle\Annotation as ES; 13 | 14 | /** 15 | * @ES\Document(type="content") 16 | */ 17 | class Content 18 | { 19 | /** 20 | * @var string 21 | * 22 | * @ES\Id() 23 | */ 24 | public $id; 25 | 26 | /** 27 | * @ES\Property(type="keyword") 28 | */ 29 | public $title; 30 | } 31 | ``` 32 | 33 | ## Manager 34 | 35 | Elasticsearch bundle provides managers able to handle several indexes to communicate with elasticsearch. 36 | 37 | Once you define managers in your `config.yml` file, you can use them in controllers and grab them from DI container via `es.manager` (alias for `es.manager.default`). If you define more than one manager, for example called `foo`, then it will be accessible via `es.manager.foo`. 38 | 39 | ```php 40 | 41 | $manager = $this->get('es.manager'); 42 | 43 | ``` 44 | 45 | ## Repositories 46 | 47 | In addition manager provides repository access, which enables direct access to the elasticsearch type. 48 | Repository represents a document. Whenever you need to do any action with a repository, you can access 49 | it like this: 50 | 51 | ```php 52 | 53 | $manager = $this->get('es.manager'); 54 | $repo = $manager->getRepository('AppBundle:Content'); 55 | 56 | ``` 57 | 58 | Alternatively: 59 | 60 | ```php 61 | 62 | $repo = $this->get('es.manager.default.content'); 63 | 64 | ``` 65 | 66 | `default` - represents a manager name and `content` an elasticsearch type name. 67 | 68 | > Important: Document with the certain type name has to be mapped in the manager. 69 | 70 | You can also get a manager from the repo instance: 71 | 72 | ```php 73 | 74 | $manager = $repo->getManager(); 75 | 76 | ``` 77 | 78 | ## Create a document 79 | 80 | ```php 81 | 82 | $content = new Content(); 83 | $content->id = 5; // Optional, if not set, elasticsearch will set a random. 84 | $content->title = 'Acme title'; 85 | $manager->persist($content); 86 | $manager->commit(); 87 | 88 | ``` 89 | 90 | ## Update a document 91 | 92 | ```php 93 | 94 | $content = $manager->find('AppBundle:Content', 5); 95 | $content->title = 'changed Acme title'; 96 | $manager->persist($content); 97 | $manager->commit(); 98 | 99 | ``` 100 | 101 | ## Partial update 102 | 103 | There is a quicker way to update a document field without creating object or fetching a whole document from elasticsearch. For this action we will use [partial update](https://www.elastic.co/guide/en/elasticsearch/guide/current/partial-updates.html) from elasticsearch. 104 | 105 | To update a field you need to know the document `ID` and fields to update. Here's an example: 106 | 107 | ```php 108 | 109 | $repo = $this->get('es.manager.default.content'); 110 | $repo->update(1, ['title' => 'new title']); 111 | 112 | ``` 113 | 114 | You can also update fields with script operation, lets say, you want to do some math: 115 | 116 | 117 | ```php 118 | 119 | $repo = $this->get('es.manager.default.product'); 120 | $repo->update(1, [], 'ctx._source.stock+=1'); 121 | 122 | ``` 123 | > Important: when using script update fields cannot be updated, leave empty array, otherwise you will get 400 exception. 124 | 125 | `ctx._source` comes from groovy scripting and you have to enable it in elasticsearch config with: `script.groovy.sandbox.enabled: false` 126 | 127 | 128 | In addition you also can get other document fields with the response of update, lets say we also want a content field and a new title, so just add them separated by a comma: 129 | 130 | 131 | ```php 132 | 133 | $repo = $this->get('es.manager.default.content'); 134 | $response = $repo->update(1, ['title' => 'new title'], null, ['fields' => 'title,content']); 135 | 136 | ``` 137 | 138 | 139 | ## Delete a document 140 | 141 | Document removal can be performed similarly to create or update action: 142 | 143 | ```php 144 | $manager->remove($content); 145 | $manager->commit(); 146 | ``` 147 | 148 | Alternatively you can remove document by ID (requires to have repository service): 149 | 150 | ```php 151 | 152 | $repo = $this->get('es.manager.default.content'); 153 | $content = $repo->remove(5); 154 | 155 | ``` 156 | -------------------------------------------------------------------------------- /Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | es.app_root_class: 'App\Kernel' 3 | es.logging.path: "%kernel.logs_dir%/elasticsearch_%kernel.environment%.log" 4 | 5 | services: 6 | es.export: 7 | class: 'ONGR\ElasticsearchBundle\Service\ExportService' 8 | public: true 9 | 10 | es.import: 11 | class: 'ONGR\ElasticsearchBundle\Service\ImportService' 12 | public: true 13 | 14 | es.client.index_suffix_finder: 15 | class: 'ONGR\ElasticsearchBundle\Service\IndexSuffixFinder' 16 | public: true 17 | 18 | es.annotations.cached_reader: 19 | class: 'Doctrine\Common\Annotations\PsrCachedReader' 20 | public: true 21 | arguments: ["@es.annotations.reader", "@es.cache_engine", "%kernel.debug%"] 22 | 23 | es.document_finder: 24 | class: 'ONGR\ElasticsearchBundle\Mapping\DocumentFinder' 25 | public: true 26 | arguments: ["%kernel.bundles%", "%es.app_root_class%"] 27 | 28 | es.document_parser: 29 | class: 'ONGR\ElasticsearchBundle\Mapping\DocumentParser' 30 | public: true 31 | arguments: ["@es.annotations.cached_reader", "@es.document_finder"] 32 | 33 | es.metadata_collector: 34 | class: 'ONGR\ElasticsearchBundle\Mapping\MetadataCollector' 35 | public: true 36 | arguments: ["@es.document_finder", "@es.document_parser", "@es.cache_engine"] 37 | calls: 38 | - [setEnableCache, ["%es.cache%"]] 39 | 40 | es.logger.collection_handler: 41 | class: 'ONGR\ElasticsearchBundle\Profiler\Handler\CollectionHandler' 42 | public: false 43 | 44 | es.tracer: 45 | class: 'Monolog\Logger' 46 | public: true 47 | arguments: ['ongr'] 48 | calls: 49 | - ['pushHandler', ["@es.logger.collection_handler"]] 50 | 51 | es.profiler: 52 | class: 'ONGR\ElasticsearchBundle\Profiler\ElasticsearchProfiler' 53 | public: true 54 | calls: 55 | - ['setManagers', ["%es.managers%"]] 56 | - ['addLogger', ["@es.tracer"]] 57 | tags: 58 | - { name: 'data_collector', template: "@ONGRElasticsearch/Profiler/profiler.html.twig", id: 'ongr.profiler' } 59 | 60 | es.result_converter: 61 | class: 'ONGR\ElasticsearchBundle\Result\Converter' 62 | public: true 63 | arguments: ["@es.metadata_collector"] 64 | 65 | es.terminate_listener: 66 | class: 'ONGR\ElasticsearchBundle\EventListener\TerminateListener' 67 | public: true 68 | arguments: ["@service_container", "%es.managers%"] 69 | tags: 70 | - { name: 'kernel.event_listener', event: 'kernel.terminate' } 71 | 72 | es.generator.document: 73 | class: 'ONGR\ElasticsearchBundle\Generator\DocumentGenerator' 74 | public: true 75 | 76 | es.generate: 77 | class: 'ONGR\ElasticsearchBundle\Service\GenerateService' 78 | public: true 79 | arguments: ["@es.generator.document", "@filesystem"] 80 | 81 | es.command.cache_clear: 82 | class: 'ONGR\ElasticsearchBundle\Command\CacheClearCommand' 83 | tags: 84 | - { name: 'console.command', command: 'ongr:es:cache:clear' } 85 | 86 | es.command.document_generate: 87 | class: 'ONGR\ElasticsearchBundle\Command\DocumentGenerateCommand' 88 | arguments: 89 | - '%kernel.bundles%' 90 | - '@es.generate' 91 | - '@es.metadata_collector' 92 | - '@es.annotations.cached_reader' 93 | tags: 94 | - { name: 'console.command', command: 'ongr:es:document:generate' } 95 | 96 | es.command.index_create: 97 | class: 'ONGR\ElasticsearchBundle\Command\IndexCreateCommand' 98 | arguments: 99 | - '@es.client.index_suffix_finder' 100 | tags: 101 | - { name: 'console.command', command: 'ongr:es:index:create' } 102 | 103 | es.command.index_export: 104 | class: 'ONGR\ElasticsearchBundle\Command\IndexExportCommand' 105 | arguments: 106 | - '@es.export' 107 | tags: 108 | - { name: 'console.command', command: 'ongr:es:index:export' } 109 | 110 | es.command.index_import: 111 | class: 'ONGR\ElasticsearchBundle\Command\IndexImportCommand' 112 | arguments: 113 | - '@es.import' 114 | tags: 115 | - { name: 'console.command', command: 'ongr:es:index:import' } 116 | 117 | es.command.index_drop: 118 | class: 'ONGR\ElasticsearchBundle\Command\IndexDropCommand' 119 | tags: 120 | - { name: 'console.command', command: 'ongr:es:index:drop' } 121 | -------------------------------------------------------------------------------- /Resources/doc/results_parsing.md: -------------------------------------------------------------------------------- 1 | # Working with results 2 | 3 | > This chapter covers what comes from [find](find_functions.md) and [search](search.md) requests. 4 | 5 | For all chapters below we will use a data example inserted in the elasticsearch content type: 6 | 7 | ``` 8 | // content type 9 | 10 | { 11 | "id": 1, 12 | "title": "Dr. Damien Yundt DVM" 13 | }, 14 | { 15 | "id": 2, 16 | "title": "Zane Heidenreich IV" 17 | }, 18 | { 19 | "id": 3, 20 | "title": "Hattie Shields MD" 21 | } 22 | 23 | ``` 24 | 25 | ## Results iterator 26 | 27 | Usually when any search action is performed the `DocumentIterator` will be returned. It has plenty of helper functions to aggregate more efficiently with the results. 28 | 29 | 30 | Lets assume you search the index with: 31 | 32 | ```php 33 | 34 | $repo = $this->get('es.manager.default.content'); 35 | $search = $repo->createSearch(); 36 | $termQuery = new MatchAllQuery(); 37 | $results = $repo->findDocuments($search); 38 | 39 | ``` 40 | 41 | So all 3 content elements will be found. `DocumentIterator`implements [`\Countable`](http://php.net/manual/en/class.countable.php), [`\Iterator`](http://php.net/manual/en/class.iterator.php) interfaces functions. The results are traversable and you can run `foreach` cycle through it. 42 | 43 | ```php 44 | 45 | echo $results->count() . "\n"; 46 | 47 | /** @var AppBundle:Content $document */ 48 | foreach ($results as $document) { 49 | echo $document->title . "\n"; 50 | } 51 | 52 | ``` 53 | 54 | it will print: 55 | 56 | ``` 57 | 3 58 | Dr. Damien Yundt DVM 59 | Zane Heidenreich IV 60 | Hattie Shields MD 61 | ``` 62 | 63 | ### Getting Document Score 64 | 65 | In most cases Elasticsearch returns result score (`_score` field) for each document. 66 | As this score might be different per search it's not treated as document field and 67 | cannot be associated with document. You can get document's score from results iterator 68 | while iterating: 69 | 70 | ```php 71 | $results = $repository->findDocuments($search); 72 | 73 | foreach ($results as $document) { 74 | echo $document->title, $results->getDocumentScore(); 75 | } 76 | ``` 77 | 78 | Example above prints titles of all documents following search score. 79 | 80 | ### Getting Document Sort 81 | 82 | Similarly to Document score, during iteration you can get document sort, provided you 83 | added a sort to your search, you can retrieve your sort value while iterating the 84 | results: 85 | 86 | ```php 87 | $results = $repository->execute($search); 88 | 89 | foreach ($results as $document) { 90 | echo $document->title, $results->getDocumentSort(); 91 | } 92 | ``` 93 | 94 | #### Important notice 95 | 96 | `DocumentIterator` doesn't cache or store generated document object. `Converter` directly returns the instance after it's requested and will generate again if it will be requested. 97 | 98 | We highly recommend to `unset()` document instance after you don't need it or manage memory at your own way. 99 | 100 | There is a possibility to change the `DocumentIterator` behaviour. Take a look at the [overwriting bundle parts](http://docs.ongr.io/ElasticsearchBundle/overwriting_bundle). 101 | 102 | ## Aggregations 103 | 104 | If your search query includes aggregations you can reach calculated aggregations 105 | by calling `getAggregation()` method. 106 | 107 | In example below we show how to build query with aggregations and how to handle 108 | aggregation results: 109 | 110 | ```php 111 | $avgPriceAggregation = new AvgAggregation('avg_price'); 112 | $avgPriceAggregation->setField('price'); 113 | 114 | $brandTermAggregation = new TermsAggregation('brand'); 115 | $brandTermAggregation->setField('manufacturer'); 116 | $brandTermAggregation->addAggregation($avgPriceAggregation); 117 | 118 | $query = new Search(); 119 | $query->addAggregation($brandTermAggregation); 120 | 121 | $result = $this->get('es.manager.default.content')->findDocuments($query); 122 | 123 | // Build a list of available choices 124 | $choices = []; 125 | 126 | foreach ($result->getAggregation('brand') as $bucket) { 127 | $choices[] = [ 128 | 'brand' => $bucket['key'], 129 | 'count' => $bucket['doc_count'], 130 | 'avg_price' => $bucket->find('avg_price')['value'], 131 | ]; 132 | } 133 | 134 | var_export($choices); 135 | ``` 136 | 137 | The example above will print similar result: 138 | 139 | ``` 140 | array ( 141 | 0 => 142 | array ( 143 | 'brand' => 'Terre Cortesi Moncaro', 144 | 'count' => 7, 145 | 'avg_price' => 20.42714282444545, 146 | ), 147 | 1 => 148 | array ( 149 | 'brand' => 'Casella Wines', 150 | 'count' => 4, 151 | 'avg_price' => 10.47249972820282, 152 | ) 153 | ) 154 | ``` 155 | -------------------------------------------------------------------------------- /Tests/Unit/Result/ConverterTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Unit\Result; 13 | 14 | use ONGR\ElasticsearchBundle\Mapping\Caser; 15 | use ONGR\ElasticsearchBundle\Mapping\MetadataCollector; 16 | use ONGR\ElasticsearchBundle\Result\Converter; 17 | use ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\Document\Product; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | class ConverterTest extends TestCase 21 | { 22 | /** 23 | * @var MetadataCollector 24 | */ 25 | private $metadataCollector; 26 | 27 | /** 28 | * @var Converter 29 | */ 30 | private $converter; 31 | 32 | /** 33 | * @inheritdoc 34 | */ 35 | protected function setUp(): void 36 | { 37 | $this->metadataCollector = $this->getMockBuilder('ONGR\ElasticsearchBundle\Mapping\MetadataCollector') 38 | ->disableOriginalConstructor() 39 | ->getMock(); 40 | 41 | $this->converter = new Converter($this->metadataCollector); 42 | } 43 | 44 | public function dataProviderForAssignArrayToObjectWhenDateIsObject() 45 | { 46 | return [ 47 | # Case 0. 48 | [ 49 | [ 50 | 'title' => 'Foo', 51 | ], 52 | ], 53 | # Case 1. 54 | [ 55 | [ 56 | 'title' => 'Boo', 57 | 'released' => new \DateTime(), 58 | ], 59 | ], 60 | # Case 2. 61 | [ 62 | [ 63 | 'title' => 'Bar', 64 | 'released' => null, 65 | ] 66 | ], 67 | # Case 3. 68 | [ 69 | [ 70 | 'limited' => null, 71 | ], 72 | [ 73 | 'limited' => false, 74 | ] 75 | ], 76 | # Case 4. 77 | [ 78 | [ 79 | 'limited' => 1, 80 | ], 81 | [ 82 | 'limited' => true, 83 | ] 84 | ], 85 | # Case 5. 86 | [ 87 | [ 88 | 'limited' => true, 89 | ], 90 | [ 91 | 'limited' => true, 92 | ] 93 | ] 94 | ]; 95 | } 96 | 97 | /** 98 | * Tests array conversion to the object. 99 | * 100 | * @param array $product 101 | * @param array $expected 102 | * 103 | * @dataProvider dataProviderForAssignArrayToObjectWhenDateIsObject 104 | */ 105 | public function testAssignArrayToObjectWhenDateIsObject($product, $expected = []) 106 | { 107 | $aliases = [ 108 | 'title' => [ 109 | 'propertyName' => 'title', 110 | 'type' => 'text', 111 | 'hashmap' => false, 112 | 'methods' => [ 113 | 'getter' => 'getTitle', 114 | 'setter' => 'setTitle', 115 | ], 116 | 'propertyType' => 'private', 117 | ], 118 | 'released' => [ 119 | 'propertyName' => 'released', 120 | 'type' => 'datetime', 121 | 'hashmap' => false, 122 | 'methods' => [ 123 | 'getter' => 'getReleased', 124 | 'setter' => 'setReleased', 125 | ], 126 | 'propertyType' => 'private', 127 | 128 | ], 129 | 'limited' => [ 130 | 'propertyName' => 'limited', 131 | 'type' => 'boolean', 132 | 'hashmap' => false, 133 | 'methods' => [ 134 | 'getter' => 'getLimited', 135 | 'setter' => 'setLimited', 136 | ], 137 | 'propertyType' => 'private', 138 | 139 | ] 140 | ]; 141 | /** @var Product $productDocument */ 142 | $productDocument = $this->converter->assignArrayToObject($product, new Product(), $aliases); 143 | 144 | foreach (array_keys($product) as $key) { 145 | $expect = isset($expected[$key]) ? $expected[$key] : $product[$key]; 146 | $this->assertEquals($expect, $productDocument->{'get'.Caser::snake($key)}()); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /Tests/Functional/Command/IndexImportCommandTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Tests\Functional\Command; 13 | 14 | use ONGR\ElasticsearchBundle\Command\IndexImportCommand; 15 | use ONGR\ElasticsearchBundle\Tests\app\fixture\TestBundle\Document\Product; 16 | use ONGR\ElasticsearchDSL\Query\MatchAllQuery; 17 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 18 | use Symfony\Component\Console\Application; 19 | use Symfony\Component\Console\Tester\CommandTester; 20 | 21 | class IndexImportCommandTest extends AbstractElasticsearchTestCase 22 | { 23 | /** 24 | * Data provider for testIndexImport. 25 | * 26 | * @return array 27 | */ 28 | public function bulkSizeProvider() 29 | { 30 | return [ 31 | [10, 9, 'command_import_9.json'], 32 | [10, 10, 'command_import_10.json'], 33 | [10, 11, 'command_import_11.json'], 34 | [5, 20, 'command_import_20.json'], 35 | ]; 36 | } 37 | 38 | /** 39 | * Compressed Data provider for testIndexImport. 40 | * 41 | * @return array 42 | */ 43 | public function compressedDataProvider() 44 | { 45 | return [ 46 | [10, 9, 'command_import_9.json.gz'], 47 | [10, 10, 'command_import_10.json.gz'], 48 | [10, 11, 'command_import_11.json.gz'], 49 | ]; 50 | } 51 | 52 | /** 53 | * Test for index import command. 54 | * 55 | * @param int $bulkSize 56 | * @param int $realSize 57 | * @param string $filename 58 | * 59 | * @dataProvider bulkSizeProvider 60 | */ 61 | public function testIndexImport($bulkSize, $realSize, $filename) 62 | { 63 | $manager = $this->getManager(); 64 | $app = new Application(); 65 | $app->add($this->getImportCommand()); 66 | 67 | $command = $app->find('ongr:es:index:import'); 68 | $commandTester = new CommandTester($command); 69 | $commandTester->execute( 70 | [ 71 | 'command' => $command->getName(), 72 | 'filename' => __DIR__ . '/../../app/fixture/data/' . $filename, 73 | '--bulk-size' => $bulkSize, 74 | ] 75 | ); 76 | 77 | $repo = $manager->getRepository('TestBundle:Product'); 78 | $search = $repo 79 | ->createSearch() 80 | ->addQuery(new MatchAllQuery()) 81 | ->setSize($realSize); 82 | $results = $repo->findDocuments($search); 83 | 84 | $ids = []; 85 | /** @var Product $doc */ 86 | foreach ($results as $doc) { 87 | $ids[] = substr($doc->getId(), 3); 88 | } 89 | sort($ids); 90 | $data = range(1, $realSize); 91 | $this->assertEquals($data, $ids); 92 | } 93 | 94 | /** 95 | * Test for index import command with gzip option. 96 | * 97 | * @param int $bulkSize 98 | * @param int $realSize 99 | * @param string $filename 100 | * 101 | * @dataProvider compressedDataProvider 102 | */ 103 | public function testIndexImportWithGzipOption($bulkSize, $realSize, $filename) 104 | { 105 | $manager = $this->getManager(); 106 | 107 | $app = new Application(); 108 | $app->add($this->getImportCommand()); 109 | 110 | $command = $app->find('ongr:es:index:import'); 111 | $commandTester = new CommandTester($command); 112 | $commandTester->execute( 113 | [ 114 | 'command' => $command->getName(), 115 | 'filename' => __DIR__ . '/../../app/fixture/data/' . $filename, 116 | '--bulk-size' => $bulkSize, 117 | '--gzip' => null, 118 | ] 119 | ); 120 | 121 | $repo = $manager->getRepository('TestBundle:Product'); 122 | $search = $repo 123 | ->createSearch() 124 | ->addQuery(new MatchAllQuery()) 125 | ->setSize($realSize); 126 | $results = $repo->findDocuments($search); 127 | 128 | $ids = []; 129 | /** @var Product $doc */ 130 | foreach ($results as $doc) { 131 | $ids[] = substr($doc->getId(), 3); 132 | } 133 | sort($ids); 134 | $data = range(1, $realSize); 135 | $this->assertEquals($data, $ids); 136 | } 137 | 138 | /** 139 | * Returns import index command with assigned container. 140 | * 141 | * @return IndexImportCommand 142 | */ 143 | private function getImportCommand() 144 | { 145 | return new IndexImportCommand( 146 | $this->getKernelContainer()->get('es.import'), 147 | ['es.manager.default' => $this->getManager()] 148 | ); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Service/ExportService.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace ONGR\ElasticsearchBundle\Service; 13 | 14 | use Elasticsearch\Helper\Iterators\SearchHitIterator; 15 | use Elasticsearch\Helper\Iterators\SearchResponseIterator; 16 | use ONGR\ElasticsearchBundle\Result\RawIterator; 17 | use ONGR\ElasticsearchBundle\Service\Json\JsonWriter; 18 | use ONGR\ElasticsearchDSL\Query\MatchAllQuery; 19 | use ONGR\ElasticsearchDSL\Search; 20 | use ONGR\ElasticsearchDSL\Sort\FieldSort; 21 | use Symfony\Component\Console\Helper\ProgressBar; 22 | use Symfony\Component\Console\Output\OutputInterface; 23 | 24 | /** 25 | * ExportService class. 26 | */ 27 | class ExportService 28 | { 29 | /** 30 | * Exports es index to provided file. 31 | * 32 | * @param Manager $manager 33 | * @param string $filename 34 | * @param array $types 35 | * @param int $chunkSize 36 | * @param int $maxLinesInFile 37 | * @param OutputInterface $output 38 | */ 39 | public function exportIndex( 40 | Manager $manager, 41 | $filename, 42 | $types, 43 | $chunkSize, 44 | OutputInterface $output, 45 | $maxLinesInFile = 300000 46 | ) { 47 | 48 | $search = new Search(); 49 | $search->addQuery(new MatchAllQuery()); 50 | $search->setSize($chunkSize); 51 | $search->addSort(new FieldSort('_doc')); 52 | 53 | $queryParameters = [ 54 | '_source' => true, 55 | 'scroll' => '10m', 56 | ]; 57 | 58 | $searchResults = $manager->search($types, $search->toArray(), $queryParameters); 59 | 60 | $results = new RawIterator( 61 | $searchResults, 62 | $manager, 63 | [ 64 | 'duration' => $queryParameters['scroll'], 65 | '_scroll_id' => $searchResults['_scroll_id'], 66 | ] 67 | ); 68 | 69 | $progress = new ProgressBar($output, $results->count()); 70 | $progress->setRedrawFrequency(100); 71 | $progress->start(); 72 | 73 | $counter = $fileCounter = 0; 74 | $count = $this->getFileCount($results->count(), $maxLinesInFile, $fileCounter); 75 | 76 | $date = date(\DateTime::ISO8601); 77 | $metadata = [ 78 | 'count' => $count, 79 | 'date' => $date, 80 | ]; 81 | 82 | $filename = str_replace('.json', '', $filename); 83 | $writer = $this->getWriter($this->getFilePath($filename.'.json'), $metadata); 84 | 85 | foreach ($results as $data) { 86 | if ($counter >= $maxLinesInFile) { 87 | $writer->finalize(); 88 | $writer = null; 89 | $fileCounter++; 90 | $count = $this->getFileCount($results->count(), $maxLinesInFile, $fileCounter); 91 | $metadata = [ 92 | 'count' => $count, 93 | 'date' => $date, 94 | ]; 95 | $writer = $this->getWriter($this->getFilePath($filename."_".$fileCounter.".json"), $metadata); 96 | $counter = 0; 97 | } 98 | 99 | $doc = array_intersect_key($data, array_flip(['_id', '_type', '_source'])); 100 | $writer->push($doc); 101 | $progress->advance(); 102 | $counter++; 103 | } 104 | 105 | $writer->finalize(); 106 | $progress->finish(); 107 | $output->writeln(''); 108 | } 109 | 110 | /** 111 | * Returns real file path. 112 | * 113 | * @param string $filename 114 | * 115 | * @return string 116 | */ 117 | protected function getFilePath($filename) 118 | { 119 | if ($filename[0] == '/' || strstr($filename, ':') !== false) { 120 | return $filename; 121 | } 122 | 123 | return getcwd() . '/' . $filename; 124 | } 125 | 126 | /** 127 | * Prepares JSON writer. 128 | * 129 | * @param string $filename 130 | * @param array $metadata 131 | * 132 | * @return JsonWriter 133 | */ 134 | protected function getWriter($filename, $metadata) 135 | { 136 | return new JsonWriter($filename, $metadata); 137 | } 138 | 139 | /** 140 | * @param int $resultsCount 141 | * @param int $maxLinesInFile 142 | * @param int $fileCounter 143 | * 144 | * @return int 145 | */ 146 | protected function getFileCount($resultsCount, $maxLinesInFile, $fileCounter) 147 | { 148 | $leftToInsert = $resultsCount - ($fileCounter * $maxLinesInFile); 149 | if ($leftToInsert <= $maxLinesInFile) { 150 | $count = $leftToInsert; 151 | } else { 152 | $count = $maxLinesInFile; 153 | } 154 | 155 | return $count; 156 | } 157 | } 158 | --------------------------------------------------------------------------------