Skip to content
This repository was archived by the owner on Dec 11, 2023. It is now read-only.

Commit 2dcde2d

Browse files
committed
Search in Elasticsearch by criteria
1 parent f968c48 commit 2dcde2d

File tree

8 files changed

+169
-36
lines changed

8 files changed

+169
-36
lines changed

databases/backoffice/courses.json

+6-3
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
"courses": {
44
"properties": {
55
"id": {
6-
"type": "keyword"
6+
"type": "keyword",
7+
"index": true
78
},
89
"name": {
9-
"type": "text"
10+
"type": "text",
11+
"index": true
1012
},
1113
"duration": {
12-
"type": "text"
14+
"type": "text",
15+
"index": true
1316
}
1417
}
1518
}
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# ELASTIC - Search
2+
POST localhost:9200/backoffice_courses/_search
3+
Content-Type: application/json
4+
5+
{
6+
"query": {
7+
"term": {
8+
"name": "Pepe"
9+
}
10+
}
11+
}
12+
13+
###
14+
# ELASTIC - Search
15+
POST localhost:9200/backoffice_courses/_search
16+
Content-Type: application/json
17+
18+
###
19+

src/Backoffice/Courses/Infrastructure/Persistence/ElasticsearchBackofficeCourseRepository.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ final class ElasticsearchBackofficeCourseRepository extends ElasticsearchReposit
1414
{
1515
protected function aggregateName(): string
1616
{
17-
return 'course';
17+
return 'courses';
1818
}
1919

2020
public function save(BackofficeCourse $course): void
@@ -29,7 +29,7 @@ public function searchAll(): array
2929

3030
public function matching(Criteria $criteria): array
3131
{
32-
return map($this->toCourse(), $this->searchAllInElastic());
32+
return map($this->toCourse(), $this->searchByCriteria($criteria));
3333
}
3434

3535
private function toCourse()

src/Shared/Domain/Criteria/FilterOperator.php

+13-4
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,25 @@
1414
*/
1515
final class FilterOperator extends Enum
1616
{
17-
public const EQUAL = '=';
18-
public const GT = '>';
19-
public const LT = '<';
20-
public const CONTAINS = 'CONTAINS';
17+
public const EQUAL = '=';
18+
public const NOT_EQUAL = '!=';
19+
public const GT = '>';
20+
public const LT = '<';
21+
public const CONTAINS = 'CONTAINS';
22+
public const NOT_CONTAINS = 'NOT_CONTAINS';
23+
24+
private static $containing = [self::CONTAINS, self::NOT_CONTAINS];
2125

2226
public static function equal(): self
2327
{
2428
return new self('=');
2529
}
2630

31+
public function isContaining(): bool
32+
{
33+
return in_array($this->value(), self::$containing, true);
34+
}
35+
2736
protected function throwExceptionForInvalidValue($value): void
2837
{
2938
throw new InvalidArgumentException(sprintf('The filter <%s> is invalid', $value));

src/Shared/Infrastructure/Elasticsearch/ElasticsearchClientFactory.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public function __invoke(
1515
string $host,
1616
string $indexPrefix,
1717
string $schemasFolder,
18-
bool $environment
18+
string $environment
1919
): ElasticsearchClient {
2020
$client = ClientBuilder::create()->setHosts([$host])->build();
2121

@@ -33,7 +33,7 @@ private function generateIndexIfNotExists(
3333
$indexes = Utils::filesIn($schemasFolder, '.json');
3434

3535
foreach ($indexes as $index) {
36-
$indexName = sprintf('%s_%s', $indexPrefix, $index);
36+
$indexName = str_replace('.json', '', sprintf('%s_%s', $indexPrefix, $index));
3737

3838
if ('prod' !== $environment && !$this->indexExists($client, $indexName)) {
3939
$indexBody = Utils::jsonDecode(file_get_contents("$schemasFolder/$index"));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace CodelyTv\Shared\Infrastructure\Persistence\Elasticsearch;
6+
7+
use CodelyTv\Shared\Domain\Criteria\Filter;
8+
use CodelyTv\Shared\Domain\Criteria\FilterOperator;
9+
use function Lambdish\Phunctional\get;
10+
11+
final class ElasticQueryGenerator
12+
{
13+
private const MUST_TYPE = 'must';
14+
private const MUST_NOT_TYPE = 'must_not';
15+
private const TERM_TERM = 'term';
16+
private const TERM_RANGE = 'range';
17+
private const TERM_WILDCARD = 'wildcard';
18+
private static $termMapping = [
19+
FilterOperator::EQUAL => self::TERM_TERM,
20+
FilterOperator::NOT_EQUAL => '!=',
21+
FilterOperator::GT => self::TERM_RANGE,
22+
FilterOperator::LT => self::TERM_RANGE,
23+
FilterOperator::CONTAINS => self::TERM_WILDCARD,
24+
FilterOperator::NOT_CONTAINS => self::TERM_WILDCARD,
25+
];
26+
private static $mustNotFields = [FilterOperator::NOT_EQUAL, FilterOperator::NOT_CONTAINS];
27+
28+
public function __invoke(array $query, Filter $filter)
29+
{
30+
$type = $this->typeFor($filter->operator());
31+
$termLevel = $this->termLeverFor($filter->operator());
32+
$valueTemplate = $filter->operator()->isContaining() ? '*%s*' : '%s';
33+
34+
return array_merge_recursive(
35+
$query,
36+
[
37+
$type => [
38+
$termLevel => [
39+
$filter->field()->value() => sprintf(
40+
$valueTemplate,
41+
strtolower($filter->value()->value())
42+
),
43+
],
44+
],
45+
]
46+
);
47+
}
48+
49+
private function typeFor(FilterOperator $operator): string
50+
{
51+
return in_array($operator->value(), self::$mustNotFields, true) ? self::MUST_NOT_TYPE : self::MUST_TYPE;
52+
}
53+
54+
private function termLeverFor(FilterOperator $operator): string
55+
{
56+
return get($operator->value(), self::$termMapping);
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace CodelyTv\Shared\Infrastructure\Persistence\Elasticsearch;
6+
7+
use CodelyTv\Shared\Domain\Criteria\Criteria;
8+
use function Lambdish\Phunctional\reduce;
9+
10+
final class ElasticsearchCriteriaConverter
11+
{
12+
public function convert(Criteria $criteria): array
13+
{
14+
return [
15+
'body' => array_merge(
16+
['from' => $criteria->offset() ?: 0, 'size' => $criteria->limit() ?: 1000],
17+
$this->formatQuery($criteria),
18+
$this->formatSort($criteria)
19+
),
20+
];
21+
}
22+
23+
private function formatQuery(Criteria $criteria): array
24+
{
25+
if ($criteria->hasFilters()) {
26+
$multipleFilters = 1 < $criteria->filters()->count();
27+
28+
return [
29+
'query' => [
30+
'bool' => reduce(new ElasticQueryGenerator(), $criteria->filters(), []),
31+
],
32+
];
33+
}
34+
35+
return [];
36+
}
37+
38+
private function formatSort(Criteria $criteria): array
39+
{
40+
if ($criteria->hasOrder()) {
41+
$order = $criteria->order();
42+
43+
return ['sort' => [$order->orderBy()->value() => ['order' => $order->orderType()->value()]]];
44+
}
45+
46+
return [];
47+
}
48+
}

src/Shared/Infrastructure/Persistence/Elasticsearch/ElasticsearchRepository.php

+21-25
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44

55
namespace CodelyTv\Shared\Infrastructure\Persistence\Elasticsearch;
66

7-
use CodelyTv\Shared\Domain\ValueObject\Uuid;
7+
use CodelyTv\Shared\Domain\Criteria\Criteria;
88
use CodelyTv\Shared\Infrastructure\Elasticsearch\ElasticsearchClient;
99
use Elasticsearch\Common\Exceptions\Missing404Exception;
10-
use function Lambdish\Phunctional\get;
1110
use function Lambdish\Phunctional\get_in;
1211
use function Lambdish\Phunctional\map;
1312

@@ -27,39 +26,36 @@ protected function persist(string $id, array $plainBody): void
2726
$this->client->persist($this->aggregateName(), $id, $plainBody);
2827
}
2928

30-
protected function searchInElasticById(Uuid $id): ?array
29+
protected function searchAllInElastic(): array
30+
{
31+
return $this->searchRawElasticsearchQuery([]);
32+
}
33+
34+
protected function searchRawElasticsearchQuery(array $params): array
3135
{
3236
try {
33-
$result = $this->client->get(
34-
[
35-
'index' => $this->indexName(),
36-
'type' => $this->typeName(),
37-
'id' => $id->value(),
38-
]
39-
);
40-
41-
return get('_source', $result);
37+
$result = $this->client->client()->search(array_merge(['index' => $this->indexName()], $params));
38+
39+
$hits = get_in(['hits', 'hits'], $result, []);
40+
41+
return map($this->elasticValuesExtractor(), $hits);
4242
} catch (Missing404Exception $unused) {
43-
return null;
43+
return [];
4444
}
4545
}
4646

47-
protected function indexName(): string
47+
public function searchByCriteria(Criteria $criteria): array
4848
{
49-
return sprintf('%s_%s', $this->client->indexPrefix(), $this->aggregateName());
50-
}
49+
$converter = new ElasticsearchCriteriaConverter();
5150

52-
protected function searchAllInElastic(): array
53-
{
54-
$result = $this->client->client()->search(
55-
[
56-
'index' => $this->indexName(),
57-
]
58-
);
51+
$query = $converter->convert($criteria);
5952

60-
$hits = get_in(['hits', 'hits'], $result, []);
53+
return $this->searchRawElasticsearchQuery($query);
54+
}
6155

62-
return map($this->elasticValuesExtractor(), $hits);
56+
protected function indexName(): string
57+
{
58+
return sprintf('%s_%s', $this->client->indexPrefix(), $this->aggregateName());
6359
}
6460

6561
private function elasticValuesExtractor(): callable

0 commit comments

Comments
 (0)