Skip to content
Snippets Groups Projects
Commit c61d9645 authored by Pavel's avatar Pavel
Browse files

init

parents
No related branches found
No related tags found
No related merge requests found
Showing
with 818 additions and 0 deletions
.DS_Store
/composer.lock
/vendor/
/.idea/
\ No newline at end of file
{
"name": "iqdev/search-es",
"description": "Search by elasticsearch",
"minimum-stability": "stable",
"license": "proprietary",
"authors": [
{
"name": "Pavel Piligrimov",
"email": "p.piligrimov@iqdev.digital"
}
],
"version": "0.0.1",
"type": "library",
"keywords": [
"search",
"elasticsearch",
"php"
],
"require": {
"php": ">=7.4",
"ramsey/collection": "^1.2",
"iqdev/search-dc": "dev-main",
"elasticsearch/elasticsearch": "^8.5",
"vlucas/phpdotenv": "^5.4.1"
},
"autoload": {
"psr-4": {
"IQDEV\\ElasticSearch\\": "src/ElasticSearch/"
}
},
"repositories": [
{
"type": "vcs",
"url": "ssh://git@gitlab.iqdev.digital:8422/piligrimov/search-dc.git"
}
]
}
<?php
namespace Docke\ElasticSearch\Config;
use IQDEV\ElasticSearch\Configuration;
class BaseConfiguration implements Configuration
{
public function getIndexName(): string
{
return $_ENV['IQ_ES_PRODUCT_SEARCH_INDEX'];
}
public function getMapping(): array
{
return include __DIR__.'/product.mappings.php';
}
}
\ No newline at end of file
<?php
return [
'properties' => [
'data' => [
'type' => 'object',
'enabled' => false,
],
'full_search_content' => [
'type' => 'text',
],
'category_id' => [
'type' => 'keyword',
'index' => false,
],
'search_data' => [
'type' => 'nested',
'properties' => [
'keyword_facet' => [
'type' => 'nested',
'properties' => [
'facet_code' => [
'type' => 'keyword',
'index' => true
],
'facet_value' => [
'type' => 'keyword',
'index' => true
],
]
],
'number_facet' => [
'type' => 'nested',
'properties' => [
'facet_code' => [
'type' => 'keyword',
'index' => true
],
'facet_value' => [
'type' => 'double'
]
]
]
]
]
],
];
\ No newline at end of file
<?php
namespace IQDEV\ElasticSearch;
interface Configuration
{
public function getIndexName(): string;
public function getMapping(): array;
}
\ No newline at end of file
<?php
namespace IQDEV\ElasticSearch\Document;
use IQDEV\ElasticSearch\Esable;
interface Document extends Esable
{
}
\ No newline at end of file
<?php
namespace IQDEV\ElasticSearch\Document;
use IQDEV\ElasticSearch\Facet\FacetCategory;
use IQDEV\ElasticSearch\Facet\FacetCollection;
class ProductDocument implements Document
{
private FacetCollection $keywordFacets;
private FacetCollection $numberFacets;
private ?string $fullSearchContent = null;
private FacetCategory $categoryFacet;
public function __construct(FacetCategory $categoryFacet)
{
$this->keywordFacets = new FacetCollection();
$this->numberFacets = new FacetCollection();
$this->categoryFacet = $categoryFacet;
}
/**
* @return FacetCollection
*/
public function getKeywordFacets(): FacetCollection
{
return $this->keywordFacets;
}
/**
* @return FacetCategory
*/
public function getCategoryFacet(): FacetCategory
{
return $this->categoryFacet;
}
/**
* @return FacetCollection
*/
public function getNumberFacets(): FacetCollection
{
return $this->numberFacets;
}
/**
* @param string|null $fullSearchContent
*/
public function setFullSearchContent(?string $fullSearchContent): void
{
$this->fullSearchContent = $fullSearchContent;
}
public function es(): array
{
$document = [
'category_id' => $this->getCategoryFacet()->es()['category_id'],
'search_data' => [
'keyword_facet' => $this->getKeywordFacets()->es(),
'number_facet' => $this->getNumberFacets()->es()
],
];
if ($this->fullSearchContent) {
$document['full_search_content'] = $this->fullSearchContent;
}
return $document;
}
}
<?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;
interface Esable
{
/**
* Получить структуру данных для запроса в Elasticsearch
* @return array
*/
public function es(): array;
}
<?php
namespace IQDEV\ElasticSearch\Facet;
use IQDEV\ElasticSearch\Esable;
interface Facet extends Esable
{
}
\ No newline at end of file
<?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\Esable;
use Ramsey\Collection\AbstractCollection;
final class FacetCollection extends AbstractCollection implements Esable
{
public function getType(): string
{
return Facet::class;
}
public function es(): array
{
return array_map(static fn(Facet $facet) => $facet->es(), $this->toArray());
}
}
\ No newline at end of file
<?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\Indexer;
use IQDEV\ElasticSearch\Esable;
final class Index implements Esable
{
private string $name;
private Esable $body;
private ?string $id;
public function __construct(
string $name,
Esable $body,
?string $id = null
) {
$this->name = $name;
$this->body = $body;
$this->id = $id;
}
public function es(): array
{
$es = [
'index' => $this->name,
'body' => $this->body->es(),
];
if ($this->id) {
$es['id'] = $this->id;
}
return $es;
}
}
<?php
namespace IQDEV\ElasticSearch\Indexer;
interface IndexProvider
{
public function get(): \Generator;
}
\ No newline at end of file
<?php
namespace IQDEV\ElasticSearch\Indexer;
use Elastic\Elasticsearch\Client;
use IQDEV\ElasticSearch\Configuration;
final class IndexRunner
{
private Client $esClient;
private Configuration $configuration;
public function __construct(
Client $esClient,
Configuration $configuration
) {
$this->esClient = $esClient;
$this->configuration = $configuration;
}
public function run(IndexProvider $indexProvider)
{
if ($this->esClient->indices()->exists(['index' => $this->configuration->getIndexName()])->asBool() === false) {
$this->esClient->indices()->create(
[
'index' => $this->configuration->getIndexName(),
'body' => [
'mappings' => $this->configuration->getMapping(),
],
]
);
}
foreach ($indexProvider->get() as $index) {
if (!$index instanceof Index) {
continue;
}
$esIndex = $index->es();
$this->esClient->index($esIndex);
}
}
}
<?php
namespace IQDEV\ElasticSearch\Search\Aggs;
use IQDEV\ElasticSearch\Esable;
use IQDEV\ElasticSearch\Search\BoolQuery\Query;
use IQDEV\ElasticSearch\Search\Nested;
final class Aggs implements Esable
{
private ?AggsCollection $aggs = null;
private ?Query $query = null;
private ?Nested $nested = null;
private ?Terms $terms = null;
private ?ExtremumTerms $extremumTerms = null;
private string $key;
public function __construct(string $key)
{
$this->key = $key;
}
/**
* @param Aggs|null $aggs
* @return Aggs
*/
public function addAggs(?Aggs $aggs): self
{
if ($this->aggs === null) {
$this->aggs = new AggsCollection();
}
$this->aggs->add($aggs);
return $this;
}
/**
* @param Query|null $query
* @return Aggs
*/
public function setQuery(?Query $query): self
{
$this->query = $query;
return $this;
}
/**
* @param Nested|null $nested
* @return Aggs
*/
public function setNested(?Nested $nested): self
{
$this->nested = $nested;
return $this;
}
/**
* @param Terms|null $terms
* @return Aggs
*/
public function setTerms(?Terms $terms): self
{
$this->terms = $terms;
return $this;
}
/**
* @param ExtremumTerms|null $terms
* @return Aggs
*/
public function setExtremumTerms(?ExtremumTerms $terms): self
{
$this->extremumTerms = $terms;
return $this;
}
/**
* @return string
*/
public function getKey(): string
{
return $this->key;
}
public function es(): array
{
$agg = [];
if ($this->aggs) {
$agg['aggs'] = array_merge($agg, $this->aggs->es()['aggs']);
}
if (isset($this->query) && $this->query->isEmpty() === false) {
$agg['filter'] = $this->query->es()[$this->query->getType()];
}
if ($this->nested) {
$agg['nested'] = $this->nested->es()['nested'];
}
if ($this->terms) {
$agg['terms'] = $this->terms->es()['terms'];
}
if ($this->extremumTerms) {
$agg = array_merge($agg, $this->extremumTerms->es());
}
return $agg;
}
}
\ No newline at end of file
<?php
namespace IQDEV\ElasticSearch\Search\Aggs;
use IQDEV\ElasticSearch\Esable;
use Ramsey\Collection\AbstractCollection;
final class AggsCollection extends AbstractCollection implements Esable
{
public function getType(): string
{
return Aggs::class;
}
public function es(): array
{
$aggs = [];
foreach ($this as $agg) {
$aggs[$agg->getKey()] = $agg->es();
}
return ['aggs' => $aggs];
}
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment