├── .coveralls.yml ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── Annotation ├── AbstractAnnotation.php ├── Embedded.php ├── Id.php ├── Index.php ├── MetaFieldInterface.php ├── NameAwareTrait.php ├── NestedType.php ├── ObjectType.php ├── PropertiesAwareInterface.php ├── Property.php ├── PropertyTypeAwareTrait.php ├── Routing.php └── Version.php ├── CHANGELOG.md ├── Command ├── AbstractIndexServiceAwareCommand.php ├── CacheClearCommand.php ├── IndexCreateCommand.php ├── IndexDropCommand.php ├── IndexExportCommand.php └── IndexImportCommand.php ├── DependencyInjection ├── Compiler │ └── MappingPass.php ├── Configuration.php └── ONGRElasticsearchExtension.php ├── Event ├── BulkEvent.php ├── CommitEvent.php ├── Events.php ├── PostCreateClientEvent.php └── PrePersistEvent.php ├── EventListener └── TerminateListener.php ├── Exception ├── BulkWithErrorsException.php ├── DocumentParserException.php └── MissingDocumentAnnotationException.php ├── LICENSE ├── Mapping ├── Caser.php ├── Converter.php ├── DocumentParser.php └── IndexSettings.php ├── ONGRElasticsearchBundle.php ├── Profiler ├── ElasticsearchProfiler.php └── Handler │ └── CollectionHandler.php ├── README.md ├── Request └── BulkRequest.php ├── Resources ├── config │ └── services.yml ├── doc │ ├── commands.md │ ├── configuration.md │ ├── crud.md │ ├── find_functions.md │ ├── index.md │ ├── mapping.md │ ├── meta_fields.md │ ├── overwriting_bundle.md │ ├── results_parsing.md │ ├── scan.md │ ├── search.md │ └── upgrade.md ├── public │ └── images │ │ ├── blue_picto_less.gif │ │ └── blue_picto_more.gif └── views │ └── Profiler │ └── profiler.html.twig ├── Result ├── AbstractResultsIterator.php ├── Aggregation │ └── AggregationValue.php ├── ArrayIterator.php ├── DocumentIterator.php ├── ObjectIterator.php └── RawIterator.php ├── Service ├── ExportService.php ├── GenerateService.php ├── ImportService.php ├── IndexService.php ├── IndexSuffixFinder.php └── Json │ ├── JsonReader.php │ └── JsonWriter.php ├── Test └── AbstractElasticsearchTestCase.php ├── Tests ├── Functional │ ├── Annotation │ │ ├── DocumentTest.php │ │ └── PropertyTest.php │ ├── Command │ │ ├── CacheClearCommandTest.php │ │ ├── CreateIndexCommandTest.php │ │ ├── DropIndexCommandTest.php │ │ ├── IndexExportCommandTest.php │ │ └── IndexImportCommandTest.php │ ├── DependencyInjection │ │ └── ElasticsearchBundleExtensionTest.php │ ├── Profiler │ │ └── ElasticsearchProfilerTest.php │ ├── Result │ │ ├── AggregationIteratorFindTest.php │ │ ├── ArrayIteratorTest.php │ │ ├── DocumentIteratorTest.php │ │ ├── DocumentWithNullObjectFieldTest.php │ │ ├── GetDocumentSortTest.php │ │ ├── ObjectIteratorTest.php │ │ └── PersistObjectsTest.php │ └── Service │ │ └── IndexServiceTest.php ├── Unit │ ├── Collection │ │ └── CollectionTest.php │ ├── ElasticsearchBundleTest.php │ ├── Event │ │ ├── BulkEventTest.php │ │ ├── CommitEventTest.php │ │ └── PrePersistEventTest.php │ ├── EventListener │ │ └── TerminateListenerTest.php │ ├── Mapping │ │ └── DocumentParserTest.php │ ├── Profiler │ │ └── ElasticsearchProfilerTest.php │ ├── Result │ │ ├── AbstractResultsIteratorTest.php │ │ ├── Aggregation │ │ │ └── AggregationValueTest.php │ │ ├── DocumentIteratorTest.php │ │ └── RawIteratorTest.php │ └── Service │ │ └── Json │ │ └── JsonWriterTest.php ├── WebTestCase.php └── app │ ├── AppKernel.php │ ├── config │ └── config_test.yml │ ├── data_seed │ ├── command_import_10.json │ ├── command_import_10.json.gz │ ├── command_import_11.json │ ├── command_import_11.json.gz │ ├── command_import_20.json │ ├── command_import_9.json │ └── command_import_9.json.gz │ └── src │ ├── Document │ ├── CollectionNested.php │ ├── CollectionObject.php │ ├── DummyDocument.php │ ├── IndexWithFieldsDataDocument.php │ └── TestDocument.php │ └── Entity │ └── DummyDocumentInTheEntityDirectory.php ├── composer.json └── phpunit.xml.dist /.coveralls.yml: -------------------------------------------------------------------------------- 1 | coverage_clover: coveralls.clover 2 | service_name: travis-ci 3 | json_path: ./coveralls.json 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /Tests/app/cache/ 3 | /Tests/app/logs/ 4 | /Tests/app/build/ 5 | /var/cache/test 6 | /phpunit.xml 7 | /composer.lock 8 | /coverage.clover -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - php 3 | filter: 4 | excluded_paths: 5 | - Tests/* 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: php 3 | php: 4 | - 7.1 5 | - 7.2 6 | - 7.3 7 | - 7.4 8 | env: 9 | global: 10 | - ES_VERSION=7.5.2 ES_DOWNLOAD_URL=https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-${ES_VERSION}-linux-x86_64.tar.gz 11 | matrix: 12 | - SYMFONY="^4.4" 13 | - SYMFONY="^5.0" 14 | jobs: 15 | exclude: 16 | - php: 7.1 17 | env: SYMFONY="^5.0" 18 | install: 19 | - wget ${ES_DOWNLOAD_URL} 20 | - tar -xzf elasticsearch-${ES_VERSION}-linux-x86_64.tar.gz 21 | - ./elasticsearch-${ES_VERSION}/bin/elasticsearch -d 22 | before_script: 23 | - composer require --no-update symfony/framework-bundle:${SYMFONY} 24 | - if [[ $TRAVIS_SECURE_ENV_VARS = "true" ]]; then composer config -g github-oauth.github.com $GITHUB_COMPOSER_AUTH; fi 25 | - composer install --no-interaction --prefer-dist 26 | script: 27 | - wget -q --waitretry=1 --retry-connrefused -T 10 -O - http://127.0.0.1:9200 28 | - vendor/bin/phpunit --coverage-clover=coverage.clover 29 | - vendor/bin/phpcs -p --standard=PSR2 --ignore=vendor/,Tests/app/,var/cache ./ 30 | after_script: 31 | - travis_retry bash <(curl -s https://codecov.io/bash) 32 | -------------------------------------------------------------------------------- /Annotation/AbstractAnnotation.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 | abstract class AbstractAnnotation 15 | { 16 | public $settings = []; 17 | 18 | public function getSettings(): array 19 | { 20 | return $this->settings; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /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 | /** 15 | * @Annotation 16 | * @Target("PROPERTY") 17 | */ 18 | final class Embedded extends AbstractAnnotation implements PropertiesAwareInterface 19 | { 20 | use NameAwareTrait; 21 | 22 | /** 23 | * Inner object class name. 24 | * 25 | * @var string Object name to map 26 | * 27 | * @Doctrine\Common\Annotations\Annotation\Required 28 | */ 29 | public $class; 30 | 31 | public $singular = false; 32 | } 33 | -------------------------------------------------------------------------------- /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 extends AbstractAnnotation implements MetaFieldInterface, PropertiesAwareInterface 21 | { 22 | const NAME = '_id'; 23 | 24 | public function getName(): ?string 25 | { 26 | return self::NAME; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Annotation/Index.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\Attributes; 15 | 16 | /** 17 | * Annotation to mark a class as an Elasticsearch index. 18 | * 19 | * @Annotation 20 | * @Target("CLASS") 21 | */ 22 | final class Index extends AbstractAnnotation 23 | { 24 | /** 25 | * Index alias name. By default the index name will be created with the timestamp appended to the alias. 26 | */ 27 | public $alias; 28 | 29 | /** 30 | * Index alias name. By default the index name will be created with the timestamp appended to the alias. 31 | */ 32 | public $hosts = [ 33 | '127.0.0.1:9200' 34 | ]; 35 | 36 | public $numberOfShards = 5; 37 | 38 | public $numberOfReplicas = 1; 39 | 40 | /** 41 | * You can select one of your indexes to be default. Useful for cli commands when you don't 42 | * need to define an alias name. If default is not set the first index found will be set as default one. 43 | */ 44 | public $default = false; 45 | } 46 | -------------------------------------------------------------------------------- /Annotation/MetaFieldInterface.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 MetaFieldInterface 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Annotation/NameAwareTrait.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 | trait NameAwareTrait 15 | { 16 | public $name; 17 | 18 | public function getName(): ?string 19 | { 20 | return $this->name; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Annotation/NestedType.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 16 | * @Target("CLASS") 17 | */ 18 | final class NestedType extends AbstractAnnotation 19 | { 20 | const TYPE = 'nested'; 21 | } 22 | -------------------------------------------------------------------------------- /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 16 | * @Target("CLASS") 17 | */ 18 | final class ObjectType extends AbstractAnnotation 19 | { 20 | const TYPE = 'object'; 21 | } 22 | -------------------------------------------------------------------------------- /Annotation/PropertiesAwareInterface.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 | * Interface which defines if the property will be included in to the index metadata. 16 | */ 17 | interface PropertiesAwareInterface 18 | { 19 | public function getName(): ?string; 20 | } 21 | -------------------------------------------------------------------------------- /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 | /** 15 | * @Annotation 16 | * @Target("PROPERTY") 17 | */ 18 | final class Property extends AbstractAnnotation implements PropertiesAwareInterface 19 | { 20 | use NameAwareTrait; 21 | use PropertyTypeAwareTrait; 22 | 23 | public $analyzer; 24 | public $fields; 25 | public $searchAnalyzer; 26 | public $searchQuoteAnalyzer; 27 | } 28 | -------------------------------------------------------------------------------- /Annotation/PropertyTypeAwareTrait.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 | trait PropertyTypeAwareTrait 17 | { 18 | /** 19 | * Field type. 20 | * 21 | * @var string 22 | * 23 | * @Doctrine\Common\Annotations\Annotation\Required 24 | * @Enum({ 25 | * "text", "keyword", 26 | * "long", "integer", "short", "byte", "double", "float", 27 | * "date", 28 | * "boolean", 29 | * "binary", 30 | * "geo_point", "geo_shape", 31 | * "ip", "completion", "token_count", "murmur3", "attachments", "percolator" 32 | * }) 33 | */ 34 | public $type; 35 | } 36 | -------------------------------------------------------------------------------- /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 16 | * @Target("PROPERTY") 17 | */ 18 | final class Routing extends AbstractAnnotation implements MetaFieldInterface, PropertiesAwareInterface 19 | { 20 | const NAME = '_routing'; 21 | 22 | public function getName(): ?string 23 | { 24 | return self::NAME; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /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 | * @Annotation 16 | * @Target("PROPERTY") 17 | */ 18 | final class Version extends AbstractAnnotation implements MetaFieldInterface 19 | { 20 | const NAME = '_version'; 21 | } 22 | -------------------------------------------------------------------------------- /Command/AbstractIndexServiceAwareCommand.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\DependencyInjection\Configuration; 15 | use ONGR\ElasticsearchBundle\Service\IndexService; 16 | use Symfony\Component\Console\Command\Command; 17 | use Symfony\Component\Console\Input\InputOption; 18 | use Symfony\Component\DependencyInjection\Container; 19 | use Symfony\Component\DependencyInjection\ContainerInterface; 20 | 21 | abstract class AbstractIndexServiceAwareCommand extends Command 22 | { 23 | private $container; 24 | 25 | const INDEX_OPTION = 'index'; 26 | 27 | public function __construct(ContainerInterface $container) 28 | { 29 | $this->container = $container; 30 | parent::__construct(); 31 | } 32 | 33 | protected function configure() 34 | { 35 | $this->addOption( 36 | self::INDEX_OPTION, 37 | 'i', 38 | InputOption::VALUE_REQUIRED, 39 | 'ElasticSearch index alias name or index name if you don\'t use aliases.' 40 | ); 41 | } 42 | 43 | protected function getIndex($name): IndexService 44 | { 45 | $name = $name ?? $this->container->getParameter(Configuration::ONGR_DEFAULT_INDEX); 46 | $indexes = $this->container->getParameter(Configuration::ONGR_INDEXES); 47 | 48 | if (isset($indexes[$name]) && $this->container->has($indexes[$name])) { 49 | return $this->container->get($indexes[$name]); 50 | } 51 | 52 | throw new \RuntimeException( 53 | sprintf( 54 | 'There is no index under `%s` name found. Available options: `%s`.', 55 | $name, 56 | implode('`, `', array_keys($this->container->getParameter(Configuration::ONGR_INDEXES))) 57 | ) 58 | ); 59 | } 60 | 61 | public function getContainer(): ContainerInterface 62 | { 63 | return $this->container; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /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 | class CacheClearCommand extends AbstractIndexServiceAwareCommand 19 | { 20 | const NAME = 'ongr:es:cache:clear'; 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | protected function configure() 26 | { 27 | parent::configure(); 28 | 29 | $this 30 | ->setName(self::NAME) 31 | ->setDescription('Clears ElasticSearch client\'s cache.'); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | protected function execute(InputInterface $input, OutputInterface $output) 38 | { 39 | $io = new SymfonyStyle($input, $output); 40 | $index = $this->getIndex($input->getOption('index')); 41 | $index->clearCache(); 42 | 43 | $io->success( 44 | sprintf( 45 | 'Elasticsearch `%s` index cache has been cleared.', 46 | $index->getIndexName() 47 | ) 48 | ); 49 | 50 | return 0; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Command/IndexCreateCommand.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\IndexSuffixFinder; 15 | use Symfony\Component\Console\Input\InputInterface; 16 | use Symfony\Component\Console\Input\InputOption; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | use Symfony\Component\Console\Style\SymfonyStyle; 19 | 20 | class IndexCreateCommand extends AbstractIndexServiceAwareCommand 21 | { 22 | const NAME = 'ongr:es:index:create'; 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | protected function configure() 28 | { 29 | parent::configure(); 30 | 31 | $this 32 | ->setName(self::NAME) 33 | ->setDescription('Creates the ElasticSearch index.') 34 | ->addOption( 35 | 'time', 36 | 't', 37 | InputOption::VALUE_NONE, 38 | 'Adds date suffix to the new index name.' 39 | ) 40 | ->addOption( 41 | 'alias', 42 | 'a', 43 | InputOption::VALUE_NONE, 44 | 'Adds alias as it is defined in the Index document annotation.' 45 | ) 46 | ->addOption( 47 | 'no-mapping', 48 | null, 49 | InputOption::VALUE_NONE, 50 | 'Do not include mapping when the index is created.' 51 | ) 52 | ->addOption( 53 | 'if-not-exists', 54 | null, 55 | InputOption::VALUE_NONE, 56 | 'Don\'t trigger an error, when the index already exists.' 57 | ) 58 | ->addOption( 59 | 'dump', 60 | null, 61 | InputOption::VALUE_NONE, 62 | 'Prints a json output of the index mapping.' 63 | ); 64 | } 65 | 66 | protected function execute(InputInterface $input, OutputInterface $output) 67 | { 68 | $io = new SymfonyStyle($input, $output); 69 | $index = $this->getIndex($input->getOption(parent::INDEX_OPTION)); 70 | $indexName = $aliasName = $index->getIndexName(); 71 | 72 | if ($input->getOption('dump')) { 73 | $io->note("Index mappings:"); 74 | $io->text( 75 | json_encode( 76 | $index->getIndexSettings()->getIndexMetadata(), 77 | JSON_PRETTY_PRINT 78 | ) 79 | ); 80 | 81 | return 0; 82 | } 83 | 84 | if ($input->getOption('time')) { 85 | /** @var IndexSuffixFinder $finder */ 86 | $finder = $this->getContainer()->get(IndexSuffixFinder::class); 87 | $indexName = $finder->getNextFreeIndex($index); 88 | } 89 | 90 | if ($input->getOption('if-not-exists') && $index->indexExists()) { 91 | $io->note( 92 | sprintf( 93 | 'Index `%s` already exists.', 94 | $index->getIndexName() 95 | ) 96 | ); 97 | 98 | return 0; 99 | } 100 | 101 | $indexesToRemoveAliases = null; 102 | if ($input->getOption('alias') && $index->getClient()->indices()->existsAlias(['name' => $aliasName])) { 103 | $indexesToRemoveAliases = $index->getClient()->indices()->getAlias( 104 | [ 105 | 'name' => $aliasName, 106 | ] 107 | ); 108 | } 109 | 110 | $index->createIndex($input->getOption('no-mapping'), array_filter([ 111 | 'index' => $indexName, 112 | ])); 113 | 114 | $io->text( 115 | sprintf( 116 | 'Created `%s` index.', 117 | $index->getIndexName() 118 | ) 119 | ); 120 | 121 | if ($input->getOption('alias')) { 122 | $index->getClient()->indices()->putAlias([ 123 | 'index' => $indexName, 124 | 'name' => $aliasName, 125 | ]); 126 | $io->text( 127 | sprintf( 128 | 'Created an alias `%s` for the `%s` index.', 129 | $aliasName, 130 | $indexName 131 | ) 132 | ); 133 | } 134 | 135 | if ($indexesToRemoveAliases) { 136 | $indexesToRemoveAliases = implode(',', array_keys($indexesToRemoveAliases)); 137 | $index->getClient()->indices()->deleteAlias([ 138 | 'index' => $indexesToRemoveAliases, 139 | 'name' => $aliasName, 140 | ]); 141 | $io->text( 142 | sprintf( 143 | 'Removed `%s` alias from `%s`.', 144 | $aliasName, 145 | $indexesToRemoveAliases 146 | ) 147 | ); 148 | } 149 | 150 | return 0; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /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 | class IndexDropCommand extends AbstractIndexServiceAwareCommand 20 | { 21 | const NAME = 'ongr:es:index:drop'; 22 | 23 | protected function configure() 24 | { 25 | parent::configure(); 26 | 27 | $this 28 | ->setName(self::NAME) 29 | ->setDescription('Drops ElasticSearch index.') 30 | ->addOption( 31 | 'force', 32 | 'f', 33 | InputOption::VALUE_NONE, 34 | 'Force option is mandatory to drop the index.' 35 | ); 36 | } 37 | 38 | protected function execute(InputInterface $input, OutputInterface $output) 39 | { 40 | $io = new SymfonyStyle($input, $output); 41 | if ($input->getOption('force')) { 42 | $index = $this->getIndex($input->getOption(parent::INDEX_OPTION)); 43 | 44 | $client = 45 | 46 | $result = $index->dropIndex(); 47 | 48 | $io->text( 49 | sprintf( 50 | 'The index `%s` was successfully dropped.', 51 | $index->getIndexName() 52 | ) 53 | ); 54 | } else { 55 | $io->error('WARNING:'); 56 | $io->text('This action should not be used in the production environment.'); 57 | $io->error('Option --force is mandatory to drop the index.'); 58 | } 59 | 60 | return 0; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /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 | class IndexExportCommand extends AbstractIndexServiceAwareCommand 22 | { 23 | const NAME = 'ongr:es:index:export'; 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | protected function configure() 29 | { 30 | parent::configure(); 31 | 32 | $this 33 | ->setName(self::NAME) 34 | ->setDescription('Exports a data from the ElasticSearch index.') 35 | ->addArgument( 36 | 'filename', 37 | InputArgument::REQUIRED, 38 | 'Define a filename to store the output' 39 | )->addOption( 40 | 'chunk', 41 | null, 42 | InputOption::VALUE_REQUIRED, 43 | 'Chunk size to use in the scan api', 44 | 500 45 | )->addOption( 46 | 'split', 47 | null, 48 | InputOption::VALUE_REQUIRED, 49 | 'Split a file content in a separate parts if a line number exceeds provided value', 50 | 300000 51 | ); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | protected function execute(InputInterface $input, OutputInterface $output) 58 | { 59 | $io = new SymfonyStyle($input, $output); 60 | $index = $this->getIndex($input->getOption(parent::INDEX_OPTION)); 61 | 62 | /** @var ExportService $exportService */ 63 | $exportService = $this->getContainer()->get(ExportService::class); 64 | $exportService->exportIndex( 65 | $index, 66 | $input->getArgument('filename'), 67 | $input->getOption('chunk'), 68 | $output, 69 | $input->getOption('split') 70 | ); 71 | 72 | $io->success('Data export completed!'); 73 | 74 | return 0; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /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 | class IndexImportCommand extends AbstractIndexServiceAwareCommand 22 | { 23 | const NAME = 'ongr:es:index:import'; 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | protected function configure() 28 | { 29 | parent::configure(); 30 | 31 | $this 32 | ->setName(self::NAME) 33 | ->setDescription('Imports data to elasticsearch index.') 34 | ->addArgument( 35 | 'filename', 36 | InputArgument::REQUIRED, 37 | 'Select file to store output' 38 | ) 39 | ->addOption( 40 | 'bulk-size', 41 | 'b', 42 | InputOption::VALUE_REQUIRED, 43 | 'Set bulk size for import', 44 | 1000 45 | ) 46 | ->addOption( 47 | 'gzip', 48 | 'z', 49 | InputOption::VALUE_NONE, 50 | 'Import a gzip file' 51 | ); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | protected function execute(InputInterface $input, OutputInterface $output) 58 | { 59 | $io = new SymfonyStyle($input, $output); 60 | $index = $this->getIndex($input->getOption(parent::INDEX_OPTION)); 61 | 62 | // Initialize options array 63 | $options = []; 64 | if ($input->getOption('gzip')) { 65 | $options['gzip'] = null; 66 | } 67 | $options['bulk-size'] = $input->getOption('bulk-size'); 68 | 69 | /** @var ImportService $importService */ 70 | $importService = $this->getContainer()->get(ImportService::class); 71 | $importService->importIndex( 72 | $index, 73 | $input->getArgument('filename'), 74 | $output, 75 | $options 76 | ); 77 | 78 | $io->success('Data import completed!'); 79 | 80 | return 0; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.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 Psr\Log\LogLevel; 15 | use Symfony\Component\Config\Definition\Builder\NodeDefinition; 16 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 17 | use Symfony\Component\Config\Definition\ConfigurationInterface; 18 | 19 | class Configuration implements ConfigurationInterface 20 | { 21 | 22 | const ONGR_CACHE_CONFIG = 'ongr.esb.cache'; 23 | const ONGR_SOURCE_DIR = 'ongr.esb.source_dir'; 24 | const ONGR_PROFILER_CONFIG = 'ongr.esb.profiler'; 25 | const ONGR_LOGGER_CONFIG = 'ongr.esb.logger'; 26 | const ONGR_ANALYSIS_CONFIG = 'ongr.esb.analysis'; 27 | const ONGR_INDEXES = 'ongr.esb.indexes'; 28 | const ONGR_DEFAULT_INDEX = 'ongr.esb.default_index'; 29 | const ONGR_INDEXES_OVERRIDE = 'ongr.esb.indexes_override'; 30 | 31 | public function getConfigTreeBuilder() 32 | { 33 | 34 | $treeBuilder = new TreeBuilder('ongr_elasticsearch'); 35 | 36 | if (method_exists($treeBuilder, 'getRootNode')) { 37 | $rootNode = $treeBuilder->getRootNode(); 38 | } else { 39 | // BC layer for symfony/config 4.1 and older 40 | $rootNode = $treeBuilder->root('ongr_elasticsearch'); 41 | } 42 | 43 | $rootNode 44 | ->children() 45 | 46 | ->booleanNode('cache') 47 | ->info( 48 | 'Enables the cache handler to store important data to the cache. '. 49 | 'Default value is kernel.debug parameter.' 50 | ) 51 | ->end() 52 | 53 | ->booleanNode('profiler') 54 | ->info( 55 | 'Enables Symfony profiler for the elasticsearch queries debug.'. 56 | 'Default value is kernel.debug parameter. ' 57 | ) 58 | ->end() 59 | 60 | ->booleanNode('logger') 61 | ->defaultTrue() 62 | ->info( 63 | 'Enables executed queries logging. Log file names are the same as index.' 64 | ) 65 | ->end() 66 | 67 | ->arrayNode('source_directories') 68 | ->prototype('scalar')->end() 69 | ->defaultValue(['/src']) 70 | ->info( 71 | 'If your project has different than `/src` source directory, or several of them,' . 72 | 'you can specify them here to look automatically for ES documents.' 73 | ) 74 | ->end() 75 | 76 | ->arrayNode('indexes') 77 | ->defaultValue([]) 78 | ->useAttributeAsKey('namespace') 79 | ->info( 80 | 'In case you want to override index settings defined in the annotation.' . 81 | ' e.g. use env variables instead.' 82 | ) 83 | ->prototype('variable')->end() 84 | ->end() 85 | 86 | ->append($this->getAnalysisNode()) 87 | 88 | ->end(); 89 | 90 | return $treeBuilder; 91 | } 92 | 93 | private function getAnalysisNode(): NodeDefinition 94 | { 95 | $builder = new TreeBuilder('analysis'); 96 | 97 | if (method_exists($builder, 'getRootNode')) { 98 | $node = $builder->getRootNode(); 99 | } else { 100 | // BC layer for symfony/config 4.1 and older 101 | $node = $builder->root('analysis'); 102 | } 103 | 104 | 105 | $node 106 | ->info('Defines analyzers, normalizers, tokenizers and filters') 107 | ->addDefaultsIfNotSet() 108 | ->children() 109 | ->arrayNode('tokenizer') 110 | ->defaultValue([]) 111 | ->prototype('variable')->end() 112 | ->end() 113 | ->arrayNode('filter') 114 | ->defaultValue([]) 115 | ->prototype('variable')->end() 116 | ->end() 117 | ->arrayNode('analyzer') 118 | ->defaultValue([]) 119 | ->prototype('variable')->end() 120 | ->end() 121 | ->arrayNode('normalizer') 122 | ->defaultValue([]) 123 | ->prototype('variable')->end() 124 | ->end() 125 | ->arrayNode('char_filter') 126 | ->defaultValue([]) 127 | ->prototype('variable')->end() 128 | ->end() 129 | ->end(); 130 | 131 | return $node; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /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 ONGR\ElasticsearchBundle\Mapping\Converter; 15 | use ONGR\ElasticsearchBundle\Mapping\DocumentParser; 16 | use Symfony\Component\Config\FileLocator; 17 | use Symfony\Component\DependencyInjection\ContainerBuilder; 18 | use Symfony\Component\DependencyInjection\Loader; 19 | use Symfony\Component\HttpKernel\DependencyInjection\Extension; 20 | 21 | class ONGRElasticsearchExtension extends Extension 22 | { 23 | public function load(array $configs, ContainerBuilder $container) 24 | { 25 | $configuration = new Configuration(); 26 | $config = $this->processConfiguration($configuration, $configs); 27 | 28 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); 29 | $loader->load('services.yml'); 30 | 31 | $container->setParameter( 32 | Configuration::ONGR_CACHE_CONFIG, 33 | $config['cache'] ?? $container->getParameter('kernel.debug') 34 | ); 35 | 36 | $container->setParameter( 37 | Configuration::ONGR_PROFILER_CONFIG, 38 | $config['profiler'] ?? $container->getParameter('kernel.debug') 39 | ); 40 | 41 | $container->setParameter( 42 | Configuration::ONGR_LOGGER_CONFIG, 43 | $config['logger'] ?? $container->getParameter('kernel.debug') 44 | ); 45 | 46 | $container->setParameter(Configuration::ONGR_INDEXES_OVERRIDE, $config['indexes']); 47 | $container->setParameter(Configuration::ONGR_ANALYSIS_CONFIG, $config['analysis']); 48 | $container->setParameter(Configuration::ONGR_SOURCE_DIR, $config['source_directories']); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /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 | use Symfony\Contracts\EventDispatcher\Event; 15 | 16 | class BulkEvent extends Event 17 | { 18 | private $operation; 19 | private $header; 20 | private $data; 21 | 22 | public function __construct(string $operation, array $header, array $data = []) 23 | { 24 | $this->operation = $operation; 25 | $this->header = $header; 26 | $this->data = $data; 27 | } 28 | 29 | public function getHeader(): array 30 | { 31 | return $this->header; 32 | } 33 | 34 | public function setHeader(array $header): BulkEvent 35 | { 36 | $this->header = $header; 37 | return $this; 38 | } 39 | 40 | public function getData(): array 41 | { 42 | return $this->data; 43 | } 44 | 45 | public function setData(array $data): BulkEvent 46 | { 47 | $this->data = $data; 48 | return $this; 49 | } 50 | 51 | public function getOperation(): string 52 | { 53 | return $this->operation; 54 | } 55 | 56 | public function setOperation(string $operation): BulkEvent 57 | { 58 | $this->operation = $operation; 59 | return $this; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /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 | use Symfony\Contracts\EventDispatcher\Event; 15 | 16 | class CommitEvent extends Event 17 | { 18 | private $commitMode; 19 | private $bulkQuery; 20 | private $bulkResponse; 21 | 22 | public function __construct(string $commitMode, array $bulkQuery = [], array $bulkResponse = []) 23 | { 24 | $this->commitMode = $commitMode; 25 | $this->bulkQuery = $bulkQuery; 26 | $this->bulkResponse = $bulkResponse; 27 | } 28 | 29 | public function getCommitMode() 30 | { 31 | return $this->commitMode; 32 | } 33 | 34 | public function setCommitMode($commitMode) 35 | { 36 | $this->commitMode = $commitMode; 37 | return $this; 38 | } 39 | 40 | public function getBulkQuery(): array 41 | { 42 | return $this->bulkQuery; 43 | } 44 | 45 | public function setBulkQuery(array $bulkQuery): CommitEvent 46 | { 47 | $this->bulkQuery = $bulkQuery; 48 | return $this; 49 | } 50 | 51 | public function getBulkResponse(): array 52 | { 53 | return $this->bulkResponse; 54 | } 55 | 56 | public function setBulkResponse(array $bulkResponse): CommitEvent 57 | { 58 | $this->bulkResponse = $bulkResponse; 59 | return $this; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /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 BULK event occurs before during the processing of bulk method 21 | */ 22 | const BULK = 'ongr.es.event.bulk'; 23 | 24 | /** 25 | * The PRE_COMMIT event occurs before committing queries to ES 26 | */ 27 | const PRE_COMMIT = 'ongr.es.event.pre_commit'; 28 | 29 | /** 30 | * The POST_COMMIT event occurs after committing queries to ES 31 | */ 32 | const POST_COMMIT = 'ongr.es.event.post_commit'; 33 | 34 | /** 35 | * The POST_CLIENT_CREATE event occurs after client is formed. It is still not build, 36 | * so you can modify or add another information to it. After this event the build() method is called. 37 | */ 38 | const POST_CLIENT_CREATE = 'ongr.es.event.post_client_create'; 39 | } 40 | -------------------------------------------------------------------------------- /Event/PostCreateClientEvent.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 | use Symfony\Contracts\EventDispatcher\Event; 16 | 17 | class PostCreateClientEvent extends Event 18 | { 19 | private $namespace; 20 | private $client; 21 | 22 | public function __construct(string $namespace, ClientBuilder $client) 23 | { 24 | $this->namespace = $namespace; 25 | $this->client = $client; 26 | } 27 | 28 | public function getNamespace(): string 29 | { 30 | return $this->namespace; 31 | } 32 | 33 | public function setNamespace(string $namespace) 34 | { 35 | $this->namespace = $namespace; 36 | return $this; 37 | } 38 | 39 | public function getClient(): ClientBuilder 40 | { 41 | return $this->client; 42 | } 43 | 44 | public function setClient(ClientBuilder $client): PostCreateClientEvent 45 | { 46 | $this->client = $client; 47 | return $this; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /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 | use Symfony\Contracts\EventDispatcher\Event; 15 | 16 | class PrePersistEvent extends Event 17 | { 18 | /** 19 | * @var object 20 | */ 21 | private $document; 22 | 23 | /** 24 | * PrePersistEvent constructor. 25 | * @param $document 26 | */ 27 | public function __construct($document) 28 | { 29 | $this->document = $document; 30 | } 31 | 32 | /** 33 | * @return object 34 | */ 35 | public function getDocument() 36 | { 37 | return $this->document; 38 | } 39 | 40 | /** 41 | * @param object $document 42 | */ 43 | public function setDocument($document) 44 | { 45 | $this->document = $document; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /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\IndexService; 15 | use Symfony\Component\DependencyInjection\Container; 16 | 17 | class TerminateListener 18 | { 19 | private $container; 20 | private $indexes; 21 | 22 | /** 23 | * @param Container $container 24 | * @param IndexService[] $indexes 25 | */ 26 | public function __construct(Container $container, array $indexes) 27 | { 28 | $this->container = $container; 29 | $this->indexes = $indexes; 30 | } 31 | 32 | /** 33 | * Forces commit to the elasticsearch on kernel terminate event 34 | */ 35 | public function onKernelTerminate() 36 | { 37 | foreach ($this->indexes as $key => $index) { 38 | /** @var IndexService $index */ 39 | $index = $this->container->get($index); 40 | $index->commit(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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\Common\Inflector\Inflector; 15 | 16 | /** 17 | * Utility for string case transformations. 18 | */ 19 | class Caser 20 | { 21 | /** 22 | * Transforms string to camel case (e.g., resultString). 23 | * 24 | * @param string $string Text to transform. 25 | * 26 | * @return string 27 | */ 28 | public static function camel($string) 29 | { 30 | return Inflector::camelize($string); 31 | } 32 | 33 | /** 34 | * Transforms string to snake case (e.g., result_string). 35 | * 36 | * @param string $string Text to transform. 37 | * 38 | * @return string 39 | */ 40 | public static function snake($string) 41 | { 42 | $string = preg_replace('#([A-Z\d]+)([A-Z][a-z])#', '\1_\2', self::camel($string)); 43 | $string = preg_replace('#([a-z\d])([A-Z])#', '\1_\2', $string); 44 | 45 | return strtolower(strtr($string, '-', '_')); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Mapping/Converter.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 ONGR\ElasticsearchBundle\Result\ObjectIterator; 15 | 16 | /** 17 | * This class converts array to document object. 18 | */ 19 | class Converter 20 | { 21 | private $propertyMetadata = []; 22 | 23 | public function addClassMetadata(string $class, array $metadata): void 24 | { 25 | $this->propertyMetadata[$class] = $metadata; 26 | } 27 | 28 | public function convertArrayToDocument(string $namespace, array $raw) 29 | { 30 | if (!isset($this->propertyMetadata[$namespace])) { 31 | throw new \Exception("Cannot convert array to object of class `$class`."); 32 | } 33 | 34 | return $this->denormalize($raw, $namespace); 35 | } 36 | 37 | public function convertDocumentToArray($document): array 38 | { 39 | $class = get_class($document); 40 | 41 | if (!isset($this->propertyMetadata[$class])) { 42 | throw new \Exception("Cannot convert object of class `$class` to array."); 43 | } 44 | 45 | return $this->normalize($document); 46 | } 47 | 48 | protected function normalize($document, $metadata = null) 49 | { 50 | if ($document === null) { 51 | return null; 52 | } 53 | 54 | $metadata = $metadata ?? $this->propertyMetadata[get_class($document)]; 55 | $result = []; 56 | 57 | foreach ($metadata as $field => $fieldMeta) { 58 | $getter = $fieldMeta['getter']; 59 | $value = $fieldMeta['public'] ? $document->{$fieldMeta['name']} : $document->$getter(); 60 | 61 | if ($fieldMeta['embeded']) { 62 | if (is_iterable($value)) { 63 | foreach ($value as $item) { 64 | $result[$field][] = $this->normalize($item, $fieldMeta['sub_properties']); 65 | } 66 | } else { 67 | $result[$field] = $this->normalize($value, $fieldMeta['sub_properties']); 68 | } 69 | } else { 70 | if ($value instanceof \DateTime) { 71 | $value = $value->format(\DateTime::ISO8601); 72 | } 73 | $result[$field] = $value; 74 | } 75 | } 76 | 77 | return $result; 78 | } 79 | 80 | protected function denormalize(array $raw, string $namespace) 81 | { 82 | $metadata = $this->propertyMetadata[$namespace]; 83 | $object = new $namespace(); 84 | 85 | foreach ($raw as $field => $value) { 86 | $fieldMeta = $metadata[$field]; 87 | $setter = $fieldMeta['setter']; 88 | 89 | if ($fieldMeta['embeded']) { 90 | $this->addClassMetadata($fieldMeta['class'], $fieldMeta['sub_properties']); 91 | 92 | if ($fieldMeta['singular']) { 93 | $object->$setter($this->denormalize($value, $fieldMeta['class'])); 94 | } else { 95 | $iterator = new ObjectIterator($fieldMeta['class'], $value, $this); 96 | 97 | if ($fieldMeta['public']) { 98 | $object->{$fieldMeta['name']} = $iterator; 99 | } else { 100 | $object->$setter($iterator); 101 | } 102 | } 103 | } else { 104 | if ($fieldMeta['type'] == 'date') { 105 | $value = \DateTime::createFromFormat(\DateTime::ISO8601, $value) ?: null; 106 | } 107 | if ($fieldMeta['public']) { 108 | $object->{$fieldMeta['name']} = $value; 109 | } else { 110 | if ($fieldMeta['identifier']) { 111 | $setter = function ($field, $value) { 112 | $this->$field = $value; 113 | }; 114 | 115 | $setter = \Closure::bind($setter, $object, $object); 116 | $setter($fieldMeta['name'], $value); 117 | } else { 118 | $object->$setter($value); 119 | } 120 | } 121 | } 122 | } 123 | 124 | return $object; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Mapping/IndexSettings.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\Mapping; 12 | 13 | class IndexSettings 14 | { 15 | private $namespace; 16 | private $indexName; 17 | private $alias; 18 | private $indexMetadata; 19 | private $hosts; 20 | private $defaultIndex = false; 21 | 22 | public function __construct( 23 | string $namespace, 24 | string $indexName, 25 | string $alias, 26 | array $indexMetadata = [], 27 | array $hosts = [], 28 | bool $defaultIndex = false 29 | ) { 30 | $this->namespace = $namespace; 31 | $this->indexName = $indexName; 32 | $this->alias = $alias; 33 | $this->indexMetadata = $indexMetadata; 34 | $this->hosts = $hosts; 35 | $this->defaultIndex = $defaultIndex; 36 | } 37 | 38 | public function getNamespace() 39 | { 40 | return $this->namespace; 41 | } 42 | 43 | public function setNamespace($namespace): self 44 | { 45 | $this->namespace = $namespace; 46 | return $this; 47 | } 48 | 49 | public function getIndexName() 50 | { 51 | return $this->indexName; 52 | } 53 | 54 | public function setIndexName($indexName): self 55 | { 56 | $this->indexName = $indexName; 57 | return $this; 58 | } 59 | 60 | public function getAlias() 61 | { 62 | return $this->alias; 63 | } 64 | 65 | public function setAlias($alias): self 66 | { 67 | $this->alias = $alias; 68 | return $this; 69 | } 70 | 71 | public function getIndexMetadata() 72 | { 73 | return $this->indexMetadata; 74 | } 75 | 76 | public function setIndexMetadata($indexMetadata): self 77 | { 78 | $this->indexMetadata = $indexMetadata; 79 | return $this; 80 | } 81 | 82 | public function getHosts() 83 | { 84 | return $this->hosts; 85 | } 86 | 87 | public function setHosts($hosts): self 88 | { 89 | $this->hosts = $hosts; 90 | return $this; 91 | } 92 | 93 | public function isDefaultIndex(): bool 94 | { 95 | return $this->defaultIndex; 96 | } 97 | 98 | public function setDefaultIndex(bool $defaultIndex): self 99 | { 100 | $this->defaultIndex = $defaultIndex; 101 | return $this; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /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\MappingPass; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | use Symfony\Component\HttpKernel\Bundle\Bundle; 17 | 18 | class ONGRElasticsearchBundle extends Bundle 19 | { 20 | public function build(ContainerBuilder $container) 21 | { 22 | parent::build($container); 23 | $container->addCompilerPass(new MappingPass()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Profiler/ElasticsearchProfiler.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; 13 | 14 | use Monolog\Logger; 15 | use ONGR\ElasticsearchBundle\Profiler\Handler\CollectionHandler; 16 | use Symfony\Component\HttpFoundation\Request; 17 | use Symfony\Component\HttpFoundation\Response; 18 | use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; 19 | 20 | /** 21 | * Data collector for profiling elasticsearch bundle. 22 | */ 23 | class ElasticsearchProfiler implements DataCollectorInterface 24 | { 25 | const UNDEFINED_ROUTE = 'undefined_route'; 26 | 27 | private $loggers = []; 28 | private $queries = []; 29 | private $count = 0; 30 | private $time = .0; 31 | private $indexes = []; 32 | 33 | public function addLogger(Logger $logger) 34 | { 35 | $this->loggers[] = $logger; 36 | } 37 | 38 | public function setIndexes(array $indexes): void 39 | { 40 | $this->indexes = $indexes; 41 | } 42 | 43 | public function collect(Request $request, Response $response, \Throwable $exception = null) 44 | { 45 | /** @var Logger $logger */ 46 | foreach ($this->loggers as $logger) { 47 | foreach ($logger->getHandlers() as $handler) { 48 | if ($handler instanceof CollectionHandler) { 49 | $this->handleRecords($this->getRoute($request), $handler->getRecords()); 50 | $handler->clearRecords(); 51 | } 52 | } 53 | } 54 | } 55 | 56 | public function reset() 57 | { 58 | $this->queries = []; 59 | $this->count = 0; 60 | $this->time = 0; 61 | } 62 | 63 | public function getTime(): float 64 | { 65 | return round($this->time * 1000, 2); 66 | } 67 | 68 | public function getQueryCount(): int 69 | { 70 | return $this->count; 71 | } 72 | 73 | /** 74 | * Returns information about executed queries. 75 | * 76 | * Eg. keys: 77 | * 'body' - Request body. 78 | * 'method' - HTTP method. 79 | * 'uri' - Uri request was sent. 80 | * 'time' - Time client took to respond. 81 | * 82 | * @return array 83 | */ 84 | public function getQueries(): array 85 | { 86 | return $this->queries; 87 | } 88 | 89 | public function getIndexes(): array 90 | { 91 | return $this->indexes; 92 | } 93 | 94 | public function getName() 95 | { 96 | return 'ongr.profiler'; 97 | } 98 | 99 | private function handleRecords($route, $records) 100 | { 101 | $this->count += count($records) / 2; 102 | $queryBody = ''; 103 | foreach ($records as $record) { 104 | // First record will never have context. 105 | if (!empty($record['context'])) { 106 | $this->time += $record['context']['duration']; 107 | $this->addQuery($route, $record, $queryBody); 108 | } else { 109 | $position = strpos($record['message'], ' -d'); 110 | $queryBody = $position !== false ? substr($record['message'], $position + 3) : ''; 111 | } 112 | } 113 | } 114 | 115 | private function addQuery($route, $record, $queryBody) 116 | { 117 | parse_str(parse_url($record['context']['uri'], PHP_URL_QUERY), $httpParameters); 118 | $body = json_decode(trim($queryBody, " '\r\t\n")); 119 | $this->queries[$route][] = array_merge( 120 | [ 121 | 'body' => $body !== null ? json_encode($body, JSON_PRETTY_PRINT) : '', 122 | 'method' => $record['context']['method'], 123 | 'httpParameters' => $httpParameters, 124 | 'time' => $record['context']['duration'] * 1000, 125 | ], 126 | array_diff_key(parse_url($record['context']['uri']), array_flip(['query'])) 127 | ); 128 | } 129 | 130 | private function getRoute(Request $request) 131 | { 132 | $route = $request->attributes->get('_route'); 133 | 134 | return empty($route) ? self::UNDEFINED_ROUTE : $route; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /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 | 16 | /** 17 | * Handler that saves all records to him self. 18 | */ 19 | class CollectionHandler extends AbstractProcessingHandler 20 | { 21 | /** 22 | * @var array 23 | */ 24 | private $records = []; 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | protected function write(array $record) 30 | { 31 | $this->records[] = $record; 32 | } 33 | 34 | /** 35 | * Returns recorded data. 36 | * 37 | * @return array 38 | */ 39 | public function getRecords() 40 | { 41 | return $this->records; 42 | } 43 | 44 | /** 45 | * Clears recorded data. 46 | */ 47 | public function clearRecords() 48 | { 49 | $this->records = []; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Request/BulkRequest.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\Request; 12 | 13 | class BulkRequest 14 | { 15 | private $index; 16 | } 17 | -------------------------------------------------------------------------------- /Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | es.logging.path: "%kernel.logs_dir%/elasticsearch_%kernel.environment%.log" 3 | 4 | services: 5 | 6 | _defaults: 7 | public: true 8 | autowire: true 9 | 10 | ONGR\ElasticsearchBundle\Command\: 11 | resource: '../../Command' 12 | tags: 13 | - { name: console.command } 14 | 15 | ongr.esb.cache: 16 | class: Doctrine\Common\Cache\PhpFileCache 17 | arguments: ["%kernel.cache_dir%/ongr/elasticsearch", ".ongr.data"] 18 | 19 | ongr.esb.cache_reader: 20 | class: Doctrine\Common\Annotations\CachedReader 21 | arguments: ["@annotations.reader", "@ongr.esb.cache", "%kernel.debug%"] 22 | 23 | ONGR\ElasticsearchBundle\Service\ExportService: ~ 24 | ONGR\ElasticsearchBundle\Service\ImportService: ~ 25 | ONGR\ElasticsearchBundle\Service\IndexSuffixFinder: ~ 26 | ONGR\ElasticsearchBundle\Mapping\Converter: ~ 27 | 28 | ONGR\ElasticsearchBundle\Mapping\DocumentParser: 29 | arguments: ["@ongr.esb.cache_reader", "@ongr.esb.cache", "%ongr.esb.analysis%"] 30 | 31 | ONGR\ElasticsearchBundle\Profiler\Handler\CollectionHandler: 32 | public: false 33 | 34 | ongr.esb.tracer: 35 | class: Monolog\Logger 36 | arguments: ['ongr'] 37 | calls: 38 | - [pushHandler, ['@ONGR\ElasticsearchBundle\Profiler\Handler\CollectionHandler']] 39 | 40 | ONGR\ElasticsearchBundle\Profiler\ElasticsearchProfiler: 41 | calls: 42 | - [setIndexes, ["%ongr.esb.indexes%"]] 43 | - [addLogger, ["@ongr.esb.tracer"]] 44 | tags: 45 | - {name: data_collector, template: "@ONGRElasticsearch/Profiler/profiler.html.twig", id: ongr.profiler} 46 | 47 | ONGR\ElasticsearchBundle\EventListener\TerminateListener: 48 | arguments: ["@service_container", "%ongr.esb.indexes%"] 49 | tags: 50 | - { name: kernel.event_listener, event: kernel.terminate } 51 | -------------------------------------------------------------------------------- /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 | source_directories: 24 | - /src/AppBundle 25 | logger: false 26 | profiler: true 27 | cache: true 28 | indexes: # overrides any index related config from anotations 29 | App\Document\Page: 30 | default: true 31 | hosts: 32 | - 'elasticsearch:9200' 33 | settings: 34 | number_of_replicas: 2 35 | number_of_shards: 3 36 | type: page # for 5.x ES compatibility 37 | ``` 38 | -------------------------------------------------------------------------------- /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/Document/Product.php 9 | 10 | namespace App\Document; 11 | 12 | use ONGR\ElasticsearchBundle\Annotation as ES; 13 | 14 | /** 15 | * //alias and default parameters in the annotation are optional. 16 | * @ES\Index(alias="content", default=true) 17 | */ 18 | class Content 19 | { 20 | /** 21 | * @ES\Id() 22 | */ 23 | public $id; 24 | 25 | /** 26 | * @ES\Property(type="text") 27 | */ 28 | public $title; 29 | } 30 | ``` 31 | 32 | > Important notice: you don't need your properties to be public, but they will 33 | >need to have getters and setters otherwise. 34 | 35 | ## Manager 36 | 37 | Elasticsearch bundle provides managers called indexes to able to handle several ES indexes 38 | for communication with elasticsearch. 39 | 40 | Each index will have its dedicated manager that can be reached via service container by the 41 | name of the namespace of the Document class that represents the index in question. So in our 42 | case: 43 | 44 | ```php 45 | 46 | $index = $container->get(Content::class); 47 | 48 | ``` 49 | 50 | > Important: The old implementation was to have manager and repositories dedicated 51 | >for each document type. In 6.0 the functionalities of these services were merged 52 | >into the index service for each document. 53 | 54 | 55 | ## Create a document 56 | 57 | ```php 58 | 59 | $content = new Content(); 60 | $content->id = 5; // Optional, if not set, elasticsearch will set a random. 61 | $content->title = 'Acme title'; 62 | $index->persist($content); 63 | $index->commit(); 64 | 65 | ``` 66 | 67 | ## Update a document 68 | 69 | ```php 70 | 71 | $content = $index->find(5); // Alternatively $index->findBy(['title' => 'acme title']); 72 | $content->title = 'changed Acme title'; 73 | $index->persist($content); 74 | $index->commit(); 75 | 76 | ``` 77 | 78 | ## Partial update 79 | 80 | 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. 81 | 82 | To update a field you need to know the document `ID` and fields to update. Here's an example: 83 | 84 | ```php 85 | 86 | $index = $this->get(Content::class); 87 | $index->update(1, ['title' => 'new title']); 88 | 89 | ``` 90 | 91 | You can also update fields with script operation, lets say, you want to do some math: 92 | 93 | 94 | ```php 95 | 96 | $index = $this->get(Content::class); 97 | $index->update(1, [], 'ctx._source.stock+=1'); 98 | 99 | ``` 100 | > Important: when using script update fields cannot be updated, leave empty array, otherwise you will get 400 exception. 101 | 102 | `ctx._source` comes from groovy scripting and you have to enable it in elasticsearch config with: `script.groovy.sandbox.enabled: false` 103 | 104 | 105 | In addition you also can get other document fields with the response of update, 106 | lets say we also want a content field and a new title, so just add them separated by a comma: 107 | 108 | 109 | ```php 110 | 111 | $index = $this->get(Content::class); 112 | $index = $repo->update(1, ['title' => 'new title'], null, ['fields' => 'title,content']); 113 | 114 | ``` 115 | 116 | 117 | ## Delete a document 118 | 119 | Document removal can be performed similarly to create or update action: 120 | 121 | ```php 122 | $repo = $this->get('es.manager.default.content'); 123 | $content = $repo->remove(5); 124 | ``` 125 | 126 | -------------------------------------------------------------------------------- /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 | $index = $this->get('Content::class'); 14 | 15 | /** @var $content Content **/ 16 | $content = $index->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 = $index->findByIds(['26', '8', '11']); 30 | 31 | ``` 32 | 33 | ## Find by field 34 | 35 | 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. 36 | 37 | > 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. 38 | 39 | 40 | ```php 41 | 42 | $index = $this->get('Content::class'); 43 | 44 | /** @var $content Content **/ 45 | $content = $index->findBy(['title' => 'Acme']); 46 | 47 | ``` 48 | 49 | The return will be: 50 | 51 | ``` 52 | Array 53 | ( 54 | [0] => Array 55 | ( 56 | [title] => Acme 57 | ) 58 | ) 59 | ``` 60 | 61 | 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: 62 | 63 | ```php 64 | 65 | $content = $index->findBy(['title' => 'Acme'], ['price' => 'asc'], 20, 10); 66 | 67 | ``` 68 | 69 | 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. 70 | 71 | ## Find one document by field 72 | 73 | Completely the same as `findBy()` function, except it will return the first document. 74 | 75 | ```php 76 | 77 | $index = $this->get('Content::class'); 78 | 79 | /** @var $content Content **/ 80 | $content = $index->findOneBy(['title' => 'Acme']); 81 | 82 | ``` 83 | 84 | The return will be: 85 | 86 | ``` 87 | Array 88 | ( 89 | [title] => Acme 90 | ) 91 | ``` 92 | -------------------------------------------------------------------------------- /Resources/doc/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | type: elasticsearch-bundle 4 | order: 1 5 | --- 6 | 7 | > This documentation is for **`6.x`** version. If you look for **`5.x`** or earlier take a look at the [Github Resources folder](https://github.com/ongr-io/ElasticsearchBundle/tree/5.2/Resources/doc). 8 | 9 | ## Reference links 10 | 11 | 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. 12 | 13 | * [Mapping explained](mapping.md) 14 | * [Using Meta-Fields](meta_fields.md) 15 | * [Configuration](configuration.md) 16 | * [Console commands](commands.md) 17 | * [How to do a simple CRUD actions](crud.md) 18 | * [Quick find functions](find_functions.md) 19 | * [How to search the index](search.md) 20 | * [Scan through the index](scan.md) 21 | * [Parsing the results](results_parsing.md) 22 | 23 | ## How to install 24 | 25 | #### Step 1: Install Elasticsearch bundle 26 | 27 | Elasticsearch bundle is installed using [Composer](https://getcomposer.org). 28 | 29 | ```bash 30 | composer require ongr/elasticsearch-bundle "~6.0" 31 | ``` 32 | 33 | > Instructions for installing and deploying Elasticsearch can be found in 34 | [Elasticsearch installation page][17]. 35 | 36 | Enable Elasticsearch bundle in your AppKernel: 37 | 38 | ```php 39 | // app/AppKernel.php 40 | 41 | public function registerBundles() 42 | { 43 | $bundles = [ 44 | // ... 45 | new ONGR\ElasticsearchBundle\ONGRElasticsearchBundle(), 46 | ]; 47 | 48 | // ... 49 | } 50 | 51 | ``` 52 | 53 | #### Step 2: (OPTIONAL) Add configuration 54 | 55 | > Since bundle v6 the configuration is not necessary. Everything can be set through annotation. 56 | 57 | ```yaml 58 | # app/config/config.yml 59 | ongr_elasticsearch: 60 | indexes: 61 | App\Document\Product: 62 | alias: product 63 | hosts: 64 | - 127.0.0.1:9200 65 | ``` 66 | 67 | The configuration might be handy if you want to set an index alias name or other parameter from `.env` or ENV. 68 | 69 | 70 | #### Step 3: Define your Elasticsearch types as `Document` objects 71 | 72 | This bundle uses objects to represent Elasticsearch documents. Lets create a `Product` class to represent products. 73 | 74 | ```php 75 | // src/Document/Product.php 76 | 77 | namespace App\Document; 78 | 79 | use ONGR\ElasticsearchBundle\Annotation as ES; 80 | 81 | /** 82 | * @ES\Index(alias="my_product") 83 | */ 84 | class Product 85 | { 86 | /** 87 | * @ES\Id() 88 | */ 89 | public $id; 90 | 91 | /** 92 | * @ES\Property(type="text") 93 | */ 94 | public $title; 95 | 96 | /** 97 | * @ES\Property(type="float") 98 | */ 99 | public $price; 100 | } 101 | 102 | ``` 103 | 104 | #### Step 4: Create index and mappings 105 | 106 | Elasticsearch bundle provides several `CLI` commands. One of them is for creating index, run command in your terminal: 107 | 108 | ```bash 109 | 110 | bin/console ongr:es:index:create 111 | 112 | ``` 113 | 114 | > More info about the rest of the commands can be found in the [commands chapter][10]. 115 | 116 | 117 | #### Step 5: Enjoy the ElasticsearchBundle 118 | 119 | Enjoy :rocket: -------------------------------------------------------------------------------- /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](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-fields.html). 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 | ### @Routing (_routing) 38 | 39 | Custom routing patterns can be implemented by specifying a custom routing value per document. 40 | The same routing value needs to be provided when getting, deleting, or updating the document. 41 | It is represented by the `@Routing` annotation. Here is an example of such a field: 42 | 43 | ```php 44 | /** 45 | * @ES\Routing() 46 | */ 47 | public $routing; 48 | ``` 49 | 50 | Forgetting the routing value can lead to a document being indexed on more than one shard. 51 | As a safeguard, the _routing field can be configured to make a custom routing value 52 | required for all CRUD operations. This can be implemented by setting the `equired` 53 | attribute to true (`@ES\Routing(required=true)`). 54 | 55 | More information on routing can be found in the [dedicated docs](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-routing-field.html) 56 | 57 | -------------------------------------------------------------------------------- /Resources/doc/overwriting_bundle.md: -------------------------------------------------------------------------------- 1 | # Overwriting bundle parts 2 | 3 | Documentation in progress... 4 | -------------------------------------------------------------------------------- /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 | $index = $this->get('MyIndexClass::class'); 35 | $search = $index->createSearch(); 36 | $termQuery = new MatchAllQuery(); 37 | $results = $index->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 = $index->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 = $index->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('MyIndexClass::class')->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 | -------------------------------------------------------------------------------- /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 does'nt work for `Repository::findArray()` method. 10 | 11 | Here's an example with scrolling: 12 | 13 | ```php 14 | 15 | $repo = $this->get('MyIndexClass::class'); 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 | -------------------------------------------------------------------------------- /Resources/doc/search.md: -------------------------------------------------------------------------------- 1 | # How to perform a Search 2 | 3 | ## Structured search with DSL 4 | 5 | If find functions are not enough, there is a possibility to perform a structured search using [query builder](https://github.com/ongr-io/ElasticsearchDSL). In a nutshell you can construct any queries, aggregations, etc. that are defined in [Elasticsearch Query DSL documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html). 6 | 7 | To begin with structured search you will need a `Search` object. You need to add all the queries and other DSL constructs 8 | to this search object and then perform the search from the repository service. There are three specialized `find` methods 9 | dedicated for this task and you may choose between depending on your needs: 10 | 11 | | method | Return Type | 12 | |:-----------------:|:-------------------------------------------------------------------------------:| 13 | | `findDocuments()` | Returns an instance of `DocumentIterator` | 14 | | `findArray()` | Returns an instance of `ArrayIterator` | 15 | | `findRaw()` | Returns an array of raw results with unaltered elasticsearch response structure | 16 | 17 | For the majority of cases, you will be using `findDocuments` that returns an iterator with hydrated documents. In addition 18 | it provides a convenient way of handling aggregations, that can be accessed via `getAggregations()` method. 19 | 20 | ### Simple Example 21 | 22 | In this example we will search for cities in Lithuania with more than 10K population 23 | 24 | ```php 25 | 26 | $index = $this->get('MyIndexClass::class'); 27 | $search = $index->createSearch(); 28 | 29 | $termQuery = new TermQuery('country', 'Lithuania'); 30 | $search->addQuery($termQuery); 31 | 32 | $rangeQuery = new RangeQuery('population', ['from' => 10000]); 33 | $search->addQuery($rangeQuery); 34 | 35 | $results = $index->findDocuments($search); 36 | 37 | ``` 38 | 39 | > Important: fields `country` & `population` are the field names in elasticsearch type, NOT the document variables. 40 | 41 | It will construct a query: 42 | 43 | ```json 44 | 45 | { 46 | "query": { 47 | "bool": { 48 | "must": [ 49 | { 50 | "term": { 51 | "country": "Lithuania" 52 | } 53 | }, 54 | { 55 | "range": { 56 | "population": { 57 | "from": 10000 58 | } 59 | } 60 | } 61 | ] 62 | } 63 | } 64 | } 65 | 66 | ``` 67 | 68 | > Important: by default result size in elasticsearch is 10, if you need more set size to your needs. 69 | 70 | Setting size or offset is to the search is very easy, because it has getters and setters for these attributes. 71 | Therefore to set size all you need to do is to write 72 | 73 | ```php 74 | 75 | $search->setSize(100); 76 | 77 | ``` 78 | 79 | Similarly other properties like Scroll, Timeout, MinScore and more can be defined. 80 | 81 | For more query and filter examples take a look at the [Elasticsearch DSL library docs](https://github.com/ongr-io/ElasticsearchDSL/blob/master/docs/index.md). 82 | We covered all examples that we found in [Elasticsearch Query DSL documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html) how to cover it in the object oriented way. 83 | 84 | ## Results count 85 | 86 | Elasticsearch bundle provides support for [Count API](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-count.html). If you need only to count the results, this is a faster way to approach this. Here's an example of how to count cars by red color: 87 | 88 | ```php 89 | 90 | $index = $this->get('MyIndexClass::class'); 91 | $search = $index->createSearch(); 92 | 93 | $termQuery = new TermQuery('color', 'red'); 94 | $search->addQuery($termQuery); 95 | 96 | $count = $index->count($search); 97 | 98 | ``` 99 | 100 | ## Searching in Multiple indexes 101 | 102 | The only way how we think it would be possible is to use same alias according several indexes. 103 | 104 | Moreover, the structure of the documents should be the same; otherwise, it won't create document objects. 105 | 106 | > Notice, that the second argument needs to be an array, not a `Search` instance. 107 | -------------------------------------------------------------------------------- /Resources/doc/upgrade.md: -------------------------------------------------------------------------------- 1 | UPGRADE FROM 5.x to 6.x 2 | === 3 | 4 | #### Breaking changes 5 | * Type support were removed. From now on 1 document class represents 1 index. 6 | * `Manager` and `Repository` services were removed in a favor of `IndexService`. 7 | 8 | UPGRADE FROM 1.x to 5.0 9 | === 10 | 11 | #### Breaking changes 12 | * Removed all deprecations from 1.x version. 13 | * Removed `_ttl` metafield annotation. 14 | * Service name `@annotations.cached_reader` changed to `@es.annotations.cached_reader` #717 15 | * From Document annotation removed all properties except `type`. From now on everything has to be defined in the `options`. 16 | * `string` property type was deprecated in elasticsearch 5.0, please use `text` or `keyword` accordingly. 17 | More info: https://www.elastic.co/blog/strings-are-dead-long-live-strings 18 | * `auth` in the configuration was removed. Use authentication information directly in host or create event listener 19 | to modify client creation. There are too many ways to authenticate elasticsearch. That said we leaving this customisation to the user due difficult support. 20 | * `connections` node in configuration was removed. Use `index` from now on. There was absolute 21 | misunderstanding to have exposed connections, we never saw any benefits to use single connection 22 | between several managers. 23 | * Changed the namespace of the `DocumentParserException` to `ONGR\ElasticsearchBundle\Mapping\Exception`. #722 24 | * `analysis` node in `index`/`connection` was deprecated. From now on used analyzers, filters, etc. must be provided in document annotations 25 | * `Results` (constants container for result type definitions) class was removed in favor for 26 | new find functions with predefined types in the names. 27 | * Export service now uses own query calling instead of elasticsearch-php. It was changes due a bug 28 | in hits iterator in elasticsearch-php. We will try to help them to resolve this issue. 29 | * `Manager::execute()` was removed. Use `Manager::search()` instead. 30 | * `Repository::execute()` was removed. Use `findDocuments()`, `findArray()` or `findRaw()` instead. 31 | * `Manager::scroll()` third argument with result type definition was removed. 32 | Now you can get only raw result data from scroll. 33 | * `AbstractElasticsearchTestCase::runTest()` was removed. It was introduced when elasticsearch 34 | in our CI was very unstable. Now there is no sense to repeat failing tests again and again. 35 | * `AbstractElasticsearchTestCase::getNumberOfRetries()` was removed. 36 | If you write tests by extending `AbstractElasticsearchTestCase` delete your retries data provides. 37 | 38 | #### Changes which should not impact the functionality 39 | 40 | * Minimum PHP required version now is 5.6 41 | * Minimum Symfony required version now is 2.8 42 | * Minimum ES version was upped to 5.0 43 | * Document annotation now has an options support. 44 | * No more needed to define analysis in manager, it will be collected automatically from documents. 45 | 46 | UPGRADE FROM 0.x to 1.0 47 | === 48 | 49 | #### Breaking changes 50 | 51 | * All stuffs which were marked as `@deprecated` is removed. 52 | * DSL query builder was exposed to standalone [ElasticsearchDSL](https://github.com/ongr-io/ElasticsearchDSL) library. So this effects a namespace. Run global search and replace (any modern IDE has this feature) through your project files. Change namespace `ONGR\ElasticsearchDSL\` to `ONGR\ElasticsearchDSL\`. 53 | * `Client` is now the part of the `Manager`. So if you had any extensions of using client directly e.g. type hinting then search and remove it or change to the `Manager`. 54 | * `Manager` and `Repository` namespace is changed to `Service`, there is no more `ORM`. Again, run search and replace to find old `ONGR\ElasticsearchBundle\ORM\` to `ONGR\ElasticsearchBundle\Service\` namespace. 55 | * `Results` namespace was completely refactored. The `Suggestion`, `RawResultScanIterator`, `DocumentScanIterator`, `DocumentHighlight`, `IndicesResult` were removed. 56 | * Events currently are disabled due previous complex integration. They will be introduced back in the v1.1.0. 57 | * `config.yml` structure lightly was changed. The `analysis` section appeared where you can define analyzers, filters etc and then reuse them in connections. See [configuration chapter](connection.md) for more information. 58 | * `document_dir` option in `config.yml` was removed. From now on `Document` namespace for ElasticsearchBundle's documents is mandatory. 59 | * Removed `createDocument()` from `Repository` class. To create documents use normal object creation way with `new`. 60 | * Mapping annotations were simplified. In the `Document` annotations `Skip` and `Inherit` annotations were removed. In `Property` there are only `type`, `name` and `options` attributes left. Fields containing `Object` and `Nested` now must be defined using `Embedded` annotation. From now on all custom fields has to be defined in `options` (e.g. index_analyzer). See [mapping chapter](mapping.md) for more info. 61 | * `AbstractDocument` and `DocumentInterface` were removed. Now any class with correct annotations can be used as a document. 62 | * `@Id`, `@Ttl`, `@ParentDocument` annotations were introduced to define Elasticsearch meta-fields like `_id`, `_ttl`, `_parent`. 63 | * From now on `Repository` always represents single document. To execute search on multiple types use `Manager`. 64 | 65 | #### Changes which should not impact the functionality 66 | 67 | * Minimum PHP required version now is 5.5 68 | * Minimum Symfony required version now is 2.7 69 | * Document Proxy class was completely removed. This should not effect any functionality. 70 | * Profiler namespace is changed from `DataCollector` to `Profiler`. 71 | * `findBy` in the Repository now uses `query_string` query instead of terms query. 72 | 73 | > If we miss forgot something here in the list, please open an issue or a PR with a suggestion. 74 | 75 | -------------------------------------------------------------------------------- /Resources/public/images/blue_picto_less.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ongr-io/ElasticsearchBundle/effce6a1d38415e4dc72aedc9351a885bfabefdd/Resources/public/images/blue_picto_less.gif -------------------------------------------------------------------------------- /Resources/public/images/blue_picto_more.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ongr-io/ElasticsearchBundle/effce6a1d38415e4dc72aedc9351a885bfabefdd/Resources/public/images/blue_picto_more.gif -------------------------------------------------------------------------------- /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 class represent`s aggregation buckets in the objective way. 16 | */ 17 | class AggregationValue implements \ArrayAccess, \IteratorAggregate 18 | { 19 | const BUCKETS_KEY = 'buckets'; 20 | const DOC_COUNT_KEY = 'doc_count'; 21 | 22 | /** 23 | * @var array 24 | */ 25 | private $rawData; 26 | 27 | public function __construct(array $rawData) 28 | { 29 | $this->rawData = $rawData; 30 | } 31 | 32 | /** 33 | * Returns aggregation value by the aggregation name. 34 | */ 35 | public function getValue(string $name) 36 | { 37 | if (!isset($this->rawData[$name])) { 38 | return null; 39 | } 40 | 41 | return $this->rawData[$name]; 42 | } 43 | 44 | public function getCount(): int 45 | { 46 | return (int) $this->getValue(self::DOC_COUNT_KEY); 47 | } 48 | 49 | /** 50 | * Returns array of bucket values. 51 | * 52 | * @return AggregationValue[] 53 | */ 54 | public function getBuckets(): array 55 | { 56 | if (!isset($this->rawData[self::BUCKETS_KEY])) { 57 | return []; 58 | } 59 | 60 | $buckets = []; 61 | 62 | foreach ($this->rawData[self::BUCKETS_KEY] as $bucket) { 63 | $buckets[] = new self($bucket); 64 | } 65 | 66 | return $buckets; 67 | } 68 | 69 | /** 70 | * Returns sub-aggregation. 71 | */ 72 | public function getAggregation(string $name): ?self 73 | { 74 | if (!isset($this->rawData[$name])) { 75 | return null; 76 | } 77 | 78 | return new self($this->rawData[$name]); 79 | } 80 | 81 | /** 82 | * Search'es the aggregation by defined path. 83 | */ 84 | public function find(string $path): ?self 85 | { 86 | $name = explode('.', $path, 2); 87 | $aggregation = $this->getAggregation($name[0]); 88 | 89 | if ($aggregation === null || !isset($name[1])) { 90 | return $aggregation; 91 | } 92 | 93 | return $aggregation->find($name[1]); 94 | } 95 | 96 | #[\ReturnTypeWillChange] 97 | public function offsetExists($offset) 98 | { 99 | return array_key_exists($offset, $this->rawData); 100 | } 101 | 102 | #[\ReturnTypeWillChange] 103 | public function offsetGet($offset) 104 | { 105 | if (!isset($this->rawData[$offset])) { 106 | return null; 107 | } 108 | 109 | return $this->rawData[$offset]; 110 | } 111 | 112 | #[\ReturnTypeWillChange] 113 | public function offsetSet($offset, $value) 114 | { 115 | throw new \LogicException('Aggregation result can not be changed on runtime.'); 116 | } 117 | 118 | #[\ReturnTypeWillChange] 119 | public function offsetUnset($offset) 120 | { 121 | throw new \LogicException('Aggregation result can not be changed on runtime.'); 122 | } 123 | 124 | public function getIterator(): \ArrayIterator 125 | { 126 | $buckets = $this->getBuckets(); 127 | 128 | if ($buckets === null) { 129 | throw new \LogicException('Can not iterate over aggregation without buckets!'); 130 | } 131 | 132 | return new \ArrayIterator($this->getBuckets()); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /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 | class ArrayIterator extends AbstractResultsIterator implements \ArrayAccess 15 | { 16 | #[\ReturnTypeWillChange] 17 | public function offsetExists($offset) 18 | { 19 | return $this->documentExists($offset); 20 | } 21 | 22 | #[\ReturnTypeWillChange] 23 | public function offsetGet($offset) 24 | { 25 | return $this->getDocument($offset); 26 | } 27 | 28 | #[\ReturnTypeWillChange] 29 | public function offsetSet($offset, $value) 30 | { 31 | $this->documents[$offset] = $value; 32 | } 33 | 34 | #[\ReturnTypeWillChange] 35 | public function offsetUnset($offset) 36 | { 37 | unset($this->documents[$offset]); 38 | } 39 | 40 | protected function convertDocument(array $raw) 41 | { 42 | if (array_key_exists('_source', $raw)) { 43 | $doc = $raw['_source']; 44 | } elseif (array_key_exists('fields', $raw)) { 45 | $doc = array_map('reset', $raw['fields']); 46 | } 47 | 48 | $doc['_id'] = $raw['_id']; 49 | 50 | return $doc; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /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 | class DocumentIterator extends AbstractResultsIterator 17 | { 18 | public function getAggregations() 19 | { 20 | $aggregations = []; 21 | 22 | foreach (parent::getAggregations() as $key => $aggregation) { 23 | $aggregations[$key] = $this->getAggregation($key); 24 | } 25 | 26 | return $aggregations; 27 | } 28 | 29 | public function getAggregation($name) 30 | { 31 | $aggregations = parent::getAggregations(); 32 | if (!array_key_exists($name, $aggregations)) { 33 | return null; 34 | } 35 | 36 | return new AggregationValue($aggregations[$name]); 37 | } 38 | 39 | protected function convertDocument(array $raw) 40 | { 41 | $data = $raw['_source'] ?? $raw['_fields'] ?? null; 42 | $data['_id'] = $raw['_id'] ?? null; 43 | 44 | return $this->getConverter()->convertArrayToDocument($this->getIndex()->getNamespace(), $data); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /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 | use ONGR\ElasticsearchBundle\Mapping\Converter; 17 | 18 | /** 19 | * This is for embedded ObjectType's or NestedType's iterator implemented with a lazy loading. 20 | */ 21 | class ObjectIterator extends AbstractLazyCollection 22 | { 23 | private $converter; 24 | protected $collection; 25 | private $namespace; 26 | 27 | public function __construct(string $namespace, array $array, Converter $converter) 28 | { 29 | $this->converter = $converter; 30 | $this->collection = new ArrayCollection($array); 31 | $this->namespace = $namespace; 32 | } 33 | 34 | protected function convertDocument(array $data) 35 | { 36 | return $this->converter->convertArrayToDocument($this->namespace, $data); 37 | } 38 | 39 | protected function doInitialize() 40 | { 41 | $this->collection = $this->collection->map(function ($rawObject) { 42 | return $this->convertDocument($rawObject); 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /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 | * A simple iterator which returns the raw result it got from the elasticsearch. 16 | */ 17 | class RawIterator extends AbstractResultsIterator 18 | { 19 | protected function convertDocument(array $raw) 20 | { 21 | return $raw; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /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 Symfony\Component\Console\Helper\ProgressBar; 21 | use Symfony\Component\Console\Output\OutputInterface; 22 | 23 | /** 24 | * ExportService class. 25 | */ 26 | class ExportService 27 | { 28 | public function exportIndex( 29 | IndexService $index, 30 | $filename, 31 | $chunkSize, 32 | OutputInterface $output, 33 | $maxLinesInFile = 300000 34 | ) { 35 | $search = new Search(); 36 | $search->addQuery(new MatchAllQuery()); 37 | $search->setSize($chunkSize); 38 | $search->setScroll('2m'); 39 | 40 | $searchResults = $index->search($search->toArray(), $search->getUriParams()); 41 | 42 | $results = new RawIterator( 43 | $searchResults, 44 | $index, 45 | null, 46 | [ 47 | 'duration' => '2m', 48 | '_scroll_id' => $searchResults['_scroll_id'], 49 | ] 50 | ); 51 | 52 | $progress = new ProgressBar($output, $results->count()); 53 | $progress->setRedrawFrequency(100); 54 | $progress->start(); 55 | 56 | $counter = $fileCounter = 0; 57 | $count = $this->getFileCount($results->count(), $maxLinesInFile, $fileCounter); 58 | 59 | $date = date(\DateTime::ISO8601); 60 | $metadata = [ 61 | 'count' => $count, 62 | 'date' => $date, 63 | ]; 64 | 65 | $filename = str_replace('.json', '', $filename); 66 | $writer = $this->getWriter($this->getFilePath($filename.'.json'), $metadata['count']); 67 | 68 | foreach ($results as $data) { 69 | if ($counter >= $maxLinesInFile) { 70 | $writer->finalize(); 71 | $writer = null; 72 | $fileCounter++; 73 | $count = $this->getFileCount($results->count(), $maxLinesInFile, $fileCounter); 74 | $metadata = [ 75 | 'count' => $count, 76 | 'date' => $date, 77 | ]; 78 | $writer = $this->getWriter($this->getFilePath($filename."_".$fileCounter.".json"), $metadata['count']); 79 | $counter = 0; 80 | } 81 | 82 | $doc = array_intersect_key($data, array_flip(['_id', '_source'])); 83 | $writer->push($doc); 84 | $progress->advance(); 85 | $counter++; 86 | } 87 | 88 | $writer->finalize(); 89 | $progress->finish(); 90 | $output->writeln(''); 91 | } 92 | 93 | /** 94 | * Returns real file path. 95 | * 96 | * @param string $filename 97 | * 98 | * @return string 99 | */ 100 | protected function getFilePath($filename): string 101 | { 102 | if ($filename[0] == '/' || strstr($filename, ':') !== false) { 103 | return $filename; 104 | } 105 | 106 | return getcwd() . '/' . $filename; 107 | } 108 | 109 | protected function getWriter(string $filename, int $count): JsonWriter 110 | { 111 | return new JsonWriter($filename, $count); 112 | } 113 | 114 | /** 115 | * @param int $resultsCount 116 | * @param int $maxLinesInFile 117 | * @param int $fileCounter 118 | * 119 | * @return int 120 | */ 121 | protected function getFileCount($resultsCount, $maxLinesInFile, $fileCounter): int 122 | { 123 | $leftToInsert = $resultsCount - ($fileCounter * $maxLinesInFile); 124 | if ($leftToInsert <= $maxLinesInFile) { 125 | $count = $leftToInsert; 126 | } else { 127 | $count = $maxLinesInFile; 128 | } 129 | 130 | return (int) $count; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | public function importIndex( 24 | IndexService $index, 25 | $filename, 26 | OutputInterface $output, 27 | $options 28 | ) { 29 | $reader = $this->getReader($index, $this->getFilePath($filename), $options); 30 | 31 | $progress = new ProgressBar($output, $reader->count()); 32 | $progress->setRedrawFrequency(100); 33 | $progress->start(); 34 | 35 | $bulkSize = $options['bulk-size']; 36 | foreach ($reader as $key => $document) { 37 | $data = $document['_source']; 38 | $data['_id'] = $document['_id']; 39 | 40 | if (array_key_exists('fields', $document)) { 41 | $data = array_merge($document['fields'], $data); 42 | } 43 | 44 | $index->bulk('index', $data); 45 | 46 | if (($key + 1) % $bulkSize == 0) { 47 | $index->commit(); 48 | } 49 | 50 | $progress->advance(); 51 | } 52 | 53 | $index->commit(); 54 | 55 | $progress->finish(); 56 | $output->writeln(''); 57 | } 58 | 59 | /** 60 | * Returns a real file path. 61 | * 62 | * @param string $filename 63 | * 64 | * @return string 65 | */ 66 | protected function getFilePath($filename) 67 | { 68 | if ($filename[0] == '/' || strstr($filename, ':') !== false) { 69 | return $filename; 70 | } 71 | 72 | return realpath(getcwd() . '/' . $filename); 73 | } 74 | 75 | protected function getReader(IndexService $manager, $filename, $options): JsonReader 76 | { 77 | return new JsonReader($manager, $filename, $options); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /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 | class IndexSuffixFinder 15 | { 16 | public function getNextFreeIndex(IndexService $index, \DateTime $time = null): string 17 | { 18 | if ($time === null) { 19 | $time = new \DateTime(); 20 | } 21 | 22 | $date = $time->format('Y.m.d'); 23 | $alias = $index->getIndexName(); 24 | $indexName = $alias . '-' . $date; 25 | $i = 0; 26 | 27 | $client = $index->getClient(); 28 | 29 | while ($client->indices()->exists(['index' => $indexName])) { 30 | $i++; 31 | $indexName = "{$indexName}-{$i}"; 32 | } 33 | 34 | return $indexName; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Service/Json/JsonReader.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 | use ONGR\ElasticsearchBundle\Service\IndexService; 15 | use Symfony\Component\OptionsResolver\OptionsResolver; 16 | 17 | /** 18 | * Reads records one by one. 19 | * 20 | * Sample input: 21 | *

22 | * [ 23 | * {"count":2}, 24 | * {"_id":"doc1","title":"Document 1"}, 25 | * {"_id":"doc2","title":"Document 2"} 26 | * ] 27 | *

28 | */ 29 | class JsonReader implements \Countable, \Iterator 30 | { 31 | private $filename; 32 | private $handle; 33 | private $key = 0; 34 | private $currentLine; 35 | private $metadata; 36 | private $index; 37 | private $optionsResolver; 38 | private $options; 39 | 40 | public function __construct(IndexService $index, string $filename, array $options = []) 41 | { 42 | $this->index = $index; 43 | $this->filename = $filename; 44 | $this->options = $options; 45 | } 46 | 47 | /** 48 | * Destructor. Closes file handler if open. 49 | */ 50 | public function __destruct() 51 | { 52 | if ($this->handle !== null) { 53 | @fclose($this->handle); 54 | } 55 | } 56 | 57 | public function getIndex(): IndexService 58 | { 59 | return $this->index; 60 | } 61 | 62 | protected function getFileHandler() 63 | { 64 | //Make sure the gzip option is resolved from a filename. 65 | if ($this->handle === null) { 66 | $isGzip = array_key_exists('gzip', $this->options); 67 | 68 | $filename = !$isGzip? 69 | $this->filename: 70 | sprintf('compress.zlib://%s', $this->filename); 71 | $fileHandler = @fopen($filename, 'r'); 72 | 73 | if ($fileHandler === false) { 74 | throw new \LogicException('Can not open file.'); 75 | } 76 | 77 | $this->handle = $fileHandler; 78 | } 79 | 80 | return $this->handle; 81 | } 82 | 83 | protected function readMetadata() 84 | { 85 | if ($this->metadata !== null) { 86 | return; 87 | } 88 | 89 | $line = fgets($this->getFileHandler()); 90 | 91 | if (trim($line) !== '[') { 92 | throw new \InvalidArgumentException('Given file does not match expected pattern.'); 93 | } 94 | 95 | $line = trim(fgets($this->getFileHandler())); 96 | $this->metadata = json_decode(rtrim($line, ','), true); 97 | } 98 | 99 | protected function readLine() 100 | { 101 | $buffer = ''; 102 | 103 | while ($buffer === '') { 104 | $buffer = fgets($this->getFileHandler()); 105 | 106 | if ($buffer === false) { 107 | $this->currentLine = null; 108 | 109 | return; 110 | } 111 | 112 | $buffer = trim($buffer); 113 | } 114 | 115 | if ($buffer === ']') { 116 | $this->currentLine = null; 117 | 118 | return; 119 | } 120 | 121 | $data = json_decode(rtrim($buffer, ','), true); 122 | $this->currentLine = $this->getOptionsResolver()->resolve($data); 123 | } 124 | 125 | protected function configureResolver(OptionsResolver $resolver) 126 | { 127 | $resolver 128 | ->setRequired(['_id', '_source']) 129 | ->setDefaults(['_score' => null, 'fields' => []]) 130 | ->addAllowedTypes('_id', ['integer', 'string']) 131 | ->addAllowedTypes('_source', 'array') 132 | ->addAllowedTypes('fields', 'array'); 133 | } 134 | 135 | #[\ReturnTypeWillChange] 136 | public function current() 137 | { 138 | if ($this->currentLine === null) { 139 | $this->readLine(); 140 | } 141 | 142 | return $this->currentLine; 143 | } 144 | 145 | #[\ReturnTypeWillChange] 146 | public function next() 147 | { 148 | $this->readLine(); 149 | 150 | $this->key++; 151 | } 152 | 153 | #[\ReturnTypeWillChange] 154 | public function key() 155 | { 156 | return $this->key; 157 | } 158 | 159 | #[\ReturnTypeWillChange] 160 | public function valid() 161 | { 162 | return !feof($this->getFileHandler()) && $this->currentLine; 163 | } 164 | 165 | #[\ReturnTypeWillChange] 166 | public function rewind() 167 | { 168 | rewind($this->getFileHandler()); 169 | $this->metadata = null; 170 | $this->readMetadata(); 171 | $this->readLine(); 172 | } 173 | 174 | #[\ReturnTypeWillChange] 175 | public function count() 176 | { 177 | $metadata = $this->getMetadata(); 178 | 179 | if (!isset($metadata['count'])) { 180 | throw new \LogicException('Given file does not contain count of documents.'); 181 | } 182 | 183 | return $metadata['count']; 184 | } 185 | 186 | public function getMetadata() 187 | { 188 | $this->readMetadata(); 189 | 190 | return $this->metadata; 191 | } 192 | 193 | private function getOptionsResolver(): OptionsResolver 194 | { 195 | if (!$this->optionsResolver) { 196 | $this->optionsResolver = new OptionsResolver(); 197 | $this->configureResolver($this->optionsResolver); 198 | } 199 | 200 | return $this->optionsResolver; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /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 | private $filename; 29 | private $handle; 30 | private $count; 31 | private $currentPosition = 0; 32 | 33 | public function __construct(string $filename, int $count) 34 | { 35 | $this->filename = $filename; 36 | $this->count = $count; 37 | } 38 | 39 | /** 40 | * Destructor. Closes file handler if open. 41 | */ 42 | public function __destruct() 43 | { 44 | $this->finalize(); 45 | } 46 | 47 | /** 48 | * Performs initialization. 49 | */ 50 | protected function initialize() 51 | { 52 | if ($this->handle !== null) { 53 | return; 54 | } 55 | 56 | $this->handle = fopen($this->filename, 'w'); 57 | fwrite($this->handle, "[\n"); 58 | fwrite($this->handle, json_encode(['count' => $this->count])); 59 | } 60 | 61 | /** 62 | * Performs finalization. 63 | */ 64 | public function finalize() 65 | { 66 | $this->initialize(); 67 | 68 | if (is_resource($this->handle)) { 69 | fwrite($this->handle, "\n]"); 70 | fclose($this->handle); 71 | } 72 | } 73 | 74 | /** 75 | * Writes single document to stream. 76 | * 77 | * @param mixed $document Object to insert into stream. 78 | * 79 | * @throws \OverflowException 80 | */ 81 | public function push($document) 82 | { 83 | $this->initialize(); 84 | $this->currentPosition++; 85 | 86 | if (isset($this->count) && $this->currentPosition > $this->count) { 87 | throw new \OverflowException( 88 | sprintf('This writer was set up to write %d documents, got more.', $this->count) 89 | ); 90 | } 91 | 92 | fwrite($this->handle, ",\n"); 93 | fwrite($this->handle, json_encode($document)); 94 | 95 | if (isset($this->count) && $this->currentPosition == $this->count) { 96 | $this->finalize(); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Test/AbstractElasticsearchTestCase.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\Test; 13 | 14 | use Elasticsearch\Common\Exceptions\BadRequest400Exception; 15 | use ONGR\ElasticsearchBundle\Service\IndexService; 16 | use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; 17 | use Symfony\Component\DependencyInjection\ContainerInterface; 18 | 19 | /** 20 | * Base test which creates unique connection to test with. 21 | */ 22 | abstract class AbstractElasticsearchTestCase extends WebTestCase 23 | { 24 | protected static $cachedContainer; 25 | 26 | /** 27 | * @var IndexService[] 28 | */ 29 | private $indexes = []; 30 | 31 | //You may use setUp() for your personal needs. 32 | protected function setUp(): void 33 | { 34 | } 35 | 36 | /** 37 | * Can be overwritten in child class to populate elasticsearch index with the data. 38 | * 39 | * Example: 40 | * "/This/Should/Be/Index/Document/Namespace" => 41 | * [ 42 | * '_doc' => [ 43 | * [ 44 | * '_id' => 1, 45 | * 'title' => 'foo', 46 | * ], 47 | * [ 48 | * '_id' => 2, 49 | * 'title' => 'bar', 50 | * ] 51 | * ] 52 | * ] 53 | * 54 | * @return array 55 | */ 56 | protected function getDataArray(): array 57 | { 58 | return []; 59 | } 60 | 61 | private function populateElastic(IndexService $indexService, array $documents = []) 62 | { 63 | foreach ($documents as $document) { 64 | $indexService->bulk('index', $document); 65 | } 66 | $indexService->commit(); 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | protected function tearDown(): void 73 | { 74 | parent::tearDown(); 75 | self::$cachedContainer = null; 76 | foreach ($this->indexes as $name => $index) { 77 | try { 78 | $index->dropIndex(); 79 | } catch (\Exception $e) { 80 | // Do nothing. 81 | } 82 | } 83 | } 84 | 85 | protected static function getContainer($reinitialize = false, $kernelOptions = []): ContainerInterface 86 | { 87 | if ($reinitialize && self::$booted) { 88 | self::ensureKernelShutdown(); 89 | self::$cachedContainer = null; 90 | } 91 | 92 | if (!self::$cachedContainer) { 93 | self::$cachedContainer = static::createClient(['environment' => 'test'])->getContainer(); 94 | } 95 | 96 | return self::$cachedContainer; 97 | } 98 | 99 | protected function getIndex($namespace, $createIndex = true): IndexService 100 | { 101 | try { 102 | if (!array_key_exists($namespace, $this->indexes)) { 103 | $this->indexes[$namespace] = self::getContainer()->get($namespace); 104 | } 105 | 106 | if (!$this->indexes[$namespace]->indexExists() && $createIndex) { 107 | $this->indexes[$namespace]->dropAndCreateIndex(); 108 | 109 | // Populates elasticsearch index with the data 110 | $data = $this->getDataArray(); 111 | if (!empty($data[$namespace])) { 112 | $this->populateElastic($this->indexes[$namespace], $data[$namespace]); 113 | } 114 | $this->indexes[$namespace]->refresh(); 115 | } 116 | 117 | return $this->indexes[$namespace]; 118 | } catch (\Exception $e) { 119 | throw new \LogicException($e->getMessage()); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /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\App\Document\DummyDocument; 15 | use ONGR\App\Entity\DummyDocumentInTheEntityDirectory; 16 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 17 | 18 | class DocumentTest extends AbstractElasticsearchTestCase 19 | { 20 | public function testDocumentIndexName() 21 | { 22 | $index = $this->getIndex(DummyDocument::class, false); 23 | $this->assertEquals(DummyDocument::INDEX_NAME, $index->getIndexName()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /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\App\Document\DummyDocument; 15 | use ONGR\ElasticsearchBundle\Service\IndexService; 16 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 17 | 18 | class PropertyTest extends AbstractElasticsearchTestCase 19 | { 20 | public function testPropertyTypeName() 21 | { 22 | /** @var IndexService $index */ 23 | $index = $this->getIndex(DummyDocument::class, false); 24 | $meta = $index->getIndexSettings()->getIndexMetadata(); 25 | 26 | $this->assertEquals( 27 | [ 28 | 'fields' => 29 | [ 30 | 'raw' => [ 31 | 'type' => 'keyword', 32 | ], 33 | 'increment' => [ 34 | 'type' => 'text', 35 | 'analyzer' => 'incrementalAnalyzer', 36 | ], 37 | ], 38 | 'type' => 'text', 39 | ], 40 | $meta['mappings']['properties']['title'] 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /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\App\Document\DummyDocument; 15 | use ONGR\ElasticsearchBundle\Command\CacheClearCommand; 16 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 17 | use Symfony\Component\Console\Application; 18 | use Symfony\Component\Console\Tester\CommandTester; 19 | 20 | class CacheClearCommandTest extends AbstractElasticsearchTestCase 21 | { 22 | 23 | /** 24 | * Tests if command is being executed. 25 | */ 26 | public function testExecute() 27 | { 28 | $this->getIndex(DummyDocument::class); 29 | 30 | $app = new Application(); 31 | $app->add($this->getCommand()); 32 | $command = $app->find(CacheClearCommand::NAME); 33 | $tester = new CommandTester($command); 34 | $tester->execute( 35 | [ 36 | 'command' => $command->getName(), 37 | ] 38 | ); 39 | 40 | $this->assertContains( 41 | 'Elasticsearch `'.DummyDocument::INDEX_NAME.'` index cache has been cleared.', 42 | $tester->getDisplay() 43 | ); 44 | $this->assertEquals(0, $tester->getStatusCode(), 'Status code should be zero.'); 45 | } 46 | 47 | /** 48 | * Tests if exception is thown when no manager is found. 49 | * 50 | * @expectedException \RuntimeException 51 | */ 52 | public function testExecuteException() 53 | { 54 | $app = new Application(); 55 | $app->add($this->getCommand()); 56 | $command = $app->find(CacheClearCommand::NAME); 57 | $tester = new CommandTester($command); 58 | $tester->execute( 59 | [ 60 | 'command' => $command->getName(), 61 | '--index' => 'notexisting', 62 | ] 63 | ); 64 | } 65 | 66 | /** 67 | * Returns cache clear command instance. 68 | * 69 | * @return CacheClearCommand 70 | */ 71 | private function getCommand() 72 | { 73 | $command = new CacheClearCommand(self::getContainer(true)); 74 | 75 | return $command; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /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\App\Document\DummyDocument; 15 | use ONGR\ElasticsearchBundle\Command\IndexDropCommand; 16 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 17 | use Symfony\Component\Console\Application; 18 | use Symfony\Component\Console\Tester\CommandTester; 19 | 20 | class DropIndexCommandTest extends AbstractElasticsearchTestCase 21 | { 22 | public function testExecute() 23 | { 24 | $index = $this->getIndex(DummyDocument::class); 25 | $index->dropAndCreateIndex(); 26 | 27 | $command = new IndexDropCommand(self::getContainer()); 28 | 29 | $app = new Application(); 30 | $app->add($command); 31 | 32 | // Does not drop index. 33 | $command = $app->find(IndexDropCommand::NAME); 34 | $commandTester = new CommandTester($command); 35 | $commandTester->execute( 36 | [ 37 | 'command' => $command->getName(), 38 | ] 39 | ); 40 | 41 | $this->assertTrue( 42 | $index 43 | ->indexExists(), 44 | 'Index should still exist.' 45 | ); 46 | 47 | $commandTester->execute( 48 | [ 49 | 'command' => $command->getName(), 50 | '--force' => true, 51 | ] 52 | ); 53 | 54 | $this->assertFalse( 55 | $index 56 | ->indexExists(), 57 | 'Index should be dropped.' 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/Functional/Command/IndexExportCommandTest.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\App\Document\DummyDocument; 15 | use ONGR\ElasticsearchBundle\Command\IndexExportCommand; 16 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 17 | use org\bovigo\vfs\vfsStream; 18 | use Symfony\Component\Console\Application; 19 | use Symfony\Component\Console\Tester\CommandTester; 20 | 21 | class IndexExportCommandTest extends AbstractElasticsearchTestCase 22 | { 23 | protected function getDataArray(): array 24 | { 25 | return [ 26 | DummyDocument::class => [ 27 | [ 28 | '_id' => 1, 29 | 'title' => 'Foo Product', 30 | 'number' => 5.00, 31 | ], 32 | [ 33 | '_id' => 2, 34 | 'title' => 'Bar Product', 35 | 'number' => 8.33, 36 | ], 37 | [ 38 | '_id' => 3, 39 | 'title' => 'Lao Product', 40 | 'number' => 1.95, 41 | ], 42 | ], 43 | ]; 44 | } 45 | 46 | /** 47 | * Data provider for testIndexExport(). 48 | * 49 | * @return array 50 | */ 51 | public function getIndexExportData() 52 | { 53 | $out = []; 54 | 55 | // Case 0 56 | $options = ['--index' => DummyDocument::INDEX_NAME]; 57 | $expectedResults = $this->transformDataToResult(DummyDocument::class); 58 | $out[] = [$options, $expectedResults]; 59 | 60 | // Case 1: product type specified with chunk. 61 | $options = ['--chunk' => 1, '--index' => DummyDocument::INDEX_NAME]; 62 | $expectedResults = $this->transformDataToResult(DummyDocument::class); 63 | $out[] = [$options, $expectedResults]; 64 | 65 | // Case 2: without parameters. 66 | $options = []; 67 | $expectedResults = $this->transformDataToResult(DummyDocument::class); 68 | $out[] = [$options, $expectedResults]; 69 | 70 | return $out; 71 | } 72 | 73 | /** 74 | * @dataProvider getIndexExportData() 75 | */ 76 | public function testIndexExport(array $options, array $expectedResults) 77 | { 78 | $this->getIndex(DummyDocument::class); 79 | vfsStream::setup('tmp'); 80 | $this->getCommandTester()->execute( 81 | array_merge( 82 | [ 83 | 'command' => IndexExportCommand::NAME, 84 | 'filename' => vfsStream::url('tmp/test.json'), 85 | ], 86 | $options 87 | ) 88 | ); 89 | 90 | $results = $this->parseResult(vfsStream::url('tmp/test.json'), count($expectedResults)); 91 | usort($results, function ($a, $b) { 92 | return (int)$a['_id'] <=> (int)$b['_id']; 93 | }); 94 | $this->assertEquals($expectedResults, $results); 95 | } 96 | 97 | /** 98 | * Transforms data provider data to ElasticSearch expected result data structure. 99 | */ 100 | private function transformDataToResult(string $class): array 101 | { 102 | $expectedResults = []; 103 | 104 | foreach ($this->getDataArray()[$class] as $document) { 105 | $id = $document['_id']; 106 | unset($document['_id']); 107 | $expectedResults[] = [ 108 | '_id' => $id, 109 | '_source' => $document, 110 | ]; 111 | } 112 | 113 | return $expectedResults; 114 | } 115 | 116 | /** 117 | * Returns export index command with assigned container. 118 | * 119 | * @return CommandTester 120 | */ 121 | private function getCommandTester() 122 | { 123 | $indexExportCommand = new IndexExportCommand(self::getContainer()); 124 | 125 | $app = new Application(); 126 | $app->add($indexExportCommand); 127 | 128 | $command = $app->find(IndexExportCommand::NAME); 129 | $commandTester = new CommandTester($command); 130 | 131 | return $commandTester; 132 | } 133 | 134 | /** 135 | * Parses provided file and sorts results. 136 | * 137 | * @param string $filePath 138 | * @param int $expectedCount 139 | * 140 | * @return array 141 | */ 142 | private function parseResult($filePath, $expectedCount) 143 | { 144 | $this->assertFileExists($filePath); 145 | $results = json_decode(file_get_contents($filePath), true); 146 | 147 | $metadata = array_shift($results); 148 | 149 | $this->assertEquals($expectedCount, $metadata['count']); 150 | 151 | return $results; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /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\App\Document\DummyDocument; 15 | use ONGR\ElasticsearchBundle\Command\IndexImportCommand; 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 | public function bulkSizeProvider(): array 24 | { 25 | return [ 26 | [10, 9, 'command_import_9.json'], 27 | [10, 10, 'command_import_10.json'], 28 | [10, 11, 'command_import_11.json'], 29 | [5, 20, 'command_import_20.json'], 30 | ]; 31 | } 32 | 33 | /** 34 | * @return array 35 | */ 36 | public function compressedDataProvider() 37 | { 38 | return [ 39 | [10, 9, 'command_import_9.json.gz'], 40 | [10, 10, 'command_import_10.json.gz'], 41 | [10, 11, 'command_import_11.json.gz'], 42 | ]; 43 | } 44 | 45 | /** 46 | * @dataProvider bulkSizeProvider 47 | */ 48 | public function testIndexImport(int $bulkSize, int $realSize, string $filename) 49 | { 50 | $index = $this->getIndex(DummyDocument::class); 51 | 52 | $app = new Application(); 53 | $app->add($this->getImportCommand()); 54 | 55 | $command = $app->find('ongr:es:index:import'); 56 | $commandTester = new CommandTester($command); 57 | $commandTester->execute( 58 | [ 59 | 'command' => $command->getName(), 60 | 'filename' => __DIR__ . '/../../app/data_seed/' . $filename, 61 | '--bulk-size' => $bulkSize, 62 | ] 63 | ); 64 | 65 | // $index->refresh(); 66 | $search = $index->createSearch()->addQuery(new MatchAllQuery())->setSize($realSize); 67 | $results = $index->findDocuments($search); 68 | 69 | $ids = []; 70 | /** @var DummyDocument $doc */ 71 | foreach ($results as $doc) { 72 | $ids[] = (int)$doc->id; 73 | } 74 | sort($ids); 75 | $data = range(1, $realSize); 76 | $this->assertEquals($data, $ids); 77 | } 78 | 79 | /** 80 | * Test for index import command with gzip option. 81 | * 82 | * @param int $bulkSize 83 | * @param int $realSize 84 | * @param string $filename 85 | * 86 | * @dataProvider compressedDataProvider 87 | */ 88 | public function testIndexImportWithGzipOption($bulkSize, $realSize, $filename) 89 | { 90 | $index = $this->getIndex(DummyDocument::class); 91 | 92 | $app = new Application(); 93 | $app->add($this->getImportCommand()); 94 | 95 | $command = $app->find('ongr:es:index:import'); 96 | $commandTester = new CommandTester($command); 97 | $commandTester->execute( 98 | [ 99 | 'command' => $command->getName(), 100 | 'filename' => __DIR__ . '/../../app/data_seed/' . $filename, 101 | '--bulk-size' => $bulkSize, 102 | '--gzip' => null, 103 | ] 104 | ); 105 | 106 | 107 | $search = $index->createSearch()->addQuery(new MatchAllQuery())->setSize($realSize); 108 | $results = $index->findDocuments($search); 109 | 110 | $ids = []; 111 | /** @var DummyDocument $doc */ 112 | foreach ($results as $doc) { 113 | $ids[] = (int)$doc->id; 114 | } 115 | sort($ids); 116 | $data = range(1, $realSize); 117 | $this->assertEquals($data, $ids); 118 | } 119 | 120 | /** 121 | * Returns import index command with assigned container. 122 | * 123 | * @return IndexImportCommand 124 | */ 125 | private function getImportCommand() 126 | { 127 | return new IndexImportCommand(self::getContainer()); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Tests/Functional/DependencyInjection/ElasticsearchBundleExtensionTest.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 Doctrine\Common\Annotations\CachedReader; 15 | use Doctrine\Common\Cache\PhpFileCache; 16 | use ONGR\App\Document\DummyDocument; 17 | use ONGR\App\Document\IndexWithFieldsDataDocument; 18 | use ONGR\App\Entity\DummyDocumentInTheEntityDirectory; 19 | use ONGR\ElasticsearchBundle\EventListener\TerminateListener; 20 | use ONGR\ElasticsearchBundle\Mapping\Converter; 21 | use ONGR\ElasticsearchBundle\Mapping\DocumentParser; 22 | use ONGR\ElasticsearchBundle\Profiler\ElasticsearchProfiler; 23 | use ONGR\ElasticsearchBundle\Service\IndexService; 24 | use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; 25 | 26 | class ElasticsearchBundleExtensionTest extends KernelTestCase 27 | { 28 | /** 29 | * @return array 30 | */ 31 | public function getTestContainerData() 32 | { 33 | return [ 34 | [ 35 | 'ongr.esb.cache', 36 | PhpFileCache::class, 37 | ], 38 | [ 39 | 'ongr.esb.cache_reader', 40 | CachedReader::class, 41 | ], 42 | [ 43 | DocumentParser::class, 44 | DocumentParser::class, 45 | ], 46 | [ 47 | Converter::class, 48 | Converter::class, 49 | ], 50 | [ 51 | ElasticsearchProfiler::class, 52 | ElasticsearchProfiler::class, 53 | ], 54 | [ 55 | TerminateListener::class, 56 | TerminateListener::class, 57 | ], 58 | 59 | //This tests if a service swap works well 60 | [ 61 | DummyDocument::class, 62 | IndexService::class, 63 | ], 64 | [ 65 | DummyDocumentInTheEntityDirectory::class, 66 | IndexService::class, 67 | ], 68 | [ 69 | IndexWithFieldsDataDocument::class, 70 | IndexService::class, 71 | ], 72 | ]; 73 | } 74 | 75 | /** 76 | * Tests if container has all services. 77 | * 78 | * @param string $id 79 | * @param string $instance 80 | * 81 | * @dataProvider getTestContainerData 82 | */ 83 | public function testContainer($id, $instance) 84 | { 85 | self::bootKernel(); 86 | 87 | $container = self::$kernel->getContainer(); 88 | 89 | $this->assertTrue($container->has($id), sprintf('Container don\'t have %s service.', $id)); 90 | $this->assertInstanceOf( 91 | $instance, 92 | $container->get($id), 93 | sprintf('The instance %s type is not as expected.', $id) 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Tests/Functional/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\Functional\Profiler; 13 | 14 | use ONGR\App\Document\DummyDocument; 15 | use ONGR\ElasticsearchBundle\Profiler\ElasticsearchProfiler; 16 | use ONGR\ElasticsearchDSL\Aggregation\Bucketing\GlobalAggregation; 17 | use ONGR\ElasticsearchDSL\Query\TermLevel\TermQuery; 18 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 19 | use Symfony\Component\HttpFoundation\Request; 20 | use Symfony\Component\HttpFoundation\Response; 21 | 22 | class ElasticsearchProfilerTest extends AbstractElasticsearchTestCase 23 | { 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | protected function getDataArray(): array 28 | { 29 | return [ 30 | DummyDocument::class => [ 31 | [ 32 | '_id' => 1, 33 | 'title' => 'foo', 34 | ], 35 | [ 36 | '_id' => 2, 37 | 'title' => 'bar', 38 | ], 39 | [ 40 | '_id' => 3, 41 | 'title' => 'pizza', 42 | ], 43 | ], 44 | ]; 45 | } 46 | 47 | /** 48 | * Tests if multiple queries are captured. 49 | */ 50 | public function testGetQueryCount() 51 | { 52 | $index = $this->getIndex(DummyDocument::class); 53 | 54 | $document = new DummyDocument(); 55 | $document->title = 'tuna'; 56 | 57 | $index->persist($document); 58 | $index->commit(); 59 | 60 | // Four queries executed while index was being created. 61 | $this->assertGreaterThanOrEqual(4, $this->getCollector()->getQueryCount()); 62 | } 63 | 64 | /** 65 | * Tests if a returned time is correct. 66 | */ 67 | public function testGetTime() 68 | { 69 | $index = $this->getIndex(DummyDocument::class); 70 | $index->find(3); 71 | 72 | $this->assertGreaterThan(0.0, $this->getCollector()->getTime(), 'Time should be greater than 0ms'); 73 | } 74 | 75 | /** 76 | * Tests if a logged query is correct. 77 | */ 78 | public function testGetQueries() 79 | { 80 | $index = $this->getIndex(DummyDocument::class); 81 | $index->find(2); 82 | $queries = $this->getCollector()->getQueries(); 83 | 84 | $lastQuery = end($queries[ElasticsearchProfiler::UNDEFINED_ROUTE]); 85 | $this->checkQueryParameters($lastQuery); 86 | 87 | $this->assertEquals( 88 | [ 89 | 'body' => '', 90 | 'method' => 'GET', 91 | 'httpParameters' => [], 92 | 'scheme' => 'http', 93 | ], 94 | $lastQuery, 95 | 'Logged data did not match expected data.' 96 | ); 97 | } 98 | 99 | /** 100 | * Tests if a term query is correct. 101 | */ 102 | public function testGetTermQuery() 103 | { 104 | $index = $this->getIndex(DummyDocument::class); 105 | $search = $index 106 | ->createSearch() 107 | ->addQuery(new TermQuery('title', 'pizza')); 108 | $index->findDocuments($search); 109 | 110 | $queries = $this->getCollector()->getQueries(); 111 | $lastQuery = end($queries[ElasticsearchProfiler::UNDEFINED_ROUTE]); 112 | $this->checkQueryParameters($lastQuery); 113 | 114 | $lastQuery['body'] = trim(preg_replace('/\s+/', '', $lastQuery['body'])); 115 | 116 | $this->assertEquals( 117 | [ 118 | 'body' => json_encode($search->toArray()), 119 | 'method' => 'POST', 120 | 'httpParameters' => [], 121 | 'scheme' => 'http', 122 | ], 123 | $lastQuery, 124 | 'Logged data did not match expected data.' 125 | ); 126 | } 127 | 128 | /** 129 | * Checks query parameters. 130 | */ 131 | public function checkQueryParameters(array &$query) 132 | { 133 | $this->assertArrayHasKey('time', $query, 'Query should have time set.'); 134 | $this->assertGreaterThan(0.0, $query['time'], 'Time should be greater than 0'); 135 | unset($query['time']); 136 | 137 | $this->assertArrayHasKey('host', $query, 'Query should have host set.'); 138 | $this->assertNotEmpty($query['host'], 'Host should not be empty'); 139 | unset($query['host']); 140 | 141 | $this->assertArrayHasKey('path', $query, 'Query should have host path set.'); 142 | $this->assertNotEmpty($query['path'], 'Path should not be empty.'); 143 | unset($query['path']); 144 | } 145 | 146 | public function testMatchAllQuery() 147 | { 148 | $index = $this->getIndex(DummyDocument::class); 149 | 150 | $search = $index 151 | ->createSearch() 152 | ->addAggregation(new GlobalAggregation('g')); 153 | $index->findDocuments($search); 154 | 155 | $queries = $this->getCollector()->getQueries(); 156 | $lastQuery = end($queries[ElasticsearchProfiler::UNDEFINED_ROUTE]); 157 | $this->checkQueryParameters($lastQuery); 158 | $lastQuery['body'] = trim(preg_replace('/\s+/', '', $lastQuery['body'])); 159 | 160 | $this->assertEquals('{"aggregations":{"g":{"global":{}}}}', $lastQuery['body']); 161 | } 162 | 163 | private function getCollector(): ElasticsearchProfiler 164 | { 165 | $collector = self::getContainer()->get(ElasticsearchProfiler::class); 166 | $collector->collect(new Request(), new Response()); 167 | 168 | return $collector; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /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\App\Document\DummyDocument; 15 | use ONGR\ElasticsearchBundle\Result\Aggregation\AggregationValue; 16 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 17 | use ONGR\ElasticsearchDSL\Aggregation\Bucketing\RangeAggregation; 18 | 19 | class AggregationIteratorFindTest extends AbstractElasticsearchTestCase 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | protected function getDataArray(): array 25 | { 26 | return [ 27 | DummyDocument::class => [ 28 | [ 29 | '_id' => 1, 30 | 'title' => 'Foo', 31 | 'number' => 10.45, 32 | ], 33 | [ 34 | '_id' => 2, 35 | 'title' => 'Bar', 36 | 'number' => 32, 37 | ], 38 | [ 39 | '_id' => 3, 40 | 'title' => 'Acme', 41 | 'number' => 15.1, 42 | ], 43 | ], 44 | ]; 45 | } 46 | 47 | /** 48 | * Aggregation iterator main test. 49 | */ 50 | public function testIteration() 51 | { 52 | $expected = [ 53 | [ 54 | 'key' => '*-20.0', 55 | 'doc_count' => 2, 56 | ], 57 | [ 58 | 'key' => '20.0-*', 59 | 'doc_count' => 1, 60 | ], 61 | ]; 62 | 63 | $index = $this->getIndex(DummyDocument::class); 64 | 65 | $rangeAggregation = new RangeAggregation('range', 'number'); 66 | $rangeAggregation->addRange(null, 20); 67 | $rangeAggregation->addRange(20, null); 68 | 69 | $search = $index 70 | ->createSearch() 71 | ->addAggregation($rangeAggregation); 72 | 73 | $results = $index->findDocuments($search); 74 | $rangeResult = $results->getAggregation('range'); 75 | 76 | $this->assertInstanceOf(AggregationValue::class, $rangeResult); 77 | 78 | foreach ($rangeResult->getBuckets() as $aggKey => $subAgg) { 79 | $this->assertInstanceOf(AggregationValue::class, $subAgg); 80 | $this->assertEquals($expected[$aggKey]['key'], $subAgg->getValue('key')); 81 | $this->assertEquals($expected[$aggKey]['doc_count'], $subAgg->getValue('doc_count')); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /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\App\Document\DummyDocument; 15 | use ONGR\App\Document\IndexWithFieldsDataDocument; 16 | use ONGR\ElasticsearchBundle\Result\ArrayIterator; 17 | use ONGR\ElasticsearchBundle\Service\IndexService; 18 | use ONGR\ElasticsearchDSL\Query\MatchAllQuery; 19 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 20 | use ONGR\ElasticsearchDSL\Sort\FieldSort; 21 | 22 | class ArrayIteratorTest extends AbstractElasticsearchTestCase 23 | { 24 | protected function getDataArray(): array 25 | { 26 | return [ 27 | DummyDocument::class => [ 28 | [ 29 | '_id' => 1, 30 | 'title' => 'foo', 31 | 'nested_collection' => [ 32 | [ 33 | 'key' => 'foo', 34 | 'value' => 'bar' 35 | ], 36 | [ 37 | 'key' => 'acme', 38 | 'value' => 'delta', 39 | ], 40 | ], 41 | ], 42 | [ 43 | '_id' => 2, 44 | 'title' => 'foo', 45 | ], 46 | ], 47 | IndexWithFieldsDataDocument::class => [ 48 | [ 49 | '_id' => 1, 50 | 'title' => 'foo', 51 | ], 52 | [ 53 | '_id' => 2, 54 | 'title' => 'bar', 55 | ], 56 | ], 57 | ]; 58 | } 59 | 60 | public function indexDataProvider() 61 | { 62 | return [ 63 | [DummyDocument::class], 64 | //This index is with fields data true setting, the response comes back not in the _source bu _fields instead. 65 | [IndexWithFieldsDataDocument::class], 66 | ]; 67 | } 68 | 69 | /** 70 | * @dataProvider indexDataProvider 71 | */ 72 | public function testIteration($indexClass) 73 | { 74 | /** @var IndexService $index */ 75 | $index = $this->getIndex($indexClass); 76 | $match = new MatchAllQuery(); 77 | 78 | $search = $index->createSearch()->addQuery($match); 79 | $search->addSort(new FieldSort('_id', FieldSort::ASC)); 80 | 81 | $iterator = $index->findArray($search); 82 | 83 | $this->assertInstanceOf(ArrayIterator::class, $iterator); 84 | 85 | $expected = $this->getDataArray()[$indexClass]; 86 | foreach ($iterator as $key => $document) { 87 | $this->assertEquals($expected[$key], $document); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /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\App\Document\CollectionNested; 15 | use ONGR\App\Document\CollectionObject; 16 | use ONGR\App\Document\DummyDocument; 17 | use ONGR\ElasticsearchBundle\Result\DocumentIterator; 18 | use ONGR\ElasticsearchBundle\Result\ObjectIterator; 19 | use ONGR\ElasticsearchBundle\Service\IndexService; 20 | use ONGR\ElasticsearchDSL\Query\MatchAllQuery; 21 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 22 | 23 | class DocumentIteratorTest extends AbstractElasticsearchTestCase 24 | { 25 | protected function getDataArray(): array 26 | { 27 | return [ 28 | DummyDocument::class => [ 29 | [ 30 | '_id' => 1, 31 | 'title' => 'foo', 32 | 'nested_collection' => [ 33 | [ 34 | 'key' => 'foo', 35 | 'value' => 'bar' 36 | ], 37 | [ 38 | 'key' => 'acme', 39 | 'value' => 'delta', 40 | ], 41 | ], 42 | 'object_collection' => [ 43 | [ 44 | 'title' => 'acme', 45 | ], 46 | [ 47 | 'title' => 'foo', 48 | ], 49 | [ 50 | 'title' => 'bar', 51 | ], 52 | ], 53 | ], 54 | [ 55 | '_id' => 2, 56 | 'title' => 'foo', 57 | 'nested_collection' => [ 58 | [ 59 | 'key' => 'foo', 60 | 'value' => 'bar' 61 | ], 62 | [ 63 | 'key' => 'acme', 64 | 'value' => 'delta', 65 | ], 66 | ], 67 | 'object_collection' => [ 68 | [ 69 | 'title' => 'acme', 70 | ], 71 | [ 72 | 'title' => 'foo', 73 | ], 74 | [ 75 | 'title' => 'bar', 76 | ], 77 | ], 78 | ], 79 | ], 80 | ]; 81 | } 82 | 83 | public function testIteration() 84 | { 85 | /** @var IndexService $index */ 86 | $index = $this->getIndex(DummyDocument::class); 87 | $match = new MatchAllQuery(); 88 | $search = $index->createSearch()->addQuery($match); 89 | $iterator = $index->findDocuments($search); 90 | 91 | $this->assertInstanceOf(DocumentIterator::class, $iterator); 92 | 93 | /** @var DummyDocument $document */ 94 | foreach ($iterator as $document) { 95 | $this->assertInstanceOf(DummyDocument::class, $document); 96 | 97 | $collection = $document->getNestedCollection(); 98 | $this->assertInstanceOf(ObjectIterator::class, $collection); 99 | 100 | foreach ($collection as $obj) { 101 | $this->assertInstanceOf(CollectionNested::class, $obj); 102 | } 103 | 104 | $collection = $document->getObjectCollection(); 105 | $this->assertInstanceOf(ObjectIterator::class, $collection); 106 | 107 | foreach ($collection as $obj) { 108 | $this->assertInstanceOf(CollectionObject::class, $obj); 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /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\App\Document\DummyDocument; 14 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 15 | 16 | class DocumentNullObjectFieldTest extends AbstractElasticsearchTestCase 17 | { 18 | protected function getDataArray(): array 19 | { 20 | return [ 21 | DummyDocument::class => [ 22 | [ 23 | '_id' => 'foo', 24 | 'title' => null, 25 | ], 26 | ], 27 | ]; 28 | } 29 | 30 | /** 31 | * Test if fetched object field is actually NULL. 32 | */ 33 | public function testResultWithNullObjectField() 34 | { 35 | /** @var DummyDocument $document */ 36 | $document = $this->getIndex(DummyDocument::class)->find('foo'); 37 | 38 | $this->assertInstanceOf( 39 | DummyDocument::class, 40 | $document 41 | ); 42 | 43 | $this->assertNull($document->title); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /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\App\Document\DummyDocument; 15 | use ONGR\ElasticsearchDSL\Query\MatchAllQuery; 16 | use ONGR\ElasticsearchDSL\Sort\FieldSort; 17 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 18 | 19 | class GetDocumentSortTest extends AbstractElasticsearchTestCase 20 | { 21 | protected function getDataArray(): array 22 | { 23 | return [ 24 | DummyDocument::class => [ 25 | [ 26 | '_id' => 'doc1', 27 | 'title' => 'Foo Product', 28 | 'number' => 5.00, 29 | ], 30 | [ 31 | '_id' => 'doc2', 32 | 'title' => 'Bar Product', 33 | 'number' => 8.33, 34 | ], 35 | [ 36 | '_id' => 'doc3', 37 | 'title' => 'Lao Product', 38 | 'number' => 1.95, 39 | ], 40 | ], 41 | ]; 42 | } 43 | 44 | public function testGetDocumentSort() 45 | { 46 | $index = $this->getIndex(DummyDocument::class); 47 | 48 | $match = new MatchAllQuery(); 49 | $sort = new FieldSort('number', 'asc'); 50 | $search = $index->createSearch()->addQuery($match); 51 | $search->addSort($sort); 52 | 53 | $results = $index->findDocuments($search); 54 | 55 | $sort_result = []; 56 | $expected = [1.95, 5, 8.33]; 57 | 58 | foreach ($results as $result) { 59 | $sort_result[] = $results->getDocumentSort(); 60 | } 61 | 62 | $this->assertEquals($sort_result, $expected); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/Functional/Result/ObjectIteratorTest.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\App\Document\CollectionNested; 15 | use ONGR\App\Document\DummyDocument; 16 | use ONGR\ElasticsearchBundle\Result\ObjectIterator; 17 | use ONGR\ElasticsearchBundle\Service\IndexService; 18 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 19 | 20 | class ObjectIteratorTest extends AbstractElasticsearchTestCase 21 | { 22 | protected function getDataArray(): array 23 | { 24 | return [ 25 | DummyDocument::class => [ 26 | [ 27 | '_id' => 1, 28 | 'title' => 'foo', 29 | 'nested_collection' => [ 30 | [ 31 | 'key' => 'foo', 32 | 'value' => 'bar' 33 | ], 34 | [ 35 | 'key' => 'acme', 36 | 'value' => 'delta', 37 | ], 38 | ], 39 | ], 40 | ], 41 | ]; 42 | } 43 | 44 | /** 45 | * Iteration test. 46 | */ 47 | public function testIteration() 48 | { 49 | /** @var IndexService $index */ 50 | $index = $this->getIndex(DummyDocument::class); 51 | 52 | /** @var DummyDocument $document */ 53 | $document = $index->find(1); 54 | 55 | $this->assertInstanceOf(ObjectIterator::class, $document->getNestedCollection()); 56 | 57 | foreach ($document->getNestedCollection() as $obj) { 58 | $this->assertInstanceOf(CollectionNested::class, $obj); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/Functional/Result/PersistObjectsTest.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\App\Document\IndexWithFieldsDataDocument; 16 | use ONGR\App\Document\CollectionNested; 17 | use ONGR\App\Document\DummyDocument; 18 | 19 | class PersistObjectsTest extends AbstractElasticsearchTestCase 20 | { 21 | /** 22 | * Test if we can add more objects into document's "multiple objects" field. 23 | */ 24 | public function testPersistObject() 25 | { 26 | $index = $this->getIndex(DummyDocument::class); 27 | 28 | $document = new DummyDocument(); 29 | $document->id = 5; 30 | $document->title = 'bar bar'; 31 | 32 | $nested = new CollectionNested(); 33 | $nested->key = 'acme'; 34 | $nested->value = 'bar'; 35 | $document->getNestedCollection()->add($nested); 36 | 37 | $nested = new CollectionNested(); 38 | $nested->key = 'foo'; 39 | $nested->value = 'delta'; 40 | $document->getNestedCollection()->add($nested); 41 | 42 | $document->setDatetimefield(new \DateTime('2010-01-01 10:10:56')); 43 | $index->persist($document); 44 | $index->commit(); 45 | 46 | $document = $index->find(5); 47 | $this->assertEquals('bar bar', $document->title); 48 | $this->assertInstanceOf(\DateTimeInterface::class, $document->getDatetimefield()); 49 | $this->assertEquals('2010-01-01', $document->getDatetimefield()->format('Y-m-d')); 50 | } 51 | 52 | public function testAddingValuesToPrivateIdsWithoutSetters() 53 | { 54 | $index = $this->getIndex(IndexWithFieldsDataDocument::class); 55 | 56 | $document = new IndexWithFieldsDataDocument(); 57 | $document->title = 'acme'; 58 | $index->persist($document); 59 | $index->commit(); 60 | 61 | $index->refresh(); 62 | 63 | $document = $index->findOneBy(['private' => 'acme']); 64 | 65 | $this->assertNotNull($document->getId()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Tests/Functional/Service/IndexServiceTest.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\Service; 13 | 14 | use ONGR\App\Document\DummyDocument; 15 | use ONGR\App\Entity\DummyDocumentInTheEntityDirectory; 16 | use ONGR\ElasticsearchBundle\Result\DocumentIterator; 17 | use ONGR\ElasticsearchBundle\Test\AbstractElasticsearchTestCase; 18 | 19 | /** 20 | * Functional tests for orm manager. 21 | */ 22 | class ManagerTest extends AbstractElasticsearchTestCase 23 | { 24 | protected function getDataArray(): array 25 | { 26 | return [ 27 | DummyDocument::class => [ 28 | [ 29 | '_id' => 1, 30 | 'title' => 'foo' 31 | ], 32 | [ 33 | '_id' => 2, 34 | 'title' => 'foo', 35 | ], 36 | [ 37 | '_id' => 3, 38 | 'title' => 'bar', 39 | ], 40 | ] 41 | ]; 42 | } 43 | 44 | public function indexNameDataProvider() 45 | { 46 | return [ 47 | [ DummyDocument::class, DummyDocument::INDEX_NAME ], 48 | // this alias is overriden in configuration 49 | [ DummyDocumentInTheEntityDirectory::class, 'field-data-index' ], 50 | ]; 51 | } 52 | 53 | /** 54 | * @dataProvider indexNameDataProvider 55 | */ 56 | public function testIndexCrate($class, $indexName) 57 | { 58 | $index = $this->getIndex($class); 59 | 60 | $client = $index->getClient(); 61 | $actualIndexExists = $client->indices()->exists(['index' => $indexName]); 62 | 63 | $this->assertTrue($actualIndexExists); 64 | } 65 | 66 | public function testDocumentInsertAndFindById() 67 | { 68 | $index = $this->getIndex(DummyDocument::class); 69 | 70 | /** @var DummyDocument $document */ 71 | $document = $index->find(3); 72 | 73 | $this->assertEquals('bar', $document->title); 74 | } 75 | 76 | public function testFindOneBy() 77 | { 78 | $index = $this->getIndex(DummyDocument::class); 79 | 80 | /** @var DummyDocument $result */ 81 | $result = $index->findOneBy(['title.raw' => 'bar']); 82 | $this->assertInstanceOf(DummyDocument::class, $result); 83 | $this->assertEquals(3, $result->id); 84 | } 85 | 86 | public function testFind() 87 | { 88 | $index = $this->getIndex(DummyDocument::class); 89 | 90 | /** @var DummyDocument $result */ 91 | $result = $index->find(3); 92 | $this->assertInstanceOf(DummyDocument::class, $result); 93 | $this->assertEquals(3, $result->id); 94 | $this->assertEquals('bar', $result->title); 95 | } 96 | 97 | public function testFindBy() 98 | { 99 | $index = $this->getIndex(DummyDocument::class); 100 | 101 | /** @var DocumentIterator $result */ 102 | $result = $index->findBy(['title.raw' => 'foo']); 103 | $this->assertInstanceOf(DocumentIterator::class, $result); 104 | $this->assertEquals(2, $result->count()); 105 | 106 | $actualList = []; 107 | /** @var DummyDocument $item */ 108 | foreach ($result as $item) { 109 | $actualList[] = (int) $item->id; 110 | } 111 | sort($actualList); 112 | 113 | $this->assertEquals([1,2], $actualList); 114 | } 115 | 116 | public function testIndexConfigOverride() 117 | { 118 | $index = $this->getIndex(DummyDocumentInTheEntityDirectory::class); 119 | $hosts = $index->getIndexSettings()->getHosts(); 120 | 121 | $this->assertEquals(['localhost:9200'], $hosts); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /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 | 16 | class CollectionTest extends \PHPUnit\Framework\TestCase 17 | { 18 | /** 19 | * @var array 20 | */ 21 | private $data = [ 22 | 'foo' => 'Bob 1', 23 | 'bar' => 'Bob 2', 24 | ]; 25 | 26 | /** 27 | * Tests \Countable implementation. 28 | */ 29 | public function testCountable() 30 | { 31 | $this->assertCount(count($this->data), new ArrayCollection($this->data)); 32 | } 33 | 34 | /** 35 | * Tests \Iterator implementation. 36 | */ 37 | public function testIterator() 38 | { 39 | $this->assertEquals($this->data, iterator_to_array(new ArrayCollection($this->data))); 40 | } 41 | 42 | /** 43 | * Tests \ArrayAccess implementation. 44 | */ 45 | public function testArrayAccess() 46 | { 47 | $collection = new ArrayCollection($this->data); 48 | 49 | $this->assertArrayHasKey('foo', $collection); 50 | $this->assertEquals($this->data['foo'], $collection['foo']); 51 | 52 | $newData = $this->data; 53 | $newData['baz'] = 'Bob 3'; 54 | $newData[] = 'Bob 4'; 55 | $collection['baz'] = 'Bob 3'; 56 | $collection[] = 'Bob 4'; 57 | 58 | $this->assertEquals($newData, iterator_to_array($collection)); 59 | 60 | unset($collection['baz']); 61 | $this->assertArrayNotHasKey('baz', $collection); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /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 Symfony\Component\DependencyInjection\Compiler\PassConfig; 16 | use Symfony\Component\DependencyInjection\ContainerBuilder; 17 | use Symfony\Component\Finder\Finder; 18 | use Symfony\Component\Finder\SplFileInfo; 19 | 20 | /** 21 | * Unit test for ONGR\ElasticsearchBundle. 22 | */ 23 | class ElasticsearchBundleTest extends \PHPUnit\Framework\TestCase 24 | { 25 | /** 26 | * @var array List of passes, which should not be added to compiler. 27 | */ 28 | protected $passesBlacklist = []; 29 | 30 | /** 31 | * Check whether all Passes in DependencyInjection/Compiler/ are added to container. 32 | */ 33 | public function testPassesRegistered() 34 | { 35 | $container = new ContainerBuilder(); 36 | $bundle = new ONGRElasticsearchBundle(); 37 | $bundle->build($container); 38 | 39 | /** @var array $loadedPasses Array of class names of loaded passes */ 40 | $loadedPasses = []; 41 | /** @var PassConfig $passConfig */ 42 | $passConfig = $container->getCompiler()->getPassConfig(); 43 | foreach ($passConfig->getPasses() as $pass) { 44 | $classPath = explode('\\', get_class($pass)); 45 | $loadedPasses[] = end($classPath); 46 | } 47 | 48 | $finder = new Finder(); 49 | $finder->files()->in(__DIR__ . '/../../DependencyInjection/Compiler/'); 50 | 51 | /** @var $file SplFileInfo */ 52 | foreach ($finder as $file) { 53 | $passName = str_replace('.php', '', $file->getFilename()); 54 | // Check whether pass is not blacklisted and not added by bundle. 55 | if (!in_array($passName, $this->passesBlacklist)) { 56 | $this->assertContains( 57 | $passName, 58 | $loadedPasses, 59 | sprintf( 60 | "Compiler pass '%s' is not added to container or not blacklisted in test.", 61 | $passName 62 | ) 63 | ); 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /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 | $operation = 'create'; 22 | $header = [ 23 | '_index' => 'index', 24 | '_id' => 15, 25 | ]; 26 | 27 | $expectedHeader = [ 28 | '_index' => 'index', 29 | '_id' => 10, 30 | ]; 31 | 32 | $data = []; 33 | 34 | $event = new BulkEvent($operation, $header, $data); 35 | $this->assertEquals('create', $event->getOperation()); 36 | $event->setOperation('update'); 37 | $this->assertEquals('update', $event->getOperation()); 38 | 39 | $event->setHeader($expectedHeader); 40 | $this->assertEquals($expectedHeader, $event->getHeader()); 41 | $this->assertEquals([], $event->getData()); 42 | $event->setData($expectedHeader); 43 | $this->assertEquals($expectedHeader, $event->getData()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /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 | $query = [ 22 | [ 23 | '_index' => 'index', 24 | '_id' => 10, 25 | ], 26 | [ 27 | 'title' => 'bar' 28 | ] 29 | ]; 30 | 31 | $response = ['status' => 'ok']; 32 | 33 | $event = new CommitEvent('flush', $query, $response); 34 | 35 | $this->assertEquals('flush', $event->getCommitMode()); 36 | $this->assertEquals($query, $event->getBulkQuery()); 37 | $this->assertEquals('flush', $event->getCommitMode()); 38 | $this->assertEquals($response, $event->getBulkResponse()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /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\App\Document\DummyDocument; 15 | use ONGR\ElasticsearchBundle\Event\PrePersistEvent; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | class PrePersistEventTest extends TestCase 19 | { 20 | public function testGetters() 21 | { 22 | $document = new DummyDocument(); 23 | $event = new PrePersistEvent($document); 24 | 25 | $this->assertInstanceOf(DummyDocument::class, $event->getDocument()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /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\App\Document\DummyDocument; 15 | use ONGR\App\Entity\DummyDocumentInTheEntityDirectory; 16 | use ONGR\ElasticsearchBundle\EventListener\TerminateListener; 17 | use ONGR\ElasticsearchBundle\Service\IndexService; 18 | use PHPUnit\Framework\TestCase; 19 | use Symfony\Component\DependencyInjection\Container; 20 | 21 | /** 22 | * Tests TerminateListener class 23 | */ 24 | class TerminateListenerTest extends TestCase 25 | { 26 | /** 27 | * Tests kernel terminate event 28 | */ 29 | public function testKernelTerminate() 30 | { 31 | $indexService = $this->getMockBuilder(IndexService::class) 32 | ->disableOriginalConstructor() 33 | ->getMock(); 34 | 35 | $indexService->expects($this->exactly(2)) 36 | ->method('commit'); 37 | 38 | $container = $this->getMockBuilder(Container::class) 39 | ->disableOriginalConstructor() 40 | ->getMock(); 41 | 42 | $container->expects($this->any()) 43 | ->method('get') 44 | ->with($this->logicalOr( 45 | DummyDocument::class, 46 | DummyDocumentInTheEntityDirectory::class 47 | )) 48 | ->willReturn($indexService); 49 | 50 | $listener = new TerminateListener( 51 | $container, 52 | [ 53 | DummyDocument::class, 54 | DummyDocumentInTheEntityDirectory::class, 55 | ] 56 | ); 57 | 58 | $listener->onKernelTerminate(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /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 Doctrine\Common\Annotations\AnnotationReader; 15 | use Doctrine\Common\Cache\Cache; 16 | use ONGR\App\Document\DummyDocument; 17 | use ONGR\App\Document\TestDocument; 18 | use ONGR\App\Entity\DummyDocumentInTheEntityDirectory; 19 | use ONGR\ElasticsearchBundle\Mapping\DocumentParser; 20 | use PHPUnit\Framework\TestCase; 21 | use Symfony\Component\Yaml\Yaml; 22 | 23 | class DocumentParserTest extends TestCase 24 | { 25 | public function testDocumentParsing() 26 | { 27 | 28 | $parser = new DocumentParser(new AnnotationReader(), $this->createMock(Cache::class)); 29 | 30 | $indexMetadata = $parser->getIndexMetadata(new \ReflectionClass(DummyDocumentInTheEntityDirectory::class)); 31 | 32 | $expected = [ 33 | 'mappings' => [ 34 | 'properties' => [ 35 | 'keyword_field' => [ 36 | 'type' => 'keyword', 37 | ] 38 | ] 39 | ] 40 | ]; 41 | 42 | $this->assertEquals($expected, $indexMetadata); 43 | } 44 | 45 | public function testParsingWithMultiFieldsMapping() 46 | { 47 | $parser = new DocumentParser(new AnnotationReader(), $this->createMock(Cache::class)); 48 | 49 | $indexMetadata = $parser->getIndexMetadata(new \ReflectionClass(TestDocument::class)); 50 | 51 | // Mapping definition for field "title" should be there 52 | $this->assertNotEmpty($indexMetadata['mappings']['properties']['title']); 53 | $title_field_def = $indexMetadata['mappings']['properties']['title']; 54 | 55 | // title should have `fields` sub-array 56 | $this->assertArrayHasKey('fields', $title_field_def); 57 | 58 | // `fields` should look like so: 59 | $expected = [ 60 | 'raw' => ['type' => 'keyword'], 61 | 'increment' => ['type' => 'text', 'analyzer' => 'incrementalAnalyzer'], 62 | 'sorting' => ['type' => 'keyword', 'normalizer' => 'lowercase_normalizer'] 63 | ]; 64 | 65 | $this->assertEquals($expected, $title_field_def['fields']); 66 | } 67 | 68 | public function testGetAnalysisConfig() 69 | { 70 | // Global analysis settings used for this test, usually set in the bundle configuration 71 | // sets custom analyzer, filter, and normalizer 72 | $config_analysis = [ 73 | 'analyzer' => [ 74 | 'incrementalAnalyzer' => [ 75 | 'type' => 'custom', 76 | 'tokenizer' => 'standard', 77 | 'filter' => [ 78 | 0 => 'lowercase', 79 | 1 => 'edge_ngram_filter', 80 | ], 81 | ], 82 | 'unusedAnalyzer' => [ 83 | 'type' => 'custom', 84 | 'tokenizer' => 'standard' 85 | ] 86 | ], 87 | 'filter' => [ 88 | 'edge_ngram_filter' => [ 89 | 'type' => 'edge_ngram', 90 | 'min_gram' => 1, 91 | 'max_gram' => 20, 92 | ], 93 | ], 94 | 'normalizer' => [ 95 | 'lowercase_normalizer' => [ 96 | 'type' => 'custom', 97 | 'filter' => ['lowercase'] 98 | ], 99 | 'unused_normalizer' => [ 100 | 'type' => 'custom' 101 | ] 102 | ] 103 | ]; 104 | 105 | $parser = new DocumentParser(new AnnotationReader(), $this->createMock(Cache::class), $config_analysis); 106 | $analysis = $parser->getAnalysisConfig(new \ReflectionClass(TestDocument::class)); 107 | 108 | $expected = [ 109 | 'analyzer' => [ 110 | 'incrementalAnalyzer' => [ 111 | 'type' => 'custom', 112 | 'tokenizer' => 'standard', 113 | 'filter' => [ 114 | 0 => 'lowercase', 115 | 1 => 'edge_ngram_filter', 116 | ], 117 | ], 118 | ], 119 | // 'unusedAnalyzer' must not be there because it is not used 120 | 'filter' => [ 121 | 'edge_ngram_filter' => [ 122 | 'type' => 'edge_ngram', 123 | 'min_gram' => 1, 124 | 'max_gram' => 20, 125 | ], 126 | ], 127 | 'normalizer' => [ 128 | 'lowercase_normalizer' => [ 129 | 'type' => 'custom', 130 | 'filter' => ['lowercase'] 131 | ] 132 | // 'unused_normalizer' must not be there 133 | ] 134 | ]; 135 | 136 | $this->assertEquals($expected, $analysis); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /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\App\Document\DummyDocument; 15 | use ONGR\ElasticsearchBundle\Profiler\ElasticsearchProfiler; 16 | 17 | class ElasticsearchProfilerTest extends \PHPUnit\Framework\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 | $indexes = [ 34 | DummyDocument::INDEX_NAME => DummyDocument::class 35 | ]; 36 | $collector = new ElasticsearchProfiler(); 37 | $collector->setIndexes($indexes); 38 | 39 | $result = $collector->getIndexes(); 40 | $this->assertEquals( 41 | $indexes, 42 | $result 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /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 ONGR\ElasticsearchBundle\Result\RawIterator; 15 | use ONGR\ElasticsearchBundle\Service\IndexService; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | class AbstractResultsIteratorTest extends TestCase 19 | { 20 | /** 21 | * Test if scroll is cleared on destructor. 22 | */ 23 | public function testClearScroll() 24 | { 25 | $rawData = [ 26 | '_scroll_id' => 'foo', 27 | ]; 28 | 29 | $index = $this->getMockBuilder(IndexService::class) 30 | ->setMethods(['getConfig', 'clearScroll']) 31 | ->disableOriginalConstructor() 32 | ->getMock(); 33 | $index->expects($this->any())->method('getConfig')->willReturn([]); 34 | $index->expects($this->once())->method('clearScroll')->with('foo'); 35 | 36 | $scroll = ['_scroll_id' => 'foo', 'duration' => '5m']; 37 | $iterator = new RawIterator($rawData, $index, null, $scroll); 38 | 39 | // Trigger destructor call 40 | unset($iterator); 41 | } 42 | 43 | /** 44 | * Test for getDocumentScore(). 45 | */ 46 | public function testGetDocumentScore() 47 | { 48 | $rawData = [ 49 | 'hits' => [ 50 | 'total' => [ 51 | 'value' => 3 52 | ], 53 | 'hits' => [ 54 | [ 55 | '_index' => 'test', 56 | '_id' => 'foo', 57 | '_score' => 1, 58 | '_source' => [ 59 | 'title' => 'Product Foo', 60 | ], 61 | ], 62 | [ 63 | '_index' => 'test', 64 | '_id' => 'bar', 65 | '_score' => 2, 66 | '_source' => [ 67 | 'title' => 'Product Bar', 68 | ], 69 | ], 70 | [ 71 | '_index' => 'test', 72 | '_id' => 'baz', 73 | '_score' => null, 74 | '_source' => [ 75 | 'title' => 'Product Baz', 76 | ], 77 | ], 78 | ], 79 | ], 80 | ]; 81 | 82 | $index = $this->getMockBuilder(IndexService::class) 83 | ->disableOriginalConstructor() 84 | ->getMock(); 85 | 86 | $results = new RawIterator($rawData, $index); 87 | 88 | $expectedScores = [1, 2, null]; 89 | $actualScores = []; 90 | 91 | $this->assertEquals($rawData['hits']['total']['value'], $results->count()); 92 | $this->assertEquals($rawData, $results->getRaw()); 93 | 94 | foreach ($results as $item) { 95 | $actualScores[] = $item['_score']; 96 | } 97 | 98 | $this->assertEquals($expectedScores, $actualScores); 99 | } 100 | 101 | /** 102 | * Test for getDocumentScore() in case called when current iterator value is not valid. 103 | * 104 | * @expectedException \LogicException 105 | * @expectedExceptionMessage Document score is available only while iterating over results 106 | */ 107 | public function testGetScoreException() 108 | { 109 | $index = $this->getMockBuilder(IndexService::class) 110 | ->disableOriginalConstructor() 111 | ->getMock(); 112 | 113 | $results = new RawIterator([], $index); 114 | $results->getDocumentScore(); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /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\App\Document\DummyDocument; 15 | use ONGR\ElasticsearchBundle\Mapping\Converter; 16 | use ONGR\ElasticsearchBundle\Result\DocumentIterator; 17 | use ONGR\ElasticsearchBundle\Service\IndexService; 18 | use PHPUnit\Framework\TestCase; 19 | use Symfony\Component\Serializer\Serializer; 20 | 21 | class DocumentIteratorTest extends TestCase 22 | { 23 | /** 24 | * Test for getAggregation() in case requested aggregation is not set. 25 | */ 26 | public function testGetAggregationOnNull() 27 | { 28 | $index = $this->getMockBuilder(IndexService::class) 29 | ->disableOriginalConstructor() 30 | ->getMock(); 31 | 32 | $iterator = new DocumentIterator([], $index); 33 | 34 | $this->assertNull($iterator->getAggregation('foo')); 35 | } 36 | 37 | public function testResultConvert() 38 | { 39 | $rawData = [ 40 | 'hits' => [ 41 | 'total' => 1, 42 | 'hits' => [ 43 | [ 44 | '_index' => 'test', 45 | '_id' => 'foo', 46 | '_score' => 1, 47 | '_source' => [ 48 | 'title' => 'Foo', 49 | ], 50 | ], 51 | ], 52 | ], 53 | ]; 54 | 55 | $converter = $this->getMockBuilder(Converter::class) 56 | ->setMethods(['convertArrayToDocument']) 57 | ->disableOriginalConstructor() 58 | ->getMock(); 59 | 60 | $document = new DummyDocument(); 61 | $document->title = 'Foo'; 62 | $converter->expects($this->any())->method('convertArrayToDocument')->willReturn($document); 63 | 64 | $index = $this->getMockBuilder(IndexService::class) 65 | ->setMethods(['getNamespace']) 66 | ->disableOriginalConstructor() 67 | ->getMock(); 68 | 69 | $index->expects($this->any())->method('getNamespace')->willReturn(DummyDocument::class); 70 | 71 | $iterator = new DocumentIterator($rawData, $index, $converter); 72 | $this->assertEquals($document, $iterator->first()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /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 ONGR\ElasticsearchBundle\Service\IndexService; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | class RawIteratorTest extends TestCase 19 | { 20 | /** 21 | * Test for getAggregations(). 22 | */ 23 | public function testGetAggregations() 24 | { 25 | $rawData = [ 26 | 'aggregations' => [ 27 | 'avg_grade' => [ 28 | 'value' => 75, 29 | ], 30 | ], 31 | ]; 32 | 33 | $index = $this->getMockBuilder(IndexService::class) 34 | ->disableOriginalConstructor() 35 | ->getMock(); 36 | 37 | $iterator = new RawIterator($rawData, $index); 38 | 39 | $this->assertEquals($rawData['aggregations'], $iterator->getAggregations()); 40 | } 41 | 42 | /** 43 | * Tests iterator. 44 | */ 45 | public function testIterator() 46 | { 47 | $rawData = [ 48 | 'hits' => [ 49 | 'total' => 1, 50 | 'hits' => [ 51 | [ 52 | '_index' => 'test', 53 | '_id' => 'foo', 54 | '_score' => 1, 55 | '_source' => [ 56 | 'title' => 'Product Foo', 57 | ], 58 | ], 59 | ], 60 | ], 61 | ]; 62 | 63 | $index = $this->getMockBuilder(IndexService::class) 64 | ->disableOriginalConstructor() 65 | ->getMock(); 66 | 67 | $iterator = new RawIterator($rawData, $index); 68 | 69 | $this->assertEquals($rawData['hits']['hits'][0], $iterator->current()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /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 | 17 | class JsonWriterTest extends \PHPUnit\Framework\TestCase 18 | { 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public static function setUpBeforeClass(): void 23 | { 24 | parent::setUpBeforeClass(); 25 | 26 | vfsStream::setup('tmp'); 27 | } 28 | 29 | /** 30 | * Data provider for testPush(). 31 | * 32 | * @return array 33 | */ 34 | public function getTestPushData() 35 | { 36 | $cases = []; 37 | 38 | // Case #0 Standard case. 39 | $documents = [ 40 | [ 41 | '_id' => 'doc1', 42 | 'title' => 'Document 1', 43 | ], 44 | [ 45 | '_id' => 'doc2', 46 | 'title' => 'Document 2', 47 | ], 48 | ]; 49 | $expectedOutput = << 'doc1', 66 | 'title' => 'Document 1', 67 | ], 68 | [ 69 | '_id' => 'doc2', 70 | 'title' => 'Document 2', 71 | ], 72 | [ 73 | '_id' => 'doc3', 74 | 'title' => 'Document 3', 75 | ], 76 | ]; 77 | $expectedOutput = <<push($document); 122 | } 123 | 124 | $writer->finalize(); 125 | 126 | $this->assertEquals($expectedOutput, file_get_contents($filename)); 127 | } 128 | 129 | /** 130 | * Test for push() in case of too many documents passed. 131 | * 132 | * @expectedException \OverflowException 133 | */ 134 | public function testPushException() 135 | { 136 | $filename = vfsStream::url('tmp/test.json'); 137 | 138 | $writer = new JsonWriter($filename, 0); 139 | $writer->push(null); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /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 | 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 | public function registerBundles() 26 | { 27 | return [ 28 | new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), 29 | new ONGR\ElasticsearchBundle\ONGRElasticsearchBundle(), 30 | ]; 31 | } 32 | 33 | /** 34 | * @inheritdoc 35 | */ 36 | public function registerContainerConfiguration(LoaderInterface $loader) 37 | { 38 | $loader->load(__DIR__ . '/config/config_' . $this->getEnvironment() . '.yml'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/app/config/config_test.yml: -------------------------------------------------------------------------------- 1 | # Framework Configuration 2 | framework: 3 | secret: "SUPER-TOP-SECRET" 4 | test: ~ 5 | 6 | ongr_elasticsearch: 7 | source_directories: 8 | - /Tests/app/src #This parameters is when you have custom project source root, in this case tests has their own kernel and source directory. 9 | analysis: 10 | filter: 11 | edge_ngram_filter: #-> analyzer name 12 | type: edge_ngram #-> all options are not strict, it will be passed as an array 13 | min_gram: 1 14 | max_gram: 20 15 | analyzer: 16 | incrementalAnalyzer: #-> analyzer name 17 | type: custom 18 | tokenizer: standard 19 | filter: 20 | - lowercase 21 | - edge_ngram_filter 22 | indexes: 23 | ONGR\App\Entity\DummyDocumentInTheEntityDirectory: 24 | alias: field-data-index 25 | hosts: 26 | - localhost:9200 27 | -------------------------------------------------------------------------------- /Tests/app/data_seed/command_import_10.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"count":10}, 3 | {"_id":"1","_source":{"title":"Document 1"}}, 4 | {"_id":"2","_source":{"title":"Document 2"}}, 5 | {"_id":"3","_source":{"title":"Document 3"}}, 6 | {"_id":"4","_source":{"title":"Document 4"}}, 7 | {"_id":"5","_source":{"title":"Document 5"}}, 8 | {"_id":"6","_source":{"title":"Document 6"}}, 9 | {"_id":"7","_source":{"title":"Document 7"}}, 10 | {"_id":"8","_source":{"title":"Document 8"}}, 11 | {"_id":"9","_source":{"title":"Document 9"}}, 12 | {"_id":"10","_source":{"title":"Document 10"}} 13 | ] 14 | -------------------------------------------------------------------------------- /Tests/app/data_seed/command_import_10.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ongr-io/ElasticsearchBundle/effce6a1d38415e4dc72aedc9351a885bfabefdd/Tests/app/data_seed/command_import_10.json.gz -------------------------------------------------------------------------------- /Tests/app/data_seed/command_import_11.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"count":11}, 3 | {"_id":"1","_source":{"title":"Document 1"}}, 4 | {"_id":"2","_source":{"title":"Document 2"}}, 5 | {"_id":"3","_source":{"title":"Document 3"}}, 6 | {"_id":"4","_source":{"title":"Document 4"}}, 7 | {"_id":"5","_source":{"title":"Document 5"}}, 8 | {"_id":"6","_source":{"title":"Document 6"}}, 9 | {"_id":"7","_source":{"title":"Document 7"}}, 10 | {"_id":"8","_source":{"title":"Document 8"}}, 11 | {"_id":"9","_source":{"title":"Document 9"}}, 12 | {"_id":"10","_source":{"title":"Document 10"}}, 13 | {"_id":"11","_source":{"title":"Document 11"}} 14 | ] -------------------------------------------------------------------------------- /Tests/app/data_seed/command_import_11.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ongr-io/ElasticsearchBundle/effce6a1d38415e4dc72aedc9351a885bfabefdd/Tests/app/data_seed/command_import_11.json.gz -------------------------------------------------------------------------------- /Tests/app/data_seed/command_import_20.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"count":20}, 3 | {"_id":"1","_source":{"title":"Document 1"}}, 4 | {"_id":"2","_source":{"title":"Document 2"}}, 5 | {"_id":"3","_source":{"title":"Document 3"}}, 6 | {"_id":"4","_source":{"title":"Document 4"}}, 7 | {"_id":"5","_source":{"title":"Document 5"}}, 8 | {"_id":"6","_source":{"title":"Document 6"}}, 9 | {"_id":"7","_source":{"title":"Document 7"}}, 10 | {"_id":"8","_source":{"title":"Document 8"}}, 11 | {"_id":"9","_source":{"title":"Document 9"}}, 12 | {"_id":"10","_source":{"title":"Document 10"}}, 13 | {"_id":"11","_source":{"title":"Document 11"}}, 14 | {"_id":"12","_source":{"title":"Document 12"}}, 15 | {"_id":"13","_source":{"title":"Document 13"}}, 16 | {"_id":"14","_source":{"title":"Document 14"}}, 17 | {"_id":"15","_source":{"title":"Document 15"}}, 18 | {"_id":"16","_source":{"title":"Document 16"}}, 19 | {"_id":"17","_source":{"title":"Document 17"}}, 20 | {"_id":"18","_source":{"title":"Document 18"}}, 21 | {"_id":"19","_source":{"title":"Document 19"}}, 22 | {"_id":"20","_source":{"title":"Document 20"}} 23 | ] -------------------------------------------------------------------------------- /Tests/app/data_seed/command_import_9.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"count":9}, 3 | {"_id":"1","_source":{"title":"Document 1"}}, 4 | {"_id":"2","_source":{"title":"Document 2"}}, 5 | {"_id":"3","_source":{"title":"Document 3"}}, 6 | {"_id":"4","_source":{"title":"Document 4"}}, 7 | {"_id":"5","_source":{"title":"Document 5"}}, 8 | {"_id":"6","_source":{"title":"Document 6"}}, 9 | {"_id":"7","_source":{"title":"Document 7"}}, 10 | {"_id":"8","_source":{"title":"Document 8"}}, 11 | {"_id":"9","_source":{"title":"Document 9"}} 12 | ] 13 | -------------------------------------------------------------------------------- /Tests/app/data_seed/command_import_9.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ongr-io/ElasticsearchBundle/effce6a1d38415e4dc72aedc9351a885bfabefdd/Tests/app/data_seed/command_import_9.json.gz -------------------------------------------------------------------------------- /Tests/app/src/Document/CollectionNested.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\App\Document; 12 | 13 | use ONGR\ElasticsearchBundle\Annotation as ES; 14 | 15 | /** 16 | * @ES\NestedType() 17 | */ 18 | class CollectionNested 19 | { 20 | /** 21 | * @ES\Property(type="keyword") 22 | */ 23 | public $key; 24 | 25 | /** 26 | * @ES\Property(type="keyword") 27 | */ 28 | public $value; 29 | } -------------------------------------------------------------------------------- /Tests/app/src/Document/CollectionObject.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\App\Document; 12 | 13 | use ONGR\ElasticsearchBundle\Annotation as ES; 14 | 15 | /** 16 | * @ES\ObjectType() 17 | */ 18 | class CollectionObject 19 | { 20 | /** 21 | * @var string 22 | * @ES\Property(type="keyword", name="title") 23 | */ 24 | private $title; 25 | 26 | public function getTitle(): string 27 | { 28 | return $this->title; 29 | } 30 | 31 | public function setTitle(string $title): CollectionObject 32 | { 33 | $this->title = $title; 34 | return $this; 35 | } 36 | } -------------------------------------------------------------------------------- /Tests/app/src/Document/DummyDocument.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\App\Document; 13 | 14 | use Doctrine\Common\Collections\ArrayCollection; 15 | use ONGR\ElasticsearchBundle\Annotation as ES; 16 | use Symfony\Component\Serializer\Annotation\SerializedName; 17 | 18 | /** 19 | * Dummy index document for the functional testing. 20 | * 21 | * @ES\Index(alias="dummy", default=true) 22 | */ 23 | class DummyDocument 24 | { 25 | // This con't is only as a helper. 26 | CONST INDEX_NAME = 'dummy'; 27 | 28 | /** 29 | * @ES\Id() 30 | */ 31 | public $id; 32 | 33 | /** 34 | * @ES\Routing() 35 | */ 36 | public $routing; 37 | 38 | /** 39 | * @ES\Property( 40 | * type="text", 41 | * name="title", 42 | * fields={ 43 | * "raw"={"type"="keyword"}, 44 | * "increment"={"type"="text", "analyzer"="incrementalAnalyzer"} 45 | * } 46 | * ) 47 | */ 48 | public $title; 49 | 50 | /** 51 | * @ES\Property(type="keyword", name="private") 52 | */ 53 | private $privateField; 54 | 55 | /** 56 | * @ES\Property(type="float") 57 | */ 58 | public $number; 59 | 60 | /** 61 | * @ES\Embedded(class="ONGR\App\Document\CollectionNested", name="nested_collection") 62 | */ 63 | private $nestedCollection; 64 | 65 | /** 66 | * @ES\Embedded(class="ONGR\App\Document\CollectionObject") 67 | */ 68 | private $objectCollection; 69 | 70 | /** 71 | * @var \DateTimeInterface 72 | * @ES\Property(type="date") 73 | */ 74 | private $datetimefield; 75 | 76 | public function __construct() 77 | { 78 | $this->nestedCollection = new ArrayCollection(); 79 | $this->objectCollection = new ArrayCollection(); 80 | } 81 | 82 | public function getPrivateField(): ?string 83 | { 84 | return $this->privateField; 85 | } 86 | 87 | public function setPrivateField(string $privateField): DummyDocument 88 | { 89 | $this->privateField = $privateField; 90 | return $this; 91 | } 92 | 93 | public function getNestedCollection() 94 | { 95 | return $this->nestedCollection; 96 | } 97 | 98 | public function setNestedCollection($nestedCollection) 99 | { 100 | $this->nestedCollection = $nestedCollection; 101 | return $this; 102 | } 103 | 104 | public function getObjectCollection() 105 | { 106 | return $this->objectCollection; 107 | } 108 | 109 | public function setObjectCollection($objectCollection) 110 | { 111 | $this->objectCollection = $objectCollection; 112 | return $this; 113 | } 114 | 115 | public function getDatetimefield(): ?\DateTimeInterface 116 | { 117 | return $this->datetimefield; 118 | } 119 | 120 | public function setDatetimefield(\DateTimeInterface $datetimefield): void 121 | { 122 | $this->datetimefield = $datetimefield; 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /Tests/app/src/Document/IndexWithFieldsDataDocument.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\App\Document; 13 | 14 | use Doctrine\Common\Collections\ArrayCollection; 15 | use ONGR\ElasticsearchBundle\Annotation as ES; 16 | use Symfony\Component\Serializer\Annotation\SerializedName; 17 | 18 | /** 19 | * @ES\Index(alias="field-data-index") 20 | */ 21 | class IndexWithFieldsDataDocument 22 | { 23 | // This con't is only as a helper. 24 | CONST INDEX_NAME = 'field-data-index'; 25 | 26 | /** 27 | * @ES\Id() 28 | */ 29 | private $id; 30 | 31 | /** 32 | * @ES\Property(type="text", name="private", settings={"fielddata"=true}) 33 | */ 34 | public $title; 35 | 36 | /** 37 | * @return mixed 38 | */ 39 | public function getId() 40 | { 41 | return $this->id; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/app/src/Document/TestDocument.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\App\Document; 13 | 14 | use Doctrine\Common\Collections\ArrayCollection; 15 | use ONGR\ElasticsearchBundle\Annotation as ES; 16 | 17 | /** 18 | * test document for unit testing of DocumentParser class 19 | * 20 | * @ES\Index(alias="testdocument") 21 | */ 22 | class TestDocument 23 | { 24 | // This con't is only as a helper. 25 | CONST INDEX_NAME = 'testdocument'; 26 | 27 | /** 28 | * @ES\Id() 29 | */ 30 | public $id; 31 | 32 | /** 33 | * @ES\Property( 34 | * type="text", 35 | * name="title", 36 | * fields={ 37 | * "raw"={"type"="keyword"}, 38 | * "increment"={"type"="text", "analyzer"="incrementalAnalyzer"}, 39 | * "sorting"={"type"="keyword", "normalizer"="lowercase_normalizer"} 40 | * } 41 | * ) 42 | */ 43 | public $title; 44 | } 45 | -------------------------------------------------------------------------------- /Tests/app/src/Entity/DummyDocumentInTheEntityDirectory.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\App\Entity; 13 | 14 | use ONGR\ElasticsearchBundle\Annotation as ES; 15 | 16 | /** 17 | * Document to test if it works from a not default directory. 18 | * 19 | * @ES\Index(alias="entity-index") 20 | */ 21 | class DummyDocumentInTheEntityDirectory 22 | { 23 | 24 | CONST INDEX_NAME = 'entity-index'; 25 | 26 | /** 27 | * @ES\Id() 28 | */ 29 | public $id; 30 | 31 | /** 32 | * @ES\Property(type="keyword") 33 | */ 34 | public $keywordField; 35 | } 36 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ongr/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 | "require": { 14 | "php": "^7.1 || ^8.0", 15 | "symfony/framework-bundle": "^4.4|^5.0", 16 | "symfony/dependency-injection": "^4.4|^5.0", 17 | "symfony/console": "^4.4|^5.0", 18 | "symfony/stopwatch": "^4.4|^5.0", 19 | "symfony/finder": "^4.4|^5.0", 20 | "symfony/serializer": "^4.4|^5.0|^6.0", 21 | "symfony/cache": "^4.4|^5.0", 22 | "symfony/property-access": "^4.4|^5.0", 23 | "doctrine/annotations": "^1.6", 24 | "doctrine/cache": "^1.7", 25 | "doctrine/inflector": "^1.3", 26 | "doctrine/collections": "^1.5", 27 | "monolog/monolog": "^1.24", 28 | "elasticsearch/elasticsearch": "^7.0|^8.0", 29 | "ongr/elasticsearch-dsl": "^7.0" 30 | }, 31 | "require-dev": { 32 | "mikey179/vfsstream": "~1.6", 33 | "phpunit/phpunit": "^7.0", 34 | "squizlabs/php_codesniffer": "^3.0", 35 | "symfony/browser-kit" : "^4.4|^5.0", 36 | "symfony/expression-language" : "^4.4|^5.0", 37 | "symfony/twig-bundle": "^4.4|^5.0", 38 | "symfony/yaml": "^4.4|^5.0", 39 | "symfony/validator": "^4.4|^5.0", 40 | "symfony/options-resolver": "^4.4|^5.0" 41 | }, 42 | "autoload": { 43 | "psr-4": { "ONGR\\ElasticsearchBundle\\": "" }, 44 | "exclude-from-classmap": ["/Tests/"] 45 | }, 46 | "autoload-dev": { 47 | "files": [ 48 | "Tests/app/AppKernel.php" 49 | ], 50 | "psr-4": { 51 | "ONGR\\App\\": "\\Tests\\app\\src\\" 52 | } 53 | }, 54 | "minimum-stability": "dev", 55 | "extra": { 56 | "branch-alias": { 57 | "dev-master": "7.0-dev" 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /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 | ./ 36 | 37 | ./Tests 38 | ./vendor 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | --------------------------------------------------------------------------------