From 939e19825b19bb36f4682cfe49f3bd1422cc8c02 Mon Sep 17 00:00:00 2001 From: Sergey Yalanskiy Date: Sun, 17 Mar 2024 01:33:51 +0300 Subject: [PATCH] HW15: ElasticSearch - new architecture. --- .env.example | 4 + README.md | 34 +- cli/Dockerfile | 34 + cli/php.ini | 0 docker-compose.yml | 36 + img.png | Bin 0 -> 14010 bytes www/books.json | 20002 ++++++++++++++++ www/composer.json | 24 + www/console.php | 23 + www/src/Application.php | 37 + www/src/Application/AddBook.php | 37 + www/src/Application/AddBookBulk.php | 43 + .../Application/Dto/AddBookBulkRequest.php | 27 + www/src/Application/Dto/AddBookRequest.php | 22 + www/src/Application/Dto/FindBookRequest.php | 17 + www/src/Application/Dto/FindBookResponse.php | 19 + www/src/Application/FindBook.php | 26 + www/src/Domain/Entity/Book.php | 69 + www/src/Domain/Entity/BookCollection.php | 25 + .../Repository/DataRepositoryInterface.php | 20 + .../Infrastructure/Command/FindCommand.php | 107 + .../Infrastructure/Command/LoadCommand.php | 68 + .../Db/ElasticDatabaseProvider.php | 323 + 23 files changed, 20995 insertions(+), 2 deletions(-) create mode 100644 .env.example create mode 100644 cli/Dockerfile create mode 100644 cli/php.ini create mode 100644 docker-compose.yml create mode 100644 img.png create mode 100644 www/books.json create mode 100644 www/composer.json create mode 100644 www/console.php create mode 100644 www/src/Application.php create mode 100644 www/src/Application/AddBook.php create mode 100644 www/src/Application/AddBookBulk.php create mode 100644 www/src/Application/Dto/AddBookBulkRequest.php create mode 100644 www/src/Application/Dto/AddBookRequest.php create mode 100644 www/src/Application/Dto/FindBookRequest.php create mode 100644 www/src/Application/Dto/FindBookResponse.php create mode 100644 www/src/Application/FindBook.php create mode 100644 www/src/Domain/Entity/Book.php create mode 100644 www/src/Domain/Entity/BookCollection.php create mode 100644 www/src/Domain/Repository/DataRepositoryInterface.php create mode 100644 www/src/Infrastructure/Command/FindCommand.php create mode 100644 www/src/Infrastructure/Command/LoadCommand.php create mode 100644 www/src/Infrastructure/Db/ElasticDatabaseProvider.php diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..4fd615fbf --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +ELASTIC_SERVER=otus-elasticsearch +ELASTIC_USERNAME=elastic +ELASTIC_PASSWORD=elastic +ELASTIC_INDEX_NAME=otus-index diff --git a/README.md b/README.md index e16b2a49b..0b051388f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,33 @@ -# PHP_2023 +# ElasticSearch -https://otus.ru/lessons/razrabotchik-php/?utm_source=github&utm_medium=free&utm_campaign=otus +Переделанная версия домашней работы по еластику, в соответствии с правильной архитектурой кода. +Запуск скриптов и результаты не поменялись. + +Запуск PHP-CLI контейнера. +Обязательно указать --network, иначе не увидит Elastic. +```` +docker run --rm -v ${PWD}/www:/www --env-file ${PWD}/.env --network homework_otus-network -it cli php console.php +```` + +Загрузка данных в индекс: +```` +console.php load +```` + +Поиск: +```` +console.php find +```` + +Пример запроса для загрузки данных: +```` +docker run --rm -v ${PWD}/www:/www --env-file ${PWD}/.env --network homework_otus-network -it cli php console.php load +```` + +Пример запроса для поиска: +```` +docker run --rm -v ${PWD}/www:/www --env-file ${PWD}/.env --network homework_otus-network -it cli php console.php find --title="рыцОри" --category="Исторический" --price="<2000" --stock=">=1" +```` + +Скрин ответа (начало таблицы): +![img.png](img.png) diff --git a/cli/Dockerfile b/cli/Dockerfile new file mode 100644 index 000000000..a927b2350 --- /dev/null +++ b/cli/Dockerfile @@ -0,0 +1,34 @@ +FROM php:8.2-cli + +RUN apt-get update && apt-get install -y \ + curl \ + libfreetype6-dev \ + libjpeg62-turbo-dev \ + libpng-dev \ + libonig-dev \ + libzip-dev \ + libmemcached-dev libssl-dev \ + libmcrypt-dev \ + libpq-dev \ + && docker-php-ext-install sockets \ + && pecl install mcrypt-1.0.6 \ + && docker-php-ext-enable mcrypt \ + && docker-php-ext-install -j$(nproc) iconv mbstring mysqli pdo_mysql pdo_pgsql pgsql zip \ + && docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install -j$(nproc) gd \ + && pecl install memcached \ + && docker-php-ext-enable memcached \ + && pecl install redis-5.3.7 \ + && docker-php-ext-enable redis \ + && pecl install xdebug-3.2.2 \ + && docker-php-ext-enable xdebug \ + && curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer + +COPY --from=composer /usr/bin/composer /usr/local/bin/composer +RUN composer self-update + +WORKDIR /www + +COPY ./php.ini /usr/local/etc/php/conf.d/php-custom.ini + +#CMD ["php", "composer", "update"] \ No newline at end of file diff --git a/cli/php.ini b/cli/php.ini new file mode 100644 index 000000000..e69de29bb diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..b84c101a9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,36 @@ +version: "3" + +services: + + # php-cli service + otus-cli: + build: + context: ./cli + dockerfile: Dockerfile + image: cli + container_name: otus-cli + volumes: + - ./www:/www + networks: + - otus-network + + # ElasticSearch + otus-elasticsearch: + image: elasticsearch:8.7.0 + container_name: ${ELASTIC_SERVER} + environment: + - ELASTIC_USERNAME=${ELASTIC_USERNAME} + - ELASTIC_PASSWORD=${ELASTIC_PASSWORD} + - node.name=otus_elasticsearch + - discovery.type=single-node + - http.host=0.0.0.0 + - transport.host=localhost + ports: + - "9200:9200" + networks: + - otus-network + +# Docker Network +networks: + otus-network: + driver: bridge \ No newline at end of file diff --git a/img.png b/img.png new file mode 100644 index 0000000000000000000000000000000000000000..d89cc9d1b186f68475d215cd3eef141d073ed463 GIT binary patch literal 14010 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 +} catch (Exception $exeption) { + echo $exeption->getMessage(); +} diff --git a/www/src/Application.php b/www/src/Application.php new file mode 100644 index 000000000..4c7f8c3f0 --- /dev/null +++ b/www/src/Application.php @@ -0,0 +1,37 @@ +dbConnection = new (self::DB_PROVIDER)(); + } + + /** + * @throws Exception + */ + public function run(): void + { + $console = new ConsoleApplication('ElasticSearch New (15)', '1.0'); + $console->addCommands([ + new LoadCommand($this->dbConnection), + new FindCommand($this->dbConnection), + ]); + $console->run(); + } +} \ No newline at end of file diff --git a/www/src/Application/AddBook.php b/www/src/Application/AddBook.php new file mode 100644 index 000000000..08bf84535 --- /dev/null +++ b/www/src/Application/AddBook.php @@ -0,0 +1,37 @@ +sku, + $request->title, + $request->category, + $request->price, + $request->stock + ); + if (!empty($request->id)) { + $book->setId($request->id); + } + + $this->provider->add($book); + } +} \ No newline at end of file diff --git a/www/src/Application/AddBookBulk.php b/www/src/Application/AddBookBulk.php new file mode 100644 index 000000000..32b78a4ff --- /dev/null +++ b/www/src/Application/AddBookBulk.php @@ -0,0 +1,43 @@ +books as $item) { + $book = new Book( + $item->sku, + $item->title, + $item->category, + $item->price, + $item->stock + ); + if (!empty($item->id)) { + $book->setId($item->id); + } + + $data->add($book); + } + + $this->provider->addBulk($data); + } +} \ No newline at end of file diff --git a/www/src/Application/Dto/AddBookBulkRequest.php b/www/src/Application/Dto/AddBookBulkRequest.php new file mode 100644 index 000000000..94c1aa7e6 --- /dev/null +++ b/www/src/Application/Dto/AddBookBulkRequest.php @@ -0,0 +1,27 @@ +books[] = new AddBookRequest( + $book['sku'], + $book['title'], + $book['category'], + $book['price'], + $book['stock'], + $book['id'] ?? '' + ); + } + } +} \ No newline at end of file diff --git a/www/src/Application/Dto/AddBookRequest.php b/www/src/Application/Dto/AddBookRequest.php new file mode 100644 index 000000000..5e1935e9e --- /dev/null +++ b/www/src/Application/Dto/AddBookRequest.php @@ -0,0 +1,22 @@ +provider->find($request->params)); + } +} \ No newline at end of file diff --git a/www/src/Domain/Entity/Book.php b/www/src/Domain/Entity/Book.php new file mode 100644 index 000000000..03e30c8a9 --- /dev/null +++ b/www/src/Domain/Entity/Book.php @@ -0,0 +1,69 @@ +sku; + } + + public function getTitle(): string + { + return $this->title; + } + + public function getCategory(): string + { + return $this->category; + } + + public function getPrice(): float + { + return $this->price; + } + + public function getStock(): array + { + return $this->stock; + } + + public function getId(): string + { + return $this->id; + } + + public function setId(string $id): void + { + $this->id = $id; + } + + public function getScore(): string + { + return $this->score; + } + + public function setScore(string $score): void + { + $this->score = $score; + } + +} \ No newline at end of file diff --git a/www/src/Domain/Entity/BookCollection.php b/www/src/Domain/Entity/BookCollection.php new file mode 100644 index 000000000..ab2a409c1 --- /dev/null +++ b/www/src/Domain/Entity/BookCollection.php @@ -0,0 +1,25 @@ +data[] = $data; + } + + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->data); + } +} \ No newline at end of file diff --git a/www/src/Domain/Repository/DataRepositoryInterface.php b/www/src/Domain/Repository/DataRepositoryInterface.php new file mode 100644 index 000000000..9d90f1726 --- /dev/null +++ b/www/src/Domain/Repository/DataRepositoryInterface.php @@ -0,0 +1,20 @@ + 'title', + 'shortcut' => 't', + 'description' => 'Book title' + ], + [ + 'name' => 'category', + 'shortcut' => 'c', + 'description' => 'Book category' + ], + [ + 'name' => 'price', + 'shortcut' => 'p', + 'description' => 'Book price (>=1000, 1000, <=1000)' + ], + [ + 'name' => 'stock', + 'shortcut' => 's', + 'description' => 'Stock amount (>=5, 5, <=5)' + ], + ]; + + public function __construct( + private DataRepositoryInterface $provider + ) + { + parent::__construct(); + } + + protected function configure(): void + { + parent::configure(); + + $this + ->setName('find') + ->setDescription("Find books by parameters") + ->setHelp( + "Find books by parameters" + ); + + foreach ($this->options as $option) { + $this->addOption($option['name'], $option['shortcut'], InputOption::VALUE_OPTIONAL, $option['description']); + } + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $searchParams = []; + foreach ($this->options as $option) { + if ($value = $input->getOption($option['name'])) { + $searchParams[$option['name']] = $value; + } + } + + $result = (new FindBook($this->provider))(new FindBookRequest($searchParams)); + + $table = new Table($output); + $table->setHeaders([ + 'Scores', + 'SKU', + 'Title', + 'Category', + 'Price', + 'Stock' + ]); + foreach ($result->response as $item) { + $stock = ''; + + foreach ($item->getStock() as $store) { + $stock .= "{$store['shop']}: {$store['stock']}\n"; + } + + $table->addRow([ + number_format(floatval($item->getScore()), decimals: 2), + $item->getSku(), + $item->getTitle(), + $item->getCategory(), + $item->getPrice(), + $stock + ]); + } + $table->render(); + + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/www/src/Infrastructure/Command/LoadCommand.php b/www/src/Infrastructure/Command/LoadCommand.php new file mode 100644 index 000000000..704b192a0 --- /dev/null +++ b/www/src/Infrastructure/Command/LoadCommand.php @@ -0,0 +1,68 @@ +setName('load') + ->setDescription("Load index from books.json") + ->setHelp("Load index from books.json"); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $data = file_get_contents(APP_ROOT . '/books.json'); + $lines = explode("\n", $data); + + $request = []; + $id = ''; + foreach ($lines as $line) { + if (empty(trim($line))) { + continue; + } + $json = json_decode($line, true); + + if (isset($json['create'])) { + $id = $json['create']['_id']; + } + else { + $request[] = [ + 'id' => $id, + 'sku' => $json['sku'], + 'title' => $json['title'], + 'category' => $json['category'], + 'price' => $json['price'], + 'stock' => $json['stock'], + ]; + } + } + + (new AddBookBulk($this->provider))(new AddBookBulkRequest($request)); + + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/www/src/Infrastructure/Db/ElasticDatabaseProvider.php b/www/src/Infrastructure/Db/ElasticDatabaseProvider.php new file mode 100644 index 000000000..9af7ee202 --- /dev/null +++ b/www/src/Infrastructure/Db/ElasticDatabaseProvider.php @@ -0,0 +1,323 @@ + [ + 'properties' => [ + 'category' => [ + 'type' => 'text', + 'fields' => [ + 'keyword' => [ + 'type' => 'keyword', + 'ignore_above' => 256, + ], + ], + ], + 'price' => [ + 'type' => 'long', + ], + 'sku' => [ + 'type' => 'text', + 'fields' => [ + 'keyword' => [ + 'type' => 'keyword', + 'ignore_above' => 256, + ], + ], + ], + 'stock' => [ + 'type' => 'nested', + 'properties' => [ + 'shop' => [ + 'type' => 'text', + 'fields' => [ + 'keyword' => [ + 'type' => 'keyword', + 'ignore_above' => 256, + ], + ], + ], + 'stock' => [ + 'type' => 'long', + ], + ], + ], + 'title' => [ + 'type' => 'text', + 'fields' => [ + 'keyword' => [ + 'type' => 'keyword', + 'ignore_above' => 256, + ], + ], + ], + ], + ], + ]; + + private array $searchTemplate = [ + 'index' => '', + 'scroll' => '1m', + 'size' => 50, + 'track_total_hits' => true, + 'body' => [ + 'query' => [ + 'bool' => [ + 'must' => [], + 'filter' => [ + [ + 'nested' => [ + 'path' => 'stock', + 'query' => [ + 'bool' => [ + 'must' => [] + ] + ] + ], + ] + ], + ], + ], + ], + ]; + + /** + * @throws AuthenticationException + * @throws ClientResponseException + * @throws ServerResponseException + * @throws MissingParameterException + */ + public function __construct() + { + $this->client = ClientBuilder::create() + ->setHosts(['https://' . ELASTIC_SERVER . ':9200']) + ->setBasicAuthentication(ELASTIC_USERNAME, ELASTIC_PASSWORD) + ->setSSLVerification(false) + ->build(); + + if (!$this->isIndexExists(ELASTIC_INDEX_NAME)) { + $this->createIndex(ELASTIC_INDEX_NAME); + } + } + + /** + * @throws ClientResponseException + * @throws ServerResponseException + */ + public function find(array $searchParams): BookCollection + { + $this->searchTemplate['index'] = ELASTIC_INDEX_NAME; + + foreach ($searchParams as $type => $value) { + switch ($type) { + case 'title': + $this->searchTemplate['body']['query']['bool']['must'] = [ + 'match' => [ + 'title' => [ + 'query' => $value, + 'fuzziness' => '2', + 'operator' => 'and', + ], + ] + ]; + break; + case 'category': + $this->searchTemplate['body']['query']['bool']['filter'][] = [ + 'match' => [ + 'category' => $value, + ] + ]; + break; + case 'price': + $value = $this->normalizeOperatrion($value); + $this->searchTemplate['body']['query']['bool']['filter'][] = [ + 'range' => [ + 'price' => $value + ] + ]; + break; + case 'stock': + $value = $this->normalizeOperatrion($value); + $this->searchTemplate['body']['query']['bool']['filter'][0]['nested']['query']['bool']['must'] = [ + 'range' => [ + 'stock.stock' => $value + ] + ]; + break; + } + } + + $searchResult = $this->client->search($this->searchTemplate); + + $out = new BookCollection(); + + if (count($searchResult['hits']['hits']) === 0) { + return $out; + } + + foreach ($searchResult['hits']['hits'] as $item) { + $book = new Book( + $item['_source']['sku'], + $item['_source']['title'], + $item['_source']['category'], + $item['_source']['price'], + $item['_source']['stock'] + ); + $book->setScore(number_format(floatval($item['_score']), decimals: 2)); + $out->add($book); + } + + while (count($searchResult['hits']['hits']) > 0) { + $searchResult = $this->client->scroll([ + 'body' => [ + 'scroll_id' => $searchResult['_scroll_id'], + 'scroll' => '1m', + ], + ]); + + foreach ($searchResult['hits']['hits'] as $item) { + $book = new Book( + $item['_source']['sku'], + $item['_source']['title'], + $item['_source']['category'], + $item['_source']['price'], + $item['_source']['stock'] + ); + $book->setScore(number_format(floatval($item['_score']), decimals: 2)); + $out->add($book); + } + } + + return $out; + } + + /** + * @throws ClientResponseException + * @throws ServerResponseException + */ + public function add(Book $data): void + { + $params = []; + $params[] = ['index' => ['_index' => ELASTIC_INDEX_NAME]]; + $params[] = ['create' => ['_index' => ELASTIC_INDEX_NAME, '_id' => $data->getId()]]; + $params[] = ['index' => ['_index' => ELASTIC_INDEX_NAME]]; + $params[] = [ + 'title' => $data->getTitle(), + 'sku' => $data->getSku(), + 'category' => $data->getCategory(), + 'price' => $data->getPrice(), + 'stock' => $data->getStock() + ]; + + $this->client->bulk(['body' => $params]); + } + + /** + * @throws ServerResponseException + * @throws ClientResponseException + */ + public function addBulk(BookCollection $data): void + { + $params = []; + foreach ($data as $item) { + $params[] = ['index' => ['_index' => ELASTIC_INDEX_NAME]]; + $params[] = ['create' => ['_index' => ELASTIC_INDEX_NAME, '_id' => $item->getId()]]; + $params[] = ['index' => ['_index' => ELASTIC_INDEX_NAME]]; + $params[] = [ + 'title' => $item->getTitle(), + 'sku' => $item->getSku(), + 'category' => $item->getCategory(), + 'price' => $item->getPrice(), + 'stock' => $item->getStock() + ]; + } + + $this->client->bulk(['body' => $params]); + } + + /** + * Normalize operators for Elastic + * + * @param string $value + * + * @return int[] + */ + private function normalizeOperatrion(string $value): array + { + $digit = (int)preg_replace('/[><=]/', '', $value); + + $operators = [ + '<=' => 'lte', + '>=' => 'gte', + '<' => 'lt', + '>' => 'gt', + ]; + + foreach ($operators as $operatorString => $operator) { + if (str_contains($value, $operatorString)) { + return [$operator => $digit]; + } + } + + return [ + 'lte' => $digit, + 'gte' => $digit, + ]; + } + + /** + * Check if index exists + * + * @param string $indexName Index Name + * @return bool + * @throws ServerResponseException + * @throws ClientResponseException + * @throws MissingParameterException + */ + private function isIndexExists(string $indexName): bool + { + return $this->client->indices()->exists(['index' => $indexName])->getStatusCode() === 200; + } + + /** + * Create index + * + * @param string $indexName Index name + * @return void + * @throws ClientResponseException + * @throws ServerResponseException + * @throws MissingParameterException + */ + private function createIndex(string $indexName): void + { + if ($this->isIndexExists($indexName)) { + return; + } + + $params = [ + 'index' => $indexName, + 'body' => $this->mapping, + ]; + $this->client->indices()->create($params); + } +} \ No newline at end of file