diff --git a/doc/00_quick_start.md b/doc/00_quick_start.md
new file mode 100644
index 0000000..258e3a7
--- /dev/null
+++ b/doc/00_quick_start.md
@@ -0,0 +1,454 @@
+# Quick Start
+
+## 1. Install
+
+Use composer to install this library.
+```bash
+composer require spameri/elastic
+```
+
+---
+
+## 2. Configure
+
+You need to set up few things first, before you can dive into ElasticSearch.
+
+### I. Register extension
+
+In your configuration neon file you need to add these lines to `extension:` section.
+
+```yaml
+extensions:
+ spameriElasticSearch: \Spameri\Elastic\DI\SpameriElasticSearchExtension
+ console: Kdyby\Console\DI\ConsoleExtension
+ monolog: Kdyby\Monolog\DI\MonologExtension
+```
+
+### II. Configure
+
+Now you need to tell library where is ElasticSearch running. Default values are **localhost**
+and port **9200**. That means if you are running ElasticSearch locally with default port, no
+need to configure anything.
+
+```yaml
+spameriElasticSearch:
+ host: 192.168.0.14
+ port: 9200
+```
+
+### III. Configure Entity
+
+Next step is to configure your first entity. This entity is for e-shop product.
+
+```yaml
+1.| spameriElasticSearch:
+2.| entities:
+3.| SimpleProduct:
+4.| index: spameri_simple_product
+5.| dynamic: strict
+6.| config: @simpleProductConfig
+7.| properties:
+```
+- First line is extensionName
+- Second line is entities config array
+- Third line is EntityName
+- Fourth line is index name for this entity
+- Fifth line is for specifying whether index should accept new not specified fields
+- Sixth line is reference to where is object with entity configuration
+- Seventh line is where you can configure your entity within this neon
+
+---
+
+## 3. Create entity class
+
+```php
+id;
+}
+
+
+public function entityVariables(): array
+{
+ return \get_object_vars($this);
+}
+```
+
+### Factory
+````php
+class SimpleProductFactory implements \Spameri\Elastic\Factory\IEntityFactory
+{
+
+ public function create(\Spameri\ElasticQuery\Response\Result\Hit $hit) : \Generator
+ {
+ yield new \App\ProductModule\Entity\SimpleProduct(
+ new \Spameri\Elastic\Entity\Property\ElasticId($hit->id()),
+ $hit->getValue('databaseId'),
+ $hit->getValue('name'),
+ $hit->getValue('content'),
+ $hit->getValue('alias'),
+ $hit->getValue('image'),
+ $hit->getValue('price'),
+ $hit->getValue('availability'),
+ $hit->getValue('tags'),
+ $hit->getValue('categories')
+ );
+ }
+
+}
+````
+
+### CollectionFactory
+````php
+class SimpleProductCollectionFactory implements \Spameri\Elastic\Factory\ICollectionFactory
+{
+
+ public function create(
+ \Spameri\Elastic\Model\IService $service
+ , array $elasticIds = []
+ , \Spameri\Elastic\Entity\IElasticEntity ... $entityCollection
+ ) : \Spameri\Elastic\Entity\IElasticEntityCollection
+ {
+ return new \App\ProductModule\Entity\ProductCollection($service, $elasticIds, ... $entityCollection);
+ }
+
+}
+````
+
+## 4. Index Configuring
+````php
+class SimpleProductConfig implements \Spameri\Elastic\Settings\IndexConfigInterface
+{
+ public function __construct(
+ string $indexName
+ )
+ {
+ $this->indexName = $indexName;
+ }
+}
+````
+
+`public function provide(): \Spameri\ElasticQuery\Mapping\Settings`
+
+````php
+$settings = new \Spameri\ElasticQuery\Mapping\Settings($this->indexName);
+$czechDictionary = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\CzechDictionary();
+$settings->addAnalyzer($czechDictionary);
+
+$lowerCase = new \Spameri\ElasticQuery\Mapping\Analyzer\Custom\Lowercase();
+$settings->addAnalyzer($lowerCase);
+````
+
+````php
+$settings->addMappingField(
+ new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field(
+ 'databaseId',
+ \Spameri\Elastic\Model\ValidateMapping\AllowedValues::TYPE_KEYWORD
+ )
+);
+$settings->addMappingField(
+ new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field(
+ 'name',
+ \Spameri\Elastic\Model\ValidateMapping\AllowedValues::TYPE_TEXT,
+ $czechDictionary
+ )
+);
+$settings->addMappingField(
+ new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field(
+ 'content',
+ \Spameri\Elastic\Model\ValidateMapping\AllowedValues::TYPE_TEXT,
+ $czechDictionary
+ )
+);
+````
+
+
+````php
+$settings->addMappingField(
+ new \Spameri\ElasticQuery\Mapping\Settings\Mapping\Field(
+ 'tags',
+ \Spameri\Elastic\Model\ValidateMapping\AllowedValues::TYPE_TEXT,
+ $lowerCase
+ )
+);
+````
+
+
+## 5. Export data to ElasticSearch
+
+````php
+class ExportToElastic extends \Spameri\Elastic\Import\Run
+{
+
+ public function __construct(
+ string $logDir = 'log',
+ \Symfony\Component\Console\Output\ConsoleOutput $output,
+ \Spameri\Elastic\Import\Run\NullLoggerHandler $loggerHandler,
+ \Spameri\Elastic\Import\Lock\NullLock $lock,
+ \Spameri\Elastic\Import\RunHandler\NullHandler $runHandler,
+
+ \App\ProductModule\Model\ExportToElastic\DataProvider $dataProvider,
+ \App\ProductModule\Model\ExportToElastic\PrepareImportData $prepareImportData,
+ \App\ProductModule\Model\ExportToElastic\DataImport $dataImport,
+
+ \Spameri\Elastic\Import\AfterImport\NullAfterImport $afterImport
+ )
+ {
+ parent::__construct($logDir, $output, $loggerHandler, $lock, $runHandler, $dataProvider, $prepareImportData, $dataImport, $afterImport);
+ }
+
+}
+````
+
+
+````php
+class DataProvider implements \Spameri\Elastic\Import\DataProviderInterface
+{
+ public function provide(\Spameri\Elastic\Import\Run\Options $options): \Generator
+ {
+ $query = $this->connection->select('*')->from('table');
+
+ while ($hasResults) {
+ $items = $query->fetchAll($offset, $limit);
+
+ yield from $items;
+
+ if ( ! \count($items)) {
+ $hasResults = FALSE;
+
+ } else {
+ $offset += $limit;
+ }
+ }
+ }
+}
+````
+
+
+````php
+
+class PrepareImportData implements \Spameri\Elastic\Import\PrepareImportDataInterface
+{
+
+ public function prepare($entityData): \Spameri\Elastic\Entity\AbstractImport
+ {
+ $imageSrc = '//via.placeholder.com/150x150';
+ $elasticId = NULL;
+ $tags = [];
+ $categories = [];
+ return new \App\ProductModule\Entity\SimpleProduct(
+ $elasticId,
+ $entityData['id'],
+ $entityData['name'],
+ $entityData['content_description'],
+ $entityData['alias'],
+ $imageSrc,
+ $entityData['amount'],
+ $entityData['availability_id'] === 1 ? 'Skladem' : 'Nedostupné',
+ $tags,
+ $categories
+ );
+ }
+
+}
+````
+
+````php
+class DataImport implements \Spameri\Elastic\Import\DataImportInterface
+{
+
+ /**
+ * @param \App\ProductModule\Entity\SimpleProduct $entity
+ */
+ public function import(
+ \Spameri\Elastic\Entity\AbstractImport $entity
+ ): \Spameri\Elastic\Import\ResponseInterface
+ {
+ $id = $this->productService->insert($entity);
+
+ return new \Spameri\Elastic\Import\Response\SimpleResponse(
+ $id,
+ $entity
+ );
+ }
+
+}
+````
+
+````php
+$options = new \Spameri\Elastic\Import\Run\Options(600);
+
+// Clear index
+try {
+ $this->delete->execute($this->simpleProductConfig->provide()->indexName());
+} catch (\Spameri\Elastic\Exception\ElasticSearchException $exception) {}
+
+// Create index
+$this->create->execute(
+ $this->simpleProductConfig->provide()->indexName(),
+ $this->simpleProductConfig->provide()->toArray()
+);
+
+// Export
+$this->exportToElastic->execute($options);
+````
+
+## 6. Presenter, Form, Template
+````php
+class SimpleProductListPresenter extends \App\Presenter\BasePresenter
+{
+
+ public function renderDefault($queryString): void
+ {
+ $query = $this->buildQuery($queryString);
+
+ try {
+ $products = $this->productService->getAllBy($query);
+
+ } catch (\Spameri\Elastic\Exception\ElasticSearchException $exception) {
+ $products = [];
+ }
+
+ $this->getTemplate()->add(
+ 'products',
+ $products
+ );
+ $this->getTemplate()->add(
+ 'queryString',
+ $queryString
+ );
+ }
+
+}
+````
+
+````php
+public function createComponentSearchForm() :\Nette\Application\UI\Form
+{
+ $form = new \Nette\Application\UI\Form();
+ $form->addText('queryString', 'query')
+ ->setAttribute('class', 'inp-text suggest')
+ ;
+
+ $form->addSubmit('search', 'Search');
+
+ $form->onSuccess[] = function () use ($form) {
+ $this->redirect(
+ 301,
+ ':Product:SimpleProductList:default',
+ [
+ 'queryString' => $form->getValues()->queryString,
+ ]
+ );
+ };
+
+ return $form;
+}
+````
+
+````php
+{control searchForm}
+
You have searched: {$queryString}
+
+
+
+ {foreach $products as $product}
+ -
+
+
+
+
+
+
+ {$product->getName()|truncate:40}
+
+````
+
+## 7. Search
+
+```php
+public function buildQuery(?string $queryString): \Spameri\ElasticQuery\ElasticQuery
+{
+ $query = new \Spameri\ElasticQuery\ElasticQuery();
+ $query->addShouldQuery(
+ new \Spameri\ElasticQuery\Query\Match(
+ 'name',
+ $queryString
+ )
+ );
+
+ return $query;
+}
+```
+
+````php
+$products = $this->productService->getAllBy($query);
+````
+
+## 8. Fine Tuning
+
+````php
+$subQuery = new \Spameri\ElasticQuery\Query\QueryCollection();
+````
+
+```php
+$subQuery->addShouldQuery(
+ new \Spameri\ElasticQuery\Query\Match(
+ 'name',
+ $queryString,
+ 3,
+ \Spameri\ElasticQuery\Query\Match\Operator::OR,
+ new \Spameri\ElasticQuery\Query\Match\Fuzziness(\Spameri\ElasticQuery\Query\Match\Fuzziness::AUTO)
+ )
+);
+$subQuery->addShouldQuery(
+ new \Spameri\ElasticQuery\Query\WildCard(
+ 'name',
+ $queryString . '*',
+ 1
+ )
+);
+$subQuery->addShouldQuery(
+ new \Spameri\ElasticQuery\Query\MatchPhrase(
+ 'name',
+ $queryString,
+ 1
+ )
+);
+$subQuery->addShouldQuery(
+ new \Spameri\ElasticQuery\Query\Match(
+ 'content',
+ $queryString,
+ 1,
+ \Spameri\ElasticQuery\Query\Match\Operator:: OR,
+ new \Spameri\ElasticQuery\Query\Match\Fuzziness(\Spameri\ElasticQuery\Query\Match\Fuzziness::AUTO)
+ )
+);
+```
+
+
diff --git a/doc/01_intro.md b/doc/01_intro.md
index 57ebbdd..ea9e501 100644
--- a/doc/01_intro.md
+++ b/doc/01_intro.md
@@ -11,49 +11,92 @@ Use composer `composer require spameri/elastic`
In your config neon, enable extensions. Kdyby/Console is there because we need it to do some command line commands.
Monolog is required by elasticsearch/elasticsearch and we can use existing extension in Kdyby/Monolog.
-```
+```neon
extensions:
- elasticSearch: \Spameri\Elastic\DI\ElasticSearchExtension
+ spameriElasticSearch: \Spameri\Elastic\DI\SpameriElasticSearchExtension
console: Kdyby\Console\DI\ConsoleExtension
monolog: Kdyby\Monolog\DI\MonologExtension
```
Then configure where is your ElasticSearch.
-```
-elasticSearch:
+```neon
+spameriElasticSearch:
host: 127.0.0.1
port: 9200
```
-For more config options see default values in `\Spameri\Elastic\DI\ElasticSearchExtension::$defaults`.
+For more config options see default values in `\Spameri\Elastic\DI\SpameriElasticSearchExtension::$defaults`. [Here](../src/DI/ElasticSearchExtension.php#L9).
+
+#### Raw client usage
+- After this configuration you are ready to use ElasticSearch in your Nette application.
+- Where needed just inject `\Spameri\Elastic\ClientProvider` and then directly call what you need, like this:
+```php
+$result = $this->clientProvider->client()->search(
+ (
+ new \Spameri\ElasticQuery\Document(
+ $index,
+ new \Spameri\ElasticQuery\Document\Body\Plain(
+ $elasticQuery->toArray()
+ ),
+ $index
+ )
+ )->toArray()
+);
+```
+- [Client](https://github.com/elastic/elasticsearch-php/blob/master/src/Elasticsearch/Client.php) is provided from **elasticsearch/elasticsearch** and you can see their [documentation](https://github.com/elastic/elasticsearch-php#quickstart)
+what methods and arrays are supported.
+- When in doubt what how many arrays or how many arguments **match** supports use [Spameri/ElasticQuery](https://github.com/Spameri/ElasticQuery/blob/master/doc/02-query-objects.md)
+- This is library used in later examples. But direct approach is also possible.
+
+---
### 2. First entity
-#### [Neon file configuration](../blob/master/doc/02_neon_configuration.md)
+#### [Neon file configuration](02_neon_configuration.md)
+
+#### [Create entity class](03_entity_class.md)
-#### [Create Entity class](../blob/master/doc/03_entity_class.md)
+#### [Create entity service](12_entity_service.md)
+
+#### [Create entity factory](11_entity_factory.md)
+
+---
### 3. Mapping
-#### [Create new index with mapping]((../blob/master/doc/05_new_index_with_mapping.md))
+#### [Create new index with mapping](05_new_index_with_mapping.md)
+
+---
### 4. Fill with data
-TODO
+
+#### [Create and save entity](06_fill_data.md)
+
+#### [Saving process explained](07_save_explained.md)
+
+---
### 5. Get data from ElasticSearch
-TODO Tady factories
-TODO
-TODO
-TODO
-TODO
+
+#### [Get data by ID](08_basic_get.md)
+
+#### [Get data by tag](13_advanced_get.md)
+
+---
### 6. Filter data from ElasticSearch
-TODO
+
+#### [Match data](09_match_get.md)
+
+---
### 7. Aggregate data from ElasticSearch
-TODO
+
+#### [Aggregate data](10_aggregate.md)
+
+---
### x. Other
-#### [Data interfaces]((../blob/master/doc/04_data_interfaces.md))
+#### [Data interfaces](04_data_interfaces.md)
diff --git a/doc/02_neon_configuration.md b/doc/02_neon_configuration.md
index 38b9c28..91e158b 100644
--- a/doc/02_neon_configuration.md
+++ b/doc/02_neon_configuration.md
@@ -1,4 +1,5 @@
# Define structure in neon file
+TODO convert to video example
- Create neon file for example in `app/ProductModule/Config/Product.neon`
- Import created file to your application config neon. Usually located in`app/config/config.neon`.
@@ -12,7 +13,7 @@ have to worry about it because of type deprecation. More details here https://ww
- Entity definition is in neon under namespace `elasticSearch.entities.EntityName`
continuing our example in file `app/ProductModule/Config/Product.neon`:
```neon
-elasticSearch:
+spameriElasticSearch:
entities:
Product:
index: shop_product
@@ -24,7 +25,7 @@ This means newly introduced fields not specified in mapping will throw error whe
all fields introduced and specify their type. But if your application can add fields as needed you need to remember this
strict limitation or just do not enable it.
```neon
-elasticSearch:
+spameriElasticSearch:
entities:
Product:
dynamic: strict
@@ -32,7 +33,7 @@ elasticSearch:
- Now to specify entity mapping. Each object or encapsulation of sub fields stars with `properties:` then property name
and under it you can specify type and analyzer.
```neon
-elasticSearch:
+spameriElasticSearch:
entities:
Product:
properties:
@@ -45,7 +46,7 @@ elasticSearch:
- ElasticSearch default analyzers: https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-analyzers.html
- Subfields example:
```neon
-elasticSearch:
+spameriElasticSearch:
entities:
Product:
properties:
diff --git a/doc/03_entity_class.md b/doc/03_entity_class.md
index f471e64..524e53b 100644
--- a/doc/03_entity_class.md
+++ b/doc/03_entity_class.md
@@ -1,11 +1,11 @@
# Entity class
-Lets create entity class, continuing our example, in folder `app/ProductModule/Entity/Product.php` given file contents:
+Lets create entity class, continuing our example, in folder `tests/SpameriTests/Data/Entity/Video.php` given file contents:
```php
-namespace App\ProductModule\Entity;
+namespace SpameriTests\Data\Entity;
-class Product implements \Spameri\Elastic\Entity\IElasticEntity
+class Video implements \Spameri\Elastic\Entity\IElasticEntity
{
/**
@@ -38,7 +38,7 @@ class Product implements \Spameri\Elastic\Entity\IElasticEntity
### Lets look at class part by part.
-- Entity is in our defined namespace for `ProductModule` in own folder `Entity` which is shared for multiple entities.
+- Entity is in our defined namespace in own folder `Entity` which is shared for multiple entities.
- Class extends interface `\Spameri\Elastic\Entity\IElasticEntity`, this is core interface for ElasticSearch document.
It has to have `id` provided by ElasticSearch, library takes care of handling this field, no need to add in mapping.
- Based on this interface, library figures out how to save this class.
@@ -47,26 +47,26 @@ It has to have `id` provided by ElasticSearch, library takes care of handling th
- Interface requires function `id()` based on returned value it updates or creates entity.
- Interface requires `entityVariables()` in this exact form. (This may be changed in future versions, but now is required)
-### Adding properties to product entity
+### Adding properties to video entity
-#### Single value property - `name`
+#### Single value property - `Video.Story.KeyWord`
-- Lets say our product has limited name length to maximum of 255 characters and also 0 characters is not enough to
-describe product.
+- Lets say our Video has limited keyword length to maximum of 55 characters and also 0 characters is not enough to
+describe keyword.
- We do not have reliable data input so lets validate this with help of interface `\Spameri\Elastic\Entity\IValue`.
-- First create value object for name `\App\ProductModule\Entity\Product\Name`.
+- First create value object for keyword `\SpameriTests\Data\Entity\Video\Story\KeyWord`.
- Class should implement interface `\Spameri\Elastic\Entity\IValue`.
- `__construct` should have one parameter **string $value**.
-- In construct do our validation for product name.
+- In construct do our validation for keyword.
- Implement `value()` method.
-- Add property to `\App\ProductModule\Entity\Product` constructor.
-- Generate getter in `\App\ProductModule\Entity\Product` entity.
+- Add property to `\SpameriTests\Data\Entity\Video` constructor.
+- Generate getter in `\SpameriTests\Data\Entity\Video` entity for KeyWord.
- Result:
```php
-namespace App\ProductModule\Entity\Product;
+namespace SpameriTests\Data\Entity\Video\Story;
-class Name implements \Spameri\Elastic\Entity\IValue
+class KeyWord implements \Spameri\Elastic\Entity\IValue
{
/**
@@ -79,11 +79,11 @@ class Name implements \Spameri\Elastic\Entity\IValue
string $value
)
{
- if (\strlen($value) < 0) {
- throw new \InvalidArgumentException('Empty string is not supported for product name: ' . $value);
+ if ($value === '') {
+ throw new \InvalidArgumentException();
}
- if (\strlen($value) > 255) {
- $value = \substr($value, 0, 255);
+ if (\strlen($value) > 55) {
+ throw new \InvalidArgumentException();
}
$this->value = $value;
@@ -98,14 +98,335 @@ class Name implements \Spameri\Elastic\Entity\IValue
}
```
-#### Value collection property - `details.tags`
-TODO
+#### Value collection property - `Video.Story.KeyWordCollection`
+- If you need array of scalar values lets create ValueCollection.
+- For easy setup you can use `\Spameri\Elastic\Entity\AbstractValueCollection` just create your collection and extend this abstract as you need.
+For more advanced and typed approach use interface, as described next.
+- Interface `\Spameri\Elastic\Entity\IValueCollection` is when you want typed and validated scalar value collection.
+- After implementing interface you need to implement **getIterator()** method.
+- Next to be type save you want to add methods **add**, **remove**, **get**, **__construct**
+- For **__construct** you best fill values to collection as here [\Spameri\Elastic\Entity\AbstractValueCollection#L20](../src/Entity/AbstractValueCollection.php#L20)
+```php
+namespace SpameriTests\Data\Entity\Video\Story;
+
+
+class KeyWordCollection implements \Spameri\Elastic\Entity\IValueCollection
+{
+
+ /**
+ * @var array<\SpameriTests\Data\Entity\Video\Story\KeyWord>
+ */
+ private $collection;
+
+
+ public function __construct(
+ KeyWord ... $entities
+ )
+ {
+ $this->collection = [];
+ foreach ($entities as $keyWord) {
+ $this->add($keyWord);
+ }
+ }
+
+
+ public function add(
+ \SpameriTests\Data\Entity\Video\Story\KeyWord $keyWord
+ ) : void
+ {
+ $this->collection[$keyWord->value()] = $keyWord;
+ }
+
+
+ public function remove(string $key) : void
+ {
+ unset($this->collection[$key]);
+ }
+
+
+ public function get(string $key) : ?\SpameriTests\Data\Entity\Video\Story\KeyWord
+ {
+ if ( ! isset($this->collection[$key])) {
+ return NULL;
+ }
+
+ return $this->collection[$key];
+ }
+
+ public function getIterator() : \ArrayIterator
+ {
+ return new \ArrayIterator($this->collection);
+ }
+}
+```
+
+#### Single entity property - `Video.Story`
+- If you need some nested structure `\Spameri\Elastic\Entity\IEntity` interface is here for you.
+- Also when feeling lazy there is `\Spameri\Elastic\Entity\AbstractEntity` for you to extend with methods implemented.
+- In our example we have entity **Story** to encapsulate keywords and other story related properties.
+- Library then can convert this entity to array and save it as array with no more help.
+
+```php
+namespace SpameriTests\Data\Entity\Video;
+
+
+class Story implements \Spameri\Elastic\Entity\IEntity
+{
+
+ /**
+ * @var \SpameriTests\Data\Entity\Video\Story\KeyWordCollection
+ */
+ private $keyWords;
+
+
+ public function __construct(
+ \SpameriTests\Data\Entity\Video\Story\KeyWordCollection $keyWord
+ )
+ {
+ $this->keyWords = $keyWord;
+ }
+
+
+ public function entityVariables() : array
+ {
+ return \get_object_vars($this);
+ }
+
+
+ public function key() : string
+ {
+ return \md5(\implode('_', $this->entityVariables()));
+ }
+}
+```
+
+#### Entity collection property - `Video.Connections.FollowsCollection`
+- ElasticSearch is powerful tool and it allows you to nest objects and collection as you need, so you can make collection of nested objects.
+- This is simple you have Entity **Story** with implemented `IEntity` interface and all you need is create collection, extend `class FollowsCollection extends \Spameri\Elastic\Entity\Collection\EntityCollection`
+and you are done.
+```php
+namespace SpameriTests\Data\Entity\Video\Connections;
+
+
+class FollowsCollection extends \Spameri\Elastic\Entity\Collection\EntityCollection
+{
+
+}
+```
+
+#### ElasticEntity collection property - `Video.People`
+- `\Spameri\Elastic\Entity\Collection\ElasticEntityCollection` provides basic relations for entities in ElasticSearch.
+- It saves **_id** to current entity as reference in raw data but when loaded you have full entity with that id.
+Any changes made to related entity/ies will be persisted when main entity is saved.
+- Entity can be manually related 1:1 with manual lazy load in Factory (example in [factory](11_entity_factory.md) documentation)
+- Or multiple entities can be in collection lazily loaded all at once, also in factory example.
+- All you need is extend `\Spameri\Elastic\Entity\Collection\ElasticEntityCollection` and fill with your entities, library will do saving and resolving for you.
+```php
+namespace SpameriTests\Data\Entity\Video;
+
+
+class People extends \Spameri\Elastic\Entity\Collection\ElasticEntityCollection
+{
+
+ public function personByImdb(
+ \SpameriTests\Data\Entity\Property\ImdbId $imdb
+ ) : ?\SpameriTests\Data\Entity\Person
+ {
+ /** @var \SpameriTests\Data\Entity\Person $entity */
+ foreach ($this->collection() as $entity) {
+ if ($imdb->value() === $entity->identification()->imdb()->value()) {
+ return $entity;
+ }
+ }
+
+ return NULL;
+ }
+}
+```
+
+## Final product [Example](../tests/SpameriTests/Data/Entity/Video.php)
+```php
+namespace SpameriTests\Data\Entity;
+
+
+class Video implements \Spameri\Elastic\Entity\IElasticEntity
+{
+
+ /**
+ * @var \Spameri\Elastic\Entity\Property\IElasticId
+ */
+ private $id;
+
+ /**
+ * @var \SpameriTests\Data\Entity\Video\Identification
+ */
+ private $identification;
+
+ /**
+ * @var \SpameriTests\Data\Entity\Property\Name
+ */
+ private $name;
+
+ /**
+ * @var \SpameriTests\Data\Entity\Property\Year
+ */
+ private $year;
+
+ /**
+ * @var \SpameriTests\Data\Entity\Video\Technical
+ */
+ private $technical;
+
+ /**
+ * @var \SpameriTests\Data\Entity\Video\Story
+ */
+ private $story;
+
+ /**
+ * @var \SpameriTests\Data\Entity\Video\Details
+ */
+ private $details;
+
+ /**
+ * @var \SpameriTests\Data\Entity\Video\HighLights
+ */
+ private $highLights;
+
+ /**
+ * @var \SpameriTests\Data\Entity\Video\Connections
+ */
+ private $connections;
+
+ /**
+ * @var \SpameriTests\Data\Entity\Video\SeasonCollection
+ */
+ private $season;
+
+ /**
+ * @var \SpameriTests\Data\Entity\Video\People
+ */
+ private $people;
+
+
+ public function __construct(
+ \Spameri\Elastic\Entity\Property\IElasticId $id
+ , \SpameriTests\Data\Entity\Video\Identification $identification
+ , \SpameriTests\Data\Entity\Property\Name $name
+ , \SpameriTests\Data\Entity\Property\Year $year
+ , \SpameriTests\Data\Entity\Video\Technical $technical
+ , \SpameriTests\Data\Entity\Video\Story $story
+ , \SpameriTests\Data\Entity\Video\Details $details
+ , \SpameriTests\Data\Entity\Video\HighLights $highLights
+ , \SpameriTests\Data\Entity\Video\Connections $connections
+ , \SpameriTests\Data\Entity\Video\People $people
+ , \SpameriTests\Data\Entity\Video\SeasonCollection $season = NULL
+ )
+ {
+ $this->id = $id;
+ $this->identification = $identification;
+ $this->name = $name;
+ $this->year = $year;
+ $this->technical = $technical;
+ $this->story = $story;
+ $this->details = $details;
+ $this->highLights = $highLights;
+ $this->connections = $connections;
+
+ if ($season === NULL) {
+ $season = new \SpameriTests\Data\Entity\Video\SeasonCollection();
+ }
+ $this->season = $season;
+ $this->people = $people;
+ }
+
+
+ public function entityVariables() : array
+ {
+ return \get_object_vars($this);
+ }
+
+
+ public function id() : \Spameri\Elastic\Entity\Property\IElasticId
+ {
+ return $this->id;
+ }
+
+
+ public function identification() : \SpameriTests\Data\Entity\Video\Identification
+ {
+ return $this->identification;
+ }
+
+
+ public function name() : \SpameriTests\Data\Entity\Property\Name
+ {
+ return $this->name;
+ }
+
+
+ public function rename(\SpameriTests\Data\Entity\Property\Name $name) : void
+ {
+ $this->name = $name;
+ }
+
+
+ public function year() : \SpameriTests\Data\Entity\Property\Year
+ {
+ return $this->year;
+ }
+
+
+ public function setYear(\SpameriTests\Data\Entity\Property\Year $year) : void
+ {
+ $this->year = $year;
+ }
+
+
+ public function technical() : \SpameriTests\Data\Entity\Video\Technical
+ {
+ return $this->technical;
+ }
+
+
+ public function setTechnicalFromImdb(\SpameriTests\Data\Entity\Video\Technical $technical) : void
+ {
+ $this->technical = $technical;
+ }
+
+
+ public function story() : \SpameriTests\Data\Entity\Video\Story
+ {
+ return $this->story;
+ }
+
+
+ public function details() : \SpameriTests\Data\Entity\Video\Details
+ {
+ return $this->details;
+ }
+
+
+ public function highLights() : \SpameriTests\Data\Entity\Video\HighLights
+ {
+ return $this->highLights;
+ }
+
+
+ public function connections() : \SpameriTests\Data\Entity\Video\Connections
+ {
+ return $this->connections;
+ }
+
-#### Single entity property - `details`
-TODO
+ public function season() : \SpameriTests\Data\Entity\Video\SeasonCollection
+ {
+ return $this->season;
+ }
-#### Entity collection property - `parameterValues`
-TODO
-#### ElasticEntity collection property - `details.accessories`
-TODO
+ public function people() : \SpameriTests\Data\Entity\Video\People
+ {
+ return $this->people;
+ }
+}
+```
diff --git a/doc/05_new_index_with_mapping.md b/doc/05_new_index_with_mapping.md
index e69de29..da64332 100644
--- a/doc/05_new_index_with_mapping.md
+++ b/doc/05_new_index_with_mapping.md
@@ -0,0 +1,49 @@
+# Create index with mapping for entity
+
+Entity is configured [link](03_entity_class.md) and defined in php [link](03_entity_class.md#L247) and next thing is to get these settings to ElasticSearch.
+
+## Index creating
+
+### Variant 1. - No previous data
+- When to use?
+- You dont have any index, your ElasticSearch installation is clean and with no index for our entities.
+- How?
+- Simple neon is configured from previous [step](02_neon_configuration.md) and we just need to run command.
+
+```bash
+php www/index spameri:elastic:create-index
+```
+
+- Can be used for specific entity
+```bash
+php www/index spameri:elastic:create-index video
+```
+
+
+### Variant 2. - Deleting previous data
+- When to use?
+- You have existing index with outdated data with old configuration, but you dont need to keep them. This is
+usually when you are generating ElasticSearch data from another database.
+- How?
+- Just add -f option. First thing command does is delete old index, then creating new empty index with new settings.
+```bash
+php www/index spameri:elastic:create-index -f
+```
+
+- Can be used for specific entity
+```bash
+php www/index spameri:elastic:create-index -f video
+```
+
+### Variant 3. - Preserving previous data
+- When to use?
+- You dont have source for data saved in ElasticSearch.
+- Best is backup your data with command. This creates bulk json document with all data from index.
+```bash
+php www/index spameri:elastic:dump-index
+```
+- With data backed up, now you can delete index and create it fresh with new mapping - following variant 2.
+- Last thing is get your old data to new index wit this command.
+```bash
+php www/index spameri:elastic:restore-index
+```
diff --git a/doc/06_fill_data.md b/doc/06_fill_data.md
new file mode 100644
index 0000000..ecc1989
--- /dev/null
+++ b/doc/06_fill_data.md
@@ -0,0 +1,25 @@
+# Create and save entity
+
+## Create
+- When creating new entity, mostly you need to construct it manually.
+- Your data is from another database or API or csv or wherever. Quality may wary.
+- So best approach is to validate all you can with objects.
+- In this example we have property **imdb** and it needs to be in range from one digit to ten digits, rather than adding
+if conditions wherever we create entity we use **ImdbId** class to validate this rule and this helps to properly convert
+entity data to array.
+- [Example](../tests/SpameriTests/Model/Insert.phpt#L16)
+```php
+$sqlData = $dibi->fetchRow();
+$video = new \SpameriTests\Data\Entity\Video(
+ new \Spameri\Elastic\Entity\Property\EmptyElasticId(),
+ new \SpameriTests\Data\Entity\Video\Identification(
+ new \SpameriTests\Data\Entity\Property\ImdbId($sqlData['imdb'])
+ )
+);
+```
+
+## Save
+- Entity is created, validated and ready to be saved. Just pass entity to [service](12_entity_service.md). And done.
+```php
+$videoService->insert($video);
+```
diff --git a/doc/07_save_explained.md b/doc/07_save_explained.md
new file mode 100644
index 0000000..047564b
--- /dev/null
+++ b/doc/07_save_explained.md
@@ -0,0 +1,41 @@
+# Insert explained
+
+## \Spameri\Elastic\Model\Insert\PrepareEntityArray
+- This class is responsible for converting ElasticSearch entity to array that can be then saved to ElasticSearch.
+- Configuring is done by implementing [interfaces](04_data_interfaces.md), no need for annotations or neon.
+
+### ::prepare(\Spameri\Elastic\Entity\IElasticEntity $entity)
+- This method is here for last before convert modifications.
+- If implemented `\Spameri\Elastic\Entity\ITrackedEntity` interface for entity tracking (and properties specified),
+this method adds timestamp and user who edited/created entity.
+- Then calling iterateVariables to prepare rest of entity array.
+
+### ::iterateVariables(array $variables)
+This method accepts entity variables and based on their type performs converting to array to ready entity for insert.
+There are 9 types of property handled:
+1. **\Spameri\Elastic\Entity\IElasticEntity** in this case service locator comes to play and locates service for related
+entity and saves connected entity. And prepares related entity's id to property. Because to ElasticSearch goes only
+string id.
+
+2. **\Spameri\Elastic\Entity\IEntity** this is structural entity and is saved directly to parent entity. Its properties
+are iterated with `::iterateVariables($property->entityVariables())`
+
+3. **\Spameri\Elastic\Entity\IValue** raw value in object, directly converted to array.
+
+4. **\Spameri\Elastic\Entity\IEntityCollection** Collection of structural class **IEntity**, iterate and act as step 2.
+
+5. **\Spameri\Elastic\Entity\IElasticEntityCollection** Collection of ElasticSearch entities **IElasticEntity**,
+iterate and act as step 1.
+
+6. **\Spameri\Elastic\Entity\IValueCollection** Collection of **IValue**, iterate and act as step 3.
+
+7. Scalar values **string**, **int**, **bool** or **NULL**, no action just pass to array.
+
+8. **\Spameri\Elastic\Entity\DateTimeInterface** Date interface with specified format by this library so ElasticSearch
+can save it without problems.
+- **\Spameri\Elastic\Entity\Property\Date** for `Y-m-d` format
+- **\Spameri\Elastic\Entity\Property\DateTime** for `Y-m-d\TH:i:s` format
+
+9. **\DateTime** All other Dates are converted to `Y-m-d\TH:i:s`
+
+10. Exception thrown property is none of above.
diff --git a/doc/08_basic_get.md b/doc/08_basic_get.md
new file mode 100644
index 0000000..dbe5546
--- /dev/null
+++ b/doc/08_basic_get.md
@@ -0,0 +1,11 @@
+# Basic Get
+
+## Description
+Basic get by id from ElasticSearch, with exact match.
+
+## Example
+```php
+$video = $videoService->get(
+ new \Spameri\Elastic\Entity\Property\ElasticId($id)
+);
+```
diff --git a/doc/09_match_get.md b/doc/09_match_get.md
new file mode 100644
index 0000000..8375272
--- /dev/null
+++ b/doc/09_match_get.md
@@ -0,0 +1,26 @@
+# Filter data
+
+## Description
+You can specify complicated ElasticSearch Query and still get pretty entity. [Service](12_entity_service.md) accepts
+`\Spameri\ElasticQuery\ElasticQuery` object and returns entity or collection depending if you want one or more results.
+
+## Example
+In this example we are looking for video named 'Avengers' made in years from 2017 to 2018.
+```php
+$elasticQuery = new \Spameri\ElasticQuery\ElasticQuery();
+$elasticQuery->query()->must()->add(
+ new \Spameri\ElasticQuery\Query\Range(
+ 'year',
+ 2017,
+ 2019
+ )
+);
+$elasticQuery->query()->must()->add(
+ new \Spameri\ElasticQuery\Query\Match(
+ 'name',
+ 'Avengers'
+ )
+);
+
+$video = $videoService->getBy($elasticQuery);
+```
diff --git a/doc/10_aggregate.md b/doc/10_aggregate.md
new file mode 100644
index 0000000..34cccbe
--- /dev/null
+++ b/doc/10_aggregate.md
@@ -0,0 +1,22 @@
+# Aggregate
+
+## Description
+When aggregating you dont get directly entity just array data, for this we have `\Spameri\ElasticQuery\Response\ResultSearch`
+object to encapsulate result.
+
+## Example
+In this example we want number of videos released in each year.
+```php
+$elasticQuery = new \Spameri\ElasticQuery\ElasticQuery();
+$elasticQuery->aggregation()->add(
+ new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection(
+ 'video-by-year',
+ NULL,
+ new \Spameri\ElasticQuery\Aggregation\Term(
+ 'year'
+ )
+ )
+);
+
+$aggregateResult = $videoService->aggregate($elasticQuery);
+```
diff --git a/doc/11_entity_factory.md b/doc/11_entity_factory.md
new file mode 100644
index 0000000..bcac360
--- /dev/null
+++ b/doc/11_entity_factory.md
@@ -0,0 +1,26 @@
+# Entity factory
+
+## Description
+TODO
+Creates entity from result hit.
+
+## Example
+```php
+namespace SpameriTests\Elastic\Factory;
+
+
+class VideoFactory implements \Spameri\Elastic\Factory\IEntityFactory
+{
+
+ public function create(\Spameri\ElasticQuery\Response\Result\Hit $hit)
+ {
+ return new \SpameriTests\Data\Entity\Video(
+ new \Spameri\Elastic\Entity\Property\ElasticId($hit->getValue('id')),
+ new \SpameriTests\Data\Entity\Video\Identification(
+ new \SpameriTests\Data\Entity\Property\ImdbId($hit->getValue('identification.imdb'))
+ )
+ );
+ }
+
+}
+```
diff --git a/doc/12_entity_service.md b/doc/12_entity_service.md
new file mode 100644
index 0000000..5c1d050
--- /dev/null
+++ b/doc/12_entity_service.md
@@ -0,0 +1,65 @@
+# Entity service
+
+## Description
+TODO
+Every service should extend BaseService which has all methods for entity manipulation. Like **Insert**, **Get**,
+**GetBy**, **GetAllBy**.
+
+## Example
+```php
+namespace SpameriTests\Data\Model;
+
+
+class VideoService extends \Spameri\Elastic\Model\BaseService
+{
+
+ /**
+ * @param \Spameri\Elastic\Entity\IElasticEntity|\SpameriTests\Data\Entity\Video $entity
+ * @return string
+ */
+ public function insert(
+ \Spameri\Elastic\Entity\IElasticEntity $entity
+ ) : string
+ {
+ return parent::insert($entity);
+ }
+
+
+ /**
+ * @param \Spameri\Elastic\Entity\Property\ElasticId $id
+ * @return \Spameri\Elastic\Entity\IElasticEntity|\SpameriTests\Data\Entity\Video
+ */
+ public function get(
+ \Spameri\Elastic\Entity\Property\ElasticId $id
+ ) : \Spameri\Elastic\Entity\IElasticEntity
+ {
+ return parent::get($id);
+ }
+
+
+ /**
+ * @param \Spameri\ElasticQuery\ElasticQuery $elasticQuery
+ * @return \Spameri\Elastic\Entity\IElasticEntity|\SpameriTests\Data\Entity\Video
+ * @throws \Spameri\Elastic\Exception\DocumentNotFound
+ */
+ public function getBy(
+ \Spameri\ElasticQuery\ElasticQuery $elasticQuery
+ ) : \Spameri\Elastic\Entity\IElasticEntity
+ {
+ return parent::getBy($elasticQuery);
+ }
+
+
+ /**
+ * @param \Spameri\ElasticQuery\ElasticQuery $elasticQuery
+ * @return \Spameri\Elastic\Entity\IElasticEntityCollection|array<\SpameriTests\Data\Entity\Video>
+ */
+ public function getAllBy(
+ \Spameri\ElasticQuery\ElasticQuery $elasticQuery
+ ) : \Spameri\Elastic\Entity\IElasticEntityCollection
+ {
+ return parent::getAllBy($elasticQuery);
+ }
+
+}
+```
diff --git a/doc/13_advanced_get.md b/doc/13_advanced_get.md
new file mode 100644
index 0000000..fac65c1
--- /dev/null
+++ b/doc/13_advanced_get.md
@@ -0,0 +1,55 @@
+# Advanced Get
+
+## Description
+
+
+## Example
+In this example we want videos with tag **action** and are public. Also we want only first 50 sorted by year, but if
+year has multiple videos sort them by score. Bonus points if movie is on beach with someone named john or here
+misspelled as jon.
+
+Query does not have to be specified all at once, this is demonstration of capabilities. Query can be constructed empty
+and extended through application runtime.
+```php
+$elasticQuery = new \Spameri\ElasticQuery\ElasticQuery(
+ new \Spameri\ElasticQuery\Query\QueryCollection(
+ new \Spameri\ElasticQuery\Query\MustCollection(
+ new \Spameri\ElasticQuery\Query\Term(
+ 'story.tag',
+ 'action'
+ ),
+ new \Spameri\ElasticQuery\Query\Term(
+ 'isPublic',
+ TRUE
+ )
+ ),
+ new \Spameri\ElasticQuery\Query\ShouldCollection(
+ new \Spameri\ElasticQuery\Query\Match(
+ 'story.description',
+ 'beach'
+ ),
+ new \Spameri\ElasticQuery\Query\Fuzzy(
+ 'story.description',
+ 'jon'
+ )
+ )
+ ),
+ NULL,
+ NULL,
+ new \Spameri\ElasticQuery\Options(
+ 50,
+ NULL,
+ new \Spameri\ElasticQuery\Options\SortCollection(
+ new \Spameri\ElasticQuery\Options\Sort(
+ 'year',
+ \Spameri\ElasticQuery\Options\Sort::DESC
+ ),
+ new \Spameri\ElasticQuery\Options\Sort(
+ '_score',
+ \Spameri\ElasticQuery\Options\Sort::DESC
+ )
+ )
+ )
+);
+$videos = $this->videoService->getAllBy($elasticQuery);
+```
diff --git a/src/Entity/AbstractEntity.php b/src/Entity/AbstractEntity.php
new file mode 100644
index 0000000..d0aef41
--- /dev/null
+++ b/src/Entity/AbstractEntity.php
@@ -0,0 +1,20 @@
+entityVariables()));
+ }
+
+}
diff --git a/src/Entity/AbstractValueCollection.php b/src/Entity/AbstractValueCollection.php
new file mode 100644
index 0000000..57265e8
--- /dev/null
+++ b/src/Entity/AbstractValueCollection.php
@@ -0,0 +1,55 @@
+
+ */
+ protected $collection;
+
+
+ public function __construct(
+ \Spameri\Elastic\Entity\IValue ... $collection
+ )
+ {
+ $this->collection = [];
+ foreach ($collection as $value) {
+ $this->add($value);
+ }
+ }
+
+
+ public function add(
+ \Spameri\Elastic\Entity\IValue $value
+ ) : void
+ {
+ $this->collection[$value->value()] = $value;
+ }
+
+
+ public function remove($key) : void
+ {
+ unset($this->collection[$key]);
+ }
+
+
+ public function get($key) : ?\Spameri\Elastic\Entity\IValue
+ {
+ if ( ! isset($this->collection[$key])) {
+ return NULL;
+ }
+
+ return $this->collection[$key];
+ }
+
+
+ public function getIterator() : \ArrayIterator
+ {
+ return new \ArrayIterator($this->collection);
+ }
+
+}
diff --git a/src/Entity/Value/BoolValue.php b/src/Entity/Value/BoolValue.php
new file mode 100644
index 0000000..9455051
--- /dev/null
+++ b/src/Entity/Value/BoolValue.php
@@ -0,0 +1,28 @@
+value = $value;
+ }
+
+
+ public function value(): bool
+ {
+ return $this->value;
+ }
+
+}
diff --git a/src/Entity/Value/IntegerValue.php b/src/Entity/Value/IntegerValue.php
new file mode 100644
index 0000000..ca8eea4
--- /dev/null
+++ b/src/Entity/Value/IntegerValue.php
@@ -0,0 +1,28 @@
+value = $value;
+ }
+
+
+ public function value(): int
+ {
+ return $this->value;
+ }
+
+}
diff --git a/src/Entity/Value/NullValue.php b/src/Entity/Value/NullValue.php
new file mode 100644
index 0000000..ae30802
--- /dev/null
+++ b/src/Entity/Value/NullValue.php
@@ -0,0 +1,28 @@
+value;
+ }
+
+}
diff --git a/src/Entity/Value/StringValue.php b/src/Entity/Value/StringValue.php
new file mode 100644
index 0000000..f0f9ae1
--- /dev/null
+++ b/src/Entity/Value/StringValue.php
@@ -0,0 +1,28 @@
+value = $value;
+ }
+
+
+ public function value(): string
+ {
+ return $this->value;
+ }
+
+}
diff --git a/src/Model/Insert/PrepareEntityArray.php b/src/Model/Insert/PrepareEntityArray.php
index cb0dbcb..18859e2 100644
--- a/src/Model/Insert/PrepareEntityArray.php
+++ b/src/Model/Insert/PrepareEntityArray.php
@@ -76,7 +76,7 @@ public function iterateVariables(
} elseif ($property instanceof \Spameri\Elastic\Entity\IValueCollection) {
$preparedArray[$key] = [];
- /** @var $value \Spameri\Elastic\Entity\IValue */
+ /** @var \Spameri\Elastic\Entity\IValue $value */
/** @var \Spameri\Elastic\Entity\IValueCollection $property */
foreach ($property as $value) {
if ($value instanceof \Spameri\Elastic\Entity\IValue) {
diff --git a/tests/SpameriTests/Data/Config/Common.neon b/tests/SpameriTests/Data/Config/Common.neon
index f68cf82..6688643 100644
--- a/tests/SpameriTests/Data/Config/Common.neon
+++ b/tests/SpameriTests/Data/Config/Common.neon
@@ -1,7 +1,7 @@
extensions:
- elasticSearch: Spameri\Elastic\DI\ElasticSearchExtension
+ spameriElasticSearch: Spameri\Elastic\DI\SpameriElasticSearchExtension
-elasticSearch:
+spameriElasticSearch:
host: 127.0.0.1
port: 9200
diff --git a/tests/SpameriTests/Data/Config/Person.neon b/tests/SpameriTests/Data/Config/Person.neon
index 06e0701..44bc514 100644
--- a/tests/SpameriTests/Data/Config/Person.neon
+++ b/tests/SpameriTests/Data/Config/Person.neon
@@ -1,4 +1,4 @@
-elasticSearch:
+spameriElasticSearch:
entities:
Person:
index: spameri_person
diff --git a/tests/SpameriTests/Data/Config/Video.neon b/tests/SpameriTests/Data/Config/Video.neon
index cffbe5d..c6015ab 100644
--- a/tests/SpameriTests/Data/Config/Video.neon
+++ b/tests/SpameriTests/Data/Config/Video.neon
@@ -1,4 +1,4 @@
-elasticSearch:
+spameriElasticSearch:
entities:
Video:
index: spameri_video