Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Showing
with 356 additions and 370 deletions
<?php
namespace IQDEV\ElasticSearch\Document;
use Ramsey\Collection\AbstractCollection;
/**
* @extends AbstractCollection<Product>
*/
final class ProductCollection extends AbstractCollection
{
public function getType(): string
{
return Product::class;
}
}
......@@ -2,22 +2,31 @@
namespace IQDEV\ElasticSearch\Document;
use IQDEV\ElasticSearch\Facet\FacetCategory;
use IQDEV\ElasticSearch\Facet\FacetCollection;
use IQDEV\ElasticSearch\Config\MappingValidator;
use IQDEV\ElasticSearch\Configuration;
use IQDEV\ElasticSearch\Facet\Collection\FacetCollection;
use IQDEV\ElasticSearch\Facet\Facet;
use IQDEV\ElasticSearch\Helper\ArrayHelper;
class ProductDocument implements Document
{
private array $properties = [];
private FacetCollection $keywordFacets;
private FacetCollection $numberFacets;
private ?string $fullSearchContent = null;
private ?string $searchContent = null;
private array $info = [];
private bool $skipEmpty = false;
private FacetCategory $categoryFacet;
public function __construct(FacetCategory $categoryFacet)
{
public function __construct(
private Facet $categoryFacet
) {
$this->keywordFacets = new FacetCollection();
$this->numberFacets = new FacetCollection();
$this->categoryFacet = $categoryFacet;
}
public static function create(Facet $categoryFacet): self
{
return new self($categoryFacet);
}
/**
......@@ -29,9 +38,9 @@ class ProductDocument implements Document
}
/**
* @return FacetCategory
* @return Facet
*/
public function getCategoryFacet(): FacetCategory
public function getCategoryFacet(): Facet
{
return $this->categoryFacet;
}
......@@ -45,11 +54,45 @@ class ProductDocument implements Document
}
/**
* @param string|null $fullSearchContent
* @param string|null $searchContent
*/
public function setSearchContent(?string $searchContent): void
{
$this->searchContent = $searchContent;
}
public function setAdditionData(array $info): self
{
$this->info = $info;
return $this;
}
/**
* Установка значения свойства документа индекса по параметрам конфигурации.
* Имеет приоритет по сравнению с вызовами функций для установки данных.
*
* @param Configuration $configuration
* @param string $property
* @param $value
*
* @return $this
*/
public function setFullSearchContent(?string $fullSearchContent): void
public function setByConfiguration(Configuration $configuration, string $property, $value): self
{
$this->fullSearchContent = $fullSearchContent;
if (!MappingValidator::isPropertyExists($configuration, $property)) {
throw new \InvalidArgumentException('Property ' . $property . ' doesnt exist');
}
$this->properties[$property] = $value;
return $this;
}
public function skipEmpty(bool $skipEmpty = false): self
{
$this->skipEmpty = $skipEmpty;
return $this;
}
public function es(): array
......@@ -60,12 +103,19 @@ class ProductDocument implements Document
'keyword_facet' => $this->getKeywordFacets()->es(),
'number_facet' => $this->getNumberFacets()->es()
],
'data' => $this->info
];
if ($this->fullSearchContent) {
$document['full_search_content'] = $this->fullSearchContent;
if (isset($this->searchContent)) {
$document['full_search_content'] = $this->searchContent;
$document['suggest_search_content'] = $this->searchContent;
}
$result = array_replace_recursive($document, $this->properties);
if (true === $this->skipEmpty) {
$result = ArrayHelper::array_filter_recursive($result, static fn ($val) => $val !== null || $val === false);
}
return $document;
return $result;
}
}
<?php
namespace IQDEV\ElasticSearch\Document\Property;
class Property
{
public function __construct(
protected string $key,
protected PropertyType $type = PropertyType::BASE,
) {
}
public function getKey(): string
{
return $this->key;
}
public function getType(): PropertyType
{
return $this->type;
}
}
<?php
namespace IQDEV\ElasticSearch\Document\Property;
enum PropertyType
{
case BASE;
case KEYWORD;
case NUMBER;
case TEXT;
}
<?php
namespace IQDEV\ElasticSearch\Domain;
use IQDEV\Search\Document\Document;
use IQDEV\Search\Facet\Facet;
use IQDEV\Search\Facet\FacetItemList;
use IQDEV\Search\Facet\FacetItemRange;
use IQDEV\Search\Facet\RangeFacetType;
use IQDEV\Search\Result;
use IQDEV\ElasticSearch\Search\Request;
use Elastic\Elasticsearch\Response\Elasticsearch;
use Http\Promise\Promise;
use IQDEV\Search\Facet\ListFacetType;
final class SearchResultFactory
{
/**
* @param Elasticsearch|Promise $response
* @param Request $request
* @return Result
*/
public static function createFromResponse($response, Request $request): Result
{
$result = new Result();
$data = json_decode($response->getBody(), true);
if (isset($data['hits']['hits'])) {
foreach ($data['hits']['hits'] as $hit) {
$result->hits->add(new Document($hit['_id'], $hit));
}
$result->numProduct = (int)$data['hits']['total']['value'];
}
if ($request->getPagination()) {
$result->numPages = ceil($result->numProduct / $request->getPagination()->size);
}
if (isset($data['aggregations']['keyword_facet']['agg_keyword_facet_code']['buckets'])) {
$buckets = $data['aggregations']['keyword_facet']['agg_keyword_facet_code']['buckets'];
$bucketsFiltered = [];
if (isset($data['aggregations']['keyword_facet_filtered']['all_keyword_facet_filtered']['agg_keyword_facet_code']['buckets'])) {
foreach ($data['aggregations']['keyword_facet_filtered']['all_keyword_facet_filtered']['agg_keyword_facet_code']['buckets'] as $bucket) {
$bucketsFiltered[$bucket['key']] = [];
foreach ($bucket['agg_keyword_facet_value']['buckets'] as $values) {
$bucketsFiltered[$bucket['key']][$values['key']] = $values;
}
}
}
foreach ($buckets as $bucket) {
$code = $bucket['key'];
$valueBucket = $bucket['agg_keyword_facet_value']['buckets'];
$facet = new Facet(new ListFacetType, $code);
foreach ($valueBucket as $value) {
$count = !empty($bucketsFiltered) || $result->numProduct === 0 ? 0 : $value['doc_count'];
if (isset($bucketsFiltered[$code][$value['key']])) {
$count = $bucketsFiltered[$code][$value['key']]['doc_count'];
}
$facet->items->add(FacetItemList::createFromValue($value['key'], $count));
}
$result->facets->add($facet);
}
}
if (isset($data['aggregations']['number_facet']['agg_number_facet_code']['buckets'])) {
$buckets = $data['aggregations']['number_facet']['agg_number_facet_code']['buckets'];
$bucketsFiltered = [];
if (isset($data['aggregations']['number_facet_filtered']['all_number_facet_filtered']['agg_number_facet_code']['buckets'])) {
foreach ($data['aggregations']['number_facet_filtered']['all_number_facet_filtered']['agg_number_facet_code']['buckets'] as $bucket) {
$bucketsFiltered[$bucket['key']] = [
'data' => $bucket,
'min' => $bucket['agg_number_facet_min_value'],
'max' => $bucket['agg_number_facet_max_value']
];
}
}
foreach ($buckets as $bucket) {
$code = $bucket['key'];
$facet = new Facet(new RangeFacetType, $code);
$count = !empty($bucketsFiltered) || $result->numProduct === 0 ? 0 : $bucket['doc_count'];
$selectedMin = $selectedMax = null;
if (isset($bucketsFiltered[$code])) {
$count = $bucketsFiltered[$code]['data']['doc_count'];
$selectedMin = $bucketsFiltered[$code]['min']['value'];
$selectedMax = $bucketsFiltered[$code]['max']['value'];
}
$facet->items->add(FacetItemRange::createFromRange(
$bucket['agg_number_facet_min_value']['value'],
$bucket['agg_number_facet_max_value']['value'],
$count,
$selectedMin,
$selectedMax
)
);
$result->facets->add($facet);
}
}
return $result;
}
}
\ No newline at end of file
<?php
namespace IQDEV\ElasticSearch\Domain;
use IQDEV\ElasticSearch\Search\Aggs\Aggs;
use IQDEV\ElasticSearch\Search\Aggs\AggsKeyWordFacet;
use IQDEV\ElasticSearch\Search\Aggs\AggsNumberFacet;
use IQDEV\ElasticSearch\Search\BoolQuery\FilterKeywordFacet;
use IQDEV\ElasticSearch\Search\BoolQuery\FilterNumberFacet;
use IQDEV\ElasticSearch\Search\BoolQuery\Query;
use IQDEV\ElasticSearch\Search\BoolQuery\Terms;
use IQDEV\ElasticSearch\Search\Nested;
use IQDEV\ElasticSearch\Search\Pagination;
use IQDEV\ElasticSearch\Search\Request;
use Elastic\Elasticsearch\Client;
use IQDEV\Search\{Filter\Filter,
Filter\FilterKeyword,
Filter\FilterCategory,
Filter\FilterNumber,
Query as DQuery,
Result,
SearchService as DomainSearchService,
Sorting\SortingFieldPair as DSortingFieldPair,
Sorting\SortingPropertyKeywordPair as DSortingPropertyKeywordPair,
Sorting\SortingPropertyNumberPair as DSortingPropertyNumberPair
};
use IQDEV\ElasticSearch\Search\Sorting\SortingCollection;
use IQDEV\ElasticSearch\Search\Sorting\SortingFieldsPair;
use IQDEV\ElasticSearch\Search\Sorting\SortingPropertyKeywordPair;
use IQDEV\ElasticSearch\Search\Sorting\SortingPropertyNumberPair;
final class SearchService implements DomainSearchService
{
private Client $esClient;
private string $sIndex;
public function __construct(Client $esClient, string $sIndex = 'product-test')
{
$this->esClient = $esClient;
$this->sIndex = $sIndex;
}
public function search(DQuery $q): Result
{
$request = new Request();
$commonQuery = new Query();
$filterKeyword = new Query();
$filterNumber = new Query();
if ($q->filters) {
foreach ($q->filters as $filter) {
/** @var Filter $filter */
if ($filter instanceof FilterCategory) {
$request->getQuery()->must(
(new Terms('category_id', $filter->value))
);
continue;
}
if ($filter instanceof FilterNumber) {
$oFacet = new FilterNumberFacet($filter->key, $filter->min, $filter->max);
$filterNumber->filter($oFacet);
$commonQuery->filter($oFacet);
continue;
}
if ($filter instanceof FilterKeyword) {
$oFacet = new FilterKeywordFacet($filter->key, $filter->value);
$filterKeyword->filter($oFacet);
$commonQuery->filter($oFacet);
continue;
}
}
}
$commonFilter = clone $commonQuery;
$commonFilter->setType(Query::TYPE_FILTER);
if ($q->pagination) {
$request->setPagination(
new Pagination($q->pagination->limit, $q->pagination->page)
);
}
if ($q->sorting && !$q->sorting->isEmpty()) {
$oSortingCollection = new SortingCollection();
foreach ($q->sorting as $sorting) {
if ($sorting instanceof DSortingFieldPair) {
$oSortingCollection->add(new SortingFieldsPair($sorting->by, $sorting->direction));
}
if ($sorting instanceof DSortingPropertyKeywordPair) {
$oSortingCollection->add(new SortingPropertyKeywordPair($sorting->by, $sorting->direction));
}
if ($sorting instanceof DSortingPropertyNumberPair) {
$oSortingCollection->add(new SortingPropertyNumberPair($sorting->by, $sorting->direction));
}
}
$request->setSorting($oSortingCollection);
}
if ($q->query) {
$request->addMatch('full_search_content', ['query' => $q->query]);
}
if ($filterKeyword->isEmpty() === false) {
$nestedFilterKeyword = new Nested();
$nestedFilterKeyword->setPath('search_data')
->setQuery($filterKeyword);
}
if ($filterNumber->isEmpty() === false) {
$nestedFilterNumber = new Nested();
$nestedFilterNumber->setPath('search_data')
->setQuery($filterNumber);
}
if ($commonQuery->isEmpty() === false) {
$nestedFilter = new Nested();
$nestedFilter->setPath('search_data')
->setQuery($commonQuery);
$request->getPostFilter()->filter($nestedFilter);
$aggsKeywordFiltered = new Aggs('keyword_facet_filtered');
$aggsKeywordFiltered->addAggs(AggsKeyWordFacet::create('all_keyword_facet_filtered', 'keyword_facet'))
->setQuery($commonFilter);
$request->getAggs()->add($aggsKeywordFiltered);
$aggsNumberFiltered = new Aggs('number_facet_filtered');
$aggsNumberFiltered->addAggs(AggsNumberFacet::create('all_number_facet_filtered', 'number_facet'))
->setQuery($commonFilter);
$request->getAggs()->add($aggsNumberFiltered);
}
$aggsKeyword = AggsKeyWordFacet::create('keyword_facet', 'keyword_facet');
$request->getAggs()->add($aggsKeyword);
$aggsNumber = AggsNumberFacet::create('number_facet', 'number_facet');
$request->getAggs()->add($aggsNumber);
$response = $this->esClient->search(
[
'index' => $this->sIndex,
'body' => $request->es(),
]
);
return SearchResultFactory::createFromResponse($response, $request);
}
}
<?php
namespace IQDEV\ElasticSearch\Facet;
namespace IQDEV\ElasticSearch\Facet\Collection;
use IQDEV\ElasticSearch\Esable;
use IQDEV\ElasticSearch\Facet\Facetable;
use Ramsey\Collection\AbstractCollection;
final class FacetCollection extends AbstractCollection implements Esable
{
public function getType(): string
{
return Facet::class;
return Facetable::class;
}
public function es(): array
{
return array_map(static fn(Facet $facet) => $facet->es(), $this->toArray());
return array_map(static fn(Facetable $facet) => $facet->es(), $this->toArray());
}
}
\ No newline at end of file
}
<?php
namespace IQDEV\ElasticSearch\Facet\Collection;
use IQDEV\ElasticSearch\Facet\FacetResult;
use Ramsey\Collection\AbstractCollection;
final class FacetResultCollection extends AbstractCollection
{
/**
* @inheritDoc
*/
public function getType(): string
{
return FacetResult::class;
}
}
......@@ -2,8 +2,18 @@
namespace IQDEV\ElasticSearch\Facet;
use IQDEV\ElasticSearch\Esable;
use IQDEV\ElasticSearch\Document\Property\Property;
interface Facet extends Esable
abstract class Facet implements Facetable
{
}
\ No newline at end of file
public function __construct(
protected Property $property,
protected mixed $value,
) {
}
public function getProperty(): Property
{
return $this->property;
}
}
<?php
namespace IQDEV\ElasticSearch\Facet;
use IQDEV\ElasticSearch\Esable;
final class FacetCategory implements Esable
{
public string $category;
/**
* @param string $category
*/
public function __construct(string $category)
{
$this->category = $category;
}
public function es(): array
{
return [
'category_id' => $this->category
];
}
}
<?php
namespace IQDEV\ElasticSearch\Facet;
use IQDEV\ElasticSearch\Document\Property\Property;
use IQDEV\ElasticSearch\Document\Property\PropertyType;
use IQDEV\ElasticSearch\Facet\Type\BaseFacet;
use IQDEV\ElasticSearch\Facet\Type\KeywordFacet;
use IQDEV\ElasticSearch\Facet\Type\NumberFacet;
class FacetFactory
{
public static function createFromProperty(Property $property, mixed $value): Facet
{
match ($property->getType()) {
PropertyType::KEYWORD => $facet = new KeywordFacet($property, $value),
PropertyType::NUMBER => $facet = new NumberFacet($property, $value),
default => $facet = new BaseFacet($property, $value),
};
return $facet;
}
}
<?php
namespace IQDEV\ElasticSearch\Facet;
final class FacetKeyword implements Facet
{
public string $key;
public $value;
/**
* @param string $key
* @param string|string[] $value
*/
public function __construct(
string $key,
$value
)
{
$this->key = $key;
$this->value = $value;
}
public function es(): array
{
return [
'facet_code' => $this->key,
'facet_value' => $this->value,
];
}
}
\ No newline at end of file
<?php
namespace IQDEV\ElasticSearch\Facet;
final class FacetNumber implements Facet
{
public string $key;
public float $value;
/**
* @param string $key
* @param float $value
*/
public function __construct(
string $key,
float $value
)
{
$this->key = $key;
$this->value = $value;
}
public function es(): array
{
return [
'facet_code' => $this->key,
'facet_value' => $this->value,
];
}
}
\ No newline at end of file
<?php
namespace IQDEV\ElasticSearch\Facet;
use IQDEV\ElasticSearch\Facet\Item\FacetItemCollection;
class FacetResult
{
public FacetItemCollection $products;
protected FacetType $type;
protected string $code;
public function __construct(FacetType $type, string $code)
{
$this->type = $type;
$this->code = $code;
$this->products = new FacetItemCollection();
}
public function getType(): FacetType
{
return $this->type;
}
public function getCode(): string
{
return $this->code;
}
}
<?php
namespace IQDEV\ElasticSearch\Facet;
enum FacetType: string
{
case LIST = 'list';
case RANGE = 'range';
}
<?php
namespace IQDEV\ElasticSearch\Search\Sorting;
namespace IQDEV\ElasticSearch\Facet;
use IQDEV\ElasticSearch\Esable;
interface Sorting extends Esable
interface Facetable extends Esable
{
}
\ No newline at end of file
}
<?php
namespace IQDEV\ElasticSearch\Facet\Item;
abstract class FacetItem
{
protected ?string $label = null;
protected ?string $value = null;
protected bool $selected = false;
protected int $count = 0;
abstract public static function create(): self;
public function getLabel(): ?string
{
return $this->label;
}
public function getValue(): ?string
{
return $this->value;
}
public function getCount(): int
{
return $this->count;
}
public function isActive(): bool
{
return $this->count > 0;
}
public function isSelected(): bool
{
return $this->selected;
}
}
<?php
namespace IQDEV\ElasticSearch\Facet\Item;
use Ramsey\Collection\AbstractCollection;
class FacetItemCollection extends AbstractCollection
{
public function getType(): string
{
return FacetItem::class;
}
}
<?php
namespace IQDEV\ElasticSearch\Facet\Item;
class FacetItemList extends FacetItem
{
public static function create(
?string $value = null,
int $count = 0,
bool $selected = false,
?string $label = null
): self
{
$instance = new self();
$instance->value = $value;
$instance->label = $label;
$instance->count = $count;
$instance->selected = $selected;
return $instance;
}
}
<?php
namespace IQDEV\ElasticSearch\Facet\Item;
class FacetItemRange extends FacetItem
{
protected FacetItemRangeDTO $data;
protected ?FacetItemRangeDTO $selectedData;
public static function create(
FacetItemRangeDTO $data = null,
FacetItemRangeDTO $selectedData = null,
int $count = 0,
bool $selected = false,
?string $label = null
): self
{
$instance = new self();
$instance->label = $label;
$instance->count = $count;
$instance->selected = $selected;
$instance->data = $data;
$instance->selectedData = $selectedData;
return $instance;
}
public function getFullRange(): array
{
return [
'min' => $this->data->min,
'max' => $this->data->max
];
}
public function getSelectedRange(): array
{
return [
'min' => $this->selectedData->min,
'max' => $this->selectedData->max
];
}
public function getData(): FacetItemRangeDTO
{
return $this->data;
}
public function getSelectedData(): FacetItemRangeDTO
{
return $this->selectedData;
}
}